示例#1
0
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)
示例#2
0
    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 _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 test_cashInterest(self) -> None:
     ts = self.activityByDate[date(2017, 8, 31)]
     self.assertEqual(len(ts), 1)
     self.assertEqual(
         ts[0],
         CashPayment(
             date=ts[0].date,
             instrument=None,
             proceeds=helpers.cashUSD(Decimal("0.02")),
         ),
     )
 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")),
         ),
     )
示例#6
0
 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")),
         ),
     )
示例#7
0
 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")),
         ),
     )
示例#8
0
 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")),
         ),
     )
示例#9
0
    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)
示例#10
0
def _parseStockLoanFee(entry: _IBSLBFee) -> Optional[Activity]:
    # We don't see accrual reversals here, because it rolls up into total interest accounting, so use the accrual postings instead.
    codes = entry.code.split(";")
    if "Po" not in codes:
        return None

    proceeds = Cash(currency=Currency[entry.currency],
                    quantity=Decimal(entry.netLendFee))

    return CashPayment(
        date=_parseIBDate(entry.valueDate),
        instrument=_parseInstrument(entry),
        proceeds=proceeds,
    )
示例#11
0
def _parseChangeInDividendAccrual(
        entry: _IBChangeInDividendAccrual) -> Optional[Activity]:
    codes = entry.code.split(";")
    if "Re" not in codes:
        return None

    # IB "reverses" dividend postings when they're paid out, so they all appear as debits.
    proceeds = Cash(currency=Currency[entry.currency],
                    quantity=-Decimal(entry.netAmount))

    return CashPayment(
        date=_parseIBDate(entry.payDate),
        instrument=_parseInstrument(entry),
        proceeds=proceeds,
    )
示例#12
0
    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)
示例#13
0
def _parseCurrencyInterestAccrual(
        entry: _IBInterestAccrualsCurrency) -> Optional[Activity]:
    # This entry includes forex translation, which we don't want.
    if entry.currency == "BASE_SUMMARY":
        return None

    # An accrual gets "reversed" when it is credited/debited. Because the
    # reversal refers to the balance of interest, accrual reversal > 0 means
    # that the cash account was debited, while accrual reversal < 0 means the
    # cash account was credited with the interest.
    proceeds = Cash(currency=Currency[entry.currency],
                    quantity=-Decimal(entry.accrualReversal))

    # Using `toDate` here since there are no dates attached to the actual
    # accruals.
    return CashPayment(date=_parseIBDate(entry.toDate),
                       instrument=None,
                       proceeds=proceeds)
示例#14
0
    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 _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])