app/brokerage.py
Rama main · réplica de lectura del repositorio original.
"""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