Ctrl K
ComunidadGuías…

app/brokerage.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""What a portfolio's broker charges for a trade.

One shape covers the fees brokers actually quote: a flat fee, plus a
percentage of the trade's value, or the minimum if that is more. Flat only,
flat plus a percentage, a percentage only, "$14.95 or 0.11%, whichever is
greater", and free (zeros) are all this.

Only ever a suggestion for the trade form: the brokerage recorded is whatever
the form sends. `tradeform.js` does the same sum as units and price are typed.
"""

from __future__ import annotations

from dataclasses import dataclass
from decimal import ROUND_HALF_UP, Decimal

from . import money
from .models import Portfolio

CENT = Decimal("0.01")
ZERO = Decimal(0)


@dataclass(frozen=True)
class Fee:
    flat: Decimal | None = None
    percent: Decimal | None = None
    minimum: Decimal | None = None

    @property
    def is_set(self) -> bool:
        """Any part given. All blank means "not set", which is not free."""
        return any(v is not None for v in (self.flat, self.percent, self.minimum))

    def on(self, value: Decimal) -> Decimal:
        # Decimal zeros, not 0: on a tie `max` keeps the first, and a plain 0
        # has no `quantize` — a free broker, or a percentage with no minimum,
        # raised on the trade form's starting figure.
        charged = (self.flat or ZERO) + (self.percent or ZERO) * value / 100
        return max(self.minimum or ZERO, charged).quantize(CENT, rounding=ROUND_HALF_UP)

    def as_json(self) -> dict | None:
        if not self.is_set:
            return None
        return {name: (f"{v.normalize():f}" if v is not None else None)
                for name, v in (("flat", self.flat), ("percent", self.percent),
                                ("minimum", self.minimum))}


def local(portfolio: Portfolio | None) -> Fee:
    """The fee for a trade in the reporting currency."""
    if portfolio is None:
        return Fee()
    return Fee(portfolio.brokerage_flat, portfolio.brokerage_percent,
               portfolio.brokerage_minimum)


def foreign(portfolio: Portfolio | None) -> Fee:
    """The fee for a trade in any other currency."""
    if portfolio is None:
        return Fee()
    return Fee(portfolio.foreign_brokerage_flat, portfolio.foreign_brokerage_percent,
               portfolio.foreign_brokerage_minimum)


def is_foreign(currency: str | None) -> bool:
    return bool(currency) and currency != money.REPORTING


def for_trade(portfolio: Portfolio | None, currency: str, value: Decimal,
              last: Decimal | None) -> Decimal | None:
    """What the trade form offers for a trade of `value` in `currency`: the
    portfolio's fee for that kind of trade, or else `last`, the brokerage on
    the last trade of that kind (queries.last_brokerage). The sale simulator
    offers the same."""
    fee = foreign(portfolio) if is_foreign(currency) else local(portfolio)
    return fee.on(value) if fee.is_set else last