Esempio n. 1
0
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))
Esempio n. 2
0
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
Esempio n. 3
0
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
Esempio n. 4
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()
        )
Esempio n. 5
0
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)
        )
Esempio n. 6
0
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"
Esempio n. 7
0
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)
Esempio n. 8
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())
Esempio n. 9
0
        # 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)
Esempio n. 11
0
        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()
Esempio n. 12
0
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))
Esempio n. 13
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('*****@*****.**', '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)
Esempio n. 14
0
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)
Esempio n. 15
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)
Esempio n. 16
0
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)