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_tBill(self) -> None: self.assertEqual(self.positions[0].instrument, Bond("942792RU5", Currency.USD)) self.assertEqual(self.positions[0].quantity, 10000) self.assertEqual( self.positions[0].costBasis, Cash(currency=Currency.USD, quantity=Decimal("9800")), )
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_tBill(self) -> None: self.assertEqual( self.positions[0].instrument, Bond( "U S TREASURY BILL CPN 0.00000 % MTD 2017-04-10 DTD 2017-08-14", currency=Currency.USD, validateSymbol=False, ), ) self.assertEqual(self.positions[0].quantity, 5000)
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_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_bondInterest(self) -> None: ts = self.activityByDate[date(2019, 1, 17)] self.assertEqual(len(ts), 1) self.assertEqual( ts[0], CashPayment( date=ts[0].date, instrument=Bond("BA 3 3/4 02/17/19", Currency.USD, validateSymbol=False), proceeds=helpers.cashUSD(Decimal("18.75")), ), ) self.assertNotIn(date(2019, 1, 15), self.activityByDate) self.assertNotIn(date(2019, 1, 14), self.activityByDate)
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 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_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_buyBond(self) -> None: # IB doesn't export the CUSIP unless the data is paid for. symbol = "ALLY 3 3/4 11/18/19" ts = self.tradesBySymbol[symbol] self.assertEqual(len(ts), 1) self.assertEqual(ts[0].date.date(), date(2019, 3, 19)) self.assertEqual( ts[0].instrument, Bond(symbol, Currency.USD, validateSymbol=False, exchange="UBSBOND"), ) self.assertEqual(ts[0].quantity, Decimal("2000")) self.assertEqual( ts[0].amount, Cash(currency=Currency.USD, quantity=Decimal("-2035.13"))) self.assertEqual(ts[0].fees, Cash(currency=Currency.USD, quantity=Decimal("2"))) self.assertEqual(ts[0].flags, TradeFlags.OPEN)
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 test_tBill(self) -> None: self.assertEqual(self.positions[0].instrument, Bond("193845XM2", Currency.USD)) self.assertEqual(self.positions[0].quantity, 10000) self.assertEqual(self.positions[0].costBasis, helpers.cashUSD(Decimal("9956.80")))
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 _extractPosition(p: IB.Position) -> Position: tag = p.contract.secType symbol = p.contract.localSymbol if p.contract.currency not in Currency.__members__: raise ValueError(f"Unrecognized currency in position: {p}") currency = Currency[p.contract.currency] exchange = p.contract.exchange or None try: instrument: Instrument if tag == "STK": instrument = Stock(symbol=symbol, currency=currency, exchange=exchange) elif tag == "BILL" or tag == "BOND": instrument = Bond( symbol=symbol, currency=currency, validateSymbol=False, exchange=exchange, ) elif tag == "OPT": instrument = _parseOption( symbol=symbol, currency=currency, multiplier=_parseFiniteDecimal(p.contract.multiplier), exchange=exchange, ) elif tag == "FUT": instrument = Future( symbol=symbol, currency=currency, multiplier=_parseFiniteDecimal(p.contract.multiplier), expiration=_parseIBDate( p.contract.lastTradeDateOrContractMonth).date(), exchange=exchange, ) elif tag == "FOP": instrument = _parseFutureOptionContract(p.contract, currency=currency, exchange=exchange) elif tag == "CASH": instrument = _parseForex(symbol=symbol, currency=currency, exchange=exchange) else: raise ValueError( f"Unrecognized/unsupported security type in position: {p}") qty = _parseFiniteDecimal(p.position) costBasis = _parseFiniteDecimal(p.avgCost) * qty return Position( instrument=instrument, quantity=qty, costBasis=Cash(currency=Currency[p.contract.currency], quantity=costBasis), ) except InvalidOperation: raise ValueError( f"One of the numeric position or contract values is out of range: {p}" )