Skip to content

Commit 178e5d7

Browse files
committed
Adding split processing for zecco files (Issue 138)
1 parent 52e3520 commit 178e5d7

File tree

6 files changed

+103
-25
lines changed

6 files changed

+103
-25
lines changed

doc/2011-03-01-v0.9-migrations.sql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- Issue 138 - Zecco import failure - needed to expand symbol field
2+
3+
ALTER TABLE position
4+
MODIFY COLUMN symbol VARCHAR(10) NOT NULL
5+
;
6+
7+
ALTER TABLE quote
8+
MODIFY COLUMN symbol VARCHAR(10) NOT NULL
9+
;
10+
11+
ALTER TABLE transaction
12+
MODIFY COLUMN symbol VARCHAR(10) NOT NULL,
13+
MODIFY COLUMN linked_symbol VARCHAR(10)
14+
;
15+
16+
-- END SEGMENT

frano/positions/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
class Position(models.Model):
2424
portfolio = models.ForeignKey(Portfolio)
2525
as_of_date = models.DateField()
26-
symbol = models.CharField(max_length = 5)
26+
symbol = models.CharField(max_length = 10)
2727
quantity = models.FloatField()
2828
cost_price = models.FloatField()
2929
realized_pl = models.FloatField()

frano/quotes/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
#----------/
2424

2525
class Quote(models.Model):
26-
symbol = models.CharField(max_length = 5, unique = True)
26+
symbol = models.CharField(max_length = 10, unique = True)
2727
name = models.CharField(max_length = 255)
2828
price = models.FloatField()
2929
last_trade = models.DateTimeField()

frano/transactions/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ class Transaction(models.Model):
5858
portfolio = models.ForeignKey(Portfolio)
5959
type = models.CharField(max_length = 10, choices = TRANSACTION_TYPES)
6060
as_of_date = models.DateField()
61-
symbol = models.CharField(max_length = 5)
61+
symbol = models.CharField(max_length = 10)
6262
quantity = models.FloatField()
6363
price = models.FloatField()
6464
total = models.FloatField()
65-
linked_symbol = models.CharField(max_length = 5, null = True)
65+
linked_symbol = models.CharField(max_length = 10, null = True)
6666

6767
class Meta:
6868
db_table = 'transaction'

frano/transactions/parsers.py

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def parse_frano_transactions(reader):
4444
'price' : float(row[4]),
4545
'total' : float(row[5]),
4646
'linked_symbol' : (row[6] if row[6] != '' else None),
47-
});
47+
})
4848

4949
return parsed
5050

@@ -78,7 +78,7 @@ def parse_google_transactions(reader):
7878
'quantity' : quantity,
7979
'price' : price,
8080
'total' : ((quantity * price) + (commission_multiplier * commission)),
81-
});
81+
})
8282

8383
return parsed
8484

@@ -138,17 +138,19 @@ def parse_ameritrade_transactions(reader):
138138
'price' : price,
139139
'total' : ((quantity * price) + (commission_multiplier * commission)),
140140
'linked_symbol': linked_symbol,
141-
});
141+
})
142142

143143
return parsed
144144

145145
def parse_zecco_transactions(reader):
146+
split_map = { }
147+
146148
parsed = []
147149
for row in reader:
150+
as_of_date = datetime.strptime(row[0], '%m/%d/%Y').date()
148151
account_type = row[1]
149152
transaction_type = row[2]
150153
description_field = row[5]
151-
date_field = row[0]
152154
symbol_field = row[3]
153155
quantity_field = row[7]
154156
price_field = row[8]
@@ -187,6 +189,12 @@ def parse_zecco_transactions(reader):
187189
price = 1.0
188190
commission = 0.0
189191
linked_symbol = symbol_field
192+
193+
194+
# splits are processed after all the parsing is done, just record and skip them
195+
elif transaction_type in [ 'Security Journal' ] and description_field.endswith('SPLIT'):
196+
_record_split(split_map, as_of_date, symbol_field, float(quantity_field))
197+
continue
190198

