@@ -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
8186def 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
110114class 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 \n Open (Short):\n %s\n \n Closed:\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
317343def _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
0 commit comments