Skip to content

Commit 4558b07

Browse files
committed
Position cost basis will not include commissions, so will realized and unrealized P/L. (Issue 147)
1 parent 13ab4c4 commit 4558b07

File tree

4 files changed

+128
-99
lines changed

4 files changed

+128
-99
lines changed

doc/2011-04-02-v0.11-migrations.sql

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ DELETE FROM tax_lot;
55
DELETE FROM position;
66

77
ALTER TABLE tax_lot
8-
CHANGE COLUMN cost_price price double precision NOT NULL,
98
MODIFY COLUMN as_of_date date,
9+
CHANGE COLUMN cost_price price double precision NOT NULL,
1010
MODIFY COLUMN quantity double precision NOT NULL,
11-
ADD COLUMN sold_as_of_date date AFTER price,
11+
ADD COLUMN total double precision NOT NULL,
12+
ADD COLUMN sold_as_of_date date AFTER total,
1213
MODIFY COLUMN sold_quantity double precision NOT NULL,
1314
MODIFY COLUMN sold_price double precision NOT NULL,
15+
ADD COLUMN sold_total double precision NOT NULL,
1416
RENAME lot
1517
;
1618

@@ -22,4 +24,8 @@ ADD INDEX lot_as_of_date (as_of_date),
2224
ADD INDEX lot_sold_as_of_date (sold_as_of_date)
2325
;
2426

27+
ALTER TABLE position
28+
ADD column cost_basis double precision NOT NULL after cost_price
29+
;
30+
2531
-- END SEGMENT

frano/positions/models.py

Lines changed: 113 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class Position(models.Model):
2727
symbol = models.CharField(max_length = 10)
2828
quantity = models.FloatField()
2929
cost_price = models.FloatField()
30+
cost_basis = models.FloatField()
3031
realized_pl = models.FloatField()
3132

3233
class Meta:
@@ -40,21 +41,25 @@ class Lot(models.Model):
4041
as_of_date = models.DateField(null = True)
4142
quantity = models.FloatField()
4243
price = models.FloatField()
44+
total = models.FloatField()
4345
sold_as_of_date = models.DateField(null = True)
4446
sold_quantity = models.FloatField()
4547
sold_price = models.FloatField()
48+
sold_total = models.FloatField()
4649

4750
class Meta:
4851
db_table = 'lot'
4952

