def test_shortSaleAndCover(self) -> None: ts = self.activityByDate[date(2018, 1, 2)] self.assertEqual(len(ts), 2) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Stock("HD", Currency.USD), quantity=Decimal("-6"), amount=Cash(currency=Currency.USD, quantity=Decimal("1017.3")), fees=Cash(currency=Currency.USD, quantity=Decimal("4.96")), flags=TradeFlags.OPEN, ), ) self.assertEqual( ts[1], Trade( date=ts[1].date, instrument=Stock("HD", Currency.USD), quantity=Decimal("6"), amount=Cash(currency=Currency.USD, quantity=Decimal("-1033.12")), fees=Cash(currency=Currency.USD, quantity=Decimal("4.95")), flags=TradeFlags.CLOSE, ), )
def test_reinvestShares(self) -> None: ts = self.activityByDate[date(2017, 11, 9)] self.assertEqual( ts[3], Trade( date=ts[3].date, instrument=Stock("ROBO", Currency.USD), quantity=Decimal("0.234"), amount=Cash(currency=Currency.USD, quantity=Decimal("-6.78")), fees=Cash(currency=Currency.USD, quantity=Decimal("0.00")), flags=TradeFlags.OPEN | TradeFlags.DRIP, ), )
def test_securityTransferSale(self) -> None: ts = self.activityByDate[date(2018, 1, 4)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Stock("MSFT", Currency.USD), quantity=Decimal("-10"), amount=Cash(currency=Currency.USD, quantity=Decimal("920.78")), fees=Cash(currency=Currency.USD, quantity=Decimal("13.65")), flags=TradeFlags.CLOSE, ), )
def test_reinvestShares(self) -> None: ts = self.activityByDate[date(2017, 3, 29)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Stock("VOO", Currency.USD), quantity=Decimal("0.1062"), amount=Cash(currency=Currency.USD, quantity=Decimal("-22.95")), fees=Cash(currency=Currency.USD, quantity=Decimal(0)), flags=TradeFlags.OPEN | TradeFlags.DRIP, ), )
def test_redeemBond(self) -> None: ts = self.activityByDate[date(2018, 6, 2)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Bond(symbol="912586AC5", currency=Currency.USD), quantity=Decimal("-10000"), amount=Cash(currency=Currency.USD, quantity=Decimal("10000")), fees=Cash(currency=Currency.USD, quantity=Decimal(0)), flags=TradeFlags.CLOSE | TradeFlags.EXPIRED, ), )
def test_reinvestShares(self) -> None: ts = self.activityByDate[date(2017, 2, 4)] self.assertEqual(len(ts), 4) self.assertEqual( ts[0], CashPayment( date=ts[0].date, instrument=Stock("VWO", Currency.USD), proceeds=helpers.cashUSD(Decimal("29.35")), ), ) self.assertEqual( ts[1], Trade( date=ts[1].date, instrument=Stock("VWO", Currency.USD), quantity=Decimal("0.123"), amount=Cash(currency=Currency.USD, quantity=Decimal("-20.15")), fees=Cash(currency=Currency.USD, quantity=Decimal("0.00")), flags=TradeFlags.OPEN | TradeFlags.DRIP, ), ) self.assertEqual( ts[3], Trade( date=ts[3].date, instrument=Stock("VOO", Currency.USD), quantity=Decimal("0.321"), amount=Cash(currency=Currency.USD, quantity=Decimal("-17.48")), fees=Cash(currency=Currency.USD, quantity=Decimal("0.00")), flags=TradeFlags.OPEN | TradeFlags.DRIP, ), )
def test_buyBond(self) -> None: ts = self.activityByDate[date(2018, 3, 25)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Bond(symbol="912586AC5", currency=Currency.USD), quantity=Decimal("10000"), amount=Cash(currency=Currency.USD, quantity=Decimal("-9956.80")), fees=Cash(currency=Currency.USD, quantity=Decimal(0)), flags=TradeFlags.OPEN, ), )
def test_buySecurity(self) -> None: ts = self.activityByDate[date(2016, 4, 20)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Stock("VTI", Currency.USD), quantity=Decimal("12"), amount=Cash(currency=Currency.USD, quantity=Decimal("-3456.78")), fees=Cash(currency=Currency.USD, quantity=Decimal("0.00")), flags=TradeFlags.OPEN, ), )
def test_buySecurity(self) -> None: ts = self.activityByDate[date(2017, 9, 23)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Stock("USFD", Currency.USD), quantity=Decimal("178"), amount=Cash(currency=Currency.USD, quantity=Decimal("-5427.15")), fees=Cash(currency=Currency.USD, quantity=Decimal("4.95")), flags=TradeFlags.OPEN, ), )
def test_buyStock(self) -> None: ts = self.activityByDate[date(2017, 2, 22)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Stock("VOO", Currency.USD), quantity=Decimal("23"), amount=Cash(currency=Currency.USD, quantity=Decimal("-4981.11")), fees=Cash(currency=Currency.USD, quantity=Decimal("6.95")), flags=TradeFlags.OPEN, ), )
def test_sellSecurity(self) -> None: ts = self.activityByDate[date(2016, 10, 13)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Stock("VWO", Currency.USD), quantity=Decimal("-4"), amount=Cash(currency=Currency.USD, quantity=Decimal("1234.56")), fees=Cash(currency=Currency.USD, quantity=Decimal("0.00")), flags=TradeFlags.CLOSE, ), )
def test_securityOutgoingTransfer(self) -> None: ts = self.activityByDate[date(2017, 9, 12)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Bond( "U S TREASURY BILL CPN 0.00000 % 2017-04-10 DTD 2017-08-14", Currency.USD, validateSymbol=False, ), quantity=Decimal("-10000"), amount=Cash(currency=Currency.USD, quantity=Decimal("0.00")), fees=Cash(currency=Currency.USD, quantity=Decimal("0.00")), flags=TradeFlags.CLOSE, ), )
def test_sellToCloseOption(self) -> None: ts = self.activityByDate[date(2017, 11, 9)] self.assertEqual( ts[4], Trade( date=ts[4].date, instrument=Option( underlying="SPY", currency=Currency.USD, optionType=OptionType.CALL, expiration=date(2018, 1, 25), strike=Decimal("260"), ), quantity=Decimal("-4"), amount=Cash(currency=Currency.USD, quantity=Decimal("94.04")), fees=Cash(currency=Currency.USD, quantity=Decimal("5.03")), flags=TradeFlags.CLOSE, ), )
def test_redeemTBill(self) -> None: ts = self.activityByDate[date(2017, 9, 23)] self.assertEqual(len(ts), 2) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Bond( "U S TREASURY BILL CPN 0.00000 % MTD 2017-03-10 DTD 2017-09-10", Currency.USD, validateSymbol=False, ), quantity=Decimal("-10000"), amount=Cash(currency=Currency.USD, quantity=Decimal("9987.65")), fees=Cash(currency=Currency.USD, quantity=Decimal("0.00")), flags=TradeFlags.CLOSE, ), )
def test_sellToCloseOption(self) -> None: ts = self.activityByDate[date(2018, 11, 9)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Option( underlying="INTC", currency=Currency.USD, optionType=OptionType.PUT, expiration=date(2018, 12, 7), strike=Decimal("48.50"), ), quantity=Decimal("-1"), amount=Cash(currency=Currency.USD, quantity=Decimal("140")), fees=Cash(currency=Currency.USD, quantity=Decimal("5.60")), flags=TradeFlags.CLOSE, ), )
def test_assignedOption(self) -> None: ts = self.activityByDate[date(2018, 2, 4)] self.assertEqual(len(ts), 4) self.assertEqual( ts[3], Trade( date=ts[3].date, instrument=Option( underlying="QQQ", currency=Currency.USD, optionType=OptionType.CALL, expiration=date(2018, 2, 1), strike=Decimal("130"), ), quantity=Decimal("1"), amount=Cash(currency=Currency.USD, quantity=Decimal(0)), fees=Cash(currency=Currency.USD, quantity=Decimal(0)), flags=TradeFlags.CLOSE | TradeFlags.ASSIGNED_OR_EXERCISED, ), )
def test_expiredShortOption(self) -> None: ts = self.activityByDate[date(2018, 12, 3)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Option( underlying="CSCO", currency=Currency.USD, optionType=OptionType.PUT, expiration=date(2018, 11, 30), strike=Decimal("44.50"), ), quantity=Decimal("1"), amount=Cash(currency=Currency.USD, quantity=Decimal(0)), fees=Cash(currency=Currency.USD, quantity=Decimal(0)), flags=TradeFlags.CLOSE | TradeFlags.EXPIRED, ), )
def test_sellToOpenOption(self) -> None: ts = self.activityByDate[date(2018, 12, 12)] self.assertEqual(len(ts), 2) self.assertEqual( ts[1], Trade( date=ts[1].date, instrument=Option( underlying="MAR", currency=Currency.USD, optionType=OptionType.CALL, expiration=date(2018, 12, 28), strike=Decimal("112"), ), quantity=Decimal("-1"), amount=Cash(currency=Currency.USD, quantity=Decimal("190")), fees=Cash(currency=Currency.USD, quantity=Decimal("5.60")), flags=TradeFlags.OPEN, ), )
def test_buyToOpenOption(self) -> None: ts = self.activityByDate[date(2017, 8, 26)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], Trade( date=ts[0].date, instrument=Option( underlying="SPY", currency=Currency.USD, optionType=OptionType.PUT, expiration=date(2018, 3, 22), strike=Decimal("198"), ), quantity=Decimal("32"), amount=Cash(currency=Currency.USD, quantity=Decimal("-3185.67")), fees=Cash(currency=Currency.USD, quantity=Decimal("25.31")), flags=TradeFlags.OPEN, ), )
def _forceParseSchwabTransaction(t: _SchwabTransaction, flags: TradeFlags) -> Trade: quantity = Decimal(t.quantity) if re.match(r"^Sell", t.action): quantity = -quantity fees = Decimal(0) if t.fees: fees = _schwabDecimal(t.fees) amount = Decimal(0) if t.amount: # Schwab automatically deducts the fees, but we need to add them back in for consistency with other brokers # (where the denominating currency of these two things may differ) amount = _schwabDecimal(t.amount) + fees return Trade( date=_parseSchwabTransactionDate(t.date), instrument=_guessInstrumentFromSymbol(t.symbol), quantity=quantity, amount=Cash(currency=Currency.USD, quantity=amount), fees=Cash(currency=Currency.USD, quantity=fees), flags=flags, )
def _forceParseVanguardTransaction(t: _VanguardTransaction, flags: TradeFlags) -> Optional[Trade]: instrument: Instrument if len(t.symbol) > 0: instrument = Stock(t.symbol, currency=Currency.USD) else: instrument = _guessInstrumentForInvestmentName(t.investmentName) totalFees = Decimal(t.commissionFees) amount = Decimal(t.principalAmount) if t.transactionDescription == "Redemption": shares = Decimal(t.shares) * (-1) else: shares = Decimal(t.shares) return Trade( date=_parseVanguardTransactionDate(t.tradeDate), instrument=instrument, quantity=shares, amount=Cash(currency=Currency.USD, quantity=amount), fees=Cash(currency=Currency.USD, quantity=totalFees), flags=flags, )
def _forceParseFidelityTransaction(t: _FidelityTransaction, flags: TradeFlags) -> Trade: quantity = Decimal(t.quantity) totalFees = Decimal(0) # Fidelity's total fees include commision and fees if t.commission: totalFees += Decimal(t.commission) if t.fees: totalFees += Decimal(t.fees) amount = Decimal(0) if t.amount: amount = Decimal(t.amount) + totalFees currency = Currency[t.currency] return Trade( date=_parseFidelityTransactionDate(t.date), instrument=_guessInstrumentFromSymbol(t.symbol, currency), quantity=quantity, amount=Cash(currency=currency, quantity=amount), fees=Cash(currency=currency, quantity=totalFees), flags=flags, )
def _parseSchwabTransaction( t: _SchwabTransaction, otherTransactionsThisDate: Iterable[_SchwabTransaction] ) -> Optional[Activity]: dividendActions = {"Cash Dividend", "Reinvest Dividend", "Non-Qualified Div"} if t.action in dividendActions: return CashPayment( date=_parseSchwabTransactionDate(t.date), instrument=Stock(t.symbol, currency=Currency.USD), proceeds=Cash(currency=Currency.USD, quantity=_schwabDecimal(t.amount)), ) interestActions = {"Credit Interest", "Margin Interest"} if t.action in interestActions: return CashPayment( date=_parseSchwabTransactionDate(t.date), instrument=None, proceeds=Cash(currency=Currency.USD, quantity=_schwabDecimal(t.amount)), ) # Bond redemptions are split into two entries, for some reason. if t.action == "Full Redemption Adj": redemption = next( ( r for r in otherTransactionsThisDate if r.symbol == t.symbol and r.action == "Full Redemption" ), None, ) if not redemption: raise ValueError( f'Expected to find "Full Redemption" action on same date as {t}' ) quantity = Decimal(redemption.quantity) amount = _schwabDecimal(t.amount) return Trade( date=_parseSchwabTransactionDate(t.date), instrument=Bond(t.symbol, currency=Currency.USD), quantity=quantity, amount=Cash(currency=Currency.USD, quantity=amount), fees=Cash(currency=Currency.USD, quantity=Decimal(0)), # TODO: Do we want a new TradeFlag? flags=TradeFlags.CLOSE | TradeFlags.EXPIRED, ) if t.action == "Full Redemption": adj = next( ( r for r in otherTransactionsThisDate if r.symbol == t.symbol and r.action == "Full Redemption Adj" ), None, ) if not adj: raise ValueError( f'Expected to find "Full Redemption Adj" action on same date as {t}' ) # Will process on the adjustment entry return None ignoredActions = { "Wire Funds", "Wire Funds Received", "MoneyLink Transfer", "MoneyLink Deposit", "Long Term Cap Gain Reinvest", "ATM Withdrawal", "Schwab ATM Rebate", "Service Fee", "Journal", "Misc Cash Entry", "Security Transfer", } if t.action in ignoredActions: return None flagsByAction = { "Buy": TradeFlags.OPEN, "Sell Short": TradeFlags.OPEN, "Buy to Open": TradeFlags.OPEN, "Sell to Open": TradeFlags.OPEN, "Reinvest Shares": TradeFlags.OPEN | TradeFlags.DRIP, "Sell": TradeFlags.CLOSE, "Buy to Close": TradeFlags.CLOSE, "Sell to Close": TradeFlags.CLOSE, "Assigned": TradeFlags.CLOSE | TradeFlags.ASSIGNED_OR_EXERCISED, "Exchange or Exercise": TradeFlags.CLOSE | TradeFlags.ASSIGNED_OR_EXERCISED, "Expired": TradeFlags.CLOSE | TradeFlags.EXPIRED, } if not t.action in flagsByAction: raise ValueError(f'Unexpected Schwab action "{t.action}" in transaction {t}') return _forceParseSchwabTransaction(t, flags=flagsByAction[t.action])
def _parseTradeConfirm(trade: _IBTradeConfirm) -> Trade: try: instrument = _parseInstrument(trade) flagsByCode = { # Codes referenced from: # https://www.interactivebrokers.com/en/software/reportguide/reportguide.htm#reportguide/codestradeconfirm.htm "A": TradeFlags.ASSIGNED_OR_EXERCISED, # Assignment "C": TradeFlags.CLOSE, # Closing Trade "Ep": TradeFlags.EXPIRED, # Resulted from an Expired Position "Ex": TradeFlags.ASSIGNED_OR_EXERCISED, # Exercise "L": TradeFlags. LIQUIDATED, # Ordered by IB (Margin Violation, Forced Futures Sell) "O": TradeFlags.OPEN, # Opening Trade "R": TradeFlags.DRIP, # Dividend Reinvestment "T": TradeFlags.OPEN, # Transfer # Ignored "D": TradeFlags.NONE, # IB acted as Dual Agent "P": TradeFlags.NONE, # Partial Execution # Currently Unsupported # 'B': TradeFlags.NONE, # Automatic Buy-in # 'Ca': TradeFlags.NONE, # Cancelled # 'Co': TradeFlags.NONE, # Corrected Trade # 'G': TradeFlags.NONE, # Trade in Guaranteed Account Segment # 'M': TradeFlags.NONE, # Entered manually by IB # 'Si': TradeFlags.NONE, # Solicited Trade (This order was solicited by Interactive Brokers). } codes = trade.code.split(";") flags = TradeFlags.NONE for c in codes: if c == "": continue if c not in flagsByCode: raise ValueError(f"Unrecognized code {c} in trade: {trade}") flags |= flagsByCode[c] # Codes are not always populated with open/close, not sure why if flags & (TradeFlags.OPEN | TradeFlags.CLOSE) == TradeFlags.NONE: if trade.buySell == "BUY": flags |= TradeFlags.OPEN else: flags |= TradeFlags.CLOSE if trade.commissionCurrency not in Currency.__members__: raise ValueError(f"Unrecognized currency in trade: {trade}") # We could choose to account for accrued interest payments as part of # the trade price or as a separate cash payment; the former seems # marginally cleaner and more sensible for a trade log. proceeds = Cash( currency=Currency[trade.currency], quantity=_parseFiniteDecimal(trade.proceeds) + _parseFiniteDecimal(trade.accruedInt), ) return Trade( date=_parseIBDate(trade.tradeDate), instrument=instrument, quantity=_parseFiniteDecimal(trade.quantity), amount=proceeds, fees=Cash( currency=Currency[trade.commissionCurrency], quantity=-(_parseFiniteDecimal(trade.commission) + _parseFiniteDecimal(trade.tax)), ), flags=flags, ) except InvalidOperation: raise ValueError( f"One of the numeric trade values is out of range: {trade}")