forked from jansel/lendingclubchecker
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlcchecker.py
executable file
·298 lines (254 loc) · 8.79 KB
/
lcchecker.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#!/usr/bin/python
"""lcchecker.py: Create a list of sell recommendations based on recent notes"""
__version__ = "1.0"
__author__ = "Jason Ansel ([email protected])"
__copyright__ = "(C) 2012. GNU GPL 3."
import lendingclub
import argparse
import logging
import smtplib
import datetime
import sys
import time
import os
import re
from pprint import pprint
from email.mime.text import MIMEText
from collections import defaultdict
from settings import smtp_server, smtp_username, smtp_password, login_email
try:
from cStringIO import StringIO
except:
from StringIO import StringIO
try:
import cPickle as pickle
except:
import pickle
buy_options = {
'days_since_payment' : 28,
'markup' : 1.001,
'payments_received' : 4,
'from_rate' : 0.17,
'price' : 25.0,
'creditdelta' : -10,
}
notes_pickle_file = lendingclub.cachedir+'/notes.pk'
def load_active_notes(lc, update, window):
if update:
lc.fetch_notes()
lc.fetch_trading_summary()
active = lc.load_notes()
sellingids = lc.get_already_selling_ids()
active = filter(lambda x: x.note_id not in sellingids, active)
active = filter(lambda x: x.want_update(window), active)
if window == 0:
return list()
logging.debug("active notes = "+str(len(active)))
for note in active:
try:
if update:
lc.fetch_details(note)
note.load_details()
except:
if not update:
lc.fetch_details(note)
note.load_details()
else:
logging.exception("failed to load note "+str(note.id))
pickle.dump(active, open(notes_pickle_file, 'wb'), pickle.HIGHEST_PROTOCOL)
return active
def create_msg(lc, sell, active, args, o):
print >>o, "examined", len(active), 'notes,', len(sell), "sell suggestions:"
print >>o
for note in sell:
note.debug(o)
print >>o, 'sell reasons', note.sell_reasons()
print >>o
if sell:
if args.sell:
print >>o, "will automatically sell note ids:",
else:
print >>o, "suggested sell note ids:",
print >>o, map(lambda x: x.note_id, sell)
print >>o
v1 = 0.0
i1 = 0.0
print >>o, "available cash %.2f" % lc.available_cash()
for days in xrange(1,33):
s = filter(lambda x: x.want_update(days), active)
v2 = sum(map(lendingclub.Note.payment_ammount, s))
i2 = sum(map(lendingclub.Note.payment_interest, s))
v = v2-v1
iv = i2-i1
v1=v2
i1=i2
if v>0:
day = (datetime.date.today()+datetime.timedelta(days=days-1)).strftime("%a, %b %d")
print >>o, "expecting %5.2f (%5.2f interest) on" % (v,iv), day
if len(s)==len(active):
break
print >>o
def send_email(me, you, subject, body):
logging.info("sending email '%s' to %s" % (subject, you))
msg = MIMEText(body)
msg['Subject'] = subject
msg['From'] = me
msg['To'] = you
s = smtplib.SMTP(smtp_server)
s.starttls()
if smtp_username:
s.login(smtp_username, smtp_password)
s.sendmail(me, [you], msg.as_string())
s.quit()
def trading_inventory_iterator(lc, args, should_continue):
done = False
for page in xrange(args.pages):
if done or not should_continue():
return
if args.update:
lc.fetch_trading_inventory(from_rate=float(buy_options['from_rate'])-0.01,
remaining_payments=60-int(buy_options['payments_received']),
page=page)
for note in lc.load_trading_inventory(page=page):
yield note
if note.markup()>buy_options['markup']:
done = True
def get_buy_suggestions(lc, args, o):
all_loan_ids = set(lc.get_all_loan_ids())
reasons = defaultdict(int)
buy = list()
cash = lc.available_cash()
ntotal = 0
nfetched = 0
for note in trading_inventory_iterator(lc, args, lambda: cash>20):
try:
ntotal += 1
if not note.want_buy_no_details(reasonlog=reasons, **buy_options):
continue
if note.loan_id in all_loan_ids:
reasons['already invested in loan'] += 1
continue
if note.asking_price > cash:
reasons['not enough cash'] += 1
continue
if args.update:
lc.fetch_details(note)
nfetched += 1
note.load_details()
if note.want_buy(reasonlog=reasons, **buy_options):
buy.append(note)
all_loan_ids.add(note.loan_id)
cash -= note.asking_price
except:
reasons['error'] += 1
logging.exception("failed to load trading note")
print >>o
print >>o, "examined",nfetched,"of",ntotal,"trading notes,", len(buy),"buy suggestions:"
print >>o
for note in buy:
note.debug(o)
print >>o
if buy:
if args.buy:
print >>o,"will automatically buy ids:",map(lambda x: x.note_id, buy)
print >>o,"cash left:", cash
else:
print >>o,"suggested buy ids:",map(lambda x: x.note_id, buy)
print >>o
print >>o,"nobuy_reason_log:"
pprint(sorted(reasons.items(), key=lambda x: -x[1]),
stream=o, indent=2, width=100)
return buy
def main(args):
o = StringIO()
logstream = StringIO()
if args.debug:
loglevel = logging.DEBUG
elif args.quiet:
loglevel = logging.WARNING
else:
loglevel = logging.INFO
logging.basicConfig(level=loglevel, stream=logstream)
if not args.quiet:
logging.getLogger().addHandler(logging.StreamHandler())
try:
if args.weekday and datetime.date.today().weekday() in (5,6):
logging.info("aborting due to it not being a weekday")
return
lc = lendingclub.LendingClubBrowser()
if args.frompickle:
active = pickle.load(open(notes_pickle_file, 'rb'))
else:
active = load_active_notes(lc, args.update, args.window)
sell = filter(lendingclub.Note.want_sell, active)
if len(sell)>0 and args.sell:
lc.sell_notes(sell, args.sellmarkup)
create_msg(lc, sell, active, args, o)
if args.buy:
buy = get_buy_suggestions(lc, args, o)
if len(buy)>0 and args.buy:
lc.buy_trading_notes(buy)
else:
buy = list()
lc.logout()
#clean cache dir:
for fname in os.listdir(lendingclub.cachedir):
if re.search("[.]html", fname):
fpath = os.path.join(lendingclub.cachedir, fname)
st = os.stat(fpath)
daysold = (time.time()-max(st.st_atime, st.st_mtime))/3600/24
if daysold>45:
logging.debug("deleting %s, %.0f days old"%(fname,daysold))
os.unlink(fpath)
except:
logging.exception("unknown error")
finally:
body = o.getvalue()
log = logstream.getvalue()
if log:
body+="\n\nlog:\n"+log
if not args.quiet:
print
print body
elif log:
print
print "log:"
print log
if body and args.email:
today = str(datetime.date.today())
subject = "[LendingClubChecker] buy %d sell %d on %s" % (len(buy), len(sell), today)
send_email(args.emailfrom, args.emailto, subject, body)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='check notes coming due soon for ones that should be sold')
parser.add_argument('--window', '-w', default=5, type=int,
help='fetch notes we expect to be payed in the next N days')
parser.add_argument('--noupdate', '-n', action='store_false', dest='update',
help="dont fetch details for notes we have cached data for")
parser.add_argument('--frompickle', action='store_true',
help="read active notes from last run cache")
parser.add_argument('--debug', '-v', action='store_true',
help="print more debugging info")
parser.add_argument('--quiet', '-q', action='store_true',
help="print less debugging info")
parser.add_argument('--emailfrom', default=login_email, help='report email from address')
parser.add_argument('--emailto', default=login_email, help='report email to address')
parser.add_argument('--email', action='store_true', help="send an email report to "+login_email)
parser.add_argument('--weekday', action='store_true', help="abort the script if run on the weekend")
parser.add_argument('--sell', action='store_true', help="automatically sell all suggestions")
parser.add_argument('--sellmarkup', default=0.993,
type=float, help='markup for --sell (default 0.993)')
parser.add_argument('--buy', action='store_true', help="automatically buy all suggestions")
parser.add_argument('--buyopt', nargs=2, action='append', help="set option for buying notes")
parser.add_argument('--buyoptlist', action='store_true', help="print buyopts and exit")
parser.add_argument('--pages', default=8, type=int, help='maximum number of trading note pages to examine')
args = parser.parse_args()
assert args.sellmarkup>0.4
assert args.sellmarkup<1.6
if args.buyopt:
for k,v in map(tuple, args.buyopt):
assert buy_options.has_key(k)
buy_options[k] = type(buy_options[k])(v)
if args.buyoptlist:
pprint(buy_options)
else:
main(args)