5053
def __unicode__(self):
51-
return "Lot: Bought %.4f @ %.4f on %s, Sold %.4f @ %.4f on %s " % (
54+
return "Lot: Bought %.4f @ %.4f on %s (%.4f), Sold %.4f @ %.4f on %s (%.4f)" % (
5255
self.quantity,
5356
self.price,
5457
self.as_of_date.strftime('%m/%d/%Y') if self.as_of_date != None else None,
58+
self.total,
5559
self.sold_quantity,
5660
self.sold_price,
5761
self.sold_as_of_date.strftime('%m/%d/%Y') if self.sold_as_of_date != None else None,
62+
self.sold_total
5863
)
5964

6065
def __cmp__(self, other):
@@ -79,13 +84,12 @@ def latest_positions(portfolio):
7984
return []
8085

8186
def decorate_position_with_prices(position, price, previous_price):
82-
"""Decorate the given position with various pieces of data that require pricing (p/l, cost_basis, market_value)"""
87+
"""Decorate the given position with various pieces of data that require pricing (p/l, market_value)"""
8388

8489
position.price = price
8590
position.previous_price = previous_price
8691

8792
position.market_value = position.quantity * position.price
88-
position.cost_basis = position.quantity * position.cost_price
8993
position.previous_market_value = position.quantity * position.previous_price
9094
position.pl = (position.market_value - position.cost_basis)
9195
position.pl_percent = (((position.pl / position.cost_basis) * 100) if position.cost_basis != 0 else 0)
@@ -109,100 +113,115 @@ def refresh_positions(portfolio, transactions = None, force = False):
109113

110114
class LotBuilder:
111115
def __init__(self):
112-
self.long_lots = []
113-
self.short_lots = []
116+
self.long_half_lots = []
117+
self.short_half_lots = []
114118
self.closed_lots = []
115119

116120
def __repr__(self):
117121
return "Open (Long):\n %s\n\nOpen (Short):\n %s\n\nClosed:\n %s" % (self.long_lots, self.short_lots, self.closed_lots)
118122

119123
def add_transaction(self, transaction):
120-
if transaction.type == 'BUY':
121-
quantity_to_buy = transaction.quantity
122-
while quantity_to_buy > 0 and len(self.short_lots) > 0:
123-
lot = self.short_lots.pop(0)
124-
if (quantity_to_buy - lot.sold_quantity) > -0.000001:
125-
lot.as_of_date = transaction.as_of_date
126-
lot.quantity = lot.sold_quantity
127-
lot.price = transaction.price
128-
129-
insort(self.closed_lots, lot)
130-
quantity_to_buy = quantity_to_buy - lot.quantity
131-
132-
else:
133-
new_lot = Lot(as_of_date = transaction.as_of_date,
134-
quantity = quantity_to_buy,
135-
price = transaction.price,
136-
sold_as_of_date = lot.sold_as_of_date,
137-
sold_quantity = quantity_to_buy,
138-
sold_price = lot.sold_price
139-
)
140-
141-
lot.sold_quantity = lot.sold_quantity - quantity_to_buy
142-
self.short_lots.insert(0, lot)
143-
insort(self.closed_lots, new_lot)
144-
quantity_to_buy = 0
145-
146-
if quantity_to_buy > 0.000001:
147-
self.long_lots.append(
148-
Lot(as_of_date = transaction.as_of_date,
149-
quantity = quantity_to_buy,
150-
price = transaction.price,
151-
sold_quantity = 0,
152-
sold_price = 0,
153-
)
154-
)
155-
156-
elif transaction.type == 'SELL':
157-
quantity_to_sell = transaction.quantity
158-
while quantity_to_sell > 0 and len(self.long_lots) > 0:
159-
lot = self.long_lots.pop(0)
160-
if (quantity_to_sell - lot.quantity) > -0.000001:
161-
lot.sold_as_of_date = transaction.as_of_date
162-
lot.sold_quantity = lot.quantity
163-
lot.sold_price = transaction.price
164-
165-
insort(self.closed_lots, lot)
166-
quantity_to_sell = quantity_to_sell - lot.sold_quantity
167-
168-
else:
169-
new_lot = Lot(as_of_date = lot.as_of_date,
170-
quantity = quantity_to_sell,
171-
price = lot.price,
172-
sold_as_of_date = transaction.as_of_date,
173-
sold_quantity = quantity_to_sell,
174-
sold_price = transaction.price
175-
)
176-
lot.quantity = lot.quantity - quantity_to_sell
177-
self.long_lots.insert(0, lot)
178-
insort(self.closed_lots, new_lot)
179-
quantity_to_sell = 0
124+
fees = transaction.total - (transaction.quantity * transaction.price)
125+
quantity = transaction.quantity
126+
poll = self.short_half_lots
127+
push = self.long_half_lots
128+
if transaction.type == 'SELL':
129+
push = poll
130+
poll = self.long_half_lots
180131

181-
if quantity_to_sell > 0.000001:
182-
self.short_lots.append(
183-
Lot(quantity = 0,
184-
price = 0,
185-
sold_as_of_date = transaction.as_of_date,
186-
sold_quantity = quantity_to_sell,
187-
sold_price = transaction.price
188-
)
189-
)
190-
132+
while quantity > 0 and len(poll) > 0:
133+
half_lot = poll.pop(0)
134+
if (quantity - half_lot.quantity) > -QUANTITY_TOLERANCE:
135+
partial_fees = fees * (half_lot.quantity / quantity)
136+
fees = fees - partial_fees
137+
quantity -= half_lot.quantity
138+
insort(self.closed_lots, half_lot.close(transaction.as_of_date, transaction.price, partial_fees))
139+
140+
else:
141+
closed = half_lot.partial_close(transaction.as_of_date, quantity, transaction.price, fees)
142+
poll.insert(0, half_lot)
143+
insort(self.closed_lots, closed)
144+
quantity = 0
145+
fees = 0
146+
147+
if quantity > QUANTITY_TOLERANCE:
148+
push.append(HalfLot(type = transaction.type,
149+
as_of_date = transaction.as_of_date,
150+
quantity = quantity,
151+
price = transaction.price,
152+
total = (quantity * transaction.price) + fees
153+
))
154+
191155
return self
192156

193157
def get_lots(self):
194158
out = []
195159
for lot in self.closed_lots:
196160
insort(out, _clone_lot(lot))
197161

198-
for lot in self.long_lots:
199-
insort(out, _clone_lot(lot))
200-
201-
for lot in self.short_lots:
202-
insort(out, _clone_lot(lot))
162+
for half_lot in (self.long_half_lots + self.short_half_lots):
163+
insort(out, half_lot.to_lot(None, 0.0, 0.0, 0.0))
203164

204165
return out
205166

167+
class HalfLot():
168+
def __init__(self, type, as_of_date, quantity, price, total):
169+
self.type = type
170+
self.as_of_date = as_of_date
171+
self.quantity = quantity
172+
self.price = price
173+
self.total = total
174+
175+
def __repr__(self):
176+
return "%s: %.4f @ %.4f on %s (%.4f)" % (
177+
self.type,
178+
self.quantity,
179+
self.price,
180+
self.as_of_date.strftime('%m/%d/%Y'),
181+
self.total,
182+
)
183+
184+
def close(self, as_of_date, price, fees):
185+
return self.to_lot(as_of_date, self.quantity, price, fees)
186+
187+
def partial_close(self, as_of_date, quantity, price, fees):
188+
split_lot = HalfLot(type = self.type,
189+
as_of_date = self.as_of_date,
190+
quantity = quantity,
191+
price = self.price,
192+
total = None,
193+
)
194+
195+
split_lot.total = self.total * (split_lot.quantity / self.quantity)
196+
self.total -= split_lot.total
197+
self.quantity -= quantity
198+
return split_lot.close(as_of_date, price, fees)
199+
200+
def to_lot(self, as_of_date, quantity, price, fees):
201+
lot = None
202+
if self.type == 'BUY':
203+
lot = Lot(as_of_date = self.as_of_date,
204+
quantity = self.quantity,
205+
price = self.price,
206+
total = self.total,
207+
sold_as_of_date = as_of_date,
208+
sold_quantity = quantity,
209+
sold_price = price,
210+
sold_total = (quantity * price) + fees,
211+
)
212+
else:
213+
lot = Lot(as_of_date = as_of_date,
214+
quantity = quantity,
215+
price = price,
216+
total = (quantity * price) + fees,
217+
sold_as_of_date = self.as_of_date,
218+
sold_quantity= self.quantity,
219+
sold_price = self.price,
220+
sold_total = self.total,
221+
)
222+
223+
return lot
224+
206225
#-------------------\
207226
# LOCAL FUNCTIONS |
208227
#-------------------/
@@ -226,6 +245,7 @@ def _refresh_positions_from_transactions(transactions):
226245
symbol = CASH_SYMBOL,
227246
quantity = 0,
228247
cost_price = 1.0,
248+
cost_basis = 0.0,
229249
realized_pl = 0.0
230250
)
231251

