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 _parseVanguardTransaction(t: _VanguardTransaction) -> Optional[Activity]: if t.transactionType == "Dividend": return CashPayment( date=_parseVanguardTransactionDate(t.tradeDate), instrument=Stock(t.symbol if t.symbol else t.investmentName, currency=Currency.USD), proceeds=Cash(currency=Currency.USD, quantity=Decimal(t.netAmount)), ) validTransactionTypes = set([ "Buy", "Sell", "Reinvestment", "Corp Action (Redemption)", "Transfer (outgoing)", ]) if t.transactionType not in validTransactionTypes: return None flagsByTransactionType = { "Buy": TradeFlags.OPEN, "Sell": TradeFlags.CLOSE, "Reinvestment": TradeFlags.OPEN | TradeFlags.DRIP, "Corp Action (Redemption)": TradeFlags.CLOSE, "Transfer (outgoing)": TradeFlags.CLOSE, } return _forceParseVanguardTransaction( t, flags=flagsByTransactionType[t.transactionType])
def _parseFidelityTransaction(t: _FidelityTransaction) -> Optional[Activity]: if t.action == "DIVIDEND RECEIVED": return CashPayment( date=_parseFidelityTransactionDate(t.date), instrument=Stock(t.symbol, currency=Currency[t.currency]), proceeds=Cash(currency=Currency[t.currency], quantity=Decimal(t.amount)), ) elif t.action == "INTEREST EARNED": return CashPayment( date=_parseFidelityTransactionDate(t.date), instrument=None, proceeds=Cash(currency=Currency[t.currency], quantity=Decimal(t.amount)), ) flags = None # TODO: Handle 'OPENING TRANSACTION' and 'CLOSING TRANSACTION' text for options transactions if t.action.startswith("YOU BOUGHT"): flags = TradeFlags.OPEN elif t.action.startswith("YOU SOLD"): flags = TradeFlags.CLOSE elif t.action.startswith("REINVESTMENT"): flags = TradeFlags.OPEN | TradeFlags.DRIP if not flags: return None return _forceParseFidelityTransaction(t, flags=flags)
def _guessInstrumentFromSymbol(symbol: str, currency: Currency) -> Instrument: if re.search(r"[0-9]+(C|P)[0-9]+$", symbol): return _parseOptionTransaction(symbol, currency) elif Bond.validBondSymbol(symbol): return Bond(symbol, currency=currency) else: return Stock(symbol, currency=currency)
def _guessInstrumentFromSymbol(symbol: str) -> Instrument: if re.search(r"\s(C|P)$", symbol): return _parseOption(symbol) elif Bond.validBondSymbol(symbol): return Bond(symbol, currency=Currency.USD) else: return Stock(symbol, currency=Currency.USD)
def test_aapl(self) -> None: self.assertEqual(self.positions[1].instrument, Stock("AAPL", Currency.USD)) self.assertEqual(self.positions[1].quantity, Decimal("100")) self.assertEqual( self.positions[1].costBasis, Cash(currency=Currency.USD, quantity=Decimal("14000")), )
def test_robo(self) -> None: self.assertEqual(self.positions[2].instrument, Stock("ROBO", Currency.USD)) self.assertEqual(self.positions[2].quantity, Decimal("10")) self.assertEqual( self.positions[2].costBasis, Cash(currency=Currency.USD, quantity=Decimal("300")), )
def test_v(self) -> None: self.assertEqual(self.positions[5].instrument, Stock("V", Currency.USD)) self.assertEqual(self.positions[5].quantity, Decimal("20")) self.assertEqual( self.positions[5].costBasis, Cash(currency=Currency.USD, quantity=Decimal("2600")), )
def _guessInstrumentForInvestmentName(name: str) -> Instrument: instrument: Instrument if re.match(r"^.+\s\%\s.+$", name): # TODO: Determine valid CUSIP for bonds instrument = Bond(name, currency=Currency.USD, validateSymbol=False) else: instrument = Stock(name, currency=Currency.USD) return instrument
def test_cashDividend(self) -> None: ts = self.activityByDate[date(2018, 3, 6)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], CashPayment( date=ts[0].date, instrument=Stock("VGLT", Currency.USD), proceeds=helpers.cashUSD(Decimal("12.85")), ), )
def test_dividendPayment(self) -> None: ts = self.activityByDate[date(2017, 11, 9)] self.assertEqual(len(ts), 5) self.assertEqual( ts[2], CashPayment( date=ts[2].date, instrument=Stock("ROBO", Currency.USD), proceeds=helpers.cashUSD(Decimal("6.78")), ), )
def test_dividendReinvested(self) -> None: ts = self.activityByDate[date(2017, 3, 28)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], CashPayment( date=ts[0].date, instrument=Stock("VOO", Currency.USD), proceeds=helpers.cashUSD(Decimal("22.95")), ), )
def test_stockLoanInterest(self) -> None: ts = self.activityByDate[date(2019, 1, 1)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], CashPayment( date=ts[0].date, instrument=Stock("TSLA", Currency.USD, exchange="NASDAQ"), proceeds=helpers.cashUSD(Decimal("0.01")), ), )
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 normalizeInstrument(instrument: Instrument) -> Instrument: if isinstance(instrument, Stock): return Stock( symbol=normalizeSymbol(instrument.symbol), currency=instrument.currency, exchange=instrument.exchange, ) elif isinstance(instrument, Option): # Handles the FutureOption subclass correctly as well. return replace(instrument, underlying=normalizeSymbol(instrument.underlying)) else: return instrument
def test_postedAndPaid(self) -> None: ts = self.activityByDate[date(2019, 2, 14)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], CashPayment( date=ts[0].date, instrument=Stock("AAPL", Currency.USD, exchange="NASDAQ"), proceeds=helpers.cashUSD(Decimal("23.36")), ), ) self.assertNotIn(date(2019, 2, 7), self.activityByDate) self.assertNotIn(date(2019, 2, 8), self.activityByDate)
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_buyUSDStock(self) -> None: symbol = "AAPL" ts = self.tradesBySymbol[symbol] self.assertEqual(len(ts), 1) self.assertEqual(ts[0].date.date(), date(2019, 2, 12)) self.assertEqual(ts[0].instrument, Stock(symbol, Currency.USD, exchange="ISLAND")) self.assertEqual(ts[0].quantity, Decimal("17")) self.assertEqual( ts[0].amount, Cash(currency=Currency.USD, quantity=Decimal("-2890"))) self.assertEqual(ts[0].fees, Cash(currency=Currency.USD, quantity=Decimal("1"))) self.assertEqual(ts[0].flags, TradeFlags.OPEN)
def test_buyGBPStock(self) -> None: symbol = "GAW" ts = self.tradesBySymbol[symbol] self.assertEqual(len(ts), 1) self.assertEqual(ts[0].date.date(), date(2019, 2, 12)) self.assertEqual(ts[0].instrument, Stock(symbol, Currency.GBP, exchange="LSE")) self.assertEqual(ts[0].quantity, Decimal("100")) self.assertEqual( ts[0].amount, Cash(currency=Currency.GBP, quantity=Decimal("-3050"))) self.assertEqual( ts[0].fees, Cash(currency=Currency.GBP, quantity=Decimal("21.25"))) self.assertEqual(ts[0].flags, TradeFlags.OPEN)
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_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_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_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_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_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 _parseInstrument(entry: _instrumentEntryTypes) -> Instrument: symbol = entry.symbol if not symbol: raise ValueError(f"Missing symbol in entry: {entry}") if entry.currency not in Currency.__members__: raise ValueError(f"Unrecognized currency in entry: {entry}") currency = Currency[entry.currency] if isinstance(entry, _IBChangeInDividendAccrual): exchange = None else: exchange = entry.exchange exchange = exchange or entry.listingExchange or None tag = entry.assetCategory if tag == "STK": return Stock(symbol=symbol, currency=currency, exchange=exchange) elif tag == "BILL" or tag == "BOND": return Bond(symbol=symbol, currency=currency, validateSymbol=False, exchange=exchange) elif tag == "OPT": return _parseOption( symbol=symbol, currency=currency, multiplier=_parseFiniteDecimal(entry.multiplier), exchange=exchange, ) elif tag == "FUT": return Future( symbol=symbol, currency=currency, multiplier=_parseFiniteDecimal(entry.multiplier), expiration=_parseIBDate(entry.expiry).date(), exchange=exchange, ) elif tag == "CASH": return _parseForex(symbol=symbol, currency=currency, exchange=exchange) elif tag == "FOP": return _parseFutureOption(entry, exchange=exchange) else: raise ValueError( f"Unrecognized/unsupported security type in entry: {entry}")
def _parseVanguardPosition(p: _VanguardPosition, activity: List[Activity]) -> Position: instrument: Instrument if len(p.symbol) > 0: instrument = Stock(p.symbol, currency=Currency.USD) else: instrument = _guessInstrumentForInvestmentName(p.investmentName) qty = Decimal(p.shares) realizedBasis = _realizedBasisForSymbol(instrument.symbol, activity) assert realizedBasis, "Invalid realizedBasis: %s for %s" % ( realizedBasis, instrument, ) return Position(instrument=instrument, quantity=qty, costBasis=realizedBasis)
def _parseSchwabPosition(p: _SchwabPosition) -> Optional[Position]: if re.match(r"Futures |Cash & Money Market|Account Total", p.symbol): return None instrument: Instrument if re.match(r"Equity|ETFs", p.securityType): instrument = Stock(p.symbol, currency=Currency.USD) elif re.match(r"Option", p.securityType): instrument = _parseOption(p.symbol) elif re.match(r"Fixed Income", p.securityType): instrument = Bond(p.symbol, currency=Currency.USD) else: raise ValueError(f"Unrecognized security type: {p.securityType}") return Position( instrument=instrument, quantity=_schwabDecimal(p.quantity), costBasis=Cash(currency=Currency.USD, quantity=_schwabDecimal(p.costBasis)), )
def _parsePositions(path: Path, lenient: bool = False) -> List[Position]: with open(path, newline="") as csvfile: stocksCriterion = csvsectionslicer.CSVSectionCriterion( startSectionRowMatch=["Stocks"], endSectionRowMatch=[""], rowFilter=lambda r: r[0:7], ) bondsCriterion = csvsectionslicer.CSVSectionCriterion( startSectionRowMatch=["Bonds"], endSectionRowMatch=[""], rowFilter=lambda r: r[0:7], ) optionsCriterion = csvsectionslicer.CSVSectionCriterion( startSectionRowMatch=["Options"], endSectionRowMatch=["", ""], rowFilter=lambda r: r[0:7], ) instrumentBySection: Dict[ csvsectionslicer.CSVSectionCriterion, _InstrumentFactory] = { stocksCriterion: lambda p: Stock(p.symbol, currency=Currency.USD), bondsCriterion: lambda p: Bond(p.symbol, currency=Currency.USD), optionsCriterion: lambda p: _parseOptionsPosition(p.description), } sections = csvsectionslicer.parseSectionsForCSV( csvfile, [stocksCriterion, bondsCriterion, optionsCriterion]) positions: List[Position] = [] for sec in sections: for r in sec.rows: pos = _parseFidelityPosition( _FidelityPosition._make(r), instrumentBySection[sec.criterion]) positions.append(pos) return positions
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, )