-
Notifications
You must be signed in to change notification settings - Fork 2
/
position.py
514 lines (407 loc) · 17.3 KB
/
position.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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
#!/usr/bin/env python
import functools
import math
import pylink
import calendar
from report import comma
import datetime
VEHICLES = ['iso', 'nso', 'rsu']
def parse_date(d):
if str == type(d):
if 4 == len(d.split('/')[-1]):
return datetime.datetime.strptime(d, '%m/%d/%Y')
else:
return datetime.datetime.strptime(d, '%m/%d/%y')
else:
return d
def mon_diff(start, end):
retval = 12*(end.year - start.year) + (end.month - start.month)
last_day_start = calendar.monthrange(start.year, start.month)[-1]
last_day_end = calendar.monthrange(end.year, end.month)[-1]
if (((last_day_start != start.day) or (last_day_end != end.day))
and start.day > end.day):
retval -= 1
retval = max(retval, 0)
return retval
def from_table(name,
vehicle,
first_date, first_val,
second_date, second_val,
last_date, last_val,
n_shares,
exercised=0,
sold=0,
strike_usd=0):
"""Generate a Grant object from rows in Shareworks.
Vesting schedules in Shareworks can be found by selecting the hyperlinked
Grant Name under Portfolio > Stock Options and Awards.
name: Grant ID from Shareworks
vehicle: one of 'iso', 'nso', or 'rsu'
first_*: The date (mm/dd/yy) and shares vested from the first row in SW
second_*: Ditto, but second row
last_*: Ditto, but last row
n_shares: Total number of shares
exercised: Number that have been exercised already
sold: Number that have been sold already
strike_usd: Strike price in USD (zero OK for rsu)
"""
first_date = parse_date(first_date)
second_date = parse_date(second_date)
last_date = parse_date(last_date)
last_mon_day = calendar.monthrange(first_date.year, first_date.month)
use_last_day = (last_mon_day == first_date.day)
# number of months as measured by a banker
full_months = mon_diff(first_date, last_date)
period_months = mon_diff(first_date, second_date)
n_periods = int(full_months / float(period_months))
assert(not (n_periods % 1))
print(n_periods)
n_cliff = first_val
negative_cliff = (last_val < (second_val-1))
# is the total number of shares sane?
regular_vest = round((n_shares-first_val-last_val)/(n_periods-1), 0)
print(n_shares, first_val, last_val, n_periods)
print(regular_vest)
if not regular_vest == second_val:
raise ValueError("Invalid number of share specified %d vs %d" %(
regular_vest, second_val))
return Grant(name=name,
vehicle=vehicle,
n_cliff=n_cliff,
n_shares=n_shares,
exercised=exercised,
strike_usd=strike_usd,
sold=sold,
start=first_date,
n_periods=n_periods,
period_months=period_months,
negative_cliff=negative_cliff)
class Grant(object):
"""Equity grants."""
def __init__(self,
name=None,
vehicle=None,
n_cliff=0,
n_shares=None,
exercised=0,
sold=0,
strike_usd=0,
start=None,
n_periods=48,
period_months=1,
negative_cliff=False):
"""Grant object, including requested actions.
name: the grant-id from palantir
vehicle: nso, iso, or rsu
n_shares: total number of shares
n_cliff: number of shares vested at cliff date
exercised: number of shares that were exercised (iso/nso only)
sold: number of shares that were sold
strike_usd: strike price of the grant (iso/nso only)
start: beginning date of regular vesting (mm/dd/yy)
n_periods: number of vesting periods
period_months: number of months in a vesting period
negative_cliff: subtract the cliff amount from the last vest period
"""
self.name = name
assert (name)
assert(vehicle in ['rsu', 'iso', 'nso'])
self.vehicle = vehicle
self.n_shares = n_shares
assert (n_shares)
self.n_cliff = n_cliff
self.exercised = exercised
assert ((exercised is not None) or ('rsu' == vehicle))
# to track the number of withheld vs sold shares
self.withheld = 0
self.liquidated = sold
self.sold = sold
self.strike_usd = strike_usd
if (not not strike_usd) == ('rsu' == vehicle):
raise ValueError()
self.start = parse_date(start)
self.n_periods = n_periods
self.period_months = period_months
self.negative_cliff = negative_cliff
# This works properly with None's
assert(exercised is None or exercised <= n_shares)
assert(exercised is None or sold <= exercised)
def unvested(self, on):
return self.n_shares - self.vested(on)
def vested(self, on):
on = parse_date(on)
if on < self.start:
# Asking for a date before vesting began
return 0
# number of months as measured by a banker
mon = mon_diff(self.start, on)
# how many vesting periods will have been completed at this time?
periods = int(min(float((math.floor(mon / self.period_months))),
self.n_periods))
# How many shares have a regular vesting cadence?
n_vesting = float(self.n_shares - self.n_cliff)
if self.negative_cliff: n_vesting += self.n_cliff
# Calculate the floating fraction of vested shares
frac_vested = float(periods) / float(self.n_periods)
# Add that many shares each vesting period
if periods >= self.n_periods:
retval = self.n_shares
else:
retval = round(frac_vested * n_vesting + self.n_cliff, 0)
# quick sanity check
assert(retval >= self.exercised)
return retval
def vested_outstanding(self, on):
return self.vested(on) - self.exercised
def outstanding(self):
return self.n_shares - self.exercised
def held(self):
return self.exercised - self.liquidated
def outstanding_cost(self):
return self.outstanding() * self.strike_usd
def vested_outstanding_cost(self, on):
outstanding = self.vested_outstanding(on)
assert(self.strike_usd >= 0)
assert(outstanding >= 0)
return outstanding * self.strike_usd
def vested_unliquidated(self, on):
return self.vested(on) - self.liquidated
def withholding(self, on, withholding_rate):
return int(round(self.vested(on) * withholding_rate, 0))
def available(self, on, withholding_rate=None):
retval = ( 0
+ self.vested(on)
- self.liquidated
+ 0 )
if withholding_rate:
retval -= self.withholding(on, withholding_rate)
return retval
def withhold(self, on, withholding_rate):
n = self.withholding(on, withholding_rate)
self.liquidated += n
self.withheld += n
def print_grant(self, on, rate):
fmt = """Grant %(name)s
Type: %(veh)s
Size: %(n)s
Strike: %(strike).2f
Vested: %(vested)s
Withholding: %(witho)s
Sold: %(sold)s
Liquidated: %(liquid)s
Withheld: %(withe)s
Available: %(avail)s
"""
a = {
'name': self.name,
'veh': self.vehicle,
'n': comma(self.n_shares, dec=False, white=False),
'strike': self.strike_usd,
'vested': comma(self.vested(on), dec=False, white=False),
'witho': comma(self.withholding(on, rate), dec=False, white=False),
'sold': comma(self.sold, dec=False, white=False),
'liquid': comma(self.liquidated, dec=False, white=False),
'with': comma(self.withheld, dec=False, white=False),
'avail': comma(self.available(on), dec=False, white=False),
}
print(fmt%a)
def sell(self,
on,
n,
fmv_usd,
withholding_rate,
prefer_exercise=True,
update=False):
"""Provides the info about a sell at a given FMV.
on: date at which the sale happens (so we can check the vesting)
n: number of shares to sell
fmv_usd: sale price (fair market value)
If you would like to exercise/sell instead of selling held
stock, then you can set prefer_exercise to True (default). If
you'd prefer to sell any held stock first, set to logic low.
"""
vested = self.vested(on)
outstanding = self.vested_outstanding(on)
held = self.held()
available = self.available(on)
assert(available >= n)
sell_held = 0
sell_outstanding = 0
if prefer_exercise:
sell_outstanding = min(n, outstanding)
sell_held = n - sell_outstanding
else:
sell_held = min(n, held)
sell_outstanding = n - sell_held
if update:
self.sold += n
self.liquidated += n
self.exercised += sell_outstanding
cost = sell_outstanding * self.strike_usd
gross = n * fmv_usd
net = gross - cost
return {
'cost': cost,
'gross_usd': gross,
'net_usd': net,
'exercised': sell_outstanding,
}
class Position(object):
def __init__(self, grants):
self.grants = grants
gdict = {}
for g in grants: gdict[g.name] = g
self.tribute = {
'position_obj': self,
'grants_lst': grants,
'grants_dict': gdict,
'max_sellable_restricted_frac': 0.2,
'shares_sellable_n': self.shares_sellable,
'shares_sellable_restricted_n': self.shares_sellable_restricted,
'end_of_year': self.end_of_year,
'shares_vested_rsu_usd': self.shares_vested_rsu_usd,
}
self._add_summation_nodes('shares_total%s_n',
'n_shares')
self._add_summation_nodes('shares_exercised%s_n',
'exercised')
self._add_summation_nodes('shares_vested%s_n',
'vested',
*['query_date'])
self._add_summation_nodes('shares_vested%s_eoy_n',
'vested',
*['end_of_year'])
self._add_summation_nodes('shares_unvested%s_n',
'unvested',
*['query_date'])
self._add_summation_nodes('shares_vested_unliquidated%s_n',
'vested_unliquidated',
*['query_date'])
self._add_summation_nodes('shares_vested_outstanding%s_n',
'vested_outstanding',
*['query_date'])
self._add_summation_nodes('shares_outstanding%s_n',
'outstanding',
call=True)
self._add_summation_nodes('shares_held%s_n',
'held',
call=True)
self._add_summation_nodes('shares_previously_sold%s_n',
'sold',
call=False)
self._add_summation_nodes('shares_withheld%s_n',
'withheld',
call=False,
rem=True)
self._add_summation_nodes('exercise_cost_outstanding%s_usd',
'outstanding_cost',
call=True)
self._add_summation_nodes('exercise_cost_vested_outstanding%s_usd',
'vested_outstanding_cost',
*['query_date'])
self._add_summation_nodes('rem_shares_total%s_n',
'n_shares',
rem=True)
self._add_summation_nodes('rem_shares_exercised%s_n',
'exercised',
rem=True)
self._add_summation_nodes('rem_shares_vested%s_n',
'vested',
*['query_date'],
rem=True)
self._add_summation_nodes('rem_shares_unvested%s_n',
'unvested',
*['query_date'],
rem=True)
self._add_summation_nodes('rem_shares_vested_unliquidated%s_n',
'vested_unliquidated',
*['query_date'],
rem=True)
self._add_summation_nodes('rem_shares_vested_outstanding%s_n',
'vested_outstanding',
*['query_date'],
rem=True)
self._add_summation_nodes('rem_shares_outstanding%s_n',
'outstanding',
call=True,
rem=True)
self._add_summation_nodes('rem_shares_held%s_n',
'held',
call=True,
rem=True)
self._add_summation_nodes('rem_exercise_cost_outstanding%s_usd',
'outstanding_cost',
call=True,
rem=True)
self._add_summation_nodes('rem_exercise_cost_vested_outstanding%s_usd',
'vested_outstanding_cost',
*['query_date'],
rem=True)
def _add_summation_nodes(self, fmt, field, *args, call=False, rem=False):
"""Generate summation nodes for counting types of stock.
OK, this method is complicated. Because of python's
late-bindings, and the need to generate and register callable
objects, we use functools to create a curried outer function
which returns the callable object we actually want to register
when executed. The outer object takes in one parameter (the
vehicle) and generates a callable which takes in one parameter
(the model). Because the vehicle is what needs to be curried,
we use partial on the outer, not the inner. We then just
immediately call the outer function and register the resulting
inner function as the callback.
The fmt is the name of the node with a single %s which will be
given the vehicle
field is the attribute to reference in the grant object, which
can be callable (as indicated either with the call kwarg, or
by including a non-zero number of *args).
Since most of these will need to be duplicated for reviewing
the equity position after the sales are processed, we include
a convenience boolean for prepending 'rem_' and referencing
the post-sale list of grant objects.
"""
if len(args): call=True
# ohhh late bindings...
for typ in VEHICLES:
def __outer(typ):
def __inner(m):
if call:
cargs = [getattr(m, node) for node in args]
f = lambda g: getattr(g, field)(*cargs) if g.vehicle == typ else 0
else:
f = lambda g: getattr(g, field) if g.vehicle == typ else 0
lst = m.rem_grants_lst if rem else m.grants_lst
return sum(list(map(f, lst)))
return __inner
f = functools.partial(__outer, typ)()
self.tribute[fmt % ('_%s'%typ)] = f
# let's not forget the summation
def __tmp(m):
return sum([ getattr(m, fmt % ('_%s'%typ)) for typ in VEHICLES ])
self.tribute[fmt % ('')] = __tmp
def end_of_year(self, m):
d = parse_date(m.query_date)
yr = d.year
mon = 12
day = calendar.monthrange(d.year, d.month)[-1]
return datetime.datetime(year=yr, month=mon, day=day)
def shares_vested_rsu_usd(self, m):
return m.shares_vested_rsu_n * m.ipo_price_usd
def shares_sellable(self, m):
return (0
+ m.shares_sellable_restricted_n
+ m.total_rsu_n
+ 0 )
def shares_sellable_restricted(self, m):
n_considered = ( 0
+ m.shares_total_nso_n
- m.shares_previously_sold_nso_n
+ m.shares_total_iso_n
- m.shares_previously_sold_iso_n
+ 0 )
base = n_considered * m.max_sellable_restricted_frac
max_shares = int(math.floor(base))
available = ( 0
+ m.shares_vested_outstanding_iso_n
+ m.shares_vested_outstanding_nso_n
+ 0 )
return min(available, max_shares)