def setUp(self): tx1 = Trade( uniqueid="", datetime=datetime(2016, 1, 1), fiaccount=None, security=1, units=Decimal("100"), cash=Decimal("-1000"), currency="USD", ) self.lot1 = Lot( opentransaction=tx1, createtransaction=tx1, units=tx1.units, price=abs(tx1.cash / tx1.units), currency=tx1.currency, ) tx2 = Trade( uniqueid="", datetime=datetime(2016, 1, 1), fiaccount=None, security=2, units=Decimal("-300"), cash=Decimal("3600"), currency="USD", ) self.lot2 = Lot( opentransaction=tx2, createtransaction=tx2, units=tx2.units, price=abs(tx2.cash / tx2.units), currency=tx2.currency, ) self.portfolio = Portfolio({ (None, 1): [self.lot1], (None, 2): [self.lot2] })
def setUp(self): tx0 = Trade( datetime=datetime(2016, 1, 1), uniqueid="", fiaccount=None, security=None, units=Decimal("100"), cash=Decimal("-1000"), currency="USD", ) self.lot0 = Lot( opentransaction=tx0, createtransaction=tx0, units=tx0.units, price=abs(tx0.cash / tx0.units), currency=tx0.currency, ) tx1 = Trade( datetime=datetime(2016, 1, 3), uniqueid="", fiaccount=None, security=None, units=Decimal("300"), cash=Decimal("-3600"), currency="USD", ) self.lot1 = Lot( opentransaction=tx1, createtransaction=tx1, units=tx1.units, price=abs(tx1.cash / tx1.units), currency=tx1.currency, ) self.portfolio = Portfolio({(None, None): [self.lot0, self.lot1]})
class SpinoffTestCase(unittest.TestCase): def setUp(self): tx1 = Trade( uniqueid="", datetime=datetime(2016, 1, 1), fiaccount=None, security=1, units=Decimal("100"), cash=Decimal("-1000"), currency="USD", ) self.lot1 = Lot( opentransaction=tx1, createtransaction=tx1, units=tx1.units, price=abs(tx1.cash / tx1.units), currency=tx1.currency, ) tx2 = Trade( uniqueid="", datetime=datetime(2016, 1, 1), fiaccount=None, security=2, units=Decimal("-300"), cash=Decimal("3600"), currency="USD", ) self.lot2 = Lot( opentransaction=tx2, createtransaction=tx2, units=tx2.units, price=abs(tx2.cash / tx2.units), currency=tx2.currency, ) self.portfolio = Portfolio({ (None, 1): [self.lot1], (None, 2): [self.lot2] }) def testSpinoff(self): """ Spinoff divides cost and preserves holding period """ # 1 for 5 on 100sh: spin 20sh@5; retain 100sh@1 spinoff = Spinoff( uniqueid=None, datetime=datetime(2016, 1, 4), fiaccount=None, security=3, numerator=Decimal("1"), denominator=Decimal("5"), units=Decimal("20"), fromsecurity=1, securityprice=Decimal("5"), fromsecurityprice=Decimal("1"), ) gains = self.portfolio.book(spinoff) self.assertEqual(len(gains), 0) # Half the original cost is left in lot1; everything else is the same position1 = self.portfolio[(None, 1)] self.assertEqual(len(position1), 1) lot1 = position1[0] self.assertEqual(lot1, self.lot1._replace(price=5)) # Half the original cost of lot1 was spun off to security3 position3 = self.portfolio[(None, 3)] self.assertEqual(len(position3), 1) lot3 = position3[0] self.assertIs(lot3.opentransaction, self.lot1.opentransaction) self.assertIs(lot3.createtransaction, spinoff) self.assertEqual(lot3.units, spinoff.units) self.assertEqual(lot3.price, Decimal("25")) self.assertEqual(lot3.currency, self.lot1.currency) # Aggregate cost is conserved over the spinoff self.assertEqual( sum([l.units * l.price for l in position1]) + sum([l.units * l.price for l in position3]), self.lot1.units * self.lot1.price, ) def testSpinoffClose(self): """ Spinoff correctly closes oppositely-signed Lots of spin Security """ # 1 for 5 on 100sh: spin 20sh@5; retain 100sh@1 spinoff = Spinoff( datetime=datetime(2016, 1, 4), uniqueid=None, fiaccount=None, security=2, numerator=Decimal("1"), denominator=Decimal("5"), units=Decimal("20"), fromsecurity=1, securityprice=Decimal("5"), fromsecurityprice=Decimal("1"), ) gains = self.portfolio.book(spinoff) self.assertEqual(len(gains), 1) gain = gains.pop() lot = gain.lot # Half the original cost of lot1 was spun off; closed some of lot2 self.assertEqual(lot, self.lot2._replace(units=-20)) self.assertEqual(gain.transaction, spinoff) # Half the original cost is left in lot1 position1 = self.portfolio[(None, 1)] self.assertEqual(len(position1), 1) lot1 = position1[0] self.assertEqual(lot1, self.lot1._replace(price=5)) # The spunoff shares closed 20 units of security 2 position2 = self.portfolio[(None, 2)] self.assertEqual(len(position2), 1) lot2 = position2[0] self.assertEqual(lot2, self.lot2._replace(units=-280))
class TransferTestCase(unittest.TestCase): def setUp(self): tx1 = Trade( uniqueid="", fiaccount=None, datetime=datetime(2016, 1, 1), security=1, units=Decimal("100"), cash=Decimal("-1000"), currency="USD", ) self.lot1 = Lot( opentransaction=tx1, createtransaction=tx1, units=tx1.units, price=abs(tx1.cash / tx1.units), currency=tx1.currency, ) tx2 = Trade( uniqueid="", fiaccount=None, datetime=datetime(2016, 1, 1), security=2, units=Decimal("-300"), cash=Decimal("3600"), currency="USD", ) self.lot2 = Lot( opentransaction=tx2, createtransaction=tx2, units=tx2.units, price=abs(tx1.cash / tx1.units), currency="USD", ) self.portfolio = Portfolio({ (None, 1): [self.lot1], (None, 2): [self.lot2] }) def testTransfer(self): """ Transfer divides cost and preserves holding period """ transfer = Transfer( uniqueid="", fiaccount=None, fromfiaccount=None, datetime=datetime(2016, 1, 4), security=3, units=Decimal("100"), fromsecurity=1, fromunits=Decimal("-50"), ) gains = self.portfolio.book(transfer) self.assertEqual(len(gains), 0) # Half of lot1 units were transferred out; everything else is the same pos1 = self.portfolio[(None, 1)] self.assertEqual(len(pos1), 1) self.assertEqual(pos1[0], self.lot1._replace(units=50)) # Holding period was preserved pos3 = self.portfolio[(None, 3)] self.assertEqual(len(pos3), 1) lot = pos3[0] self.assertEqual(lot.opentransaction, self.lot1.opentransaction) self.assertEqual(lot.createtransaction, transfer) self.assertEqual(lot.units, transfer.units) self.assertEqual( lot.price, self.lot1.price * -transfer.fromunits / transfer.units) self.assertEqual(lot.currency, self.lot1.currency) def testTransferClose(self): """ Transfer correctly closes oppositely-signed Lots of transfer Security """ transfer = Transfer( uniqueid="", datetime=datetime(2016, 1, 4), fiaccount=None, fromfiaccount=None, security=2, units=Decimal("100"), fromsecurity=1, fromunits=Decimal("-50"), ) gains = self.portfolio.book(transfer) pos1 = self.portfolio[(None, 1)] self.assertEqual(len(pos1), 1) self.assertEqual(pos1[0], self.lot1._replace(units=50)) pos2 = self.portfolio[(None, 2)] self.assertEqual(len(pos2), 1) self.assertEqual(pos2[0], self.lot2._replace(units=-200)) self.assertEqual(len(gains), 1) gain = gains[0] self.assertEqual(gain.lot, self.lot2._replace(units=-100)) self.assertEqual(gain.transaction, transfer) def testTransferBadTransaction(self): """ Transactions that can't be satisfied raise errors. """ # fromunits and units must have opposite signs transfer = Transfer( uniqueid="", datetime=datetime(2016, 1, 4), fiaccount=None, fromfiaccount=None, security=2, units=Decimal("100"), fromsecurity=1, fromunits=Decimal("50"), ) with self.assertRaises(ValueError): self.portfolio.book(transfer) # Must have an existing position in (accountfrom, fromsecurity) transfer = Transfer( uniqueid="", datetime=datetime(2016, 1, 4), fiaccount=None, fromfiaccount=None, security=2, units=Decimal("100"), fromsecurity=3, fromunits=Decimal("-50"), ) with self.assertRaises(Inconsistent): self.portfolio.book(transfer) # Existing position must have enough units to satisfy fromunits transfer = Transfer( uniqueid="", datetime=datetime(2016, 1, 4), fiaccount=None, fromfiaccount=None, security=2, units=Decimal("100"), fromsecurity=1, fromunits=Decimal("-150"), ) with self.assertRaises(Inconsistent): self.portfolio.book(transfer)
class SplitTestCase(unittest.TestCase): def setUp(self): tx0 = Trade( datetime=datetime(2016, 1, 1), uniqueid="", fiaccount=None, security=None, units=Decimal("100"), cash=Decimal("-1000"), currency="USD", ) self.lot0 = Lot( opentransaction=tx0, createtransaction=tx0, units=tx0.units, price=abs(tx0.cash / tx0.units), currency=tx0.currency, ) tx1 = Trade( datetime=datetime(2016, 1, 3), uniqueid="", fiaccount=None, security=None, units=Decimal("300"), cash=Decimal("-3600"), currency="USD", ) self.lot1 = Lot( opentransaction=tx1, createtransaction=tx1, units=tx1.units, price=abs(tx1.cash / tx1.units), currency=tx1.currency, ) self.portfolio = Portfolio({(None, None): [self.lot0, self.lot1]}) def testSplitDatetime(self): """ Splits respect Lot.createdt not Lot.opendt """ split = Split( datetime=datetime(2016, 1, 2), uniqueid=None, fiaccount=None, security=None, numerator=Decimal("1"), denominator=Decimal("10"), units=Decimal("-90"), ) gains = self.portfolio.book(split) self.assertEqual(len(gains), 0) position = self.portfolio[(None, None)] self.assertEqual(len(position), 2) self.assertEqual(position[0], self.lot0._replace(units=10, price=100)) self.assertEqual(position[1], self.lot1) def testSplitWrongUnits(self): """ Split.units must match total Lot.units before Split.datetime """ split = Split( datetime=datetime(2016, 1, 2), uniqueid="", fiaccount=None, security=None, numerator=Decimal("1"), denominator=Decimal("10"), units=Decimal("90"), ) with self.assertRaises(Inconsistent): self.portfolio.book(split)
def setUp(self): tx1 = Trade( uniqueid="", datetime=datetime(2016, 1, 1), fiaccount="", security="", units=Decimal("100"), cash=Decimal("1000"), currency="USD", ) self.lot1 = Lot( opentransaction=tx1, createtransaction=tx1, units=tx1.units, price=abs(tx1.cash / tx1.units), currency=tx1.currency, ) tx2 = Trade( uniqueid="", datetime=datetime(2016, 1, 2), fiaccount="", security="", units=Decimal("200"), cash=Decimal("2200"), currency="USD", ) self.lot2 = Lot( opentransaction=tx2, createtransaction=tx2, units=tx2.units, price=abs(tx2.cash / tx2.units), currency=tx2.currency, ) tx3 = Trade( uniqueid="", datetime=datetime(2016, 1, 1), fiaccount="", security="", units=Decimal("300"), cash=Decimal("3600"), currency="USD", ) tx3c = Trade( uniqueid="", datetime=datetime(2016, 1, 3), fiaccount="", security="", cash=None, currency=None, units=None, ) self.lot3 = Lot( opentransaction=tx3, createtransaction=tx3c, units=tx3.units, price=abs(tx3.cash / tx3.units), currency=tx3.currency, ) self.portfolio = Portfolio({ (None, None): [self.lot1, self.lot2, self.lot3] })
class ReturnOfCapitalTestCase(unittest.TestCase): def setUp(self): tx1 = Trade( uniqueid="", datetime=datetime(2016, 1, 1), fiaccount="", security="", units=Decimal("100"), cash=Decimal("1000"), currency="USD", ) self.lot1 = Lot( opentransaction=tx1, createtransaction=tx1, units=tx1.units, price=abs(tx1.cash / tx1.units), currency=tx1.currency, ) tx2 = Trade( uniqueid="", datetime=datetime(2016, 1, 2), fiaccount="", security="", units=Decimal("200"), cash=Decimal("2200"), currency="USD", ) self.lot2 = Lot( opentransaction=tx2, createtransaction=tx2, units=tx2.units, price=abs(tx2.cash / tx2.units), currency=tx2.currency, ) tx3 = Trade( uniqueid="", datetime=datetime(2016, 1, 1), fiaccount="", security="", units=Decimal("300"), cash=Decimal("3600"), currency="USD", ) tx3c = Trade( uniqueid="", datetime=datetime(2016, 1, 3), fiaccount="", security="", cash=None, currency=None, units=None, ) self.lot3 = Lot( opentransaction=tx3, createtransaction=tx3c, units=tx3.units, price=abs(tx3.cash / tx3.units), currency=tx3.currency, ) self.portfolio = Portfolio({ (None, None): [self.lot1, self.lot2, self.lot3] }) def testRetOfCapBasic(self): """ Test ReturnOfCapital less than basis reduces cost without gain """ transaction = ReturnOfCapital( uniqueid="a", datetime=datetime(2016, 1, 4), fiaccount=None, security=None, cash=Decimal("600"), currency="USD", ) gains = self.portfolio.book(transaction) self.assertEqual(len(gains), 0) position = self.portfolio[(None, None)] self.assertEqual(position[0], self.lot1._replace(price=Decimal("9"))) self.assertEqual(position[1], self.lot2._replace(price=Decimal("10"))) self.assertEqual(position[2], self.lot3._replace(price=Decimal("11"))) def testRetOfCapDatetime(self): """ Test ReturnOfCapital respects Lot.createdt """ transaction = ReturnOfCapital( uniqueid="a", datetime=datetime(2016, 1, 2, 1), fiaccount=None, security=None, cash=Decimal("600"), currency="USD", ) gains = self.portfolio.book(transaction) self.assertEqual(len(gains), 0) position = self.portfolio[(None, None)] self.assertEqual(position[0], self.lot1._replace(price=Decimal("8"))) self.assertEqual(position[1], self.lot2._replace(price=Decimal("9"))) # Lot 3 has opendt before the ReturnOfCapital.datetime, but createdt # afterwards - it should follow the createdt and NOT reduce basis self.assertEqual(position[2], self.lot3._replace(price=Decimal("12"))) def testRetOfCapZero(self): """ Test ReturnOfCapital reduce basis to zero """ transaction = ReturnOfCapital( uniqueid="a", datetime=datetime(2016, 1, 4), fiaccount=None, security=None, cash=Decimal("6000"), currency="USD", ) gains = self.portfolio.book(transaction) # Lot 1 cost should be reduced to zero but no Gain generated self.assertEqual(len(gains), 0) position = self.portfolio[(None, None)] self.assertEqual(position[0], self.lot1._replace(price=Decimal("0"))) self.assertEqual(position[1], self.lot2._replace(price=Decimal("1"))) self.assertEqual(position[2], self.lot3._replace(price=Decimal("2"))) def testRetOfCapLessThanZero(self): """ Test ReturnOfCapital in excess of cost basis """ transaction = ReturnOfCapital( uniqueid="a", datetime=datetime(2016, 1, 4), fiaccount=None, security=None, cash=Decimal("7200"), currency="USD", ) gains = self.portfolio.book(transaction) # Lot 1 & 2 cost should be reduced to zero with Gain generated position = self.portfolio[(None, None)] self.assertEqual(position[0], self.lot1._replace(price=Decimal("0"))) self.assertEqual(position[1], self.lot2._replace(price=Decimal("0"))) # Lot 3 cost should be reduced to zero with no Gain generated self.assertEqual(position[2], self.lot3._replace(price=Decimal("0"))) # Gains should store Lot instances with full price (not reduced # for the ReturnOfCapital) self.assertEqual(len(gains), 2) gain0 = gains[0] self.assertEqual(gain0.lot, self.lot1) self.assertEqual(gain0.transaction, transaction) self.assertEqual(gain0.price, Decimal("12")) # $7,200 over 600sh gain1 = gains[1] self.assertEqual(gain1.lot, self.lot2) self.assertEqual(gain1.transaction, transaction) self.assertEqual(gain1.price, Decimal("12")) # $7,200 over 600sh self.assertEqual(gain1.lot.price, Decimal("11")) def testRetOfCapMultiplePositions(self): """ Test Portfolio route ReturnOfCapital to correct (account, security) """ tx4 = Trade( uniqueid="", datetime=datetime(2016, 1, 1), fiaccount="", security="", units=Decimal("100"), cash=Decimal("1000"), currency="USD", ) lot4 = Lot( opentransaction=tx4, createtransaction=tx4, units=tx4.units, price=abs(tx4.cash / tx4.units), currency=tx4.currency, ) tx5 = Trade( uniqueid="", datetime=datetime(2016, 1, 2), fiaccount="", security="", units=Decimal("200"), cash=Decimal("2200"), currency="USD", ) lot5 = Lot( opentransaction=tx5, createtransaction=tx5, units=tx5.units, price=abs(tx5.cash / tx5.units), currency=tx5.currency, ) tx6 = Trade( uniqueid="", datetime=datetime(2016, 1, 2), fiaccount="", security="", units=Decimal("300"), cash=Decimal("3600"), currency="USD", ) lot6 = Lot( opentransaction=tx6, createtransaction=tx6, units=tx6.units, price=abs(tx6.cash / tx6.units), currency=tx6.currency, ) self.portfolio[(None, "sec4")].append(lot4) self.portfolio[("acct5", "sec5")].append(lot5) self.portfolio[("acct6", None)].append(lot6) # This routes to (None, None) - [self.lot1, self.lot2, self.lot3]; 600sh retofcap1 = ReturnOfCapital( uniqueid="", fiaccount=None, security=None, datetime=datetime(2016, 6, 1), cash=Decimal("1200"), currency="USD", ) # This routes to ('acct6', None) - lot6; 300sh retofcap2 = ReturnOfCapital( uniqueid="", fiaccount="acct6", security=None, datetime=datetime(2016, 6, 1), cash=Decimal("600"), currency="USD", ) # This routes to (None, 'sec4') - lot4; 100sh retofcap3 = ReturnOfCapital( uniqueid="", fiaccount=None, security="sec4", datetime=datetime(2016, 6, 1), cash=Decimal("300"), currency="USD", ) # This routes to ('acct5', 'sec5') - lot5; 200sh retofcap4 = ReturnOfCapital( uniqueid="", fiaccount="acct5", security="sec5", datetime=datetime(2016, 6, 1), cash=Decimal("800"), currency="USD", ) for retofcap in (retofcap1, retofcap2, retofcap3, retofcap4): self.portfolio.book(retofcap) position = self.portfolio[(None, None)] self.assertEqual(len(position), 3) self.assertEqual(position[0], self.lot1._replace(price=Decimal("8"))) self.assertEqual(position[1], self.lot2._replace(price=Decimal("9"))) self.assertEqual(position[2], self.lot3._replace(price=Decimal("10"))) position = self.portfolio[(None, "sec4")] self.assertEqual(len(position), 1) self.assertEqual(position[0], lot4._replace(price=Decimal("7"))) position = self.portfolio[("acct5", "sec5")] self.assertEqual(len(position), 1) self.assertEqual(position[0], lot5._replace(price=Decimal("7"))) position = self.portfolio[("acct6", None)] self.assertEqual(len(position), 1) self.assertEqual(position[0], lot6._replace(price=Decimal("10")))
def _testTradeSort(self, sort, matchedTrades, partialClose): """ Args: sort: FIFO/LIFO/MAXGAIN/MINGAIN/None/list of Trade.ids matchedTrades: tuple of (open index, close index, units) partialClose: tuple of (index, units) """ # Predict the Gains that will be generated by booking the Transactions def matchTrades(indexopen, indexclose, units): opentx = self.trades[indexopen] closetx = self.trades[indexclose] lot = Lot( opentransaction=opentx, createtransaction=opentx, units=units, price=abs(opentx.cash / opentx.units), currency=opentx.currency, ) return Gain(lot=lot, transaction=closetx, price=abs(closetx.cash / closetx.units)) testGains = [ matchTrades(*matchedTrade) for matchedTrade in matchedTrades ] # Book the trades and collect the Gains portfolio = Portfolio() gains = [] for t in self.trades: g = portfolio.book(t, sort=sort) gains.extend(g) testGains.sort(key=lambda x: str(x.lot.opentransaction.uniqueid)) gains.sort(key=lambda x: str(x.lot.opentransaction.uniqueid) + str( x.transaction.uniqueid)) # Generated Gains should match prediction self.assertEqual(len(gains), len(testGains)) for i, gain in enumerate(gains): testGain = testGains[i] self.assertEqual(gain.lot.opentransaction, testGain.lot.opentransaction) self.assertEqual(gain.lot.createtransaction, testGain.lot.createtransaction) self.assertEqual(gain.lot.units, testGain.lot.units) self.assertEqual(gain.lot.price, testGain.lot.price) self.assertEqual(gain.lot.currency, testGain.lot.currency) self.assertEqual(gain.transaction, testGain.transaction) # The rest of the trades up to the covering buys remain open testLots = [] for i in range(0, 22): t = self.trades[i] testLots.append( Lot( opentransaction=t, createtransaction=t, units=t.units, price=abs(t.cash / t.units), currency=t.currency, )) indices = list({matchedTrade[0] for matchedTrade in matchedTrades}) indices.sort(reverse=True) for i in indices: del testLots[i] partialindex, partialunits = partialClose partial = self.trades[partialindex] testLots.append( Lot( opentransaction=partial, createtransaction=partial, units=partialunits, price=abs(partial.cash / partial.units), currency=partial.currency, )) testLots.sort(**FIFO) position = portfolio[(None, None)] position.sort(**FIFO) self.assertEqual(len(position), len(testLots)) for i, lot in enumerate(position): testLot = testLots[i] self.assertEqual(lot, testLot)