@@ -256,7 +276,9 @@ def _refresh_positions_from_transactions(transactions):
256276
builder.add_transaction(transaction)
257277

258278
# add current cash to positions.
259-
positions.append(_clone_position(cash, date))
279+
days_cash = _clone_position(cash, date)
280+
days_cash.cost_basis = days_cash.quantity
281+
positions.append(days_cash)
260282

261283
# compose current lots into a position.
262284
lots = []
@@ -266,7 +288,8 @@ def _refresh_positions_from_transactions(transactions):
266288
symbol = symbol,
267289
quantity = 0.0,
268290
cost_price = 0.0,
269-
realized_pl = 0.0
291+
cost_basis = 0.0,
292+
realized_pl = 0.0,
270293
)
271294

272295
for lot in builder.get_lots():
@@ -278,12 +301,13 @@ def _refresh_positions_from_transactions(transactions):
278301
quantity = 0.0
279302

280303
if abs(quantity) > QUANTITY_TOLERANCE:
304+
position.cost_basis += (lot.total - lot.sold_total)
281305
total = (position.quantity * position.cost_price) + (quantity * lot.price)
282306
position.quantity += quantity
283307
position.cost_price = (total / position.quantity if quantity <> 0.0 else 0.0)
284-
285-
if abs(lot.sold_quantity) > QUANTITY_TOLERANCE:
286-
position.realized_pl += (lot.sold_quantity * (lot.sold_price - lot.price))
308+
309+
else:
310+
position.realized_pl += lot.sold_total - lot.total
287311

