1
0
fin-depo/fin_depo/data.py
Jon Michael Aanes 53d0ce744a
Some checks failed
Python Ruff Code Quality / ruff (push) Failing after 22s
Run Python tests (through Pytest) / Test (push) Successful in 30s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 25s
Ruff
2024-12-02 18:57:16 +01:00

218 lines
6.3 KiB
Python

"""Datastructures for `fin-depo`."""
import abc
import dataclasses
import datetime
from collections.abc import Iterable, Mapping
from decimal import Decimal
from typing import TypeVar
import dataclassabc
import enforce_typing
from fin_defs import Asset, AssetAmount
@enforce_typing.enforce_types
@dataclasses.dataclass(frozen=True)
class Depo(abc.ABC):
"""A depository tracking several assets.
Depo can either be DepoSingle, which is the base layer of the depository
structure, or nested in DepoGroup, which allows for a complex hierarcy of
depositories.
The depository structure exposed by each backend depends upon the logical
structure of the relevant service and the API of this service.
"""
name: str
updated_time: datetime.datetime
@abc.abstractmethod
def assets(self) -> Iterable[Asset]:
"""Returns the different assets managed by this depo."""
@abc.abstractmethod
def get_amount_of_asset(self, asset: Asset) -> Decimal:
"""Returns the amount of owned assets for all nested depos.
Must return 0 if depo does not contain the given asset.
"""
@enforce_typing.enforce_types
@dataclasses.dataclass(frozen=True)
class DepoSingle(Depo):
"""Base level of depository."""
_assets: Mapping[Asset, Decimal]
def __post_init__(self):
if None in self._assets:
msg = 'DepoSingle must not containg a None Asset key'
raise ValueError(msg)
def assets(self) -> Iterable[Asset]:
return self._assets
def get_amount_of_asset(self, asset: Asset) -> Decimal:
return self._assets.get(asset, Decimal(0))
@enforce_typing.enforce_types
@dataclasses.dataclass(frozen=True)
class DepoGroup(Depo):
"""Nested depository."""
nested: list[Depo]
def __post_init__(self):
if None in self.nested:
msg = 'DepoGroup must not containg an None depository'
raise ValueError(msg)
def assets(self) -> Iterable[Asset]:
assets: set[Asset] = set()
for nested_depo in self.nested:
assets.update(nested_depo.assets())
return assets
def get_amount_of_asset(self, asset: Asset) -> Decimal:
summed: Decimal = Decimal(0)
for nested_depo in self.nested:
summed += nested_depo.get_amount_of_asset(asset)
del nested_depo
return summed
T = TypeVar('T')
class DepoFetcher(abc.ABC):
"""Base `Depo` fetcher interface.
Used to get depository information for specific websites/backends.
"""
@abc.abstractmethod
def get_depo(self) -> Depo:
"""Fetches the `Depo`s available for the fetcher, possibly as
a `DepoGroup`.
"""
def assert_param(self, param_name: str, param_type: type[T], param_value: T) -> T:
if not isinstance(param_value, param_type):
msg = f'{self} expected {param_name} parameter of type {param_type}, but got: {param_value}'
raise TypeError(msg)
return param_value
@enforce_typing.enforce_types
class DoubleRegister(abc.ABC):
"""Information about some movement of assets."""
@property
@abc.abstractmethod
def input(self) -> AssetAmount | None:
"""How much and what kind of asset have been placed into the depository."""
raise NotImplementedError
@property
@abc.abstractmethod
def output(self) -> AssetAmount | None:
"""How much and what kind of asset have been removed from the depository."""
raise NotImplementedError
@property
@abc.abstractmethod
def fee(self) -> AssetAmount | None:
"""How much and what kind of asset was spent as a service fee."""
raise NotImplementedError
@property
@abc.abstractmethod
def executed_time(self) -> datetime.datetime:
"""The precise point in time when the asset transaction was executed."""
raise NotImplementedError
@enforce_typing.enforce_types
@dataclassabc.dataclassabc(frozen=True)
class TradeOrderDetails(DoubleRegister):
"""Information about an executed trade.
Includes both well-structured data about the order, and unstructured data
from the backend. The unstructured data might be needed for tax purposes.
Fields:
- `input_asset`: Asset that have been sold
- `input_amount`: Amount of asset that has been sold
- `output_asset`: Asset that have been bought
- `output_amount`: Amount of asset that have been bought.
- `fee_asset`: Asset used to pay fee.
- `fee_amount`: Amount of asset that have been used to pay fee.
- `executed_time`: Point in time when order was executed.
- `order_id`: Unique identifier of order. Determined by backend.
- `raw_order_details`: For storing arbitrary unstructured data from backend.
"""
input: AssetAmount
output: AssetAmount
fee: AssetAmount
executed_time: datetime.datetime
order_id: object
raw_order_details: object
@enforce_typing.enforce_types
@dataclassabc.dataclassabc(frozen=True)
class WithdrawalDetails(DoubleRegister):
"""Information about a withdrawal of assets from a specific service.
Withdraw assets may appear in another depository as a `DepositDetails`, but
these cannot be automatically linked by `fin-depo`.
Includes both well-structured data about the order, and unstructured data
from the backend. The unstructured data might be needed for tax purposes.
"""
withdrawn: AssetAmount
fee: AssetAmount
executed_time: datetime.datetime
raw_details: object
@property
def input(self) -> AssetAmount:
return self.withdrawn
@property
def output(self) -> None:
return None
@enforce_typing.enforce_types
@dataclassabc.dataclassabc(frozen=True)
class DepositDetails(DoubleRegister):
"""Information about a deposit of assets to a specific service.
Deposited assets may appear in another depository as a `WithdrawalDetails`, but
these cannot be automatically linked by `fin-depo`.
Includes both well-structured data about the order, and unstructured data
from the backend. The unstructured data might be needed for tax purposes.
"""
deposit: AssetAmount
fee: AssetAmount
executed_time: datetime.datetime
raw_details: object
@property
def input(self) -> None:
return None
@property
def output(self) -> AssetAmount:
return self.deposit