From c36911d70676cc3a3f2287789763456f2b831cb6 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Sat, 12 Oct 2024 19:42:34 +0200 Subject: [PATCH] Integrating features from an old project --- fin_defs/__init__.py | 126 +++++++++++++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 39 deletions(-) diff --git a/fin_defs/__init__.py b/fin_defs/__init__.py index 272f20e..cc4fbb6 100644 --- a/fin_defs/__init__.py +++ b/fin_defs/__init__.py @@ -187,6 +187,11 @@ class Asset: return self.to_string_id() +@enforce_typing.enforce_types +@dataclasses.dataclass(frozen=True) +class UnknownAsset(Asset): + pass + @enforce_typing.enforce_types @dataclasses.dataclass(frozen=True) class Currency(Asset): @@ -198,7 +203,7 @@ class Currency(Asset): class FiatCurrency(Currency): iso_code: str - def __post_init__(self): + def __post_init__(self) -> None: if not re.match(RE_TICKER_FORMAT, self.iso_code): msg = f'iso_code was not in correct format: {self.iso_code}' raise ValueError(msg) @@ -206,6 +211,22 @@ class FiatCurrency(Currency): def raw_short_name(self) -> str: return self.iso_code + @staticmethod + def from_currency_symbol(symbol: str) -> 'Currency | None': + for (currency, currency_symbol) in CURRENCY_SYMBOLS.items(): + if currency_symbol == symbol: + return currency + return None + + def to_currency_symbol(self) -> str: + return CURRENCY_SYMBOLS[self] + +FiatCurrency.DKK = FiatCurrency('DKK') +FiatCurrency.NOK = FiatCurrency('NOK') +FiatCurrency.USD = FiatCurrency('USD') +FiatCurrency.EUR = FiatCurrency('EUR') +FiatCurrency.GBP = FiatCurrency('GBP') +FiatCurrency.JPY = FiatCurrency('JPY') @enforce_typing.enforce_types @dataclasses.dataclass(frozen=True, eq=True) @@ -213,7 +234,7 @@ class CryptoCurrency(Currency): ccxt_symbol: str coingecko_id: str | None = None - def __post_init__(self): + def __post_init__(self) -> None: """TODO if not re.match(RE_CRYPTO_TICKER_FORMAT, self.ccxt_symbol): msg = f'ccxt_symbol was not in correct format: {self.ccxt_symbol}' @@ -231,7 +252,7 @@ class Stock(Asset): exchange: StockExchange nordnet_id: int | None = None - def __post_init__(self): + def __post_init__(self) -> None: if not re.match(RE_TICKER_FORMAT_FLEXIBLE, self.ticker): msg = f'ticker was not in correct format: {self.ticker}' raise ValueError(msg) @@ -249,7 +270,7 @@ class Stock(Asset): class Index(Asset): ticker: str - def __post_init__(self): + def __post_init__(self) -> None: if not re.match(RE_TICKER_FORMAT_FLEXIBLE, self.ticker): msg = f'ticker was not in correct format: {self.ticker}' raise ValueError(msg) @@ -263,7 +284,7 @@ class Index(Asset): class Commodity(Asset): alpha_vantage_id: str - def __post_init__(self): + def __post_init__(self) -> None: if not re.match(RE_TICKER_FORMAT, self.alpha_vantage_id): msg = f'alpha_vantage_id was not in correct format: {self.alpha_vantage_id}' raise ValueError(msg) @@ -284,11 +305,10 @@ Commodity.COFFEE = Commodity('COFFEE') # Well known -DKK = FiatCurrency('DKK') -NOK = FiatCurrency('NOK') -USD = FiatCurrency('USD') -EUR = FiatCurrency('EUR') -GBP = FiatCurrency('GBP') +DKK = FiatCurrency.DKK +USD = FiatCurrency.USD +EUR = FiatCurrency.EUR + BTC = CryptoCurrency('BTC', coingecko_id='bitcoin') MPC = CryptoCurrency('MPC', coingecko_id='partisia-blockchain') SPX = Index('SPX') @@ -324,36 +344,42 @@ COMMON_NAMES: dict[Asset, list[str]] = { USDT: ['Tether USDT'], } +CURRENCY_CODES = { + sym: FiatCurrency(sym) + for sym in [ + 'USD', + 'DKK', + 'EUR', + 'JPY', + 'CAD', + 'GBP', + 'CNY', + 'PLN', + 'ILS', + 'CHF', + 'ZAR', + 'CZK', + ] +} + + WELL_KNOWN_SYMBOLS = ( - { - sym: FiatCurrency(sym) - for sym in [ - 'USD', - 'DKK', - 'EUR', - 'JPY', - 'CAD', - 'GBP', - 'CNY', - 'PLN', - 'ILS', - 'CHF', - 'ZAR', - 'CZK', - ] - } + CURRENCY_CODES | {'XXBT': BTC} | {k.raw_short_name(): k for k in COMMON_NAMES} | {'SPX500': SPX, 'SP500': SPX, 'Nasdaq 100': NDX} ) -ASSET_PREFIX: dict[Asset, str] = { +CURRENCY_SYMBOLS: dict[Currency, str] = { USD: '$', EUR: '€', - GBP: '£', + FiatCurrency.GBP: '£', BTC: '₿', + FiatCurrency.JPY: '¥', } +ASSET_PREFIX: dict[Asset, str] = CURRENCY_SYMBOLS # TODO: Remove at some point. + NYSE = StockExchange( name='New York Stock Exchange', mic='XNYS', @@ -467,23 +493,23 @@ class AssetAmount: asset: Asset amount: Decimal - def __str__(self): + def __str__(self) -> str: return self.human_readable_str() - def human_readable_str(self): + def human_readable_str(self) -> str: specificity = '2' if self.amount >= 0.10 else '3' prefix = ASSET_PREFIX.get(self.asset, '') return ('{}{:.' + specificity + 'f} {}').format( prefix, self.amount, self.asset.raw_short_name(), ) - def __mul__(self, other: Decimal): + def __mul__(self, other: Decimal) -> 'AssetAmount': if not isinstance(other, Decimal): msg = f'other must be decimal, but was {type(other)}' raise TypeError(msg) return AssetAmount(self.asset, self.amount * other) - def __add__(self, other): + def __add__(self, other: 'AssetAmount') -> 'AssetAmount': if not isinstance(other, AssetAmount): msg = f'other must be AssetAmount, but was {type(other)}' raise TypeError(msg) @@ -495,7 +521,7 @@ class AssetAmount: return AssetAmount(self.asset, self.amount + other.amount) - def __sub__(self, other): + def __sub__(self, other: 'AssetAmount') -> 'AssetAmount': if not isinstance(other, AssetAmount): msg = f'other must be AssetAmount, but was {type(other)}' raise TypeError(msg) @@ -506,7 +532,7 @@ class AssetAmount: raise ValueError(msg) return AssetAmount(self.asset, self.amount - other.amount) - def __truediv__(self, other): + def __truediv__(self, other: 'Decimal | AssetAmount') -> 'Decimal | AssetAmount': if isinstance(other, AssetAmount): if self.asset != other.asset: msg = f'AssetAmount must have same asset, but: {self.asset} != {other.asset}' @@ -514,16 +540,38 @@ class AssetAmount: return self.amount / other.amount return AssetAmount(self.asset, self.amount / other) - def __lt__(self, other): + def __neg__(self) -> 'AssetAmount': + return AssetAmount(self.asset, -self.amount) + + def cmp(self, other) -> Decimal: if not isinstance(other, AssetAmount): msg = f'other must be AssetAmount, but was {type(other)}' raise TypeError(msg) + if self.is_zero() or other.is_zero(): + return self.amount - other.amount if self.asset != other.asset: msg = ( f'AssetAmount must have same asset, but: {self.asset} != {other.asset}' ) raise ValueError(msg) - return self.amount < other.amount + return self.amount - other.amount + + def is_zero(self) -> bool: + return self.amount == 0 + + def __lt__(self, other: 'AssetAmount') -> bool: + return self.cmp(other) < 0 + + def __le__(self, other: 'AssetAmount') -> bool: + return self.cmp(other) <= 0 + + def __gt__(self, other: 'AssetAmount') -> bool: + return self.cmp(other) > 0 + + def __ge__(self, other: 'AssetAmount') -> bool: + return self.cmp(other) >= 0 + +AssetAmount.ZERO = AssetAmount(UnknownAsset(), Decimal(0)) AssetPair = tuple[Asset, Asset] @@ -550,10 +598,10 @@ class ExchangeRateSample: return self._average return (self.high + self.low) / 2 - def replace_timestamp(self, timestamp): + def replace_timestamp(self, timestamp: datetime.date | datetime.datetime) -> 'ExchangeRateSample': return dataclasses.replace(self, timestamp=timestamp) - def invert_exchange_rate(self): + def invert_exchange_rate(self) -> 'ExchangeRateSample': one = Decimal('1') return dataclasses.replace( self,