288312
if abs(position.quantity) < QUANTITY_TOLERANCE:
289313
position.quantity = 0.0
@@ -309,9 +333,11 @@ def _clone_lot(lot):
309333
return Lot(as_of_date = lot.as_of_date,
310334
quantity = lot.quantity,
311335
price = lot.price,
336+
total = lot.total,
312337
sold_as_of_date = lot.sold_as_of_date,
313338
sold_quantity = lot.sold_quantity,
314-
sold_price = lot.sold_price
339+
sold_price = lot.sold_price,
340+
sold_total = lot.sold_total
315341
)
316342

317343
def _clone_position(position, new_as_of_date = None):
@@ -323,4 +349,4 @@ def _clone_position(position, new_as_of_date = None):
323349
out.cost_price = position.cost_price
324350
out.realized_pl = position.realized_pl
325351

326-
return out;
352+
return out

frano/positions/templates/positions.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@
159159
<td class="center">{{ lot.as_of_date|date:'m/d/Y' }}</td>
160160
<td>{{ lot.quantity|num_format:"4" }}</td>
161161
<td>${{ lot.price|num_format }}</td>
162-
<td>${{ lot.buy_total|num_format }}</td>
162+
<td>${{ lot.total|num_format }}</td>
163163
<td class="center">{{ lot.sold_as_of_date|date:'m/d/Y' }}</td>
164164
<td>{{ lot.sold_quantity|num_format:"4" }}</td>
165165
<td>${{ lot.sold_price|num_format }}</td>
166-
<td>${{ lot.sell_total|num_format }}</td>
166+
<td>${{ lot.sold_total|num_format }}</td>
167167
<td class="{{ lot.pl|sign_choice:'pos,neg,' }}">${{ lot.pl|num_format }}</td>
168168
<td class="{{ lot.pl|sign_choice:'pos,neg,' }}">{{ lot.pl_percent|floatformat:2 }}%</td>
169169
</tr>

frano/positions/views.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,20 +156,17 @@ def _decorate_positions_with_lots(positions):
156156
for position in positions:
157157
lots = []
158158
for lot in position.lot_set.order_by('-as_of_date'):
159-
lot.buy_total = lot.price * lot.quantity
160-
lot.sell_total = lot.sold_price * lot.sold_quantity
161-
total = max(lot.buy_total, lot.sell_total)
159+
total = max(lot.total, lot.sold_total)
162160

163161
days_open = (datetime.now().date() - (lot.as_of_date if lot.as_of_date != None else lot.sold_as_of_date)).days
164162
if abs(lot.quantity - lot.sold_quantity) < 0.0001:
165163
lot.status = 'Closed'
166-
lot.pl = lot.sold_quantity * (lot.sold_price - lot.price)
164+
lot.pl = lot.sold_total - lot.total
167165
else:
168166
lot.status = 'Open'
169-
open_price = (lot.price if lot.quantity > lot.sold_quantity else lot.sold_price)
170-
lot.pl = abs(lot.quantity - lot.sold_quantity) * (position.price - open_price)
167+
lot.pl = ((lot.quantity - lot.sold_quantity) * position.price) - (lot.total - lot.sold_total)
171168

172-
lot.pl_percent = ((lot.pl / total) if total <> 0.0 else 0)
169+
lot.pl_percent = (((lot.pl / total) * 100) if total <> 0.0 else 0)
173170
lots.append(lot)
174171

175172
position.lots = lots
@@ -191,7 +188,7 @@ def _get_summary(positions, transactions):
191188
previous_market_value = 0
192189
for position in positions:
193190
market_value += position.market_value
194-
cost_basis += position.cost_price * position.quantity
191+
cost_basis += position.cost_basis
195192
realized_pl += position.realized_pl
196193
previous_market_value += position.previous_market_value
197194

0 commit comments

Comments
 (0)