lendingclub (empty) → 0.1.0.0
raw patch · 12 files changed
+673/−0 lines, 12 filesdep +HsOpenSSLdep +aesondep +basesetup-changed
Dependencies added: HsOpenSSL, aeson, base, blaze-builder, bytestring, http-streams, io-streams, mtl, scientific, text, vector
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- lendingclub.cabal +44/−0
- src/LendingClub.hs +44/−0
- src/LendingClub/Account.hs +27/−0
- src/LendingClub/Authorization.hs +23/−0
- src/LendingClub/Commands.hs +67/−0
- src/LendingClub/Internal/JSON.hs +111/−0
- src/LendingClub/Invest.hs +87/−0
- src/LendingClub/Listing.hs +206/−0
- src/LendingClub/Money.hs +3/−0
- src/LendingClub/Note.hs +29/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2015, Matthew Wraith++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Matthew Wraith nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ lendingclub.cabal view
@@ -0,0 +1,44 @@+name: lendingclub+version: 0.1.0.0+synopsis: Bindings for the LendingClub marketplace API+description: Bindings for the LendingClub marketplace API+homepage: https://www.lendingclub.com/developers/lc-api.action+license: BSD3+license-file: LICENSE+author: Matthew Wraith+maintainer: wraithm@gmail.com+copyright: (c) 2015 Matthew Wraith+category: Finance+build-type: Simple+cabal-version: >=1.10++source-repository head+ type: git+ location: git://github.com/WraithM/lendingclub.git++library+ exposed-modules: + LendingClub, + LendingClub.Account, + LendingClub.Listing, + LendingClub.Note, + LendingClub.Authorization, + LendingClub.Money, + LendingClub.Commands, + LendingClub.Invest, + LendingClub.Internal.JSON++ build-depends: + base >=4.8 && <4.9, + text >=1.2 && <1.3, + bytestring >=0.10 && <0.11, + mtl >=2.2 && <2.3,+ aeson,+ scientific,+ vector,+ io-streams,+ HsOpenSSL,+ http-streams,+ blaze-builder+ hs-source-dirs: src+ default-language: Haskell2010
+ src/LendingClub.hs view
@@ -0,0 +1,44 @@+module LendingClub+ (+ -- * Commands+ invest+ , account+ , allListings+ , notes+ , listingFromNote++ -- * Account+ , Account (..)++ -- * Authorization+ , Authorization (..)+ , InvestorId (..)++ -- * Invest+ , InvestResponse (..)+ , ErrorMessage (..)+ , InvestConfirmation (..)+ , ExecutionStatus (..)++ -- * Listing+ , Listing (..)+ , Offer (..)+ , Credit (..)+ , Grade (..)+ , SubGrade (..)+ , Purpose (..)++ -- * Note+ , Note (..)++ -- * Types+ , Money+ ) where++import LendingClub.Account+import LendingClub.Authorization+import LendingClub.Commands+import LendingClub.Invest+import LendingClub.Listing+import LendingClub.Money+import LendingClub.Note hiding (loanId)
+ src/LendingClub/Account.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE DeriveGeneric #-}++module LendingClub.Account (Account (..)) where++import Data.Aeson++import GHC.Generics++import LendingClub.Authorization+import LendingClub.Money++-- | A data structure for holding account information for LendingClub+data Account = Account+ { investorId :: InvestorId+ , availableCash :: Money+ , accountTotal :: Money+ , accruedInterest :: Money+ , infundingBalance :: Money+ , receivedInterest :: Money+ , receivedPrincipal :: Money+ , receivedLateFees :: Maybe Money+ , outstandingPrincipal :: Money+ , totalNotes :: Int+ , totalPortfolios :: Int+ } deriving (Generic, Show, Eq)++instance FromJSON Account where
+ src/LendingClub/Authorization.hs view
@@ -0,0 +1,23 @@+module LendingClub.Authorization + ( Authorization (..)+ , InvestorId (..)+ ) where++import Control.Applicative (pure)+import Control.Monad (mzero)++import Data.Aeson+import Data.ByteString+import Data.Scientific (toBoundedInteger)++newtype Authorization = Authorization+ { authorization :: ByteString }+ deriving (Show, Eq)++newtype InvestorId = InvestorId Int+ deriving (Show, Eq)++instance FromJSON InvestorId where+ parseJSON (Number x) = + maybe mzero (pure . InvestorId) (toBoundedInteger x)+ parseJSON _ = mzero
+ src/LendingClub/Commands.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++module LendingClub.Commands+ ( invest+ , account+ , allListings+ , notes+ , listingFromNote+ ) where++import Data.Aeson+import Data.ByteString.Char8 as C+import Data.Functor ((<$>))+import Data.Monoid ((<>))+import Data.Vector (Vector)+import qualified Data.Vector as V++import GHC.Generics++import LendingClub.Account+import LendingClub.Authorization+import LendingClub.Invest+import LendingClub.Internal.JSON+import LendingClub.Listing+import LendingClub.Note as N+import LendingClub.Money++invest+ :: Authorization+ -> InvestorId+ -> Money+ -> Listing+ -> IO InvestResponse+invest auth i amt l = investRequest auth i (listingId l) amt++newtype LCListings = LCListings+ { loans :: Vector Listing }+ deriving Generic++instance FromJSON LCListings where++allListings :: Authorization -> IO (Vector Listing)+allListings auth = loans <$> jsonGet auth "loans/listing"++account :: Authorization -> InvestorId -> IO Account+account auth (InvestorId i) =+ jsonGet auth ("accounts/" <> C.pack (show i) <> "/summary")++newtype LCNotes = LCNotes+ { myNotes :: Vector Note }+ deriving Generic++instance FromJSON LCNotes where++notes :: Authorization -> InvestorId -> IO (Vector Note)+notes auth (InvestorId i) =+ myNotes <$> jsonGet auth ("accounts/" <> C.pack (show i) <> "/notes")++-- | Does not talk to LendingClub's listings endpoint.+-- This uses in-memory data to retrieve listings.+--+-- TODO This thing is pretty much useles... Need to access historical listings+listingFromNote :: Authorization -> Note -> IO (Maybe Listing)+listingFromNote auth (Note { N.loanId = lid }) = do+ listings <- allListings auth+ return $ V.find (\l -> listingId l == lid) listings
+ src/LendingClub/Internal/JSON.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++{-# OPTIONS_GHC -fno-warn-unused-binds #-}++module LendingClub.Internal.JSON+ ( jsonGet+-- , csvGet+-- , csvGetStream+ , investRequest+ ) where++import Blaze.ByteString.Builder (fromLazyByteString)+import Network.Http.Client+import OpenSSL+import System.IO.Streams (InputStream, write)++import Control.Monad.Reader (liftIO)++import Data.Aeson (FromJSON, ToJSON, encode)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as C+import Data.Monoid ((<>))++import GHC.Generics++import LendingClub.Authorization+import LendingClub.Invest+import LendingClub.Money++-- | Make a GET request to LendingClub's JSON api, return the parsed JSON data+jsonGet+ :: FromJSON a+ => Authorization -- ^ The authorization for the lending club user+ -> ByteString -- ^ The name of the API service, relative to the API url+ -> IO a -- ^ JSON response+jsonGet auth url = issueRequest auth url GET "application/json" jsonHandler (Nothing :: Maybe ())++{- TODO Redo for LendingClub+-- | Make a GET request to Prosper's CSV api, return the raw CSV data+csvGet+ :: UserInfo -- ^ The user name and password for the prosper user+ -> ByteString -- ^ The name of the API service, relative to the API url+ -> Prosper ByteString -- ^ Raw CSV response+csvGet userInfo url = issueRequest userInfo url GET "text/csv" concatHandler []++-- | Make a GET request to Prosper's CSV api, return the raw CSV data+csvGetStream+ :: UserInfo -- ^ The user name and password for the prosper user+ -> ByteString -- ^ The name of the API service, relative to the API url+ -> (InputStream ByteString -> IO a)+ -> Prosper a -- ^ Raw CSV response+csvGetStream userInfo url handler =+ issueRequest userInfo url GET "text/csv" (\_ is -> handler is) []+-}++data InvestRequest = InvestRequest+ { aid :: !Int+ , orders :: ![Order]+ } deriving (Generic, Show)++data Order = Order+ { loanId :: !Int+ , requestedAmount :: !Money+ -- , portfolioId :: !Int -- ^ Implement later+ } deriving (Generic, Show)++instance ToJSON Order where+instance ToJSON InvestRequest where++investRequest+ :: Authorization+ -> InvestorId+ -> Int -- ^ The listing id+ -> Money -- ^ Amount+ -> IO InvestResponse -- ^ JSON response from LendingClub+investRequest auth (InvestorId invId) listingId amount =+ issueRequest auth ("accounts/" <> C.pack (show invId) <> "/orders") POST "application/json" jsonHandler $+ Just InvestRequest+ { aid = invId+ , orders = [Order listingId amount]+ }++apiUrl :: ByteString+apiUrl = "api.lendingclub.com"++issueRequest+ :: (FromJSON a, ToJSON b)+ => Authorization+ -> ByteString+ -> Method+ -> ContentType+ -> (Response -> InputStream ByteString -> IO a)+ -> Maybe b+ -> IO a+issueRequest (Authorization auth) url method ct handler mBody = withOpenSSL $ do+ ctx <- baselineContextSSL+ con <- openConnectionSSL ctx apiUrl 443+ req <- buildRequest $ do+ http method $ "/api/investor/v1/" <> url+ setHeader "Authorization" auth+ setContentType ct+ sendRequest con req (setBody mBody)+ resp <- receiveResponse con handler+ closeConnection con+ return resp+ where+ setBody Nothing = emptyBody+ setBody (Just body) = write (Just (buildJSON body))++ buildJSON = fromLazyByteString . encode
+ src/LendingClub/Invest.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++module LendingClub.Invest + ( InvestResponse (..)+ , ErrorMessage (..)+ , InvestConfirmation (..)+ , ExecutionStatus (..)+ ) where++import Control.Applicative (pure, (<|>), (<$>), (<*>))++import Data.Aeson+import Data.Text (Text)++import GHC.Generics++import LendingClub.Money++data InvestResponse+ = InvestResponse+ { orderInstructId :: Int+ , orderConfirmations :: [InvestConfirmation]+ }+ | InvestError+ { errors :: [ErrorMessage] }+ deriving (Show, Eq)++instance FromJSON InvestResponse where+ parseJSON (Object v) =+ InvestResponse + <$> v .: "orderInstructId" + <*> v .: "orderConfirmations"+ <|> InvestError <$> v .: "errors"+ parseJSON _ = pure (InvestError [])+ +data ErrorMessage = ErrorMessage+ { field :: Text+ , code :: Text+ , message :: Text+ } deriving (Generic, Show, Eq)++instance FromJSON ErrorMessage where++data InvestConfirmation = InvestConfirmation+ { loanId :: Int+ , requestedAmount :: Money+ , investedAmount :: Money+ , executionStatus :: [ExecutionStatus]+ } deriving (Generic, Show, Eq)++instance FromJSON InvestConfirmation where++data ExecutionStatus+ = OrderFulfilled+ | LoanAmountExceeded+ | NotAnInfundingLoan+ | RequestedAmountLow+ | RequestedAmountRounded+ | AugmentedByMerge+ | ElimByMerge+ | InsufficientCash+ | NotAnInvestor+ | NotAValidInvestment+ | NoteAddedToPortfolio+ | NotAValidPortfolio+ | ErrorAddingNoteToPortfolio+ | SystemBusy+ | UnknownError+ deriving (Show, Eq)++instance FromJSON ExecutionStatus where+ parseJSON (String "ORDER_FULFILLED") = pure OrderFulfilled+ parseJSON (String "LOAN_AMNT_EXCEEDED") = pure LoanAmountExceeded+ parseJSON (String "NOT_AN_INFUNDING_LOAN") = pure NotAnInfundingLoan+ parseJSON (String "REQUESTED_AMNT_LOW") = pure RequestedAmountLow+ parseJSON (String "REQUESTED_AMNT_ROUNDED") = pure RequestedAmountRounded+ parseJSON (String "AUGMENTED_BY_MERGE") = pure AugmentedByMerge+ parseJSON (String "ELIM_BY_MERGE") = pure ElimByMerge+ parseJSON (String "INSUFFICIENT_CASH") = pure InsufficientCash+ parseJSON (String "NOT_AN_INVESTOR") = pure NotAnInvestor+ parseJSON (String "NOT_A_VALID_INVESTMENT") = pure NotAValidInvestment+ parseJSON (String "NOTE_ADDED_TO_PORTFOLIO") = pure NoteAddedToPortfolio+ parseJSON (String "NOT_A_VALID_PORTFOLIO") = pure NotAValidPortfolio+ parseJSON (String "ERROR_ADDING_NOTE_TO_PORTFOLIO") = pure ErrorAddingNoteToPortfolio+ parseJSON (String "SYSTEM_BUSY") = pure SystemBusy+ parseJSON _ = pure UnknownError
+ src/LendingClub/Listing.hs view
@@ -0,0 +1,206 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++module LendingClub.Listing+ ( Listing (..)+ , Offer (..)+ , Credit (..)+ , Grade (..)+ , SubGrade (..)+ , Purpose (..)+ ) where++import Control.Applicative+import Control.Monad (mzero)++import Data.Aeson+import Data.Typeable++import GHC.Generics++import LendingClub.Money++data Listing = Listing+ { listingId :: !Int++ , listingGrade :: !Grade -- ^ analogous to ProsperRating+ , subGrade :: !SubGrade -- ^ maybe analogous to score?+ , purpose :: !Purpose -- ^ "purpose" in the API++ -- Market data+ , fundedAmount :: !Money++ , offer :: !Offer -- ^ Contract data+ , credit :: !Credit -- ^ Credit data+ } deriving Show++-- | Two listings are equivalent if their serial numbers are equal+instance Eq Listing where+ (Listing { listingId = id1 }) == (Listing { listingId = id2 }) =+ id1 == id2++-- | Data related to the listing's offer and contract terms+data Offer = Offer+ { requestAmount :: !Money+ , rate :: !Double -- ^ Interest rate for the borrower+ , termInMonths :: !Int -- ^ Integer number of months+ } deriving Show+-- estimatedLossRate :: Double -- Maybe Money?+-- estimatedReturn :: Double -- Maybe Money?+-- startDate :: Date -- Add dates later+-- creationDate :: Date+-- verificationStage :: Maybe Int -- Don't know what VerificationStage is...+-- Add WholeLoanStartDate and WholeLoanEndDate++-- | Data related to the credibility of the listing+data Credit = Credit+ { fico :: !Int+ , bankcardUtilization :: !Double+ , isHomeowner :: !Bool+ , debtToIncome :: !Double+ , monthsEmployed :: !(Maybe Int)+ , currentDelinquencies :: !(Maybe Int)+ , amountDelinquent :: !(Maybe Money)+ , openCreditLines :: !(Maybe Int)+ , totOpenRevolvingAccts :: !(Maybe Int)+ , revolvingBalance :: !(Maybe Money)+ , revolvingAvailableCredit :: !(Maybe Int) -- ^ Percent+ } deriving Show+-- firstCreditLine :: Date+-- creditLinesLast7Years :: Int+-- inquiriesLast6Months :: Int+-- delinquenciesLast7Years :: Int -- These are maybe too specific to Prosper+-- publicRecordsLast10Years :: Int+-- oldestTradeOpenDate :: Date++-- TODO Need to compare nullables to Prosper's nullables+instance FromJSON Listing where+ parseJSON (Object v) = Listing+ <$> v .: "id"++ -- Lending club specific data+ <*> v .: "grade"+ <*> v .: "subGrade"+ <*> v .: "purpose"++ -- Market data+ <*> v .: "fundedAmount"++ <*> (Offer+ <$> v .: "loanAmount"+ <*> v .: "intRate" -- Interest Rate, might not be the same as "borrower rate"+ <*> v .: "term")++ <*> (Credit+ <$> v .: "ficoRangeLow" -- Choose the low-end of the FICO range+ <*> v .: "bcUtil"+ <*> (lcHomeowner <$> v .: "homeOwnership")+ <*> v .: "dti"+ <*> v .:? "empLength"+ <*> v .:? "accNowDelinq"+ <*> v .:? "delinqAmnt"+ <*> v .:? "openAcc" -- TODO check if "open credit lines" is the same as in Prosper+ <*> v .:? "numRevAccts"+ <*> v .:? "revolBal"+ <*> v .:? "revolUtil" -- TODO check if this is the same as in Prosper+ )+ parseJSON _ = mzero++data Homeownership+ = RENT+ | OWN+ | MORTGAGE+ | OTHER+ deriving (Show, Eq, Typeable, Generic)++instance FromJSON Homeownership where++lcHomeowner :: Homeownership -> Bool+lcHomeowner OWN = True+lcHomeowner MORTGAGE = True -- TODO This is debatable... We should think about this+lcHomeowner _ = False++data Grade+ = G+ | F+ | E+ | D+ | C+ | B+ | A+ deriving (Show, Eq, Ord, Typeable, Generic, Read, Enum)++instance FromJSON Grade where+instance ToJSON Grade where++data SubGrade+ = G5+ | G4+ | G3+ | G2+ | G1+ | F5+ | F4+ | F3+ | F2+ | F1+ | E5+ | E4+ | E3+ | E2+ | E1+ | D5+ | D4+ | D3+ | D2+ | D1+ | C5+ | C4+ | C3+ | C2+ | C1+ | B5+ | B4+ | B3+ | B2+ | B1+ | A5+ | A4+ | A3+ | A2+ | A1+ deriving (Show, Eq, Ord, Typeable, Generic, Read, Enum)++instance FromJSON SubGrade where++data Purpose+ = DebtConsolidation+ | Medical+ | HomeImprovement+ | RenewableEnergy+ | SmallBusiness+ | Wedding+ | Vacation+ | Moving+ | House+ | Car+ | MajorPurchase+ | CreditCard+ | Other+ deriving (Show, Eq, Read)++instance FromJSON Purpose where+ parseJSON (String "debt_consolidation") = pure DebtConsolidation+ parseJSON (String "medical") = pure Medical+ parseJSON (String "home_improvement") = pure HomeImprovement+ parseJSON (String "renewable_energy") = pure RenewableEnergy+ parseJSON (String "small_business") = pure SmallBusiness+ parseJSON (String "wedding") = pure Wedding+ parseJSON (String "vacation") = pure Vacation+ parseJSON (String "moving") = pure Moving+ parseJSON (String "house") = pure House+ parseJSON (String "car") = pure Car+ parseJSON (String "major_purchase") = pure MajorPurchase+ parseJSON (String "credit_card") = pure CreditCard+ parseJSON _ = pure Other
+ src/LendingClub/Money.hs view
@@ -0,0 +1,3 @@+module LendingClub.Money where++type Money = Double
+ src/LendingClub/Note.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE DeriveGeneric #-}++module LendingClub.Note (Note (..)) where++import Data.Aeson+import Data.Text (Text)++import GHC.Generics++import LendingClub.Listing (Grade (..))+import LendingClub.Money++data Note = Note+ { loanId :: Int+ , noteId :: Int+ , orderId :: Int+ , interestRate :: Double+ , loanLength :: Int -- Term+ , loanStatus :: Text -- Check if should be an ADT+ , grade :: Grade+ , loanAmount :: Money+ , noteAmount :: Money+ , paymentsReceived :: Money+ } deriving (Generic, Show, Eq)+ -- , issueDate :: Time+ -- , orderDate :: Time+ -- , loanStatusDate :: Time++instance FromJSON Note where