packages feed

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 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