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)
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)
Example #3
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()
Example #4
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()