def test_sellOption(self) -> None: symbol = "MTCH 190215P00045000" ts = self.tradesBySymbol[symbol] self.assertEqual(len(ts), 1) self.assertEqual(ts[0].date.date(), date(2019, 2, 4)) self.assertEqual( ts[0].instrument, Option( underlying="MTCH", currency=Currency.USD, optionType=OptionType.PUT, expiration=date(2019, 2, 15), strike=Decimal("45"), exchange="CBOE2", ), ) self.assertEqual(ts[0].quantity, Decimal("-1")) self.assertEqual(ts[0].amount, Cash(currency=Currency.USD, quantity=Decimal("55"))) self.assertEqual( ts[0].fees, Cash(currency=Currency.USD, quantity=Decimal("1.320915"))) self.assertEqual(ts[0].price, Cash(currency=Currency.USD, quantity=Decimal("0.55"))) self.assertEqual(ts[0].flags, TradeFlags.CLOSE)
def test_subtractCash(self, cur: Currency, a: Decimal, b: Decimal) -> None: cashA = Cash(currency=cur, quantity=a) cashB = Cash(currency=cur, quantity=b) cashC = cashA - cashB self.assertEqual(cashC.currency, cur) self.assertEqual(cashC.quantity, Cash.quantize(a - b))
def test_cashInequality(self, cur: Currency, a: Decimal, b: Decimal) -> None: cashA = Cash(currency=cur, quantity=a) cashB = Cash(currency=cur, quantity=a + b) self.assertNotEqual(cashA, cashB) cashB = Cash(currency=cur, quantity=a - b) self.assertNotEqual(cashA, cashB)
def test_currencyInterest(self) -> None: # IBKR interest accruals don't have dates associated with them, so # they'll be tagged with the last day in the period being looked at. ts = self.activityByDate[date(2019, 3, 1)] # BASE_SUMMARY should be excluded self.assertEqual(len(ts), 2) self.assertEqual( ts[0], CashPayment( date=ts[0].date, instrument=None, proceeds=Cash(currency=Currency.AUD, quantity=Decimal("-4.29")), ), ) self.assertEqual( ts[1], CashPayment( date=ts[1].date, instrument=None, proceeds=Cash(currency=Currency.USD, quantity=Decimal("2.26")), ), )
def test_buyForex(self) -> None: symbol = "GBPUSD" ts = self.tradesBySymbol[symbol] self.assertEqual(len(ts), 2) self.assertEqual(ts[0].date.date(), date(2019, 2, 12)) self.assertEqual( ts[0].instrument, Forex( baseCurrency=Currency.GBP, quoteCurrency=Currency.USD, exchange="IDEALFX", ), ) self.assertEqual(ts[0].quantity, Decimal("3060")) self.assertEqual( ts[0].amount, Cash(currency=Currency.USD, quantity=Decimal("-3936.231"))) self.assertEqual(ts[0].fees, Cash(currency=Currency.USD, quantity=Decimal("2"))) self.assertEqual(ts[0].flags, TradeFlags.OPEN) self.assertEqual( ts[1].instrument, Forex( baseCurrency=Currency.GBP, quoteCurrency=Currency.USD, exchange="IDEALFX", ), ) self.assertEqual(ts[1].quantity, Decimal("50")) self.assertEqual( ts[1].amount, Cash(currency=Currency.USD, quantity=Decimal("-64.36"))) self.assertEqual(ts[1].fees, Cash(currency=Currency.USD, quantity=Decimal("2"))) self.assertEqual(ts[1].flags, TradeFlags.OPEN)
def convertCashToCurrency(quoteCurrency: Currency, cash: Sequence[Cash], dataProvider: MarketDataProvider) -> Cash: currencyRates = dict( currencyConversionRates( quoteCurrency=quoteCurrency, otherCurrencies=(c.currency for c in cash if c.currency != quoteCurrency), dataProvider=dataProvider, )) currencyRates[quoteCurrency] = Cash(currency=quoteCurrency, quantity=Decimal(1)) for c in cash: if not c.currency in currencyRates: raise RuntimeError( f"Unable to fetch currency rate for {c.currency} to convert {c}" ) return reduce( operator.add, (Cash( currency=quoteCurrency, quantity=c.quantity * currencyRates[c.currency].quantity, ) for c in cash), Cash(currency=quoteCurrency, quantity=Decimal(0)), )
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_buyOption(self) -> None: symbol = "HYG 191115P00087000" ts = self.tradesBySymbol[symbol] self.assertEqual(len(ts), 1) self.assertEqual(ts[0].date.date(), date(2019, 2, 12)) self.assertEqual( ts[0].instrument, Option( underlying="HYG", currency=Currency.USD, optionType=OptionType.PUT, expiration=date(2019, 11, 15), strike=Decimal("87"), exchange="PSE", ), ) self.assertEqual(ts[0].quantity, Decimal("1")) self.assertEqual(ts[0].amount, Cash(currency=Currency.USD, quantity=Decimal("-565"))) self.assertEqual( ts[0].fees, Cash(currency=Currency.USD, quantity=Decimal("0.7182"))) self.assertEqual(ts[0].price, Cash(currency=Currency.USD, quantity=Decimal("5.65"))) self.assertEqual(ts[0].flags, TradeFlags.OPEN)
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 test_subtractIncompatibleCash( self, curs: List[Currency], a: Decimal, b: Decimal ) -> None: cashA = Cash(currency=curs[0], quantity=a) cashB = Cash(currency=curs[1], quantity=b) with self.assertRaises(ValueError): cashA - cashB
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_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_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_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_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_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_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 currencyConversionRates( quoteCurrency: Currency, otherCurrencies: Iterable[Currency], dataProvider: MarketDataProvider, ) -> Iterable[Tuple[Currency, Cash]]: instruments = (Forex( baseCurrency=min(currency, quoteCurrency), quoteCurrency=max(currency, quoteCurrency), ) for currency in otherCurrencies) return ( (instrument.baseCurrency, quote.market) if instrument.quoteCurrency == quoteCurrency else ( instrument.quoteCurrency, Cash( currency=instrument.baseCurrency, # FIXME: This unfortunately does not retain much precision when # dividing by JPY in particular (where the integral portion can # be quite large). # See https://github.com/bankroll-py/bankroll/issues/37. quantity=Decimal(1) / quote.market.quantity, ), ) for instrument, quote in dataProvider.fetchQuotes(instruments) if quote.market and isinstance(instrument, Forex))
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 uniformCurrencyQuotes( currency: SearchStrategy[Currency] = from_type(Currency), bid: SearchStrategy[Optional[Decimal]] = optionals(cashAmounts()), ask: SearchStrategy[Optional[Decimal]] = optionals(cashAmounts()), last: SearchStrategy[Optional[Decimal]] = optionals(cashAmounts()), close: SearchStrategy[Optional[Decimal]] = optionals(cashAmounts()), grow_ask: bool = True, ) -> SearchStrategy[Quote]: return currency.flatmap(lambda cur: quotes( bid=bid.map(lambda x: Cash(currency=cur, quantity=x) if x else None), ask=ask.map(lambda x: Cash(currency=cur, quantity=x) if x else None), last=last.map(lambda x: Cash(currency=cur, quantity=x) if x else None), close=close.map(lambda x: Cash(currency=cur, quantity=x) if x else None), grow_ask=grow_ask, ))
def test_buyFutureOption(self) -> None: symbol = "GBUJ9 C1335" ts = self.tradesBySymbol[symbol] self.assertEqual(len(ts), 1) self.assertEqual(ts[0].date.date(), date(2019, 3, 4)) self.assertEqual( ts[0].instrument, FutureOption( symbol=symbol, currency=Currency.USD, underlying="BPM9", optionType=OptionType.CALL, expiration=date(2019, 4, 5), strike=Decimal("1.335"), multiplier=Decimal(62500), exchange="GLOBEX", ), ) self.assertEqual(ts[0].quantity, Decimal("1")) self.assertEqual(ts[0].amount, helpers.cashUSD(Decimal("-918.75"))) self.assertEqual(ts[0].fees, helpers.cashUSD(Decimal("2.47"))) self.assertEqual( ts[0].price, Cash(currency=Currency.USD, quantity=Decimal("0.0147"))) self.assertEqual(ts[0].flags, TradeFlags.OPEN)
def fetchQuotes( self, instruments: Iterable[Instrument], dataType: _MarketDataType = _MarketDataType.DELAYED_FROZEN, ) -> Iterable[Tuple[Instrument, Quote]]: self._client.reqMarketDataType(dataType.value) contractsByInstrument = self.qualifyContracts(instruments) # Note: this blocks until all tickers come back. When we want this to be async, we'll need to use reqMktData(). # See https://github.com/jspahrsummers/bankroll/issues/13. tickers = self._client.reqTickers(*contractsByInstrument.values()) for ticker in tickers: instrument = next((i for (i, c) in contractsByInstrument.items() if c == ticker.contract)) bid: Optional[Cash] = None ask: Optional[Cash] = None last: Optional[Cash] = None close: Optional[Cash] = None factor = 1 # Tickers are quoted in GBX despite all the other data being in GBP. if instrument.currency == Currency.GBP: factor = 100 if (ticker.bid and math.isfinite(ticker.bid)) and not ticker.bidSize == 0: bid = Cash(currency=instrument.currency, quantity=Decimal(ticker.bid) / factor) if (ticker.ask and math.isfinite(ticker.ask)) and not ticker.askSize == 0: ask = Cash(currency=instrument.currency, quantity=Decimal(ticker.ask) / factor) if (ticker.last and math.isfinite( ticker.last)) and not ticker.lastSize == 0: last = Cash(currency=instrument.currency, quantity=Decimal(ticker.last) / factor) if ticker.close and math.isfinite(ticker.close): close = Cash( currency=instrument.currency, quantity=Decimal(ticker.close) / factor, ) yield (instrument, Quote(bid=bid, ask=ask, last=last, close=close))
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 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_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")), )