191199
# otherwise just skip it for now
192200
else:
@@ -197,15 +205,26 @@ def parse_zecco_transactions(reader):
197205
commission_multiplier = -1.0
198206

199207
parsed.append({
200-
'date' : datetime.strptime(date_field, '%m/%d/%Y').date(),
208+
'date' : as_of_date,
201209
'type' : type,
202210
'symbol' : symbol,
203211
'quantity' : quantity,
204212
'price' : price,
205213
'total' : ((quantity * price) + (commission_multiplier * commission)),
206214
'linked_symbol': linked_symbol,
207-
});
208-
215+
})
216+
217+
# split processing - adjust price and quantity of all pre-split transactions
218+
# the double loop is intentional since a stock can split more than once, start processing by earliest date
219+
splits = [ split for sub_list in split_map.values() for split in sub_list ]
220+
for split in sorted(splits, key = (lambda split: split.as_of_date)):
221+
for transaction in parsed:
222+
if transaction.get('symbol') == split.from_symbol and transaction.get('date') <= split.as_of_date:
223+
factor = split.to_quantity / split.from_quantity
224+
transaction['symbol'] = split.to_symbol
225+
transaction['quantity'] = transaction.get('quantity') * factor
226+
transaction['price'] = transaction.get('price') / factor
227+
209228
return parsed
210229

211230
def parse_scottrade_transactions(reader):
@@ -247,7 +266,7 @@ def parse_scottrade_transactions(reader):
247266
'quantity' : (price * quantity),
248267
'price' : 1.0,
249268
'total' : (price * quantity),
250-
});
269+
})
251270

252271
symbol = symbol_field
253272
type = 'BUY'
@@ -274,7 +293,7 @@ def parse_scottrade_transactions(reader):
274293
'price' : price,
275294
'total' : ((quantity * price) + (commission_multiplier * commission)),
276295
'linked_symbol': linked_symbol,
277-
});
296+
})
278297

279298
return parsed
280299

@@ -320,7 +339,7 @@ def parse_charles_transactions(reader):
320339
'quantity' : (price * quantity),
321340
'price' : 1.0,
322341
'total' : (price * quantity),
323-
});
342+
})
324343

325344
type = 'BUY'
326345
commission = 0.0
@@ -346,7 +365,7 @@ def parse_charles_transactions(reader):
346365
'price' : price,
347366
'total' : ((quantity * price) + (commission_multiplier * commission)),
348367
'linked_symbol': linked_symbol,
349-
});
368+
})
350369

351370
return parsed
352371

@@ -403,7 +422,7 @@ def parse_fidelity_transactions(reader):
403422
'price' : price,
404423
'total' : total,
405424
'linked_symbol': linked_symbol,
406-
});
425+
})
407426

408427
return parsed
409428

@@ -435,7 +454,7 @@ def parse_mercer_401_transactions(reader):
435454
'quantity' : amount,
436455
'price' : 1.0,
437456
'total' : amount,
438-
});
457+
})
439458

440459
type = 'BUY'
441460

@@ -455,7 +474,7 @@ def parse_mercer_401_transactions(reader):
455474
'price' : 1.0,
456475
'total' : amount,
457476
'linked_symbol' : symbol,
458-
});
477+
})
459478

460479
type = ('BUY' if action == 'DIVIDEND' else 'SELL')
461480
quantity = abs(quantity)
@@ -472,10 +491,53 @@ def parse_mercer_401_transactions(reader):
472491
'price' : price,
473492
'total' : amount,
474493
'linked_symbol' : linked_symbol,
475-
});
494+
})
476495

477496
return parsed
478497

