Ejemplo n.º 1
0
    def __init__(self, email=None, password=None, logger=None):
        self.session = Session(email, password)
        self.order = Order(self.session)

        if logger is not None:
            self.set_logger(logger)
Ejemplo n.º 2
0
    def __init__(self, email=None, password=None, logger=None):
        self.session = Session(email, password)
        self.order = Order(self.session)

        if logger is not None:
            self.set_logger(logger)
Ejemplo n.º 3
0
class LendingClub(object):
    """
    The main entry point for interacting with Lending Club.

    Parameters
    ----------
    email : string
        The email of a user on Lending Club
    password : string
        The user's password, for authentication.
    logger : `Logger <http://docs.python.org/2/library/logging.html>`_
        A python logger used to get debugging output from this module.

    Examples
    --------

    Get the cash balance in your lending club account:

        >>> from lendingclub import LendingClub
        >>> lc = LendingClub()
        >>> lc.authenticate()         # Authenticate with your lending club credentials
        Email:[email protected]
        Password:
        True
        >>> lc.get_cash_balance()     # See the cash you have available for investing
        463.80000000000001

    You can also enter your email and password when you instantiate the LendingClub class, in one line:

        >>> from lendingclub import LendingClub
        >>> lc = LendingClub(email='*****@*****.**', password='******')
        >>> lc.authenticate()
        True
    """

    _logger = None
    session = None
    order = None

    def __init__(self, email=None, password=None, logger=None):
        self.session = Session(email, password)
        self.order = Order(self.session)

        if logger is not None:
            self.set_logger(logger)

    def _log(self, message):
        """
        Log a debugging message
        """
        if self._logger:
            self._logger.debug(message)

    def set_logger(self, logger):
        """
        Set a logger to send debug messages to

        Parameters
        ----------
        logger : `Logger <http://docs.python.org/2/library/logging.html>`_
            A python logger used to get debugging output from this module.
        """
        self._logger = logger
        self.session.set_logger(self._logger)

    @staticmethod
    def version():
        """
        Return the version number of the Lending Club Investor tool

        Returns
        -------
        string
            The version number string
        """
        this_path = os.path.dirname(os.path.realpath(__file__))
        with open(os.path.join(this_path, 'VERSION')) as version_file:
            return version_file.read().strip()

    def authenticate(self, email=None, password=None):
        """
        Attempt to authenticate the user.

        Parameters
        ----------
        email : string
            The email of a user on Lending Club
        password : string
            The user's password, for authentication.

        Returns
        -------
        boolean
            True if the user authenticated or raises an exception if not

        Raises
        ------
        session.AuthenticationError
            If authentication failed
        session.NetworkError
            If a network error occurred
        """
        if self.session.authenticate(email, password):
            return True

    def is_site_available(self):
        """
        Returns true if we can access LendingClub.com
        This is also a simple test to see if there's an internet connection

        Returns
        -------
        boolean
        """
        return self.session.is_site_available()

    def get_cash_balance(self):
        """
        Returns the account cash balance available for investing

        Returns
        -------
        Decimal
            The cash balance in your account.
        """
        cash = Decimal(0)
        response = None
        try:
            response = self.session.get('/browse/cashBalanceAj.action')
            json_response = response.json()

            if self.session.json_success(json_response):
                self._log('Cash available: {0}'.format(
                    json_response['cashBalance']))
                cash_value = json_response['cashBalance']

                # Convert currency to float value
                # Match values like $1,000.12 or 1,0000$
                cash_match = re.search('^[^0-9]?([0-9\.,]+)[^0-9]?',
                                       cash_value)
                if cash_match:
                    cash_str = cash_match.group(1)
                    cash_str = cash_str.replace(',', '')
                    cash = Decimal(cash_str)
            else:
                self._log('Could not get cash balance: {0}'.format(
                    response.text))

        except Exception as e:
            if response:
                self._log(
                    'Could not get the cash balance on the account: Error: {0}\nJSON: {1}'
                    .format(e, response.text))
            else:
                self._log(
                    'Could not get the cash balance on the account: Error: {0}\n No response from server.'
                    .format(e, response.text))
            raise e

        return cash

    def get_investable_balance(self):
        """
        Returns the amount of money from your account that you can invest.
        Loans are multiples of $25, so this is your total cash balance, adjusted to be a multiple of 25.

        Returns
        -------
        Decimal
            The amount of cash you can invest
        """
        return (self.get_cash_balance() // 25) * 25

    def get_portfolio_list(self, names_only=False):
        """
        Get your list of named portfolios from the lendingclub.com

        Parameters
        ----------
        names_only : boolean, optional
            If set to True, the function will return a list of portfolio names, instead of portfolio objects

        Returns
        -------
        list
            A list of portfolios (or names, if `names_only` is True)
        """
        folios = []
        response = self.session.get(
            '/data/portfolioManagement?method=getLCPortfolios')
        json_response = response.json()

        # Get portfolios and create a list of names
        if self.session.json_success(json_response):
            folios = json_response['results']

            if names_only:
                folios = [folio['portfolioName'] for folio in folios]

        return folios

    def get_saved_filters(self):
        """
        Get a list of all the saved search filters you've created on lendingclub.com

        Returns
        -------
        list
            List of :class:`lendingclub.filters.SavedFilter` objects
        """
        return SavedFilter.all_filters(self)

    def get_saved_filter(self, filter_id):
        """
        Load a single saved search filter from the site by ID

        Parameters
        ----------
        filter_id : int
            The ID of the saved filter

        Returns
        -------
        SavedFilter
            A :class:`lendingclub.filters.SavedFilter` object or False
        """
        return SavedFilter(self, filter_id)

    def assign_to_portfolio(self, portfolio_name, loan_id, order_id):
        """
        Assign a note to a named portfolio. `loan_id` and `order_id` can be either
        integer values or lists. If choosing lists, they both **MUST** be the same length
        and line up. For example, `order_id[5]` must be the order ID for `loan_id[5]`

        Parameters
        ----------
        portfolio_name : string
            The name of the portfolio to assign a the loan note to -- new or existing
        loan_id : int or list
            The loan ID, or list of loan IDs, to assign to the portfolio
        order_id : int or list
            The order ID, or list of order IDs, that this loan note was invested with.
            You can find this in the dict returned from `get_note()`

        Returns
        -------
        boolean
            True on success
        """
        assert isinstance(loan_id, type(
            order_id)), "Both loan_id and order_id need to be the same type"
        assert isinstance(
            loan_id,
            (int, list)), "loan_id and order_id can only be int or list types"
        assert isinstance(loan_id, int) or (
            isinstance(loan_id, list) and len(loan_id) == len(order_id)
        ), "If order_id and loan_id are lists, they both need to be the same length"

        # Data
        post = {'loan_id': loan_id, 'record_id': loan_id, 'order_id': order_id}
        query = {
            'method': 'createLCPortfolio',
            'lcportfolio_name': portfolio_name
        }

        # Is it an existing portfolio
        existing = self.get_portfolio_list()
        for folio in existing:
            if folio['portfolioName'] == portfolio_name:
                query['method'] = 'addToLCPortfolio'
                break

        # Send
        response = self.session.post('/data/portfolioManagement',
                                     query=query,
                                     data=post)
        json_response = response.json()

        # Failed
        if not self.session.json_success(json_response):
            raise LendingClubError(
                'Could not assign order to portfolio "{0}"'.format(
                    portfolio_name), response)

        # Success
        else:

            # Assigned to another portfolio, for some reason, raise warning
            if 'portfolioName' in json_response and json_response[
                    'portfolioName'] != portfolio_name:
                raise LendingClubError(
                    'Added order to portfolio "{0}" - NOT - "{1}", and I don\'t know why'
                    .format(json_response['portfolioName'], portfolio_name))

            # Assigned to the correct portfolio
            else:
                self._log(
                    'Added order to portfolio "{0}"'.format(portfolio_name))

            return True

    def search(self, filters=None, start_index=0, limit=100):
        """
        Search for a list of notes that can be invested in.
        (similar to searching for notes in the Browse section on the site)

        Parameters
        ----------
        filters : lendingclub.filters.*, optional
            The filter to use to search for notes. If no filter is passed, a wildcard search
            will be performed.
        start_index : int, optional
            The result index to start on. By default only 100 records will be returned at a time, so use this
            to start at a later index in the results. For example, to get results 200 - 300, set `start_index` to 200.
            (default is 0)
        limit : int, optional
            The number of results to return per request. (default is 100)

        Returns
        -------
        dict
            A dictionary object with the list of matching loans under the `loans` key.
        """
        assert filters is None or isinstance(
            filters, Filter), 'filter is not a lendingclub.filters.Filter'

        results = {}

        # Set filters
        if filters:
            filter_string = filters.search_string()
        else:
            filter_string = 'default'
        payload = {
            'method': 'search',
            'filter': filter_string,
            'startindex': start_index,
            'pagesize': limit
        }

        # Make request
        response = self.session.post('/browse/browseNotesAj.action',
                                     data=payload)
        json_response = response.json()

        if self.session.json_success(json_response):
            results = json_response['searchresult']

            # Normalize results by converting loanGUID -> loan_id
            for loan in results['loans']:
                loan['loan_id'] = int(loan['loanGUID'])

            # Validate that fractions do indeed match the filters
            if filters is not None:
                filters.validate(results['loans'])

        return results

    def build_portfolio(self,
                        cash,
                        max_per_note=25,
                        min_percent=0,
                        max_percent=20,
                        filters=None,
                        automatically_invest=False,
                        do_not_clear_staging=False):
        """
        Returns a list of loan notes that are diversified by your min/max percent request and filters.
        One way to invest in these loan notes, is to start an order and use add_batch to add all the
        loan fragments to them. (see examples)

        Parameters
        ----------
        cash : int
            The total amount you want to invest across a portfolio of loans (at least $25).
        max_per_note : int, optional
            The maximum dollar amount you want to invest per note. Must be a multiple of 25
        min_percent : int, optional
            THIS IS NOT PER NOTE, but the minimum average percent of return for the entire portfolio.
        max_percent : int, optional
            THIS IS NOT PER NOTE, but the maxmimum average percent of return for the entire portfolio.
        filters : lendingclub.filters.*, optional
            The filters to use to search for portfolios
        automatically_invest : boolean, optional
            If you want the tool to create an order and automatically invest in the portfolio that matches your filter.
            (default False)
        do_not_clear_staging : boolean, optional
            Similar to automatically_invest, don't do this unless you know what you're doing.
            Setting this to True stops the method from clearing the loan staging area before returning

        Returns
        -------
        dict
            A dict representing a new portfolio or False if nothing was found.
            If `automatically_invest` was set to `True`, the dict will contain an `order_id` key with
            the ID of the completed investment order.

        Notes
        -----
        **The min/max_percent parameters**

        When searching for portfolios, these parameters will match a portfolio of loan notes which have
        an **AVERAGE** percent return between these values. If there are multiple portfolio matches, the
        one closes to the max percent will be chosen.

        Examples
        --------
        Here we want to invest $400 in a portfolio with only B, C, D and E grade notes with an average overall return between 17% - 19%. This similar to finding a portfolio in the 'Invest' section on lendingclub.com::

            >>> from lendingclub import LendingClub
            >>> from lendingclub.filters import Filter
            >>> lc = LendingClub()
            >>> lc.authenticate()
            Email:[email protected]
            Password:
            True
            >>> filters = Filter()                  # Set the search filters (only B, C, D and E grade notes)
            >>> filters['grades']['C'] = True
            >>> filters['grades']['D'] = True
            >>> filters['grades']['E'] = True
            >>> lc.get_cash_balance()               # See the cash you have available for investing
            463.80000000000001

            >>> portfolio = lc.build_portfolio(400, # Invest $400 in a portfolio...
                    min_percent=17.0,               # Return percent average between 17 - 19%
                    max_percent=19.0,
                    max_per_note=50,                # As much as $50 per note
                    filters=filters)                # Search using your filters

            >>> len(portfolio['loan_fractions'])    # See how many loans are in this portfolio
            16
            >>> loans_notes = portfolio['loan_fractions']
            >>> order = lc.start_order()            # Start a new order
            >>> order.add_batch(loans_notes)        # Add the loan notes to the order
            >>> order.execute()                     # Execute the order
            1861880

        Here we do a similar search, but automatically invest the found portfolio. **NOTE** This does not allow
        you to review the portfolio before you invest in it.

            >>> from lendingclub import LendingClub
            >>> from lendingclub.filters import Filter
            >>> lc = LendingClub()
            >>> lc.authenticate()
            Email:[email protected]
            Password:
            True
                                                    # Filter shorthand
            >>> filters = Filter({'grades': {'B': True, 'C': True, 'D': True, 'E': True}})
            >>> lc.get_cash_balance()               # See the cash you have available for investing
            463.80000000000001

            >>> portfolio = lc.build_portfolio(400,
                    min_percent=17.0,
                    max_percent=19.0,
                    max_per_note=50,
                    filters=filters,
                    automatically_invest=True)      # Same settings, except invest immediately

            >>> portfolio['order_id']               # See order ID
            1861880
        """
        assert filters is None or isinstance(
            filters, Filter), 'filter is not a lendingclub.filters.Filter'
        assert max_per_note >= 25, 'max_per_note must be greater than or equal to 25'

        # Set filters
        if filters:
            filter_str = filters.search_string()
        else:
            filter_str = 'default'

        # Start a new order
        self.session.clear_session_order()

        # Make request
        payload = {
            'amount': cash,
            'max_per_note': max_per_note,
            'filter': filter_str
        }
        self._log('POST VALUES -- amount: {0}, max_per_note: {1}, filter: ...'.
                  format(cash, max_per_note))
        response = self.session.post('/portfolio/lendingMatchOptionsV2.action',
                                     data=payload)
        json_response = response.json()

        # Options were found
        if self.session.json_success(
                json_response) and 'lmOptions' in json_response:
            options = json_response['lmOptions']

            # Nothing found
            if not isinstance(options,
                              list) or json_response['numberTicks'] == 0:
                self._log(
                    'No lending portfolios were returned with your search')
                return False

            # Choose an investment option based on the user's min/max values
            i = 0
            match_index = -1
            match_option = None
            for option in options:

                # A perfect match
                # Take it and move on
                if option['percentage'] == max_percent:
                    match_option = option
                    match_index = i
                    break

                # Over the max
                elif option['percentage'] > max_percent:
                    break

                # Higher than the minimum percent and the current matched option
                # Take it and keep looking
                elif option['percentage'] >= min_percent:
                    if match_option is None or match_option[
                            'percentage'] < option['percentage']:
                        match_option = option
                        match_index = i

                i += 1

            # Nothing matched
            if match_option is None:
                self._log('No portfolios matched your percentage requirements')
                return False

            # Mark this portfolio for investing (in order to get a list of all notes)
            payload = {
                'order_amount': cash,
                'lending_match_point': match_index,
                'lending_match_version': 'v2'
            }
            self.session.get('/portfolio/recommendPortfolio.action',
                             query=payload)

            # Get all loan fractions
            payload = {'method': 'getPortfolio'}
            response = self.session.get('/data/portfolio', query=payload)
            json_response = response.json()

            # Extract fractions from response
            fractions = []
            if 'loanFractions' in json_response:
                fractions = json_response['loanFractions']

                # Normalize by converting loanFractionAmount to invest_amount
                for frac in fractions:
                    frac['invest_amount'] = frac['loanFractionAmount']

                    # Raise error if amount is greater than max_per_note
                    if frac['invest_amount'] > max_per_note:
                        raise LendingClubError(
                            'ERROR: LendingClub tried to invest ${0} in a loan note. Your max per note is set to ${1}. Portfolio investment canceled.'
                            .format(frac['invest_amount'], max_per_note))

            if not fractions:
                self._log('The selected portfolio didn\'t have any loans')
                return False
            match_option['loan_fractions'] = fractions

            # Validate that fractions do indeed match the filters
            if filters is not None:
                filters.validate(fractions)

            # Not investing -- reset portfolio search session and return
            if not automatically_invest:
                if not do_not_clear_staging:
                    self.session.clear_session_order()

            # Invest in this porfolio
            elif automatically_invest:  # just to be sure
                order = self.start_order()

                # This should probably only be ever done here...ever.
                order._already_staged = True
                order._i_know_what_im_doing = True

                order.add_batch(match_option['loan_fractions'])
                order_id = order.execute()
                match_option['order_id'] = order_id

            return match_option
        else:
            raise LendingClubError(
                'Could not find any portfolio options that match your filters',
                response)

    def my_notes(self,
                 start_index=0,
                 limit=100,
                 get_all=False,
                 sort_by='loanId',
                 sort_dir='asc'):
        """
        Return all the loan notes you've already invested in. By default it'll return 100 results at a time.

        Parameters
        ----------
        start_index : int, optional
            The result index to start on. By default only 100 records will be returned at a time, so use this
            to start at a later index in the results. For example, to get results 200 - 300, set `start_index` to 200.
            (default is 0)
        limit : int, optional
            The number of results to return per request. (default is 100)
        get_all : boolean, optional
            Return all results in one request, instead of 100 per request.
        sort_by : string, optional
            What key to sort on
        sort_dir : {'asc', 'desc'}, optional
            Which direction to sort

        Returns
        -------
        dict
            A dictionary with a list of matching notes on the `loans` key
        """

        index = start_index
        notes = {'loans': [], 'total': 0, 'result': 'success'}
        while True:
            payload = {
                'sortBy': sort_by,
                'dir': sort_dir,
                'startindex': index,
                'pagesize': limit,
                'namespace': '/account'
            }
            response = self.session.post('/account/loansAj.action',
                                         data=payload)
            json_response = response.json()

            # Notes returned
            if self.session.json_success(json_response):
                notes['loans'] += json_response['searchresult']['loans']
                notes['total'] = json_response['searchresult']['totalRecords']

            # Error
            else:
                notes['result'] = json_response['result']
                break

            # Load more
            if get_all and len(notes['loans']) < notes['total']:
                index += limit

            # End
            else:
                break

        return notes

    def get_note(self, note_id):
        """
        Get a loan note that you've invested in by ID

        Parameters
        ----------
        note_id : int
            The note ID

        Returns
        -------
        dict
            A dictionary representing the matching note or False

        Examples
        --------
            >>> from lendingclub import LendingClub
            >>> lc = LendingClub(email='*****@*****.**', password='******')
            >>> lc.authenticate()
            True
            >>> notes = lc.my_notes()                  # Get the first 100 loan notes
            >>> len(notes['loans'])
            100
            >>> notes['total']                          # See the total number of loan notes you have
            630
            >>> notes = lc.my_notes(start_index=100)   # Get the next 100 loan notes
            >>> len(notes['loans'])
            100
            >>> notes = lc.my_notes(get_all=True)       # Get all notes in one request (may be slow)
            >>> len(notes['loans'])
            630
        """

        index = 0
        while True:
            notes = self.my_notes(start_index=index, sort_by='noteId')

            if notes['result'] != 'success':
                break

            # If the first note has a higher ID, we've passed it
            if notes['loans'][0]['noteId'] > note_id:
                break

            # If the last note has a higher ID, it could be in this record set
            if notes['loans'][-1]['noteId'] >= note_id:
                for note in notes['loans']:
                    if note['noteId'] == note_id:
                        return note

            index += 100

        return False

    def search_my_notes(self,
                        loan_id=None,
                        order_id=None,
                        grade=None,
                        portfolio_name=None,
                        status=None,
                        term=None):
        """
        Search for notes you are invested in. Use the parameters to define how to search.
        Passing no parameters is the same as calling `my_notes(get_all=True)`

        Parameters
        ----------
        loan_id : int, optional
            Search for notes for a specific loan. Since a loan is broken up into a pool of notes, it's possible
            to invest multiple notes in a single loan
        order_id : int, optional
            Search for notes from a particular investment order.
        grade : {A, B, C, D, E, F, G}, optional
            Match by a particular loan grade
        portfolio_name : string, optional
            Search for notes in a portfolio with this name (case sensitive)
        status : string, {issued, in-review, in-funding, current, charged-off, late, in-grace-period, fully-paid}, optional
            The funding status string.
        term : {60, 36}, optional
            Term length, either 60 or 36 (for 5 year and 3 year, respectively)

        Returns
        -------
        dict
            A dictionary with a list of matching notes on the `loans` key
        """
        assert grade is None or isinstance(grade,
                                           str), 'grade must be a string'
        assert portfolio_name is None or isinstance(
            portfolio_name, str), 'portfolio_name must be a string'

        index = 0
        found = []
        sort_by = 'orderId' if order_id is not None else 'loanId'
        group_id = order_id if order_id is not None else loan_id  # first match by order, then by loan

        # Normalize grade
        if grade is not None:
            grade = grade[0].upper()

        # Normalize status
        if status is not None:
            status = re.sub('[^a-zA-Z\-]', ' ',
                            status.lower())  # remove all non alpha characters
            status = re.sub('days', ' ', status)  # remove days
            status = re.sub('\s+', '-',
                            status.strip())  # replace spaces with dash
            status = re.sub('(^-+)|(-+$)', '', status)

        while True:
            notes = self.my_notes(start_index=index, sort_by=sort_by)

            if notes['result'] != 'success':
                break

            # If the first note has a higher ID, we've passed it
            if group_id is not None and notes['loans'][0][sort_by] > group_id:
                break

            # If the last note has a higher ID, it could be in this record set
            if group_id is None or notes['loans'][-1][sort_by] >= group_id:
                for note in notes['loans']:

                    # Order ID, no match
                    if order_id is not None and note['orderId'] != order_id:
                        continue

                    # Loan ID, no match
                    if loan_id is not None and note['loanId'] != loan_id:
                        continue

                    # Grade, no match
                    if grade is not None and note['rate'][0] != grade:
                        continue

                    # Portfolio, no match
                    if portfolio_name is not None and note['portfolioName'][
                            0] != portfolio_name:
                        continue

                    # Term, no match
                    if term is not None and note['loanLength'] != term:
                        continue

                    # Status
                    if status is not None:
                        # Normalize status message
                        nstatus = re.sub('[^a-zA-Z\-]', ' ',
                                         note['status'].lower(
                                         ))  # remove all non alpha characters
                        nstatus = re.sub('days', ' ', nstatus)  # remove days
                        nstatus = re.sub(
                            '\s+', '-',
                            nstatus.strip())  # replace spaces with dash
                        nstatus = re.sub('(^-+)|(-+$)', '', nstatus)

                        # No match
                        if nstatus != status:
                            continue

                    # Must be a match
                    found.append(note)

            index += 100

        return found

    def start_order(self):
        """
        Start a new investment order for loans

        Returns
        -------
        lendingclub.Order
            The :class:`lendingclub.Order` object you can use for investing in loan notes.
        """
        order = Order(lc=self)
        return order
Ejemplo n.º 4
0
class LendingClub:
    """
    The main entry point for interacting with Lending Club.

    Parameters
    ----------
    email : string
        The email of a user on Lending Club
    password : string
        The user's password, for authentication.
    logger : `Logger <http://docs.python.org/2/library/logging.html>`_
        A python logger used to get debugging output from this module.

    Examples
    --------

    Get the cash balance in your lending club account:

        >>> from lendingclub import LendingClub
        >>> lc = LendingClub()
        >>> lc.authenticate()         # Authenticate with your lending club credentials
        Email:[email protected]
        Password:
        True
        >>> lc.get_cash_balance()     # See the cash you have available for investing
        463.80000000000001

    You can also enter your email and password when you instantiate the LendingClub class, in one line:

        >>> from lendingclub import LendingClub
        >>> lc = LendingClub(email='*****@*****.**', password='******')
        >>> lc.authenticate()
        True
    """

    __logger = None
    session = None
    order = None

    def __init__(self, email=None, password=None, logger=None):
        self.session = Session(email, password)
        self.order = Order(self.session)

        if logger is not None:
            self.set_logger(logger)

    def __log(self, message):
        """
        Log a debugging message
        """
        if self.__logger:
            self.__logger.debug(message)

    def set_logger(self, logger):
        """
        Set a logger to send debug messages to

        Parameters
        ----------
        logger : `Logger <http://docs.python.org/2/library/logging.html>`_
            A python logger used to get debugging output from this module.
        """
        self.__logger = logger
        self.session.set_logger(self.__logger)

    def version(self):
        """
        Return the version number of the Lending Club Investor tool

        Returns
        -------
        string
            The version number string
        """
        this_path = os.path.dirname(os.path.realpath(__file__))
        version_file = os.path.join(this_path, 'VERSION')
        return open(version_file).read().strip()

    def authenticate(self, email=None, password=None):
        """
        Attempt to authenticate the user.

        Parameters
        ----------
        email : string
            The email of a user on Lending Club
        password : string
            The user's password, for authentication.

        Returns
        -------
        boolean
            True if the user authenticated or raises an exception if not

        Raises
        ------
        session.AuthenticationError
            If authentication failed
        session.NetworkError
            If a network error occurred
        """
        if self.session.authenticate(email, password):
            return True

    def is_site_available(self):
        """
        Returns true if we can access LendingClub.com
        This is also a simple test to see if there's an internet connection

        Returns
        -------
        boolean
        """
        return self.session.is_site_available()

    def get_cash_balance(self):
        """
        Returns the account cash balance available for investing

        Returns
        -------
        float
            The cash balance in your account.
        """
        cash = False
        try:
            response = self.session.get('/browse/cashBalanceAj.action')
            json_response = response.json()

            if self.session.json_success(json_response):
                self.__log('Cash available: {0}'.format(json_response['cashBalance']))
                cash_value = json_response['cashBalance']

                # Convert currency to float value
                # Match values like $1,000.12 or 1,0000$
                cash_match = re.search('^[^0-9]?([0-9\.,]+)[^0-9]?', cash_value)
                if cash_match:
                    cash_str = cash_match.group(1)
                    cash_str = cash_str.replace(',', '')
                    cash = float(cash_str)
            else:
                self.__log('Could not get cash balance: {0}'.format(response.text))

        except Exception as e:
            self.__log('Could not get the cash balance on the account: Error: {0}\nJSON: {1}'.format(str(e), response.text))
            raise e

        return cash

    def get_investable_balance(self):
        """
        Returns the amount of money from your account that you can invest.
        Loans are multiples of $25, so this is your total cash balance, adjusted to be a multiple of 25.

        Returns
        -------
        int
            The amount of cash you can invest
        """
        cash = int(self.get_cash_balance())
        while cash % 25 != 0:
            cash -= 1
        return cash

    def get_portfolio_list(self, names_only=False):
        """
        Get your list of named portfolios from the lendingclub.com

        Parameters
        ----------
        names_only : boolean, optional
            If set to True, the function will return a list of portfolio names, instead of portfolio objects

        Returns
        -------
        list
            A list of portfolios (or names, if `names_only` is True)
        """
        folios = []
        response = self.session.get('/data/portfolioManagement?method=getLCPortfolios')
        json_response = response.json()

        # Get portfolios and create a list of names
        if self.session.json_success(json_response):
            folios = json_response['results']

            if names_only is True:
                for i, folio in enumerate(folios):
                    folios[i] = folio['portfolioName']

        return folios

    def get_saved_filters(self):
        """
        Get a list of all the saved search filters you've created on lendingclub.com

        Returns
        -------
        list
            List of :class:`lendingclub.filters.SavedFilter` objects
        """
        return SavedFilter.all_filters(self)

    def get_saved_filter(self, filter_id):
        """
        Load a single saved search filter from the site by ID

        Parameters
        ----------
        filter_id : int
            The ID of the saved filter

        Returns
        -------
        SavedFilter
            A :class:`lendingclub.filters.SavedFilter` object or False
        """
        return SavedFilter(self, filter_id)

    def assign_to_portfolio(self, portfolio_name, loan_id, order_id):
        """
        Assign a note to a named portfolio. `loan_id` and `order_id` can be either
        integer values or lists. If choosing lists, they both **MUST** be the same length
        and line up. For example, `order_id[5]` must be the order ID for `loan_id[5]`

        Parameters
        ----------
        portfolio_name : string
            The name of the portfolio to assign a the loan note to -- new or existing
        loan_id : int or list
            The loan ID, or list of loan IDs, to assign to the portfolio
        order_id : int or list
            The order ID, or list of order IDs, that this loan note was invested with.
            You can find this in the dict returned from `get_note()`

        Returns
        -------
        boolean
            True on success
        """
        response = None

        assert type(loan_id) == type(order_id), "Both loan_id and order_id need to be the same type"
        assert type(loan_id) in (int, list), "loan_id and order_id can only be int or list types"
        assert type(loan_id) is int or (type(loan_id) is list and len(loan_id) == len(order_id)), "If order_id and loan_id are lists, they both need to be the same length"

        # Data
        post = {
            'loan_id': loan_id,
            'record_id': loan_id,
            'order_id': order_id
        }
        query = {
            'method': 'createLCPortfolio',
            'lcportfolio_name': portfolio_name
        }

        # Is it an existing portfolio
        existing = self.get_portfolio_list()
        for folio in existing:
            if folio['portfolioName'] == portfolio_name:
                query['method'] = 'addToLCPortfolio'

        # Send
        response = self.session.post('/data/portfolioManagement', query=query, data=post)
        json_response = response.json()

        # Failed
        if not self.session.json_success(json_response):
            raise LendingClubError('Could not assign order to portfolio \'{0}\''.format(portfolio_name), response)

        # Success
        else:

            # Assigned to another portfolio, for some reason, raise warning
            if 'portfolioName' in json_response and json_response['portfolioName'] != portfolio_name:
                raise LendingClubError('Added order to portfolio "{0}" - NOT - "{1}", and I don\'t know why'.format(json_response['portfolioName'], portfolio_name))

            # Assigned to the correct portfolio
            else:
                self.__log('Added order to portfolio "{0}"'.format(portfolio_name))

            return True

        return False

    def search(self, filters=None, start_index=0, limit=100):
        """
        Search for a list of notes that can be invested in.
        (similar to searching for notes in the Browse section on the site)

        Parameters
        ----------
        filters : lendingclub.filters.*, optional
            The filter to use to search for notes. If no filter is passed, a wildcard search
            will be performed.
        start_index : int, optional
            The result index to start on. By default only 100 records will be returned at a time, so use this
            to start at a later index in the results. For example, to get results 200 - 300, set `start_index` to 200.
            (default is 0)
        limit : int, optional
            The number of results to return per request. (default is 100)

        Returns
        -------
        dict
            A dictionary object with the list of matching loans under the `loans` key.
        """
        assert filters is None or isinstance(filters, Filter), 'filter is not a lendingclub.filters.Filter'

        # Set filters
        if filters:
            filter_string = filters.search_string()
        else:
            filter_string = 'default'
        payload = {
            'method': 'search',
            'filter': filter_string,
            'startindex': start_index,
            'pagesize': limit
        }

        # Make request
        response = self.session.post('/browse/browseNotesAj.action', data=payload)
        json_response = response.json()

        if self.session.json_success(json_response):
            results = json_response['searchresult']

            # Normalize results by converting loanGUID -> loan_id
            for loan in results['loans']:
                loan['loan_id'] = int(loan['loanGUID'])

            # Validate that fractions do indeed match the filters
            if filters is not None:
                filters.validate(results['loans'])

            return results

        return False

    def build_portfolio(self, cash, max_per_note=25, min_percent=0, max_percent=20, filters=None, automatically_invest=False, do_not_clear_staging=False):
        """
        Returns a list of loan notes that are diversified by your min/max percent request and filters.
        One way to invest in these loan notes, is to start an order and use add_batch to add all the
        loan fragments to them. (see examples)

        Parameters
        ----------
        cash : int
            The total amount you want to invest across a portfolio of loans (at least $25).
        max_per_note : int, optional
            The maximum dollar amount you want to invest per note. Must be a multiple of 25
        min_percent : int, optional
            THIS IS NOT PER NOTE, but the minimum average percent of return for the entire portfolio.
        max_percent : int, optional
            THIS IS NOT PER NOTE, but the maxmimum average percent of return for the entire portfolio.
        filters : lendingclub.filters.*, optional
            The filters to use to search for portfolios
        automatically_invest : boolean, optional
            If you want the tool to create an order and automatically invest in the portfolio that matches your filter.
            (default False)
        do_not_clear_staging : boolean, optional
            Similar to automatically_invest, don't do this unless you know what you're doing.
            Setting this to True stops the method from clearing the loan staging area before returning

        Returns
        -------
        dict
            A dict representing a new portfolio or False if nothing was found.
            If `automatically_invest` was set to `True`, the dict will contain an `order_id` key with
            the ID of the completed investment order.

        Notes
        -----
        **The min/max_percent parameters**

        When searching for portfolios, these parameters will match a portfolio of loan notes which have
        an **AVERAGE** percent return between these values. If there are multiple portfolio matches, the
        one closes to the max percent will be chosen.

        Examples
        --------
        Here we want to invest $400 in a portfolio with only B, C, D and E grade notes with an average overall return between 17% - 19%. This similar to finding a portfolio in the 'Invest' section on lendingclub.com::

            >>> from lendingclub import LendingClub
            >>> from lendingclub.filters import Filter
            >>> lc = LendingClub()
            >>> lc.authenticate()
            Email:[email protected]
            Password:
            True
            >>> filters = Filter()                  # Set the search filters (only B, C, D and E grade notes)
            >>> filters['grades']['C'] = True
            >>> filters['grades']['D'] = True
            >>> filters['grades']['E'] = True
            >>> lc.get_cash_balance()               # See the cash you have available for investing
            463.80000000000001

            >>> portfolio = lc.build_portfolio(400, # Invest $400 in a portfolio...
                    min_percent=17.0,               # Return percent average between 17 - 19%
                    max_percent=19.0,
                    max_per_note=50,                # As much as $50 per note
                    filters=filters)                # Search using your filters

            >>> len(portfolio['loan_fractions'])    # See how many loans are in this portfolio
            16
            >>> loans_notes = portfolio['loan_fractions']
            >>> order = lc.start_order()            # Start a new order
            >>> order.add_batch(loans_notes)        # Add the loan notes to the order
            >>> order.execute()                     # Execute the order
            1861880

        Here we do a similar search, but automatically invest the found portfolio. **NOTE** This does not allow
        you to review the portfolio before you invest in it.

            >>> from lendingclub import LendingClub
            >>> from lendingclub.filters import Filter
            >>> lc = LendingClub()
            >>> lc.authenticate()
            Email:[email protected]
            Password:
            True
                                                    # Filter shorthand
            >>> filters = Filter({'grades': {'B': True, 'C': True, 'D': True, 'E': True}})
            >>> lc.get_cash_balance()               # See the cash you have available for investing
            463.80000000000001

            >>> portfolio = lc.build_portfolio(400,
                    min_percent=17.0,
                    max_percent=19.0,
                    max_per_note=50,
                    filters=filters,
                    automatically_invest=True)      # Same settings, except invest immediately

            >>> portfolio['order_id']               # See order ID
            1861880
        """
        assert filters is None or isinstance(filters, Filter), 'filter is not a lendingclub.filters.Filter'
        assert max_per_note >= 25, 'max_per_note must be greater than or equal to 25'

        # Set filters
        if filters:
            filter_str = filters.search_string()
        else:
            filter_str = 'default'

        # Start a new order
        self.session.clear_session_order()

        # Make request
        payload = {
            'amount': cash,
            'max_per_note': max_per_note,
            'filter': filter_str
        }
        self.__log('POST VALUES -- amount: {0}, max_per_note: {1}, filter: ...'.format(cash, max_per_note))
        response = self.session.post('/portfolio/lendingMatchOptionsV2.action', data=payload)
        json_response = response.json()

        # Options were found
        if self.session.json_success(json_response) and 'lmOptions' in json_response:
            options = json_response['lmOptions']

            # Nothing found
            if type(options) is not list or json_response['numberTicks'] == 0:
                self.__log('No lending portfolios were returned with your search')
                return False

            # Choose an investment option based on the user's min/max values
            i = 0
            match_index = -1
            match_option = None
            for option in options:

                # A perfect match
                if option['percentage'] == max_percent:
                    match_option = option
                    match_index = i
                    break

                # Over the max
                elif option['percentage'] > max_percent:
                    break

                # Higher than the minimum percent and the current matched option
                elif option['percentage'] >= min_percent and (match_option is None or match_option['percentage'] < option['percentage']):
                    match_option = option
                    match_index = i

                i += 1

            # Nothing matched
            if match_option is None:
                self.__log('No portfolios matched your percentage requirements')
                return False

            # Mark this portfolio for investing (in order to get a list of all notes)
            payload = {
                'order_amount': cash,
                'lending_match_point': match_index,
                'lending_match_version': 'v2'
            }
            self.session.get('/portfolio/recommendPortfolio.action', query=payload)

            # Get all loan fractions
            payload = {
                'method': 'getPortfolio'
            }
            response = self.session.get('/data/portfolio', query=payload)
            json_response = response.json()

            # Extract fractions from response
            fractions = []
            if 'loanFractions' in json_response:
                fractions = json_response['loanFractions']

                # Normalize by converting loanFractionAmount to invest_amount
                for frac in fractions:
                    frac['invest_amount'] = frac['loanFractionAmount']

                    # Raise error if amount is greater than max_per_note
                    if frac['invest_amount'] > max_per_note:
                        raise LendingClubError('ERROR: LendingClub tried to invest ${0} in a loan note. Your max per note is set to ${1}. Portfolio investment canceled.'.format(frac['invest_amount'], max_per_note))

            if len(fractions) == 0:
                self.__log('The selected portfolio didn\'t have any loans')
                return False
            match_option['loan_fractions'] = fractions

            # Validate that fractions do indeed match the filters
            if filters is not None:
                filters.validate(fractions)

            # Not investing -- reset portfolio search session and return
            if automatically_invest is not True:
                if do_not_clear_staging is not True:
                    self.session.clear_session_order()

            # Invest in this porfolio
            elif automatically_invest is True:  # just to be sure
                order = self.start_order()

                # This should probably only be ever done here...ever.
                order._Order__already_staged = True
                order._Order__i_know_what_im_doing = True

                order.add_batch(match_option['loan_fractions'])
                order_id = order.execute()
                match_option['order_id'] = order_id

            return match_option
        else:
            raise LendingClubError('Could not find any portfolio options that match your filters', response)

        return False

    def my_notes(self, start_index=0, limit=100, get_all=False, sort_by='loanId', sort_dir='asc'):
        """
        Return all the loan notes you've already invested in. By default it'll return 100 results at a time.

        Parameters
        ----------
        start_index : int, optional
            The result index to start on. By default only 100 records will be returned at a time, so use this
            to start at a later index in the results. For example, to get results 200 - 300, set `start_index` to 200.
            (default is 0)
        limit : int, optional
            The number of results to return per request. (default is 100)
        get_all : boolean, optional
            Return all results in one request, instead of 100 per request.
        sort_by : string, optional
            What key to sort on
        sort_dir : {'asc', 'desc'}, optional
            Which direction to sort

        Returns
        -------
        dict
            A dictionary with a list of matching notes on the `loans` key
        """

        index = start_index
        notes = {
            'loans': [],
            'total': 0,
            'result': 'success'
        }
        while True:
            payload = {
                'sortBy': sort_by,
                'dir': sort_dir,
                'startindex': index,
                'pagesize': limit,
                'namespace': '/account'
            }
            response = self.session.post('/account/loansAj.action', data=payload)
            json_response = response.json()

            # Notes returned
            if self.session.json_success(json_response):
                notes['loans'] += json_response['searchresult']['loans']
                notes['total'] = json_response['searchresult']['totalRecords']

            # Error
            else:
                notes['result'] = json_response['result']
                break

            # Load more
            if get_all is True and len(notes['loans']) < notes['total']:
                index += limit

            # End
            else:
                break

        return notes

    def get_note(self, note_id):
        """
        Get a loan note that you've invested in by ID

        Parameters
        ----------
        note_id : int
            The note ID

        Returns
        -------
        dict
            A dictionary representing the matching note or False

        Examples
        --------
            >>> from lendingclub import LendingClub
            >>> lc = LendingClub(email='*****@*****.**', password='******')
            >>> lc.authenticate()
            True
            >>> notes = lc.my_notes()                  # Get the first 100 loan notes
            >>> len(notes['loans'])
            100
            >>> notes['total']                          # See the total number of loan notes you have
            630
            >>> notes = lc.my_notes(start_index=100)   # Get the next 100 loan notes
            >>> len(notes['loans'])
            100
            >>> notes = lc.my_notes(get_all=True)       # Get all notes in one request (may be slow)
            >>> len(notes['loans'])
            630
        """

        index = 0
        while True:
            notes = self.my_notes(start_index=index, sort_by='noteId')

            if notes['result'] != 'success':
                break

            # If the first note has a higher ID, we've passed it
            if notes['loans'][0]['noteId'] > note_id:
                break

            # If the last note has a higher ID, it could be in this record set
            if notes['loans'][-1]['noteId'] >= note_id:
                for note in notes['loans']:
                    if note['noteId'] == note_id:
                        return note

            index += 100

        return False

    def search_my_notes(self, loan_id=None, order_id=None, grade=None, portfolio_name=None, status=None, term=None):
        """
        Search for notes you are invested in. Use the parameters to define how to search.
        Passing no parameters is the same as calling `my_notes(get_all=True)`

        Parameters
        ----------
        loan_id : int, optional
            Search for notes for a specific loan. Since a loan is broken up into a pool of notes, it's possible
            to invest multiple notes in a single loan
        order_id : int, optional
            Search for notes from a particular investment order.
        grade : {A, B, C, D, E, F, G}, optional
            Match by a particular loan grade
        portfolio_name : string, optional
            Search for notes in a portfolio with this name (case sensitive)
        status : string, {issued, in-review, in-funding, current, charged-off, late, in-grace-period, fully-paid}, optional
            The funding status string.
        term : {60, 36}, optional
            Term length, either 60 or 36 (for 5 year and 3 year, respectively)

        Returns
        -------
        dict
            A dictionary with a list of matching notes on the `loans` key
        """
        assert grade is None or type(grade) is str, 'grade must be a string'
        assert portfolio_name is None or type(portfolio_name) is str, 'portfolio_name must be a string'

        index = 0
        found = []
        sort_by = 'orderId' if order_id is not None else 'loanId'
        group_id = order_id if order_id is not None else loan_id   # first match by order, then by loan

        # Normalize grade
        if grade is not None:
            grade = grade[0].upper()

        # Normalize status
        if status is not None:
            status = re.sub('[^a-zA-Z\-]', ' ', status.lower())  # remove all non alpha characters
            status = re.sub('days', ' ', status)  # remove days
            status = re.sub('\s+', '-', status.strip())  # replace spaces with dash
            status = re.sub('(^-+)|(-+$)', '', status)

        while True:
            notes = self.my_notes(start_index=index, sort_by=sort_by)

            if notes['result'] != 'success':
                break

            # If the first note has a higher ID, we've passed it
            if group_id is not None and notes['loans'][0][sort_by] > group_id:
                break

            # If the last note has a higher ID, it could be in this record set
            if group_id is None or notes['loans'][-1][sort_by] >= group_id:
                for note in notes['loans']:

                    # Order ID, no match
                    if order_id is not None and note['orderId'] != order_id:
                        continue

                    # Loan ID, no match
                    if loan_id is not None and note['loanId'] != loan_id:
                        continue

                    # Grade, no match
                    if grade is not None and note['rate'][0] != grade:
                        continue

                    # Portfolio, no match
                    if portfolio_name is not None and note['portfolioName'][0] != portfolio_name:
                        continue

                    # Term, no match
                    if term is not None and note['loanLength'] != term:
                        continue

                    # Status
                    if status is not None:
                        # Normalize status message
                        nstatus = re.sub('[^a-zA-Z\-]', ' ', note['status'].lower())  # remove all non alpha characters
                        nstatus = re.sub('days', ' ', nstatus)  # remove days
                        nstatus = re.sub('\s+', '-', nstatus.strip())  # replace spaces with dash
                        nstatus = re.sub('(^-+)|(-+$)', '', nstatus)

                        # No match
                        if nstatus != status:
                            continue

                    # Must be a match
                    found.append(note)

            index += 100

        return found

    def start_order(self):
        """
        Start a new investment order for loans

        Returns
        -------
        lendingclub.Order
            The :class:`lendingclub.Order` object you can use for investing in loan notes.
        """
        order = Order(lc=self)
        return order