2024-11-28 23:45:31 +00:00
|
|
|
"""Datastructures for `fin-depo`."""
|
|
|
|
|
2024-06-02 15:24:53 +00:00
|
|
|
import abc
|
2024-11-29 10:06:43 +00:00
|
|
|
import dataclassabc
|
2024-05-29 20:29:15 +00:00
|
|
|
import dataclasses
|
2024-06-02 14:14:46 +00:00
|
|
|
import datetime
|
2024-06-02 15:24:53 +00:00
|
|
|
from collections.abc import Iterable, Mapping
|
2024-06-02 14:14:46 +00:00
|
|
|
from decimal import Decimal
|
2024-07-18 22:22:29 +00:00
|
|
|
from typing import TypeVar
|
2024-06-02 14:14:46 +00:00
|
|
|
|
|
|
|
import enforce_typing
|
2024-11-27 23:15:57 +00:00
|
|
|
from fin_defs import Asset, AssetAmount
|
2024-06-02 14:14:46 +00:00
|
|
|
|
2024-05-29 20:29:15 +00:00
|
|
|
|
|
|
|
@enforce_typing.enforce_types
|
2024-07-20 18:21:50 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
2024-06-02 15:24:53 +00:00
|
|
|
class Depo(abc.ABC):
|
2024-11-28 23:45:31 +00:00
|
|
|
"""A depository tracking several assets.
|
2024-06-20 21:43:45 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2024-05-29 20:29:15 +00:00
|
|
|
name: str
|
|
|
|
updated_time: datetime.datetime
|
|
|
|
|
2024-06-02 15:24:53 +00:00
|
|
|
@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.
|
|
|
|
"""
|
|
|
|
|
2024-06-02 14:14:46 +00:00
|
|
|
|
2024-05-29 20:29:15 +00:00
|
|
|
@enforce_typing.enforce_types
|
2024-07-20 18:21:50 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
2024-05-29 20:29:15 +00:00
|
|
|
class DepoSingle(Depo):
|
2024-06-20 21:43:45 +00:00
|
|
|
"""Base level of depository."""
|
|
|
|
|
2024-06-02 15:24:53 +00:00
|
|
|
_assets: Mapping[Asset, Decimal]
|
|
|
|
|
2024-10-20 09:03:31 +00:00
|
|
|
def __post_init__(self):
|
2024-11-01 10:30:18 +00:00
|
|
|
if None in self._assets:
|
|
|
|
msg = 'DepoSingle must not containg a None Asset key'
|
|
|
|
raise ValueError(msg)
|
2024-10-20 09:03:31 +00:00
|
|
|
|
2024-06-02 15:24:53 +00:00
|
|
|
def assets(self) -> Iterable[Asset]:
|
|
|
|
return self._assets
|
|
|
|
|
|
|
|
def get_amount_of_asset(self, asset: Asset) -> Decimal:
|
|
|
|
return self._assets.get(asset, Decimal(0))
|
2024-05-29 20:29:15 +00:00
|
|
|
|
2024-06-02 14:14:46 +00:00
|
|
|
|
2024-05-29 20:29:15 +00:00
|
|
|
@enforce_typing.enforce_types
|
2024-07-20 18:21:50 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
2024-05-29 20:29:15 +00:00
|
|
|
class DepoGroup(Depo):
|
2024-06-20 21:43:45 +00:00
|
|
|
"""Nested depository."""
|
|
|
|
|
2024-05-29 20:29:15 +00:00
|
|
|
nested: list[Depo]
|
2024-06-02 15:24:53 +00:00
|
|
|
|
2024-10-20 09:03:31 +00:00
|
|
|
def __post_init__(self):
|
2024-11-01 10:30:18 +00:00
|
|
|
if None in self.nested:
|
|
|
|
msg = 'DepoGroup must not containg an None depository'
|
|
|
|
raise ValueError(msg)
|
2024-10-20 09:03:31 +00:00
|
|
|
|
2024-06-02 15:24:53 +00:00
|
|
|
def assets(self) -> Iterable[Asset]:
|
2024-07-18 23:24:59 +00:00
|
|
|
assets: set[Asset] = set()
|
2024-06-02 15:24:53 +00:00
|
|
|
for nested_depo in self.nested:
|
2024-07-18 23:24:59 +00:00
|
|
|
assets.update(nested_depo.assets())
|
2024-06-02 15:24:53 +00:00
|
|
|
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
|
2024-07-18 22:22:29 +00:00
|
|
|
|
2024-07-18 22:22:54 +00:00
|
|
|
|
2024-07-18 22:22:29 +00:00
|
|
|
T = TypeVar('T')
|
|
|
|
|
2024-07-18 22:22:54 +00:00
|
|
|
|
2024-07-18 22:22:29 +00:00
|
|
|
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
|
2024-07-20 19:54:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
@enforce_typing.enforce_types
|
2024-11-29 10:06:43 +00:00
|
|
|
class DoubleRegister(abc.ABC):
|
2024-12-02 17:11:44 +00:00
|
|
|
"""Information about some movement of assets."""
|
2024-11-29 10:06:43 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
@abc.abstractmethod
|
|
|
|
def input(self) -> AssetAmount | None:
|
2024-12-02 17:11:44 +00:00
|
|
|
"""How much and what kind of asset have been placed into the depository."""
|
2024-11-29 10:06:43 +00:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@property
|
|
|
|
@abc.abstractmethod
|
|
|
|
def output(self) -> AssetAmount | None:
|
2024-12-02 17:11:44 +00:00
|
|
|
"""How much and what kind of asset have been removed from the depository."""
|
2024-11-29 10:06:43 +00:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@property
|
|
|
|
@abc.abstractmethod
|
|
|
|
def fee(self) -> AssetAmount | None:
|
2024-12-02 17:11:44 +00:00
|
|
|
"""How much and what kind of asset was spent as a service fee."""
|
2024-11-29 10:06:43 +00:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@property
|
|
|
|
@abc.abstractmethod
|
|
|
|
def executed_time(self) -> datetime.datetime:
|
2024-12-02 17:11:44 +00:00
|
|
|
"""The precise point in time when the asset transaction was executed."""
|
2024-11-29 10:06:43 +00:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
|
|
@enforce_typing.enforce_types
|
|
|
|
@dataclassabc.dataclassabc(frozen=True)
|
|
|
|
class TradeOrderDetails(DoubleRegister):
|
2024-07-20 19:54:29 +00:00
|
|
|
"""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.
|
2024-07-22 14:04:52 +00:00
|
|
|
|
|
|
|
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.
|
2024-07-20 19:54:29 +00:00
|
|
|
"""
|
|
|
|
|
2024-11-27 23:15:57 +00:00
|
|
|
input: AssetAmount
|
|
|
|
output: AssetAmount
|
|
|
|
fee: AssetAmount
|
2024-07-22 14:04:52 +00:00
|
|
|
executed_time: datetime.datetime
|
2024-07-20 19:54:29 +00:00
|
|
|
|
|
|
|
order_id: object
|
|
|
|
raw_order_details: object
|
2024-11-27 20:56:13 +00:00
|
|
|
|
2024-11-27 23:45:46 +00:00
|
|
|
|
2024-11-27 20:56:13 +00:00
|
|
|
@enforce_typing.enforce_types
|
2024-11-29 10:06:43 +00:00
|
|
|
@dataclassabc.dataclassabc(frozen=True)
|
|
|
|
class WithdrawalDetails(DoubleRegister):
|
2024-12-02 17:11:44 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
|
|
|
|
2024-11-27 23:15:57 +00:00
|
|
|
withdrawn: AssetAmount
|
|
|
|
fee: AssetAmount
|
2024-11-27 20:56:13 +00:00
|
|
|
executed_time: datetime.datetime
|
|
|
|
raw_details: object
|
|
|
|
|
2024-11-29 10:06:43 +00:00
|
|
|
@property
|
|
|
|
def input(self) -> AssetAmount:
|
|
|
|
return self.withdrawn
|
|
|
|
|
|
|
|
@property
|
|
|
|
def output(self) -> None:
|
|
|
|
return None
|
|
|
|
|
2024-11-27 23:45:46 +00:00
|
|
|
|
2024-11-27 20:56:13 +00:00
|
|
|
@enforce_typing.enforce_types
|
2024-11-29 10:06:43 +00:00
|
|
|
@dataclassabc.dataclassabc(frozen=True)
|
|
|
|
class DepositDetails(DoubleRegister):
|
2024-12-02 17:11:44 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
|
|
|
|
2024-11-27 23:15:57 +00:00
|
|
|
deposit: AssetAmount
|
|
|
|
fee: AssetAmount
|
2024-11-27 20:56:13 +00:00
|
|
|
executed_time: datetime.datetime
|
|
|
|
raw_details: object
|
|
|
|
|
2024-11-29 10:06:43 +00:00
|
|
|
@property
|
|
|
|
def input(self) -> None:
|
|
|
|
return None
|
2024-11-27 23:45:46 +00:00
|
|
|
|
2024-11-29 10:06:43 +00:00
|
|
|
@property
|
|
|
|
def output(self) -> AssetAmount:
|
|
|
|
return self.deposit
|