498+
#-----------------\
499+
# VALUE OBJECTS |
500+
#-----------------/
501+
502+
class Split:
503+
def __init__(self, as_of_date, symbol, quantity):
504+
self.as_of_date = as_of_date
505+
506+
if quantity > 0:
507+
self.to_symbol = symbol
508+
self.to_quantity = quantity
509+
510+
else:
511+
self.from_symbol = symbol
512+
self.from_quantity = abs(quantity)
513+
514+
def __repr__(self):
515+
return "Split: %.4f of %s to %.4f of %s" % (self.from_quantity, self.from_symbol, self.to_quantity, self.to_symbol)
516+
479517
#-------------------\
480518
# LOCAL FUNCTIONS |
481519
#-------------------/
520+
521+
def _record_split(split_map, as_of_date, symbol, quantity):
522+
splits_on_date = split_map.get(as_of_date, None)
523+
if splits_on_date == None:
524+
splits_on_date = [ Split(as_of_date, symbol, quantity) ]
525+
split_map[as_of_date] = splits_on_date
526+
527+
else:
528+
found = False
529+
for split in splits_on_date:
530+
if quantity > 0 and split.from_symbol != None and split.from_symbol.startswith(symbol):
531+
split.to_symbol = symbol
532+
split.to_quantity = quantity
533+
found = True
534+
break
535+
536+
elif split.to_symbol != None and symbol.startswith(split.to_symbol):
537+
split.from_symbol = symbol
538+
split.from_quantity = abs(quantity)
539+
found = True
540+
break
541+
542+
if not found:
543+
splits_on_date.append(Split(as_of_date, symbol, quantity))

frano/transactions/views.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def import_form(request, portfolio, is_sample, read_only):
188188
by_date_map.get(transaction.as_of_date).append(transaction)
189189

190190
for transaction in transactions:
191-
if len(transaction.symbol) < 1 or len(transaction.symbol) > 5:
191+
if len(transaction.symbol) < 1 or len(transaction.symbol) > 10:
192192
raise Exception("Invalid symbol: %s" % transaction.symbol)
193193

194194
is_duplicate = False
@@ -269,19 +269,19 @@ def request_import_type(request, portfolio, is_sample, read_only):
269269
class TransactionForm(forms.Form):
270270
type = forms.ChoiceField(choices = TRANSACTION_TYPES)
271271
as_of_date = forms.DateField()
272-
symbol = forms.CharField(min_length = 1, max_length = 5)
272+
symbol = forms.CharField(min_length = 1, max_length = 10)
273273
quantity = forms.FloatField()
274274
price = forms.FloatField(min_value = 0.01)
275275
commission = forms.FloatField(min_value = 0.01, required = False)
276276

277277
class UpdateTransactionForm(forms.Form):
278278
type = forms.ChoiceField(choices = TRANSACTION_TYPES, required = False)
279279
date = forms.DateField(required = False)
280-
symbol = forms.CharField(required = False, min_length = 1, max_length = 5)
280+
symbol = forms.CharField(required = False, min_length = 1, max_length = 10)
281281
quantity = forms.FloatField(required = False)
282282
price = forms.FloatField(required = False, min_value = 0.01)
283283
total = forms.FloatField(required = False)
284-
linkedsymbol = forms.CharField(required = False, max_length = 5) # underscore removed due to JS split issue
284+
linkedsymbol = forms.CharField(required = False, max_length = 10) # underscore removed due to JS split issue
285285

286286
def __init__(self, data):
287287
forms.Form.__init__(self, data)
@@ -313,11 +313,11 @@ class RequestImportForm(forms.Form):
313313
class ImportTransactionForm(forms.Form):
314314
type = forms.ChoiceField(choices = TRANSACTION_TYPES)
315315
as_of_date = forms.DateField()
316-
symbol = forms.CharField(min_length = 1, max_length = 5)
316+
symbol = forms.CharField(min_length = 1, max_length = 10)
317317
quantity = forms.FloatField()
318318
price = forms.FloatField(min_value = 0.01)
319319
total = forms.FloatField()
320-
linked_symbol = forms.CharField(max_length = 5, required = False)
320+
linked_symbol = forms.CharField(max_length = 10, required = False)
321321
exclude = forms.BooleanField(required = False)
322322

323323
ImportTransactionFormSet = formset_factory(ImportTransactionForm)

0 commit comments

Comments
 (0)