class TestOrder(unittest.TestCase): lc = None order = None logger = None def setUp(self): 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('*****@*****.**', 'supersecret') # Make sure session is enabled and clear self.lc.session.post('/session/enabled') self.lc.session.request('delete', '/session') # Use version 2 of browseNotesAj.json self.lc.session.post('/session', data={'browseNotesAj': '2'}) # Start order self.order = self.lc.start_order() def tearDown(self): pass def test_add(self): self.order.add(123, 50) self.assertEqual(len(self.order.loans), 1) self.assertEqual(self.order.loans[123], 50) def test_update(self): self.order.add(123, 50) self.assertEqual(self.order.loans[123], 50) self.order.add(123, 100) self.assertEqual(len(self.order.loans), 1) self.assertEqual(self.order.loans[123], 100) def test_remove(self): self.order.add(123, 50) self.order.add(234, 75) self.assertEqual(len(self.order.loans), 2) self.order.remove(234) self.assertEqual(len(self.order.loans), 1) self.assertEqual(self.order.loans[123], 50) self.assertFalse(234 in self.order.loans) def test_multiple_of_25(self): self.assertRaises(AssertionError, lambda: self.order.add(123, 0)) self.assertRaises(AssertionError, lambda: self.order.add(123, 26))
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 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
class TestBatchOrder(unittest.TestCase): lc = None order = None logger = None def setUp(self): 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('*****@*****.**', 'supersecret') # Make sure session is enabled and clear self.lc.session.post('/session/enabled') self.lc.session.request('delete', '/session') # Use version 3 of browseNotesAj.json self.lc.session.post('/session', data={'browseNotesAj': '3'}) # Start order self.order = self.lc.start_order() def tearDown(self): pass def test_add_batch_dict(self): """ test_add_batch_dict Add a batch of dict loan objects """ self.order.add_batch([ { 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 } ]) self.assertEqual(len(self.order.loans), 2) self.assertEqual(self.order.loans[123], 50) self.assertEqual(self.order.loans[234], 75) def test_add_batch_dict_amount(self): """ test_add_batch_dict_amount Add a batch dict with a batch_amount parameter value to override the individual values """ self.order.add_batch([ { 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 } ], 100) self.assertEqual(len(self.order.loans), 2) self.assertEqual(self.order.loans[123], 100) self.assertEqual(self.order.loans[234], 100) def test_add_batch_list(self): """ test_add_batch_list Add a batch of IDs from a list, not a dict """ self.order.add_batch([123, 234], 75) self.assertEqual(len(self.order.loans), 2) self.assertEqual(self.order.loans[123], 75) self.assertEqual(self.order.loans[234], 75) def test_add_batch_list_no_amount(self): """ test_add_batch_list_no_amount Send a list of IDs to add_batch, without an amount """ self.assertRaises( AssertionError, lambda: self.order.add_batch([123, 234]) ) def test_add_batch_object(self): """ test_add_batch_object Pulling loans from the 'loan_fractions' value is no longer supported """ loanDict = { 'loan_fractions': [ { 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 } ] } self.assertRaises( AssertionError, lambda: self.order.add_batch(loanDict) ) def test_execute(self): self.order.add_batch([ { 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 } ]) order_id = self.order.execute() self.assertNotEqual(order_id, 0) def test_execute_wrong_id(self): """ test_execute_wrong_id Server returns an ID that doesn't match an ID added to batch (345) """ self.order.add_batch([234, 345], 75) self.assertRaises( FilterValidationError, lambda: self.order.execute() ) def test_execute_existing_portfolio(self): self.order.add_batch([ { 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 } ]) portfolio = 'New Portfolio' order_id = self.order.execute(portfolio) self.assertNotEqual(order_id, 0) # Check portfolio name request = self.lc.session.get('/session') http_session = request.json() self.assertEqual(http_session['new_portfolio'], portfolio) def test_execute_new_portfolio(self): self.order.add_batch([ { 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 } ]) portfolio = 'Existing Portfolio' order_id = self.order.execute(portfolio) self.assertNotEqual(order_id, 0) # Check portfolio name request = self.lc.session.get('/session') http_session = request.json() self.assertEqual(http_session['existing_portfolio'], portfolio) def test_double_execute(self): """ test_double_execute An order can only be executed once """ self.order.add_batch([ { 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 } ]) order_id = self.order.execute() self.assertNotEqual(order_id, 0) self.assertRaises( AssertionError, lambda: self.order.execute() )
class TestOrder(unittest.TestCase): lc = None order = None logger = None def setUp(self): 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('*****@*****.**', 'supersecret') # Make sure session is enabled and clear self.lc.session.post('/session/enabled') self.lc.session.request('delete', '/session') # Use version 2 of browseNotesAj.json self.lc.session.post('/session', data={'browseNotesAj': '2'}) # Start order self.order = self.lc.start_order() def tearDown(self): pass def test_add(self): self.order.add(123, 50) self.assertEqual(len(self.order.loans), 1) self.assertEqual(self.order.loans[123], 50) def test_update(self): self.order.add(123, 50) self.assertEqual(self.order.loans[123], 50) self.order.add(123, 100) self.assertEqual(len(self.order.loans), 1) self.assertEqual(self.order.loans[123], 100) def test_remove(self): self.order.add(123, 50) self.order.add(234, 75) self.assertEqual(len(self.order.loans), 2) self.order.remove(234) self.assertEqual(len(self.order.loans), 1) self.assertEqual(self.order.loans[123], 50) self.assertFalse(234 in self.order.loans) def test_multiple_of_25(self): self.assertRaises( AssertionError, lambda: self.order.add(123, 0) ) self.assertRaises( AssertionError, lambda: self.order.add(123, 26) )
class Autobuy: lc = None #main_filter = 'low-risk2' saved_filters = None main_filters = [] main_filter_ids = [8641686, 9061019] def __init__(self, email = None, password = None, filter_ids = None): if filter_ids != None: self.main_filter_ids = filter_ids self.lc = LendingClub(email, password) self.lc.authenticate() self.saved_filters = self.lc.get_saved_filters() self.main_filters = \ filter(lambda x: x.id in self.main_filter_ids, self.saved_filters) def lc(self): self.lc def run_all(self): loan_ids = [] for f in self.main_filters: loan_ids = list(set(loan_ids) | set(self.run(f))) if len(loan_ids) > 0: order = self.lc.start_order() order.add_batch(loan_ids, 25) order.execute() print "%s loans processed" % len(loan_ids) return len(loan_ids) print "No loans processed" return False def run(self, _filter = None, just_loans = True): if _filter is None: raise Exception("No filter specified") results = self.lc.search(_filter) if results[u'totalRecords'] > 30: raise Exception("Filter probably broken, returning > 30 results") elif results[u'totalRecords'] == 0: return [] if not just_loans: order = self.lc.start_order() loans = [] for result in results[u'loans']: loan_id = result[u'loan_id'] additional_details = self.lc.loan_details(loan_id) if additional_details[u'currentJobTitle'] == u'n/a': continue rate = float(re.findall(r"\d+\.\d+|\d+", additional_details[u'rate'])[0]) term = int(additional_details[u'loanLength']) if (term == 36 and rate < 0.19) or rate < 0.22: continue loans.append(loan_id) if len(loans) > 0: if just_loans: return loans order.add_batch(loans, 25) order.execute() return True print "No loans found" if just_loans: return [] else: return True def __str__(self): print "Autobuyer"
class TestLendingClub(unittest.TestCase): lc = None logger = None def setUp(self): 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('*****@*****.**', 'supersecret') # Make sure session is enabled and clear self.lc.session.post('/session/enabled') self.lc.session.request('delete', '/session') def tearDown(self): pass def test_cash_balance(self): cash = self.lc.get_cash_balance() self.assertEqual(cash, 216.02) def test_portfolios(self): portfolios = self.lc.get_portfolio_list() self.assertEqual(len(portfolios), 2) self.assertEqual(portfolios[0]['portfolioName'], 'Existing Portfolio') def test_build_portfolio(self): portfolio = self.lc.build_portfolio(200, 25, 15, 16) self.assertNotEqual(portfolio, False) self.assertEqual(portfolio['percentage'], 15.28) self.assertTrue('loan_fractions' in portfolio) self.assertEqual(len(portfolio['loan_fractions']), 15) def test_build_portfolio_session_fail(self): """ test_build_portfolio_session_fail" If the session isn't saved, fractions shouldn't be found, which should make the entire method return False """ # Disable session self.lc.session.post('/session/disabled') portfolio = self.lc.build_portfolio(200, 25, 15, 16) self.assertFalse(portfolio) def test_build_portfolio_no_match(self): """ test_build_portfolio_no_match" Enter a min/max percent that cannot match dummy returned JSON """ portfolio = self.lc.build_portfolio(200, 25, 17.6, 18.5) self.assertFalse(portfolio) def test_search(self): results = self.lc.search() self.assertTrue(results is not False) self.assertTrue('loans' in results) self.assertTrue(len(results['loans']) > 0)
class TestBatchOrder(unittest.TestCase): lc = None order = None logger = None def setUp(self): 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('*****@*****.**', 'supersecret') # Make sure session is enabled and clear self.lc.session.post('/session/enabled') self.lc.session.request('delete', '/session') # Use version 3 of browseNotesAj.json self.lc.session.post('/session', data={'browseNotesAj': '3'}) # Start order self.order = self.lc.start_order() def tearDown(self): pass def test_add_batch_dict(self): """ test_add_batch_dict Add a batch of dict loan objects """ self.order.add_batch([{ 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 }]) self.assertEqual(len(self.order.loans), 2) self.assertEqual(self.order.loans[123], 50) self.assertEqual(self.order.loans[234], 75) def test_add_batch_dict_amount(self): """ test_add_batch_dict_amount Add a batch dict with a batch_amount parameter value to override the individual values """ self.order.add_batch([{ 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 }], 100) self.assertEqual(len(self.order.loans), 2) self.assertEqual(self.order.loans[123], 100) self.assertEqual(self.order.loans[234], 100) def test_add_batch_list(self): """ test_add_batch_list Add a batch of IDs from a list, not a dict """ self.order.add_batch([123, 234], 75) self.assertEqual(len(self.order.loans), 2) self.assertEqual(self.order.loans[123], 75) self.assertEqual(self.order.loans[234], 75) def test_add_batch_list_no_amount(self): """ test_add_batch_list_no_amount Send a list of IDs to add_batch, without an amount """ self.assertRaises(AssertionError, lambda: self.order.add_batch([123, 234])) def test_add_batch_object(self): """ test_add_batch_object Pulling loans from the 'loan_fractions' value is no longer supported """ loanDict = { 'loan_fractions': [{ 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 }] } self.assertRaises(AssertionError, lambda: self.order.add_batch(loanDict)) def test_execute(self): self.order.add_batch([{ 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 }]) order_id = self.order.execute() self.assertNotEqual(order_id, 0) def test_execute_wrong_id(self): """ test_execute_wrong_id Server returns an ID that doesn't match an ID added to batch (345) """ self.order.add_batch([234, 345], 75) self.assertRaises(FilterValidationError, lambda: self.order.execute()) def test_execute_existing_portfolio(self): self.order.add_batch([{ 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 }]) portfolio = 'New Portfolio' order_id = self.order.execute(portfolio) self.assertNotEqual(order_id, 0) # Check portfolio name request = self.lc.session.get('/session') http_session = request.json() self.assertEqual(http_session['new_portfolio'], portfolio) def test_execute_new_portfolio(self): self.order.add_batch([{ 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 }]) portfolio = 'Existing Portfolio' order_id = self.order.execute(portfolio) self.assertNotEqual(order_id, 0) # Check portfolio name request = self.lc.session.get('/session') http_session = request.json() self.assertEqual(http_session['existing_portfolio'], portfolio) def test_double_execute(self): """ test_double_execute An order can only be executed once """ self.order.add_batch([{ 'loan_id': 123, 'invest_amount': 50 }, { 'loan_id': 234, 'invest_amount': 75 }]) order_id = self.order.execute() self.assertNotEqual(order_id, 0) self.assertRaises(AssertionError, lambda: self.order.execute())
# Find by Grade grade = note['rate'][0] found = lc.search_my_notes(grade=grade) self.assertTrue(len(found) > 0) for note in found: self.assertEqual(grade, note['rate'][0]) print(""" !!!WARNING !!! This is a live test of the module communicating with LendingClub.com with your account!!! Your account must have at least $25 to continue. Tests will attempt to get full API test coverage coming just short of investing money from your account. However, this is not guaranteed if something in the tests are broken. Please continue at your own risk. """) res = input('Continue with the tests? [yes/no]') if res.lower() != 'yes': exit() print('\n\nEnter a valid LendingClub account information...') email = input('Email:') password = getpass.getpass() assert lc.is_site_available(), 'No network connection or cannot access lendingclub.com' assert lc.authenticate(email, password), 'Could not authenticate' assert lc.get_investable_balance(), 'You do not have at least $25 in your account.' if __name__ == '__main__': unittest.main()
class AutoInvestor: """ Regularly check a LendingClub account for available cash and reinvest it automatically. """ lc = None authed = False verbose = False auto_execute = True settings = None loop = False app_dir = None # The file that the summary from the last investment is saved to last_investment_file = 'last_investment.json' def __init__(self, verbose=False, auto_execute=True): """ Create an AutoInvestor instance - Set verbose to True if you want to see debugging logs """ self.verbose = verbose self.auto_execute = auto_execute self.logger = util.create_logger(verbose) self.app_dir = util.get_app_directory() self.lc = LendingClub() # Set logger on lc if self.verbose: self.lc.set_logger(self.logger) # Create settings object self.settings = Settings(investor=self, settings_dir=self.app_dir, logger=self.logger, verbose=self.verbose) self.settings.investor = self # create a link back to this instance def version(self): """ Return the version number of the Lending Club Investor tool """ return util.get_version() def welcome_screen(self): print( "\n///--------------------------- $$$ ---------------------------\\\\\\" ) print( '| Welcome to the unofficial Lending Club investment tool |' ) print( " ---------------------------------------------------------------- \n" ) def get_auth(self): print( 'To start, we need to log you into Lending Club (your password will never be saved)\n' ) while True: self.settings.get_auth_settings() print('\nAuthenticating...') try: return self.authenticate() except Exception as e: print('\nLogin failed: {0}'.format(str(e.value))) print("Please try again\n") def setup(self): """ Setup the investor to run """ if self.verbose: print('VERBOSE OUTPUT IS ON\n') if not self.authed: self.get_auth() self.settings.select_profile() print('You have ${0} in your account, free to invest\n'.format( self.lc.get_cash_balance())) # Investment settings print('Now let\'s define what you want to do') # Use the settings from last time if self.settings.profile_loaded is not False: summary = self.settings.show_summary() if summary is False: # there was an error with saved settings print( '\nThere was an error with your saved settings. Please go through the prompts again.\n' ) self.settings.get_investment_settings() if util.prompt_yn('Would you like to use these settings?', 'y'): self.settings.save() # to save the email that was just entered else: self.settings.get_investment_settings() else: self.settings.get_investment_settings() # All ready to start running print( '\nThat\'s all we need. Now, as long as this is running, your account will be checked every {0} minutes and invested if enough funds are available.\n' .format(self.settings['frequency'])) def authenticate(self): """ Attempt to authenticate the user with the email/pass from the Settings object. This is just a wrapper for LendingClub.authenticate() Returns True or raises an exceptions """ self.authed = self.lc.authenticate(self.settings.auth['email'], self.settings.auth['pass']) return self.authed def run(self): """ Alias for investment_loop. This is used by python-runner """ self.investment_loop() def run_once(self): """ Try to invest, based on your settings, and then end the program. """ self.loop = False # Make sure the site is available attempts = 0 while not self.lc.is_site_available(): attempts += 1 if attempts % 5 == 0: self.logger.warn( 'LendingClub is not responding. Trying again in 10 seconds...' ) sleep(10) # Invest self.attempt_to_invest() def stop(self): """ Called when the investment loop should end. If the loop is currently attempting to invest cash, this will not be canceled. """ self.loop = False self.logger.info("Stopping investor...") def get_order_summary(self, portfolio): """ Log a summary of the investment portfolio which was ordered """ summary = 'Investment portfolio summary: {0} loan notes ('.format( portfolio['numberOfLoans']) breakdown = [] for grade in ['a', 'aa', 'b', 'c', 'd', 'e', 'f', 'g']: if portfolio[grade] > 0.0: percent = int(round(portfolio[grade])) breakdown.append('{0}:{1}%'.format(grade.upper(), percent)) if len(breakdown) > 0: summary += ', '.join(breakdown) summary += ')' return summary def attempt_to_invest(self): """ Attempt an investment if there is enough available cash and matching investment option Returns true if money was invested """ # Authenticate try: self.authenticate() self.logger.info('Authenticated') except Exception as e: self.logger.error('Could not authenticate: {0}'.format(e.value)) return False # Try to invest self.logger.info('Checking for funds to invest...') try: # Get current cash balance cash = self.lc.get_investable_balance() if cash > 0 and cash >= self.settings['min_cash']: # Invest self.logger.info( " $ $ $ $ $ $ $ $ $ $") # Create break in logs try: # Refresh saved filter filters = self.settings['filters'] if type(filters) is SavedFilter: filters.reload() # Find investment portfolio, starting will all your cash, # down to the minimum you're willing to invest # No more than 10 searches i = 0 portfolio = False decrement = None while portfolio is False and cash >= self.settings[ 'min_cash'] and i < 10: i += 1 # Try to find a portfolio try: self.logger.info( 'Searching for a portfolio for ${0}'.format( cash)) portfolio = self.lc.build_portfolio( cash, max_per_note=self.settings['max_per_note'], min_percent=self.settings['min_percent'], max_percent=self.settings['max_percent'], filters=filters, do_not_clear_staging=True) except LendingClubError as e: pass # Try a lower amount of cash to invest if not portfolio: self.logger.info( 'Could not find any matching portfolios for ${0}' .format(cash)) # Create decrement value that will search up to 5 more times if decrement is None: delta = cash - self.settings['min_cash'] if delta < 25: break elif delta <= 100: decrement = 25 else: decrement = delta / 4 # Just to be safe, shouldn't decrement in $10 increments if decrement < 10: break # We are at our lowest if cash <= self.settings['min_cash']: break # New amount to search for cash -= decrement if cash < self.settings['min_cash']: cash = self.settings['min_cash'] else: cash = util.nearest_25(cash) if portfolio: # Invest assign_to = self.settings['portfolio'] order = self.lc.start_order() order.add_batch(portfolio['loan_fractions']) if self.auto_execute: self.logger.info( 'Auto investing ${0} at {1}%...'.format( cash, portfolio['percentage'])) sleep(5) # last chance to cancel order._Order__already_staged = True # Don't try this at home kids order._Order__i_know_what_im_doing = True # Seriously, don't do it order_id = order.execute(portfolio_name=assign_to) else: self.logger.info( 'Order staged but not completed, please to go LendingClub website to complete the order. (see the "--no-auto-execute" command flag)' ) return False # Success! Show summary and save the order summary = self.get_order_summary(portfolio) self.logger.info(summary) self.logger.info('Done\n') self.save_last_investment(cash, portfolio, order_id, portfolio_name=assign_to) else: self.logger.warning( 'No investment portfolios matched your filters at this time -- Trying again in {2} minutes' .format(self.settings['min_percent'], self.settings['max_percent'], self.settings['frequency'])) except Exception as e: self.logger.exception( 'Failed trying to invest: {0}'.format(str(e))) else: self.logger.info( 'Only ${0} available for investing (of your ${1} balance)'. format(cash, self.lc.get_cash_balance())) return False except Exception as e: self.logger.error(str(e)) return False def save_last_investment(self, cash, portfolio, order_id, portfolio_name=None): """" Save a log of the last investment to the last_investment file """ try: last_invested = { 'timestamp': int(time.time()), 'order_id': order_id, 'portfolio': portfolio_name, 'cash': cash, 'investment': portfolio } # Convert to JSON json_out = json.dumps(last_invested) self.logger.debug( 'Saving last investment file with JSON: {0}'.format(json_out)) # Save file_path = os.path.join(self.app_dir, self.last_investment_file) f = open(file_path, 'w') f.write(json_out) f.close() except Exception as e: self.logger.warning( 'Couldn\'t save the investment summary to file (this warning can be ignored). {0}' .format(str(e))) def get_last_investment(self): """ Return the last investment summary that has been saved to the last_investment file """ try: file_path = os.path.join(self.app_dir, self.last_investment_file) if os.path.exists(file_path): # Read file f = open(file_path, 'r') json_str = f.read() f.close() # Convert to dictionary and return return json.loads(json_str) except Exception as e: self.logger.warning( 'Couldn\'t read the last investment file. {0}'.format(str(e))) return None def investment_loop(self): """ Start the investment loop Check the account every so often (default is every 60 minutes) for funds to invest The frequency is defined by the 'frequency' value in the ~/.lcinvestor/settings.yaml file """ self.loop = True frequency = self.settings.user_settings['frequency'] while self.loop: # Make sure the site is available (network could be reconnecting after sleep) attempts = 0 while not self.lc.is_site_available() and self.loop: attempts += 1 if attempts % 5 == 0: self.logger.warn( 'LendingClub is not responding. Trying again in 10 seconds...' ) sleep(10) # Invest self.attempt_to_invest() pause.minutes(frequency)
grade = note['rate'][0] found = lc.search_my_notes(grade=grade) self.assertTrue(len(found) > 0) for note in found: self.assertEqual(grade, note['rate'][0]) print """ !!!WARNING !!! This is a live test of the module communicating with LendingClub.com with your account!!! Your account must have at least $25 to continue. Tests will attempt to get full API test coverage coming just short of investing money from your account. However, this is not guaranteed if something in the tests are broken. Please continue at your own risk. """ res = raw_input('Continue with the tests? [yes/no]') if res.lower() != 'yes': exit() print '\n\nEnter a valid LendingClub account information...' email = raw_input('Email:') password = getpass.getpass() assert lc.is_site_available(), 'No network connection or cannot access lendingclub.com' assert lc.authenticate(email, password), 'Could not authenticate' assert lc.get_investable_balance(), 'You do not have at least $25 in your account.' if __name__ == '__main__': unittest.main()
class TestSavedFilters(unittest.TestCase): filters = None logger = None lc = None loan_list = None def setUp(self): 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') def tearDown(self): pass def test_get_all_filters(self): filters = SavedFilter.all_filters(self.lc) self.assertEqual(len(filters), 2) self.assertEqual(filters[0].name, 'Filter 1') def test_get_saved_filters(self): saved = SavedFilter(self.lc, 1) self.assertEqual(saved.name, 'Filter 1') self.assertEqual(saved.id, 1) self.assertNotEqual(saved.search_string(), None) def test_validation_1(self): """ test_validation_1 Filter 1 against filter_validation 1 """ saved = SavedFilter(self.lc, 1) # Get loan list response = self.lc.session.get('/filter_validation', query={'id': 1}) json_response = response.json() self.loan_list = json_response['loanFractions'] # Validate, should fail on 'exclude_invested' try: saved.validate(self.loan_list) assert False, 'Test should fail on exclude_existing' except FilterValidationError as e: print(e.criteria) self.assertTrue(matches('exclude loans', e.criteria)) def test_validation_2(self): """ test_validation_2 Filter 2 against filter_validation 2 """ saved = SavedFilter(self.lc, 2) # Get loan list response = self.lc.session.get('/filter_validation', query={'id': 2}) json_response = response.json() self.loan_list = json_response['loanFractions'] # Validate, should fail on 'exclude_invested' try: saved.validate(self.loan_list) assert False, 'Test should fail on loan_purpose' except FilterValidationError as e: print(e.criteria) self.assertTrue(matches('loan purpose', e.criteria)) def test_validation_2_1(self): """ test_validation_2_1 Filter 2 against filter_validation 1 """ saved = SavedFilter(self.lc, 2) # Get loan list response = self.lc.session.get('/filter_validation', query={'id': 1}) json_response = response.json() self.loan_list = json_response['loanFractions'] # Validate, should not fail saved.validate(self.loan_list) def test_validation_2_3(self): """ test_validation_3 Filter 2 against filter_validation 3 """ saved = SavedFilter(self.lc, 2) # Get loan list response = self.lc.session.get('/filter_validation', query={'id': 3}) json_response = response.json() self.loan_list = json_response['loanFractions'] # Validate, should fail on 'exclude_invested' try: saved.validate(self.loan_list) assert False, 'Test should fail on grade' except FilterValidationError as e: print(e.criteria) self.assertTrue(matches('grade', e.criteria))
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)
class TestLendingClub(unittest.TestCase): lc = None logger = None def setUp(self): 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('*****@*****.**', 'supersecret') # Make sure session is enabled and clear self.lc.session.post('/session/enabled') self.lc.session.request('delete', '/session') def tearDown(self): pass def test_cash_balance(self): cash = self.lc.get_cash_balance() self.assertEqual(cash, 216.02) def test_portfolios(self): portfolios = self.lc.get_portfolio_list() self.assertEquals(len(portfolios), 2) self.assertEquals(portfolios[0]['portfolioName'], 'Existing Portfolio') def test_build_portfolio(self): portfolio = self.lc.build_portfolio(200, 25, 15, 16) self.assertNotEqual(portfolio, False) self.assertEqual(portfolio['percentage'], 15.28) self.assertTrue('loan_fractions' in portfolio) self.assertEqual(len(portfolio['loan_fractions']), 15) def test_build_portfolio_session_fail(self): """ test_build_portfolio_session_fail" If the session isn't saved, fractions shouldn't be found, which should make the entire method return False """ # Disable session self.lc.session.post('/session/disabled') portfolio = self.lc.build_portfolio(200, 25, 15, 16) self.assertFalse(portfolio) def test_build_portfolio_no_match(self): """ test_build_portfolio_no_match" Enter a min/max percent that cannot match dummy returned JSON """ portfolio = self.lc.build_portfolio(200, 25, 17.6, 18.5) self.assertFalse(portfolio) def test_search(self): results = self.lc.search() self.assertTrue(results is not False) self.assertTrue('loans' in results) self.assertTrue(len(results['loans']) > 0)
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('*****@*****.**', 'supersecret') 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)
class TestSavedFilters(unittest.TestCase): filters = None logger = None lc = None loan_list = None def setUp(self): 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('*****@*****.**', 'supersecret') def tearDown(self): pass def test_get_all_filters(self): filters = SavedFilter.all_filters(self.lc) self.assertEqual(len(filters), 2) self.assertEqual(filters[0].name, 'Filter 1') def test_get_saved_filters(self): saved = SavedFilter(self.lc, 1) self.assertEqual(saved.name, 'Filter 1') self.assertEqual(saved.id, 1) self.assertNotEqual(saved.search_string(), None) def test_validation_1(self): """ test_validation_1 Filter 1 against filter_validation 1 """ saved = SavedFilter(self.lc, 1) # Get loan list response = self.lc.session.get('/filter_validation', query={'id': 1}) json_response = response.json() self.loan_list = json_response['loanFractions'] # Validate, should fail on 'exclude_invested' try: saved.validate(self.loan_list) assert False, 'Test should fail on exclude_existing' except FilterValidationError as e: print e.criteria self.assertTrue(matches('exclude loans', e.criteria)) def test_validation_2(self): """ test_validation_2 Filter 2 against filter_validation 2 """ saved = SavedFilter(self.lc, 2) # Get loan list response = self.lc.session.get('/filter_validation', query={'id': 2}) json_response = response.json() self.loan_list = json_response['loanFractions'] # Validate, should fail on 'exclude_invested' try: saved.validate(self.loan_list) assert False, 'Test should fail on loan_purpose' except FilterValidationError as e: print e.criteria self.assertTrue(matches('loan purpose', e.criteria)) def test_validation_2_1(self): """ test_validation_2_1 Filter 2 against filter_validation 1 """ saved = SavedFilter(self.lc, 2) # Get loan list response = self.lc.session.get('/filter_validation', query={'id': 1}) json_response = response.json() self.loan_list = json_response['loanFractions'] # Validate, should not fail saved.validate(self.loan_list) def test_validation_2_3(self): """ test_validation_3 Filter 2 against filter_validation 3 """ saved = SavedFilter(self.lc, 2) # Get loan list response = self.lc.session.get('/filter_validation', query={'id': 3}) json_response = response.json() self.loan_list = json_response['loanFractions'] # Validate, should fail on 'exclude_invested' try: saved.validate(self.loan_list) assert False, 'Test should fail on grade' except FilterValidationError as e: print e.criteria self.assertTrue(matches('grade', e.criteria))
class AutoInvestor: """ Regularly check a LendingClub account for available cash and reinvest it automatically. """ lc = None authed = False verbose = False auto_execute = True settings = None loop = False app_dir = None # The file that the summary from the last investment is saved to last_investment_file = 'last_investment.json' def __init__(self, verbose=False, auto_execute=True): """ Create an AutoInvestor instance - Set verbose to True if you want to see debugging logs """ self.verbose = verbose self.auto_execute = auto_execute self.logger = util.create_logger(verbose) self.app_dir = util.get_app_directory() self.lc = LendingClub() # Set logger on lc if self.verbose: self.lc.set_logger(self.logger) # Create settings object self.settings = Settings(investor=self, settings_dir=self.app_dir, logger=self.logger, verbose=self.verbose) self.settings.investor = self # create a link back to this instance def version(self): """ Return the version number of the Lending Club Investor tool """ return util.get_version(); def welcome_screen(self): print "\n///--------------------------- $$$ ---------------------------\\\\\\" print '| Welcome to the unofficial Lending Club investment tool |' print " ---------------------------------------------------------------- \n" def get_auth(self): print 'To start, we need to log you into Lending Club (your password will never be saved)\n' while True: self.settings.get_auth_settings() print '\nAuthenticating...' try: return self.authenticate() except Exception as e: print '\nLogin failed: {0}'.format(str(e.value)) print "Please try again\n" def setup(self): """ Setup the investor to run """ if self.verbose: print 'VERBOSE OUTPUT IS ON\n' if not self.authed: self.get_auth() self.settings.select_profile() print 'You have ${0} in your account, free to invest\n'.format(self.lc.get_cash_balance()) # Investment settings print 'Now let\'s define what you want to do' # Use the settings from last time if self.settings.profile_loaded is not False: summary = self.settings.show_summary() if summary is False: # there was an error with saved settings print '\nThere was an error with your saved settings. Please go through the prompts again.\n' self.settings.get_investment_settings() if util.prompt_yn('Would you like to use these settings?', 'y'): self.settings.save() # to save the email that was just entered else: self.settings.get_investment_settings() else: self.settings.get_investment_settings() # All ready to start running print '\nThat\'s all we need. Now, as long as this is running, your account will be checked every {0} minutes and invested if enough funds are available.\n'.format(self.settings['frequency']) def authenticate(self): """ Attempt to authenticate the user with the email/pass from the Settings object. This is just a wrapper for LendingClub.authenticate() Returns True or raises an exceptions """ self.authed = self.lc.authenticate(self.settings.auth['email'], self.settings.auth['pass']) return self.authed def run(self): """ Alias for investment_loop. This is used by python-runner """ self.investment_loop() def run_once(self): """ Try to invest, based on your settings, and then end the program. """ self.loop = False # Make sure the site is available attempts = 0 while not self.lc.is_site_available(): attempts += 1 if attempts % 5 == 0: self.logger.warn('LendingClub is not responding. Trying again in 10 seconds...') sleep(10) # Invest self.attempt_to_invest() def stop(self): """ Called when the investment loop should end. If the loop is currently attempting to invest cash, this will not be canceled. """ self.loop = False self.logger.info("Stopping investor...") def get_order_summary(self, portfolio): """ Log a summary of the investment portfolio which was ordered """ summary = 'Investment portfolio summary: {0} loan notes ('.format(portfolio['numberOfLoans']) breakdown = [] for grade in ['a', 'aa', 'b', 'c', 'd', 'e', 'f', 'g']: if portfolio[grade] > 0.0: percent = int(round(portfolio[grade])) breakdown.append('{0}:{1}%'.format(grade.upper(), percent)) if len(breakdown) > 0: summary += ', '.join(breakdown) summary += ')' return summary def attempt_to_invest(self): """ Attempt an investment if there is enough available cash and matching investment option Returns true if money was invested """ # Authenticate try: self.authenticate() self.logger.info('Authenticated') except Exception as e: self.logger.error('Could not authenticate: {0}'.format(e.value)) return False # Try to invest self.logger.info('Checking for funds to invest...') try: # Get current cash balance cash = self.lc.get_investable_balance() if cash > 0 and cash >= self.settings['min_cash']: # Invest self.logger.info(" $ $ $ $ $ $ $ $ $ $") # Create break in logs try: # Refresh saved filter filters = self.settings['filters'] if type(filters) is SavedFilter: filters.reload() # Find investment portfolio, starting will all your cash, # down to the minimum you're willing to invest # No more than 10 searches i = 0 portfolio = False decrement = None while portfolio is False and cash >= self.settings['min_cash'] and i < 10: i += 1 # Try to find a portfolio try: self.logger.info('Searching for a portfolio for ${0}'.format(cash)) portfolio = self.lc.build_portfolio(cash, max_per_note=self.settings['max_per_note'], min_percent=self.settings['min_percent'], max_percent=self.settings['max_percent'], filters=filters, do_not_clear_staging=True) except LendingClubError as e: pass # Try a lower amount of cash to invest if not portfolio: self.logger.info('Could not find any matching portfolios for ${0}'.format(cash)) # Create decrement value that will search up to 5 more times if decrement is None: delta = cash - self.settings['min_cash'] if delta < 25: break elif delta <= 100: decrement = 25 else: decrement = delta / 4 # Just to be safe, shouldn't decrement in $10 increments if decrement < 10: break # We are at our lowest if cash <= self.settings['min_cash']: break # New amount to search for cash -= decrement if cash < self.settings['min_cash']: cash = self.settings['min_cash'] else: cash = util.nearest_25(cash) if portfolio: # Invest assign_to = self.settings['portfolio'] order = self.lc.start_order() order.add_batch(portfolio['loan_fractions']) if self.auto_execute: self.logger.info('Auto investing ${0} at {1}%...'.format(cash, portfolio['percentage'])) sleep(5) # last chance to cancel order._Order__already_staged = True # Don't try this at home kids order._Order__i_know_what_im_doing = True # Seriously, don't do it order_id = order.execute(portfolio_name=assign_to) else: self.logger.info('Order staged but not completed, please to go LendingClub website to complete the order. (see the "--no-auto-execute" command flag)') return False # Success! Show summary and save the order summary = self.get_order_summary(portfolio) self.logger.info(summary) self.logger.info('Done\n') self.save_last_investment(cash, portfolio, order_id, portfolio_name=assign_to) else: self.logger.warning('No investment portfolios matched your filters at this time -- Trying again in {2} minutes'.format(self.settings['min_percent'], self.settings['max_percent'], self.settings['frequency'])) except Exception as e: self.logger.error('Failed trying to invest: {0}'.format(str(e))) else: self.logger.info('Only ${0} available for investing (of your ${1} balance)'.format(cash, self.lc.get_cash_balance())) return False except Exception as e: self.logger.error(str(e)) return False def save_last_investment(self, cash, portfolio, order_id, portfolio_name=None): """" Save a log of the last investment to the last_investment file """ try: last_invested = { 'timestamp': int(time.time()), 'order_id': order_id, 'portfolio': portfolio_name, 'cash': cash, 'investment': portfolio } # Convert to JSON json_out = json.dumps(last_invested) self.logger.debug('Saving last investment file with JSON: {0}'.format(json_out)) # Save file_path = os.path.join(self.app_dir, self.last_investment_file) f = open(file_path, 'w') f.write(json_out) f.close() except Exception as e: self.logger.warning('Couldn\'t save the investment summary to file (this warning can be ignored). {0}'.format(str(e))) def get_last_investment(self): """ Return the last investment summary that has been saved to the last_investment file """ try: file_path = os.path.join(self.app_dir, self.last_investment_file) if os.path.exists(file_path): # Read file f = open(file_path, 'r') json_str = f.read() f.close() # Convert to dictionary and return return json.loads(json_str) except Exception as e: self.logger.warning('Couldn\'t read the last investment file. {0}'.format(str(e))) return None def investment_loop(self): """ Start the investment loop Check the account every so often (default is every 60 minutes) for funds to invest The frequency is defined by the 'frequency' value in the ~/.lcinvestor/settings.yaml file """ self.loop = True frequency = self.settings.user_settings['frequency'] while self.loop: # Make sure the site is available (network could be reconnecting after sleep) attempts = 0 while not self.lc.is_site_available() and self.loop: attempts += 1 if attempts % 5 == 0: self.logger.warn('LendingClub is not responding. Trying again in 10 seconds...') sleep(10) # Invest self.attempt_to_invest() pause.minutes(frequency)