def setUp(self): self.filters = Filter() self.filters['exclude_existing'] = False self.logger = TestLogger() self.lc = LendingClub(logger=self.logger) self.lc.session.base_url = 'http://127.0.0.1:8000/' self.lc.session.set_logger(None) self.lc.authenticate('*****@*****.**', 'supersecret12') response = self.lc.session.get('/filter_validation', query={'id': 1}) json_response = response.json() self.loan_list = json_response['loanFractions']
def get_default_investing_settings(self): """ Return the default investing settings dict """ investing = copy.deepcopy(self.default_investing) investing['filters'] = Filter() return investing
def test_build_portfolio(self): f = Filter({'grades': {'B': True}}) portfolio = lc.build_portfolio(25, 25, 9.0, 14.0, filters=f) self.assertEqual(len(portfolio['loan_fractions']), 1) self.assertEqual(portfolio['b'], 100) self.assertEqual(portfolio['number_of_b_loans'], 1) self.assertTrue(14.0 > portfolio['percentage'] > 9.0)
def select_profile(self, profile_email=None): """ Load a profile from the investing JSON into the settings dictionary. If profile_email is None, we attempt to load the profile for auth['email'] """ # No investing JSON if self.investing_json is None or 'profiles' not in self.investing_json: return False # Reset investing settings from defaults self.investing = self.get_default_investing_settings() # Get profile email from auth if profile_email is None: if self.auth['email'] is None: profile_email = 'none' profile_email = self.auth['email'] self.logger.debug('Select investing profile: {0}'.format(profile_email)) # Get profile profile = None if profile_email in self.investing_json['profiles']: profile = self.investing_json['profiles'][profile_email] elif 'none' in self.investing_json['profiles']: profile = self.investing_json['profiles']['none'] self.logger.debug('Load profile: {0}'.format(profile)) # Load profile if profile: # Load saved filter if 'filter_id' in profile and profile['filter_id']: profile['filter'] = False # Add values to dictionary for key, value in self.investing.items(): if key in profile: self.investing[key] = profile[key] self.is_dirty = True # Create filter object try: if self.investing['filters'] and type(self.investing['filters']) is dict and len(self.investing['filters']) > 0: self.investing['filters'] = Filter(filters=self.investing['filters']) elif self.investing['filter_id']: pass else: self.investing['filters'] = False except Exception as e: raise Exception('Could load filter settings: {0}'.format(str(e))) self.profile_loaded = True return True return False
def test_search_with_filters(self): f = Filter({'grades': {'B': True}}) results = lc.search(f) self.assertTrue(len(results['loans']) > 0) # Ensure all the notes are B grade for loan in results['loans']: self.assertEqual(loan['loanGrade'][0], 'B')
def test_build_portfolio_invest(self): f = Filter({'grades': {'B': True}}) portfolio = lc.build_portfolio(25, 25, 9.0, 14.0, filters=f, automatically_invest=True) self.assertEqual(len(portfolio['loan_fractions']), 1) self.assertEqual(portfolio['b'], 100) self.assertTrue('order_id' in portfolio) self.assertEqual(portfolio['order_id'], 12345) # Try to reinvest this same portfolio o = lc.start_order() self.assertRaises( AssertionError, lambda: o.add_batch(portfolio) )
def request_loan_data(): ''' Requests list of loans that can be invested in, then makes individual call for details of the loans. Results stored in MongoDB database. Returns: loan_results: Results obtained from initial API request. list. loan_details: Individual loan details, obtained with individual API request of loan ids obtained in loan_results. list. ''' filter_search = { 'exclude_existing': False, 'funding_progress': 0, 'grades': { 'All': False, 'A': True, 'B': True, 'C': True, 'D': True, 'E': False, 'F': False, 'G': False }, 'term': { 'Year3': True, 'Year5': False } } club = LendingClub() filter_search = Filter(filter_search) club.authenticate() loan_results = club.search(filter_search, start_index=0, limit=1000) loan_results = loan_results['loans'] loan_ids = [loan['loan_id'] for loan in loan_results] loan_details = [] for loan_id in loan_ids: print "loan_id", loan_id request = club.session.get('/browse/loanDetailAj.action', query={'loan_id': loan_id}) loan_details.append(request.json()) time.sleep(1) return loan_results, loan_details
def setUp(self): self.filters = Filter()
class TestFilters(unittest.TestCase): filters = None def setUp(self): self.filters = Filter() def tearDown(self): pass def get_values(self, m_id): """ Get the m_values array from the filter JSON for this m_id """ search_json = self.filters.search_string() json = pyjson.loads(search_json) for filterObj in json: if filterObj['m_id'] == m_id: return filterObj['m_value'] def test_term_all(self): """ test_term_all Test the default state, having 36 and 60 month terms """ values = self.get_values(39) self.assertEqual(len(values), 2) self.assertEqual(values[0]['value'], 'Year3') self.assertEqual(values[1]['value'], 'Year5') def test_term_36(self): """ test_term_36 36 month term only """ self.filters['term']['Year3'] = True self.filters['term']['Year5'] = False values = self.get_values(39) self.assertEqual(len(values), 1) self.assertEqual(values[0]['value'], 'Year3') def test_term_60(self): """ test_term_60 60 month term only """ self.filters['term']['Year3'] = False self.filters['term']['Year5'] = True values = self.get_values(39) self.assertEqual(len(values), 1) self.assertEqual(values[0]['value'], 'Year5') def test_exclude_existing(self): self.filters['exclude_existing'] = True values = self.get_values(38) self.assertEqual(len(values), 1) def test_include_existing(self): self.filters['exclude_existing'] = False values = self.get_values(38) self.assertEqual(values, None) def test_default_funding_progress(self): values = self.get_values(15) self.assertEqual(values, None) def test_funding_progress_rounding(self): """ test_funding_progress_rounding Funding progress should be rounded to the nearest 10 """ self.filters['funding_progress'] = 56 values = self.get_values(15) self.assertEqual(values[0]['value'], 60) def test_funding_progress_set(self): """ test_funding_progress_round_up Funding progress set to 90 """ self.filters['funding_progress'] = 90 values = self.get_values(15) self.assertEqual(values[0]['value'], 90) def test_funding_progress_round_up(self): """ test_funding_progress_round_up Test the progress rounding. It should round to the nearest 10 """ self.filters['funding_progress'] = 67 values = self.get_values(15) self.assertEqual(values[0]['value'], 70) def test_funding_progress_round_down(self): """ test_funding_progress_round_down Test the progress rounding. It should round to the nearest 10 """ self.filters['funding_progress'] = 63 values = self.get_values(15) self.assertEqual(values[0]['value'], 60) def test_grades(self): """ test_grade Test setting grades to 'B' """ self.filters['grades']['All'] = False self.filters['grades']['B'] = True values = self.get_values(10) self.assertEqual(len(values), 1) self.assertEqual(values[0]['value'], 'B') def test_grades_all(self): """ test_grades_all All should be set to False if another grade is set to True """ self.filters['grades']['C'] = True values = self.get_values(10) self.assertEqual(len(values), 1) self.assertEqual(values[0]['value'], 'C')
class TestFilterValidation(unittest.TestCase): filters = None logger = None lc = None loan_list = None def setUp(self): self.filters = Filter() self.filters['exclude_existing'] = False self.logger = TestLogger() self.lc = LendingClub(logger=self.logger) self.lc.session.base_url = 'http://127.0.0.1:8000/' self.lc.session.set_logger(None) self.lc.authenticate('*****@*****.**', 'supersecret12') response = self.lc.session.get('/filter_validation', query={'id': 1}) json_response = response.json() self.loan_list = json_response['loanFractions'] def tearDown(self): pass def test_validation_defaults(self): """ test_validation_defaults Default filters should match """ self.assertTrue(self.filters.validate(self.loan_list)) def test_validation_grade_valid(self): self.filters['C'] = True self.assertTrue(self.filters.validate(self.loan_list)) def test_validation_grade_fail(self): self.filters['grades']['B'] = True self.assertRaises( FilterValidationError, lambda: self.filters.validate(self.loan_list) ) def test_validation_term_36(self): """ test_validation_term_36 Should fail on the 60 month loan, loan_id: 12345 """ self.filters['term']['Year3'] = True self.filters['term']['Year5'] = False try: self.filters.validate(self.loan_list) # Check the loan it failed on except FilterValidationError as e: self.assertEqual(e.loan['loan_id'], 12345) # Invalid Exception except Exception: self.assertTrue(False) def test_validation_term_60(self): """ test_validation_term_60 Should fail on the 36 month loan, loan_id: 23456 """ self.filters['term']['Year3'] = False self.filters['term']['Year5'] = True try: self.filters.validate(self.loan_list) # Check the loan it failed on except FilterValidationError as e: self.assertEqual(e.loan['loan_id'], 23456) # Invalid Exception except Exception: self.assertTrue(False) def test_validation_progress_70(self): """ test_validation_progress_70 Loan 12345 is 91 percent funded Loan 23456 is 77 percent funded """ self.filters['funding_progress'] = 70 self.assertTrue(self.filters.validate(self.loan_list)) def test_validation_progress_90(self): """ test_validation_term_90 Should fail Loan 12345 is 91 percent funded Loan 23456 is 77 percent funded """ self.filters['funding_progress'] = 90 try: self.filters.validate(self.loan_list) # Check the loan it failed on except FilterValidationError as e: self.assertEqual(e.loan['loan_id'], 23456) # Invalid Exception except Exception: self.assertTrue(False) def test_validation_progress_95(self): """ test_validation_progress_95 Should fail Loan 12345 is 91 percent funded Loan 23456 is 77 percent funded """ self.filters['funding_progress'] = 95 try: self.filters.validate(self.loan_list) # Check the loan it failed on except FilterValidationError as e: self.assertEqual(e.loan['loan_id'], 12345) # Invalid Exception except Exception: self.assertTrue(False) def test_validation_exclude_existing(self): """ test_validation_exclude_existing Should fail on loan 23456, which the user is already invested in. """ self.filters['exclude_existing'] = True try: self.filters.validate(self.loan_list) # Check the loan it failed on except FilterValidationError as e: self.assertEqual(e.loan['loan_id'], 23456) # Invalid Exception except Exception: self.assertTrue(False)
def get_filter_settings(self): """ Setup the advanced portfolio filters (terms, grades, existing notes, etc.) """ # Saved filter saved_filters = self.investor.lc.get_saved_filters() if len(saved_filters) > 0 and util.prompt_yn('Would you like to select one of your saved filters from LendingClub.com?', self.investing['filter_id'] is not None): # Get the selected one from list (has to be same-same object) selected = None if self.investing['filter_id']: selected = self.investing['filter_id'] print('\nSelect a saved filter from the list below:') saved = self.list_picker( items=saved_filters, default=selected, label_key='name', id_key='id') if saved is False: print('\nDefine all your filters manually...') else: print('Using {0}'.format(saved)) self.investing['filters'] = saved self.investing['filter_id'] = saved.id return filters = Filter() # Manual entry print('The following questions are from the filters section of the Invest page on LendingClub\n') # Existing loans filters['exclude_existing'] = util.prompt_yn('Exclude loans already invested in?', filters['exclude_existing']) # Funding progress rounded to the nearest tenth print('---------') print('Funding progress') progress = util.prompt_float('Only include loans which already have at least __% funding (0 - 100)', filters['funding_progress']) filters['funding_progress'] = int(round(progress / 10) * 10) print('---------') print('Choose term (36 - 60 month)') while(True): filters['term']['Year3'] = util.prompt_yn('Include 36 month term loans?', filters['term']['Year3']) filters['term']['Year5'] = util.prompt_yn('Include 60 month term loans?', filters['term']['Year5']) # Validate 1 was chosen if not filters['term']['Year3'] and not filters['term']['Year5']: print('You have to AT LEAST choose one term length!') else: break print('---------') print('Choose interest rate grades (7.4% - 24.84%)') while(True): if util.prompt_yn('Include ALL interest rate grades', filters['grades']['All']): filters['grades']['All'] = True else: filters['grades']['All'] = False filters['grades']['A'] = util.prompt_yn('A - ~7.41%', filters['grades']['A']) filters['grades']['B'] = util.prompt_yn('B - ~12.12%', filters['grades']['B']) filters['grades']['C'] = util.prompt_yn('C - ~15.80%', filters['grades']['C']) filters['grades']['D'] = util.prompt_yn('D - ~18.76%', filters['grades']['D']) filters['grades']['E'] = util.prompt_yn('E - ~21.49%', filters['grades']['E']) filters['grades']['F'] = util.prompt_yn('F - ~23.49%', filters['grades']['F']) filters['grades']['G'] = util.prompt_yn('G - ~24.84%', filters['grades']['G']) # Verify one was chosen gradeChosen = False for grade in filters['grades']: if filters['grades'][grade] is True: gradeChosen = True if not gradeChosen: print('You have to AT LEAST choose one interest rate grade!') else: break self.investing['filters'] = filters
class Settings(): """ Handles all the user and investment settings """ logger = None investor = None settings_file_version = 2 # Files settings_dir = None # The directory that holds all the settings files settings_file = 'settings.yaml' # User settings investing_file = 'investing.json' # Investment settings investing_json = None # A dictionary representing the loaded investing JSON file profile_loaded = False # True if a investing profile has been loaded from the investing JSON # Auth settings auth = {'email': False, 'pass': None} # Defines the investment funding settings default_investing = { 'min_cash': 500, 'min_percent': False, 'max_percent': False, 'max_per_note': 25, 'portfolio': None, 'filter_id': None, 'filters': Filter() } investing = {} # If the investing settings have been updated # False: The settings are still set to the defaults # True: The settings have been updated by the user or file is_dirty = False # Default user settings user_settings = {'frequency': 60} def __init__(self, investor, investing_file=None, settings_dir=None, logger=False, verbose=False): """ file: A JSON file will the settings to use. Setting this parameter will prevent saving settings to the cache file. settings_dir: The directory that will be used to save the user and investment settings files """ self.investing = copy.deepcopy(self.default_investing) self.is_dirty = False self.investor = investor self.settings_dir = settings_dir # Create logger if none was passed in if not logger: self.logger = util.create_logger(verbose) else: self.logger = logger # Create the settings directory, if it doesn't exist if investing_file is None and self.settings_dir is not None and not os.path.exists( self.settings_dir): os.mkdir(self.settings_dir) self.get_user_settings() self.load_investment_settings_file() # Default email to the last profile used if 'last_profile' in self.investing_json and self.investing_json[ 'last_profile'] != 'none': self.auth['email'] = self.investing_json['last_profile'] def __getitem__(self, key): """ Attempts to a value from one of the dictionaries """ if key in self.investing: # Load saved filter by ID if key == 'filters' and 'filter_id' in self.investing and self.investing[ 'filter_id'] and type( self.investing['filters']) is not SavedFilter: try: self.investing['filters'] = SavedFilter( self.investor.lc, self.investing['filter_id']) except Exception: self.investing['filters'] = None return self.investing[key] if key in self.user_settings: return self.user_settings[key] if key in self.auth: return self.auth[key] return None def __setitem__(self, key, value): """ Add a setting """ if key in self.investing: self.investing[key] = value self.is_dirty = True elif key in self.user_settings: self.user_settings[key] = value elif key in self.auth: self.auth[key] = value def get_user_settings(self): """ Load the settings.yaml file into memory. If this file doesn't exist, create in """ if not self.settings_dir: return file_path = os.path.join(self.settings_dir, self.settings_file) # Create the file, if it doesn't exist if not os.path.exists(file_path): this_path = os.path.dirname(os.path.realpath(__file__)) default_file = os.path.join(this_path, 'settings.yaml') shutil.copy2(default_file, file_path) # Read file self.user_settings = yaml.load(open(file_path).read()) return self.user_settings def process_json(self, jsonStr): """ Preprocess a JSON string. Currently this simply removes all single line comments """ # Remove comment lines jsonStr = re.sub(r'\s*//.*?\n', '\n\n', jsonStr) return jsonStr def save(self): """ Save the investment settings dict to a file """ if self.settings_dir is None or self.auth['email'] is None: return try: self.logger.debug('Save investment settings to file') # Add current profile to the investing JSON file object profile = self.investing.copy() try: # Load existing JSON file to_save = self.read_investment_settings_file() except Exception as e: # Create new JSON to_save = {'profiles': {}} # Add filter_id if 'filters' in profile and type( profile['filters']) is SavedFilter: profile['filter_id'] = profile['filters'].id profile['filters'] = None else: profile['filter_id'] = None # Add profile and version to_save['profiles'][self.auth['email']] = profile to_save['last_profile'] = self.auth['email'] to_save['version'] = util.get_version() to_save['format'] = self.settings_file_version json_out = json.dumps(to_save) # Save self.logger.debug( 'Saving investment settings file: {0}'.format(json_out)) investing_file = os.path.join(self.settings_dir, self.investing_file) f = open(investing_file, 'w') f.write(json_out) f.close() self.logger.debug('Saved') except Exception as e: self.logger.warning( 'Could not save the investment settings to file: {0}'.format( str(e))) def migrate_settings(self, settings): """ Migrate old settings to what they should be now """ # Create profiles object if 'profiles' not in settings: email = 'none' if 'email' in settings: email = settings['email'].lower() self.logger.debug( 'Creating profiles object with "{0}"'.format(email)) newSettings = { 'version': util.get_version(), 'format': self.settings_file_version, 'last_profile': email, 'profiles': { email: settings } } settings = newSettings # Migrate each profile for email, profile in settings['profiles'].iteritems(): # Investing filters if 'filters' in profile and profile['filters']: self.logger.debug('Normalize profile for "{0}"'.format(email)) if 'filter_id' not in profile: profile['filter_id'] = None if type(profile['filters']) is dict: if 'term' not in profile['filters']: profile['filters']['term'] = {} if '36month' in profile['filters']: profile['filters']['term']['Year3'] = profile[ 'filters']['36month'] del profile['filters']['36month'] if 'term36month' in profile['filters']: profile['filters']['term']['Year3'] = profile[ 'filters']['term36month'] del profile['filters']['term36month'] if '60month' in profile['filters']: profile['filters']['term']['Year5'] = profile[ 'filters']['60month'] del profile['filters']['60month'] if 'term60month' in profile['filters']: profile['filters']['term']['Year5'] = profile[ 'filters']['term60month'] del profile['filters']['term60month'] if 'minPercent' in profile: profile['min_percent'] = profile['minPercent'] del profile['minPercent'] if 'maxPercent' in profile: profile['max_percent'] = profile['maxPercent'] del profile['maxPercent'] if 'minCash' in profile: profile['min_cash'] = profile['minCash'] del profile['minCash'] if 'max_per_note' not in profile: profile['max_per_note'] = 25 return settings def read_investment_settings_file(self, file_path=None): """ Read a JSON file with investment settings and return the dictionary """ # Get default file path if file_path is None: file_path = os.path.join(self.settings_dir, self.investing_file) if os.path.exists(file_path): self.logger.debug( 'Reading investment settings file: {0}'.format(file_path)) try: # Read file f = open(file_path, 'r') jsonStr = f.read() f.close() self.logger.debug( 'Investment settings JSON: {0}'.format(jsonStr)) # Convert JSON to dictionary jsonStr = self.process_json(jsonStr) saved_settings = json.loads(jsonStr) # Migrations saved_settings = self.migrate_settings(saved_settings) self.logger.debug( 'Normalized settings JSON: {0}'.format(saved_settings)) return saved_settings except Exception as e: self.logger.debug( 'Could not read investment settings file: {0}'.format( str(e))) print jsonStr raise Exception('Could not process file \'{0}\': {1}'.format( file_path, str(e))) else: self.logger.debug( 'The file \'{0}\' doesn\'t exist'.format(file_path)) raise Exception( 'The file \'{0}\' doesn\'t exist'.format(file_path)) return False def load_investment_settings_file(self, file_path=None): """ Load the JSON settings file into investing_json dict """ self.investing_json = self.read_investment_settings_file(file_path) def select_profile(self, profile_email=None): """ Load a profile from the investing JSON into the settings dictionary. If profile_email is None, we attempt to load the profile for auth['email'] """ # No investing JSON if self.investing_json is None or 'profiles' not in self.investing_json: return False # Reset investing settings from defaults self.investing = copy.deepcopy(self.default_investing) # Get profile email from auth if profile_email is None: if self.auth['email'] is None: profile_email = 'none' profile_email = self.auth['email'] self.logger.debug( 'Select investing profile: {0}'.format(profile_email)) # Get profile profile = None if profile_email in self.investing_json['profiles']: profile = self.investing_json['profiles'][profile_email] elif 'none' in self.investing_json['profiles']: profile = self.investing_json['profiles']['none'] self.logger.debug('Load profile: {0}'.format(profile)) # Load profile if profile: # Load saved filter if 'filter_id' in profile and profile['filter_id']: profile['filter'] = False # Add values to dictionary for key, value in self.investing.iteritems(): if key in profile: self.investing[key] = profile[key] self.is_dirty = True # Create filter object try: if self.investing['filters'] and type( self.investing['filters']) is dict and len( self.investing['filters']) > 0: self.investing['filters'] = Filter( filters=self.investing['filters']) elif self.investing['filter_id']: pass else: self.investing['filters'] = False except Exception as e: raise Exception('Could load filter settings: {0}'.format( str(e))) self.profile_loaded = True return True return False def get_auth_settings(self): """ Get the email and password from the user """ self.auth['email'] = util.prompt('LendingClub email', self.auth['email']) self.auth['pass'] = util.get_password() return self.auth def show_summary(self, title='Summary'): """ Show a summary of the settings that will be used for auto investing """ ret = True print '\n========= {0} ========='.format(title) print 'Invest ALL available funds...\n' print 'With at LEAST ${0} available to invest'.format( self.investing['min_cash']) print 'Select a portfolio with an average interest rate between {0}% - {1}%'.format( self.investing['min_percent'], self.investing['max_percent']) print 'Invest as much as ${0} per loan note'.format( self.investing['max_per_note']) if self.investing['portfolio']: print 'Add investments to: "{0}"'.format( self.investing['portfolio']) # Filters if self.investing['filter_id'] and type( self['filters']) is not SavedFilter: print '\n!!! ERROR !!!' print 'Saved filter \'{0}\' could not be loaded from LendingClub. Check the ID and try again.\n'.format( self.investing['filter_id']) ret = False elif type(self['filters']) is SavedFilter: filters = self.investing['filters'] print '\nUsing saved filter "{0}" (id:{1})'.format( filters.name, filters.id) elif self['filters'] is not False: print '\nAdvanced filters:' filters = self['filters'] # Exclude existing if filters['exclude_existing']: print ' + Exclude loans already invested in' # Funding progress if filters['funding_progress'] > 0: print ' + Only loans which are at least {0}% funded'.format( filters['funding_progress']) # Loan term terms = [] if filters['term']['Year3'] is True: terms.append('36') if filters['term']['Year5'] is True: terms.append('60') print ' + Term: {0} months'.format(' & '.join(terms)) # Interest rate grades if filters['grades']['All']: print ' + All interest rate grades' else: grades = [] for grade in filters['grades']: if grade != 'All' and filters['grades'][grade] is True: grades.append(grade) print ' + Interest rates grades: {0}'.format(', '.join( sorted(grades))) print '=========={0}==========\n'.format(''.center(len(title), '=')) return ret def confirm_settings(self): self.show_summary() if util.prompt_yn('Would you like to continue with these settings?', 'y'): self.save() else: self.get_investment_settings() def get_investment_settings(self): """ Display a series of prompts to get how the user wants to invest their available cash. This fills out the investing dictionary. """ # Minimum cash print '---------' print 'The auto investor will automatically try to invest ALL available cash into a diversified portfolio' while (True): self.investing['min_cash'] = util.prompt_int( 'What\'s the MINIMUM amount of cash you want to invest each round?', self.investing['min_cash']) if self.investing['min_cash'] < 25: print '\nYou cannot invest less than $25. Please try again.' else: break # Min/max percent print '---------' while (True): print 'When auto investing, the LendingClub API will search for diversified investment portfolios available at that moment.' print 'To pick the appropriate one for you, it needs to know what the minimum and maximum AVERAGE interest rate value you will accept.' print 'The investment option closest to the maximum value will be chosen and all your available cash will be invested in it.\n' self.investing['min_percent'] = util.prompt_float( 'What\'s MININUM average interest rate portfolio that you will accept?', self.investing['min_percent']) # Max percent should default to being larger than the min percent if self.investing['max_percent'] is False or self.investing[ 'max_percent'] < self.investing['min_percent']: self.investing[ 'max_percent'] = self.investing['min_percent'] + 1 self.investing['max_percent'] = util.prompt_float( 'What\'s MAXIMUM average interest rate portfolio that you will accept?', self.investing['max_percent']) # Validation if self.investing['max_percent'] < self.investing['min_percent']: print 'The maximum value must be larger than, or equal to, the minimum value. Please try again.' elif self.investing['max_percent'] == self.investing[ 'min_percent']: print 'It\'s very uncommon to find an available portfolio that will match an exact percent.' if not util.prompt_yn( 'Would you like to specify a broader range?'): break else: break # Max per note print '---------' while (True): self.investing['max_per_note'] = util.prompt_int( 'How much are you willing to invest per loan note (max per note)?', self.investing['max_per_note']) if self.investing['max_per_note'] < 25: print 'You have to invest AT LEAST $25 per note.' self.investing['max_per_note'] = 25 else: break # Portfolio print '---------' folioOption = False if self.investing[ 'portfolio']: # if saved settings has a portfolio set, default the prompt to 'Y' to choose folioOption = 'y' if util.prompt_yn( 'Do you want to put your new investments into a named portfolio?', folioOption): self.investing['portfolio'] = self.portfolio_picker( self.investing['portfolio']) else: self.investing['portfolio'] = False print '\n---------' # Advanced filter settings if util.prompt_yn('Would you like to set advanced filter settings?', self.investing['filters'] is not False): self.get_filter_settings() # Review summary self.confirm_settings() return True def get_filter_settings(self): """ Setup the advanced portfolio filters (terms, grades, existing notes, etc.) """ # Saved filter saved_filters = self.investor.lc.get_saved_filters() if len(saved_filters) > 0 and util.prompt_yn( 'Would you like to select one of your saved filters from LendingClub.com?', self.investing['filter_id'] is not None): # Get the selected one from list (has to be same-same object) selected = None if self.investing['filter_id']: selected = self.investing['filter_id'] print '\nSelect a saved filter from the list below:' saved = self.list_picker(items=saved_filters, default=selected, label_key='name', id_key='id') if saved is False: print '\nDefine all your filters manually...' else: print 'Using {0}'.format(saved) self.investing['filters'] = saved self.investing['filter_id'] = saved.id return filters = Filter() # Manual entry print 'The following questions are from the filters section of the Invest page on LendingClub\n' # Existing loans filters['exclude_existing'] = util.prompt_yn( 'Exclude loans already invested in?', filters['exclude_existing']) # Funding progress rounded to the nearest tenth print '---------' print 'Funding progress' progress = util.prompt_float( 'Only include loans which already have at least __% funding (0 - 100)', filters['funding_progress']) filters['funding_progress'] = int(round(progress / 10) * 10) print '---------' print 'Choose term (36 - 60 month)' while (True): filters['term']['Year3'] = util.prompt_yn( 'Include 36 month term loans?', filters['term']['Year3']) filters['term']['Year5'] = util.prompt_yn( 'Include 60 month term loans?', filters['term']['Year5']) # Validate 1 was chosen if not filters['term']['Year3'] and not filters['term']['Year5']: print 'You have to AT LEAST choose one term length!' else: break print '---------' print 'Choose interest rate grades (7.4% - 24.84%)' while (True): if util.prompt_yn('Include ALL interest rate grades', filters['grades']['All']): filters['grades']['All'] = True else: filters['grades']['All'] = False filters['grades']['A'] = util.prompt_yn( 'A - ~7.41%', filters['grades']['A']) filters['grades']['B'] = util.prompt_yn( 'B - ~12.12%', filters['grades']['B']) filters['grades']['C'] = util.prompt_yn( 'C - ~15.80%', filters['grades']['C']) filters['grades']['D'] = util.prompt_yn( 'D - ~18.76%', filters['grades']['D']) filters['grades']['E'] = util.prompt_yn( 'E - ~21.49%', filters['grades']['E']) filters['grades']['F'] = util.prompt_yn( 'F - ~23.49%', filters['grades']['F']) filters['grades']['G'] = util.prompt_yn( 'G - ~24.84%', filters['grades']['G']) # Verify one was chosen gradeChosen = False for grade in filters['grades']: if filters['grades'][grade] is True: gradeChosen = True if not gradeChosen: print 'You have to AT LEAST choose one interest rate grade!' else: break self.investing['filters'] = filters def portfolio_picker(self, default=None): """ Shows a list picker of porfolios Parameters: default -- The portfolio name to have selected by default """ folios = self.investor.lc.get_portfolio_list(names_only=True) print '\nPortfolios...' folios.sort() while True: if len(folios) == 0: picked = util.prompt('Enter the name for your new portfolio') else: picked = self.list_picker( items=folios, default=default, allow_other=True, other_prompt='Enter the name for your new portfolio') # Validate custom value if picked and picked not in folios and re.search( '[^a-zA-Z0-9 ,_\-#\.]', picked): print 'The portfolio name \'{0}\' is not valid! Only alphanumeric, spaces , _ - # and . are allowed.'.format( picked) else: break return picked def list_picker(self, items, default=None, label_key=None, id_key=None, allow_other=None, other_prompt='Enter a value'): """ Shows a list of items the user can pick from. Parameters items -- The list of items to display. This is either a list of strings or objects. If objects, the label_key must be set default -- The item or ID that should be selected by default. label_key -- If items is a list of objects, this is the key or attribute of the object with the label to show for each item. id_key -- If items is a list of objects, this defined what the ID key/attribute is on each object. allow_other -- If an 'Other' option should be allowed. If selected the user will be able to type their own item, which will be returned. other_prompt -- The prompt to show when the user selects 'Other' Returns The item chosen from the list, a string if the user choose 'Other' or False if the user cancels the selection """ assert len( items ) > 0 or default is not False, 'You cannot select from a list without any items' try: string_list = False if (len(items) > 0 and type(items[0]) in [str, unicode]) or ( len(items) == 0 and type(default) is str): string_list = True # If the default item isn't in the list of strings, add it if default and default not in items and string_list: items.append(default) # Print out the list i = 1 other_index = -1 cancel_index = 0 default_index = False for item in items: gutter = ' ' is_default = False # Get item label if string_list: label = item else: label = str(item) if label_key: if type(item) is dict and label_key in item: label = item[label_key] elif hasattr(item, label_key): label = getattr(item, label_key) # Selected indicator if default is not None: if string_list and default == item: is_default = True elif id_key is not None: if type(item) is dict and id_key in item and item[ id_key] == default: is_default = True elif hasattr(item, label_key) and getattr( item, id_key) == default: is_default = True if is_default: gutter = '> ' default_index = str(i) print '{0}{1}: {2}'.format(gutter, i, label) i += 1 if allow_other: other_index = i print ' {0}: Other'.format(other_index) i += 1 cancel_index = i print ' {0}: Cancel'.format(cancel_index) # Choose a portfolio while (True): choice = util.prompt('Choose one', default_index) # If no digit was chosen, ask again unless a default portfolio is present if not choice.isdigit(): if not default_index: continue else: return default choice = int(choice) # Out of range if choice == 0 or choice > cancel_index: continue # List item chosen if choice <= len(items): return items[choice - 1] # Other elif choice == other_index: while (True): other = util.prompt(other_prompt) # Empty string entered, show list again if other.strip() == '': break # Return custom portfolio name else: return other # Cancel else: return False except Exception as e: self.logger.error(e)
def load_investment_settings_file(self, file_path=None): """ Returned the saved settings used last time this program was run """ if not self.settings_dir and file_path is None: return False if file_path is None: file_path = os.path.join(self.settings_dir, self.investing_file) if os.path.exists(file_path): self.logger.debug( 'Loading investment settings file: {0}'.format(file_path)) try: # Read file f = open(file_path, 'r') jsonStr = f.read() f.close() self.logger.debug( 'Investment settings JSON: {0}'.format(jsonStr)) # Convert JSON to dictionary jsonStr = self.process_json(jsonStr) saved_settings = json.loads(jsonStr) # Migrations saved_settings = self.migrate_settings(saved_settings) # Load saved filter if saved_settings['filter_id']: saved_settings['filter'] = False # Add values to dictionary for key, value in self.investing.iteritems(): if key in saved_settings: self.investing[key] = saved_settings[key] self.is_dirty = True # Add email to auth if 'email' in saved_settings: self.auth['email'] = saved_settings['email'] except Exception as e: self.logger.debug( 'Could not read investment settings file: {0}'.format( str(e))) print jsonStr raise Exception('Could not process file \'{0}\': {1}'.format( file_path, str(e))) # Create filter object try: if self.investing['filters'] and type( self.investing['filters']) is dict and len( self.investing['filters']) > 0: self.investing['filters'] = Filter( filters=self.investing['filters']) elif self.investing['filter_id']: pass else: self.investing['filters'] = False except Exception as e: raise Exception('Could load filter settings: {0}'.format( str(e))) return True else: self.logger.debug( 'The file \'{0}\' doesn\'t exist'.format(file_path)) return False