diff --git a/experiments/notebooks/buy-and-sell/1_mento1.ipynb b/experiments/notebooks/buy-and-sell/1_mento1.ipynb index 8bdafdd..512850c 100644 --- a/experiments/notebooks/buy-and-sell/1_mento1.ipynb +++ b/experiments/notebooks/buy-and-sell/1_mento1.ipynb @@ -94,7 +94,7 @@ "import experiments.default_experiment as default_experiment\n", "\n", "# types\n", - "from model.types import *\n", + "from model.types.base import *\n", "from model.entities.balance import Balance\n", "\n", "# options\n", @@ -164,8 +164,10 @@ "source": [ "simulation_analysis_1.model.initial_state.update({\n", " 'reserve_balance': Balance({\n", - " CryptoAsset.CELO: 120.0e6,\n", - " Stable.CUSD: 0.0\n", + " CryptoAsset.CELO: 10000000.0,\n", + " CryptoAsset.BTC: 1000.0,\n", + " CryptoAsset.ETH: 15000.0,\n", + " CryptoAsset.DAI: 80000000.0,\n", " }),\n", "})" ] @@ -206,6 +208,16 @@ "pprint(simulation_analysis_1.model.params)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "60da36c1", + "metadata": {}, + "outputs": [], + "source": [ + "simulation_analysis_1.model.params['mento_exchanges_config'][0].keys()" + ] + }, { "cell_type": "markdown", "id": "7471a5ed", @@ -221,9 +233,9 @@ "metadata": {}, "outputs": [], "source": [ - "simulation_analysis_1.model.params.update({\n", - " \"reserve_fraction\": [0.5],\n", - "})" + "#simulation_analysis_1.model.params.update({\n", + "# \"reserve_fraction\": [0.5],\n", + "#})" ] }, { @@ -297,7 +309,8 @@ "metadata": {}, "outputs": [], "source": [ - "df.head()" + "pd.set_option(\"display.max_columns\", None)\n", + "df.tail(20)" ] }, { @@ -463,6 +476,36 @@ "visualizations.plot_helper(data_frame=df, column_label='reserve_balance_celo', y_label='# tokens')" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "07afc390", + "metadata": {}, + "outputs": [], + "source": [ + "visualizations.plot_helper(data_frame=df, column_label='reserve_balance_in_usd')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5511ab57", + "metadata": {}, + "outputs": [], + "source": [ + "visualizations.plot_helper(data_frame=df, column_label='reserve_ratio')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a9ce188", + "metadata": {}, + "outputs": [], + "source": [ + "visualizations.plot_helper(data_frame=df, column_label='floating_supply_stables_in_usd')" + ] + }, { "cell_type": "code", "execution_count": null, @@ -495,9 +538,9 @@ "hash": "e0d943ef511eb062da37f0005de9c5ca6dc4c50b1f75cb4cb85f428d08d55d84" }, "kernelspec": { - "display_name": "Python 3.8.9 ('mento2-model-ZNlqjaYt')", + "display_name": "mento2-model", "language": "python", - "name": "python3" + "name": "mento2-model" }, "language_info": { "codemirror_mode": { @@ -509,7 +552,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.9" + "version": "3.8.12" } }, "nbformat": 4, diff --git a/experiments/simulation_configuration.py b/experiments/simulation_configuration.py index a0aba21..5373fbf 100644 --- a/experiments/simulation_configuration.py +++ b/experiments/simulation_configuration.py @@ -4,7 +4,7 @@ from model.constants import blocks_per_day, blocks_per_year BLOCKS_PER_TIMESTEP = 1 # number of blocks per timestep (=1 if sim on per-block-basis) -SIMULATION_TIME_DAYS = 1 # number of days +SIMULATION_TIME_DAYS = 2 # number of days # number of simulation_configuration timesteps TIMESTEPS = SIMULATION_TIME_DAYS * blocks_per_day // BLOCKS_PER_TIMESTEP TIMESTEPS_PER_YEAR = blocks_per_year // BLOCKS_PER_TIMESTEP diff --git a/model/entities/balance.py b/model/entities/balance.py index b748b86..74a8fe6 100644 --- a/model/entities/balance.py +++ b/model/entities/balance.py @@ -5,8 +5,12 @@ Balance(celo=2, cusd=10) + Balance(celo=5, cusd=0) = Balance(celo=7, cusd=10) """ from typing import Callable, Dict, TYPE_CHECKING + +from model.types.base import Fiat +from model.types.pair import Pair if TYPE_CHECKING: - from model.types import Currency + from model.types.base import Currency + class Balance(dict): """ @@ -26,7 +30,6 @@ def __str__(self) -> str: ]) return f"Balance({values})" - @staticmethod def zero(): return Balance() @@ -43,6 +46,12 @@ def __combine__(self, other: "Balance", combinator: Callable[[int, int], int]): for currency in set(self.keys()).union(other.keys()) }) + def values_in_usd(self, prev_state): + values_in_usd = { + key: inventory * Pair(key, Fiat.USD).get_rate(prev_state).value + for key, inventory in self.items()} + return values_in_usd + @property def any_negative(self) -> bool: for (_, value) in self.items(): diff --git a/model/entities/oracle_provider.py b/model/entities/oracle_provider.py index f1d3785..40f4b55 100644 --- a/model/entities/oracle_provider.py +++ b/model/entities/oracle_provider.py @@ -4,7 +4,8 @@ from typing import List from uuid import UUID -from model.types import OracleConfig, Pair +from model.types.configs import OracleConfig +from model.types.pair import Pair from model.constants import blocktime_seconds from model.utils.rng_provider import RNGProvider diff --git a/model/entities/strategies/strategy_arbitrage_trader.py b/model/entities/strategies/strategy_arbitrage_trader.py index 55ab24c..3e49181 100644 --- a/model/entities/strategies/strategy_arbitrage_trader.py +++ b/model/entities/strategies/strategy_arbitrage_trader.py @@ -4,9 +4,10 @@ from enum import Enum import numpy as np -from model.types import MentoBuckets +from model.types.base import MentoBuckets from .trader_strategy import TraderStrategy + class TradingRegime(Enum): SELL_STABLE = "SELL_STABLE" SELL_RESERVE_ASSET = "SELL_RESERVE_ASSET" @@ -135,10 +136,10 @@ def sell_order_stable(self, buckets: MentoBuckets, market_price: float, spread: } def sell_order_reserve_asset( - self, - buckets: MentoBuckets, - market_price: float, - spread: float): + self, + buckets: MentoBuckets, + market_price: float, + spread: float): """ Calculate order for selling the reserve currency """ diff --git a/model/entities/strategies/trader_strategy.py b/model/entities/strategies/trader_strategy.py index 8e6b86f..50a1c69 100644 --- a/model/entities/strategies/trader_strategy.py +++ b/model/entities/strategies/trader_strategy.py @@ -13,10 +13,13 @@ from cvxpy import Maximize, Minimize, Problem, Variable import cvxpy -from model.types import MentoBuckets, MentoExchangeConfig, Pair +from model.types.base import MentoBuckets +from model.types.pair import Pair +from model.types.configs import MentoExchangeConfig if TYPE_CHECKING: from model.entities.trader import Trader + class TraderStrategy: """ Base trader strategy class to solve a convex optimisation problem. @@ -85,7 +88,8 @@ def define_constraints(self, params, prev_state): max_budget_stable = self.parent.balance.get(self.stable) max_budget_reserve_asset = self.parent.balance.get(self.reserve_asset) if self.sell_reserve_asset(params, prev_state): - self.constraints.append(self.variables["sell_amount"] <= max_budget_reserve_asset) + self.constraints.append( + self.variables["sell_amount"] <= max_budget_reserve_asset) else: self.constraints.append(self.variables["sell_amount"] <= max_budget_stable) @@ -150,12 +154,14 @@ def minimise_price_impact(self, sell_amount, sell_reserve_asset, params): # Todo logic is probably wrong, fix! if sell_reserve_asset: sell_amount_adjusted = min( - params["average_daily_volume"].get(Pair(self.stable, self.reference_fiat)), + params["average_daily_volume"].get( + Pair(self.stable, self.reference_fiat)), sell_amount ) elif not sell_reserve_asset: sell_amount_adjusted = min( - params["average_daily_volume"].get(Pair(self.reserve_asset, self.reference_fiat)), + params["average_daily_volume"].get( + Pair(self.reserve_asset, self.reference_fiat)), sell_amount ) return sell_amount_adjusted @@ -181,7 +187,8 @@ def return_optimal_trade(self, params, prev_state): trade = None else: sell_reserve_asset = self.sell_reserve_asset(params, prev_state) - sell_amount = self.minimise_price_impact(sell_amount, sell_reserve_asset, params) + sell_amount = self.minimise_price_impact( + sell_amount, sell_reserve_asset, params) buy_amount = self.mento.get_buy_amount( exchange=self.parent.config.exchange, sell_amount=sell_amount, @@ -199,7 +206,8 @@ def return_optimal_trade(self, params, prev_state): def market_price(self, prev_state) -> float: # TODO: Do we need to quote in equivalent Fiat for Stable? return ( - prev_state["market_price"].get(Pair(self.reserve_asset, self.reference_fiat)) + prev_state["market_price"].get( + Pair(self.reserve_asset, self.reference_fiat)) / prev_state["market_price"].get(Pair(self.stable, self.reference_fiat)) ) diff --git a/model/entities/trader.py b/model/entities/trader.py index ede2c97..44f1a27 100644 --- a/model/entities/trader.py +++ b/model/entities/trader.py @@ -10,12 +10,14 @@ from model.generators.mento import MentoExchangeGenerator from model.entities import strategies from model.entities.account import Account, Balance -from model.types import MentoExchangeConfig, Pair, TraderConfig +from model.types.pair import Pair +from model.types.configs import MentoExchangeConfig, TraderConfig from model.utils.rng_provider import RNGProvider if TYPE_CHECKING: from model.generators.accounts import AccountGenerator + class Trader(Account): """ The Trader extens an account with a trading strategy. diff --git a/model/generators/accounts.py b/model/generators/accounts.py index ae48a11..a7ce101 100644 --- a/model/generators/accounts.py +++ b/model/generators/accounts.py @@ -9,7 +9,7 @@ from model.entities.account import Account from model.entities.trader import Trader from model.entities.balance import Balance -from model.types import TraderConfig +from model.types.configs import TraderConfig from model.utils import update_from_signal from model.utils.generator import Generator, state_update_blocks from model.utils.generator_container import GeneratorContainer @@ -32,11 +32,11 @@ class AccountGenerator(Generator): rngp: RNGProvider def __init__(self, - reserve_inventory: Balance, - initial_floating_supply: Balance, - traders: List[TraderConfig], - container: GeneratorContainer, - rngp: RNGProvider): + reserve_inventory: Balance, + initial_floating_supply: Balance, + traders: List[TraderConfig], + container: GeneratorContainer, + rngp: RNGProvider): self.container = container self.rngp = rngp self.reserve = self.create_reserve_account( @@ -116,7 +116,7 @@ def traders(self) -> List[Trader]: account for account in self.accounts_by_id.values() if isinstance(account, Trader) - ] + ] def get(self, account_id) -> Account: account = self.accounts_by_id.get(account_id) @@ -131,7 +131,7 @@ def tracked_floating_supply(self) -> Balance: return sum( [account.balance for account in self.accounts_by_id.values()], Balance.zero() - ) + ) @property def floating_supply(self) -> Balance: diff --git a/model/generators/markets.py b/model/generators/markets.py index 30663cc..f87a3c3 100644 --- a/model/generators/markets.py +++ b/model/generators/markets.py @@ -8,7 +8,7 @@ from experiments.simulation_configuration import TOTAL_BLOCKS from model.system_parameters import Parameters -from model.types import MarketPriceModel +from model.types.base import MarketPriceModel from model.utils.data_feed import DATA_FOLDER, DataFeed from model.utils.generator import Generator from model.utils.price_impact_valuator import PriceImpactValuator diff --git a/model/generators/mento.py b/model/generators/mento.py index f14f645..8dec9b3 100644 --- a/model/generators/mento.py +++ b/model/generators/mento.py @@ -9,7 +9,9 @@ from model.constants import blocktime_seconds from model.entities.balance import Balance -from model.types import MentoBuckets, MentoExchange, MentoExchangeConfig, Pair, Stable +from model.types.base import MentoBuckets, MentoExchange, Stable +from model.types.pair import Pair +from model.types.configs import MentoExchangeConfig from model.utils.generator import Generator, state_update_blocks from model.utils import update_from_signal diff --git a/model/generators/oracles.py b/model/generators/oracles.py index 75a7609..91703a1 100644 --- a/model/generators/oracles.py +++ b/model/generators/oracles.py @@ -8,7 +8,8 @@ from model.entities.oracle_provider import OracleProvider -from model.types import OracleConfig, Pair +from model.types.pair import Pair +from model.types.configs import OracleConfig from model.utils import update_from_signal from model.utils.generator import Generator, state_update_blocks from model.utils.rng_provider import RNGProvider diff --git a/model/parts/celo_system.py b/model/parts/celo_system.py index 36f2f2f..99949bb 100644 --- a/model/parts/celo_system.py +++ b/model/parts/celo_system.py @@ -6,7 +6,8 @@ from model.entities.balance import Balance from model.generators.accounts import AccountGenerator from model.constants import target_epoch_rewards_downscaled, seconds_per_epoch, blocktime_seconds -from model.types import CryptoAsset, Fiat, Pair, Stable +from model.types.base import CryptoAsset, Fiat, Stable +from model.types.pair import Pair from model.utils.generator_container import inject @@ -33,8 +34,6 @@ def p_epoch_rewards(_params, _substep, _state_history, prev_state, validator_rewards / prev_state["oracle_rate"].get(Pair(CryptoAsset.CELO, Fiat.USD)) ) - - account_generator.reserve.balance += Balance({ CryptoAsset.CELO: validator_rewards, }) diff --git a/model/parts/reserve.py b/model/parts/reserve.py new file mode 100644 index 0000000..8a78f4f --- /dev/null +++ b/model/parts/reserve.py @@ -0,0 +1,38 @@ +""" +Reserve metric and advanced balance calculation +""" + + +from model.types.base import CryptoAsset + + +def p_reserve_statistics( + params, + _substep, + _state_history, + prev_state, +): + """ + calculates reserve statistics + """ + reserve_values_usd = prev_state['reserve_balance'].values_in_usd( + prev_state) + reserve_balance_usd = sum(list(reserve_values_usd.values())) + + reserve_celo_usd = reserve_values_usd.get(CryptoAsset.CELO) + + floating_supply_values_usd = prev_state['floating_supply'].values_in_usd( + prev_state) + floating_supply_balance_usd = sum(list(floating_supply_values_usd.values())) + + reserve_ratio = (reserve_celo_usd / + params['reserve_target_weight'] / floating_supply_balance_usd) + + collateralisation_ratio = reserve_balance_usd / floating_supply_balance_usd + + return { + 'reserve_balance_in_usd': reserve_balance_usd, + 'floating_supply_stables_in_usd': floating_supply_balance_usd, + 'reserve_ratio': reserve_ratio, + 'collateralisation_ratio': collateralisation_ratio + } diff --git a/model/state_update_blocks.py b/model/state_update_blocks.py index 738d6e0..380dd47 100644 --- a/model/state_update_blocks.py +++ b/model/state_update_blocks.py @@ -8,8 +8,9 @@ AccountGenerator, OracleRateGenerator, MentoExchangeGenerator - ) +) from model.parts import celo_system +from model.parts import reserve import model.parts.market_prices as market_price from model.utils import update_from_signal from model.utils.generator import generator_state_update_block @@ -53,6 +54,21 @@ } } +state_update_block_reserve_statistics = { + "description": """ + reserve statistics + """, + 'policies': { + 'reserve_statistics': reserve.p_reserve_statistics + }, + 'variables': { + 'reserve_balance_in_usd': update_from_signal('reserve_balance_in_usd'), + 'reserve_ratio': update_from_signal('reserve_ratio'), + 'collateralisation_ratio': update_from_signal('collateralisation_ratio'), + 'floating_supply_stables_in_usd': update_from_signal('floating_supply_stables_in_usd') + } +} + # Create state_update blocks list _state_update_blocks = [ state_update_block_market_price_change, @@ -61,6 +77,7 @@ generator_state_update_block(AccountGenerator, "traders"), state_update_block_epoch_rewards, state_update_block_price_impact, + state_update_block_reserve_statistics, ] price_impact_not_last = (state_update_block_price_impact in _state_update_blocks) and ( diff --git a/model/state_variables.py b/model/state_variables.py index e51d0c9..c640687 100644 --- a/model/state_variables.py +++ b/model/state_variables.py @@ -6,17 +6,17 @@ * Ensure that all State Variables are initialized """ from typing import Dict, TypedDict -from model.entities.balance import Balance -from model.types import ( + +from model.types.base import ( CryptoAsset, Fiat, MentoBuckets, MentoExchange, - Pair, Stable, ) - +from model.types.pair import Pair +from model.entities.balance import Balance from data.historical_values import ( CELO_SUPPLY_MEAN, CUSD_SUPPLY_MEAN, @@ -35,6 +35,10 @@ class StateVariables(TypedDict): reserve_balance: Balance mento_buckets: Dict[MentoExchange, MentoBuckets] market_price: Dict[Pair, float] + reserve_balance_in_usd: float + floating_supply_stables_in_usd: float + reserve_ratio: float + # Initialize State Variables instance with default values initial_state = StateVariables( @@ -50,9 +54,11 @@ class StateVariables(TypedDict): Pair(CryptoAsset.CELO, Fiat.BRL): 15, }, reserve_balance=Balance({ - CryptoAsset.CELO: 120000000.0 + CryptoAsset.CELO: 10000000.0, + CryptoAsset.BTC: 1000.0, + CryptoAsset.ETH: 15000.0, + CryptoAsset.DAI: 80000000.0, }), - # TODO initial calibration of buckets mento_buckets={ MentoExchange.CUSD_CELO: MentoBuckets(stable=0, reserve_asset=0), MentoExchange.CEUR_CELO: MentoBuckets(stable=0, reserve_asset=0), @@ -68,5 +74,9 @@ class StateVariables(TypedDict): Pair(CryptoAsset.ETH, Fiat.USD): 2000, Pair(CryptoAsset.BTC, Fiat.USD): 30000, Pair(CryptoAsset.DAI, Fiat.USD): 1, - } + }, + reserve_balance_in_usd=0.0, + floating_supply_stables_in_usd=0.0, + reserve_ratio=0.0, + collateralisation_ratio=0.0 ) diff --git a/model/system_parameters.py b/model/system_parameters.py index 9a01459..7227366 100644 --- a/model/system_parameters.py +++ b/model/system_parameters.py @@ -10,23 +10,25 @@ from QuantLib import GeometricBrownianMotionProcess from model.entities.balance import Balance -from model.types import ( +from model.types.base import ( CryptoAsset, Currency, Fiat, - ImpactDelayConfig, ImpactDelayType, - MarketPriceConfig, MarketPriceModel, MentoExchange, - MentoExchangeConfig, - OracleConfig, OracleType, - Pair, Stable, - TraderConfig, TraderType, ) +from model.types.pair import Pair +from model.types.configs import ( + MarketPriceConfig, + MentoExchangeConfig, + OracleConfig, + TraderConfig, + ImpactDelayConfig +) from model.utils.rng_provider import RNGProvider @@ -49,6 +51,7 @@ class Parameters(TypedDict): variance_market_price: Dict[Currency, Dict[Fiat, float]] traders: List[TraderConfig] reserve_inventory: Dict[Currency, float] + reserve_target_weight: float oracles: List[OracleConfig] @@ -69,6 +72,7 @@ class InitParameters(TypedDict): variance_market_price: List[Dict[Currency, Dict[Fiat, float]]] traders: List[List[TraderConfig]] reserve_inventory: List[Dict[Currency, float]] + reserve_target_weight: List[float] oracle_pairs: List[List[Pair]] oracles: List[List[OracleConfig]] @@ -245,9 +249,15 @@ class InitParameters(TypedDict): ], reserve_inventory=[{ - CryptoAsset.CELO: 12000000, + CryptoAsset.CELO: 10000000.0, + CryptoAsset.BTC: 1000.0, + CryptoAsset.ETH: 15000.0, + CryptoAsset.DAI: 80000000.0, }], + reserve_target_weight=[0.5 + ], + oracle_pairs=[[ Pair(CryptoAsset.CELO, Fiat.USD), Pair(CryptoAsset.CELO, Fiat.EUR), diff --git a/model/types.py b/model/types.py deleted file mode 100644 index b743058..0000000 --- a/model/types.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Various Python types used in the model -""" -from typing import Any, NamedTuple, TypedDict, Union -from enum import Enum - -from model.entities.balance import Balance - -# Celo system types -Gas = int -Wei = int -Gwei = float -GweiPerGas = float - -# Price types -UsdPerToken = float -TokenPerToken = float - -# TODO: Use decimal precision according to celo account balance precision -# Balance types -TokenBalance = float - -# Price types -TokenPriceInUSD = float -TokenPriceInToken = float - -# Simulation types -Run = int -Timestep = int - -# Time-related types -Blocknumber = int -Day = int - - -class SerializableEnum(Enum): - def __str__(self): - return self.value - - -class TraderType(Enum): - """ - different account holders - """ - ARBITRAGE_TRADER = "ArbitrageTrading" - RANDOM_TRADER = "RandomTrading" - MAX_TRADER = "SellMax" - - -class Stable(SerializableEnum): - """ - Celo Stable assets - """ - CUSD = "cusd" - CREAL = "creal" - CEUR = "ceur" - - -class CryptoAsset(SerializableEnum): - CELO = "celo" - ETH = "eth" - BTC = "btc" - DAI = "dai" - - -class Fiat(SerializableEnum): - USD = "usd" - EUR = "eur" - BRL = "brl" - - -class MentoExchange(SerializableEnum): - CUSD_CELO = "cusd_celo" - CREAL_CELO = "creal_celo" - CEUR_CELO = "ceur_celo" - - -Currency = Union[Stable, Fiat, CryptoAsset] - - -class Pair(NamedTuple): - base: Currency - quote: Currency - - def __str__(self): - return f"{self.base.value}_{self.quote.value}" - - -class MentoBuckets(TypedDict): - stable: float - reserve_asset: float - - -class TraderConfig(NamedTuple): - trader_type: TraderType - count: int - balance: Balance - exchange: MentoExchange - - -class MentoExchangeConfig(NamedTuple): - reserve_asset: CryptoAsset - stable: Stable - reference_fiat: Fiat - reserve_fraction: float - spread: float - bucket_update_frequency_second: int - max_sell_fraction_of_float: float - - -class MarketPriceConfig(NamedTuple): - pair: Pair - process: Any - param_1: float - param_2: float - - -class MarketPriceModel(Enum): - QUANTLIB = "quantlib" - PRICE_IMPACT = "price_impact" - HIST_SIM = "hist_sim" - SCENARIO = "scenario" - - -class PriceImpact(Enum): - ROOT_QUANTITY = "root_quantity" - CUSTOM = "custom" - - -class ImpactDelayType(Enum): - INSTANT = "instant" - NBLOCKS = "nblocks" - - -class AggregationMethod(Enum): - IDENTITY = 'indentity' - - -class OracleType(Enum): - SINGLE_SOURCE = 'single_source' - - -class OracleConfig(NamedTuple): - type: OracleType - count: int - aggregation: AggregationMethod - delay: int - reporting_interval: int - price_threshold: int - - -class ImpactDelayConfig(NamedTuple): - model: ImpactDelayType - param_1: float diff --git a/model/types/__init__.py b/model/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/types/base.py b/model/types/base.py new file mode 100644 index 0000000..d5c6a9f --- /dev/null +++ b/model/types/base.py @@ -0,0 +1,81 @@ +""" +Various Python types used in the model +""" +from __future__ import annotations +from typing import TypedDict, Union +from enum import Enum + + +class SerializableEnum(Enum): + def __str__(self): + return self.value + + +class TraderType(Enum): + """ + different account holders + """ + ARBITRAGE_TRADER = "ArbitrageTrading" + RANDOM_TRADER = "RandomTrading" + MAX_TRADER = "SellMax" + + +class Stable(SerializableEnum): + """ + Celo Stable assets + """ + CUSD = "cusd" + CREAL = "creal" + CEUR = "ceur" + + +class CryptoAsset(SerializableEnum): + CELO = "celo" + ETH = "eth" + BTC = "btc" + DAI = "dai" + + +class Fiat(SerializableEnum): + USD = "usd" + EUR = "eur" + BRL = "brl" + + +class MentoExchange(SerializableEnum): + CUSD_CELO = "cusd_celo" + CREAL_CELO = "creal_celo" + CEUR_CELO = "ceur_celo" + + +Currency = Union[Stable, Fiat, CryptoAsset] + + +class MentoBuckets(TypedDict): + stable: float + reserve_asset: float + + +class MarketPriceModel(Enum): + QUANTLIB = "quantlib" + PRICE_IMPACT = "price_impact" + HIST_SIM = "hist_sim" + SCENARIO = "scenario" + + +class PriceImpact(Enum): + ROOT_QUANTITY = "root_quantity" + CUSTOM = "custom" + + +class ImpactDelayType(Enum): + INSTANT = "instant" + NBLOCKS = "nblocks" + + +class AggregationMethod(Enum): + IDENTITY = 'indentity' + + +class OracleType(Enum): + SINGLE_SOURCE = 'single_source' diff --git a/model/types/configs.py b/model/types/configs.py new file mode 100644 index 0000000..924a993 --- /dev/null +++ b/model/types/configs.py @@ -0,0 +1,53 @@ +""" +Typing for Configs +""" + +from typing import Any, NamedTuple +from model.entities.balance import Balance + +from model.types.base import (AggregationMethod, + CryptoAsset, + Fiat, + ImpactDelayType, + MentoExchange, + OracleType, + Stable, + TraderType) +from model.types.pair import Pair + + +class TraderConfig(NamedTuple): + trader_type: TraderType + count: int + balance: Balance + exchange: MentoExchange + + +class MentoExchangeConfig(NamedTuple): + reserve_asset: CryptoAsset + stable: Stable + reference_fiat: Fiat + reserve_fraction: float + spread: float + bucket_update_frequency_second: int + max_sell_fraction_of_float: float + + +class MarketPriceConfig(NamedTuple): + pair: Pair + process: Any + param_1: float + param_2: float + + +class OracleConfig(NamedTuple): + type: OracleType + count: int + aggregation: AggregationMethod + delay: int + reporting_interval: int + price_threshold: int + +class ImpactDelayConfig(NamedTuple): + model: ImpactDelayType + param_1: float diff --git a/model/types/pair.py b/model/types/pair.py new file mode 100644 index 0000000..a652a17 --- /dev/null +++ b/model/types/pair.py @@ -0,0 +1,103 @@ +""" +Provides a Pair class with exchange rate functionality +""" +from __future__ import annotations +from typing import TYPE_CHECKING, NamedTuple, Union +from model.types.base import CryptoAsset, Currency, Fiat, Stable + +if TYPE_CHECKING: + from model.state_variables import StateVariables + + +class Rate(NamedTuple): + """ + Provides pair and value to an exchange rate + """ + value: float + pair: Pair + + def __mul__(self, other: Rate): + common_ccy = set([self.pair.base, self.pair.quote]).intersection( + set([other.pair.base, other.pair.quote])) + assert common_ccy, "Pairs are not compatible" + + self_aux = self + if self.pair.quote == other.pair.quote: + other = Rate(1/other.value, other.pair.inverse) + elif self.pair.base == other.pair.quote: + self_aux = Rate(1/self.value, self.pair.inverse) + other = Rate(1/other.value, other.pair.inverse) + elif self.pair.base == other.pair.base: + self_aux = Rate(1/self.value, self.pair.inverse) + + return Rate(self_aux.value * other.value, Pair(self_aux.pair.base, other.pair.quote)) + + +class Pair(NamedTuple): + """ + Base Class for Exchange Pairs + """ + base: Currency + quote: Currency + + def __str__(self): + return f"{self.base.value}_{self.quote.value}" + + @property + def inverse(self) -> Pair: + return Pair(self.quote, self.base) + + def get_rate(self, state: StateVariables) -> float: + """ + Get the market rate for any pair as long as there's a path + of other pairs with prices in market_price + """ + if self in state['market_price']: + rate = Rate(state['market_price'].get(self), self) + elif self.inverse in state['market_price']: + rate = Rate(1 / state['market_price'].get(self.inverse), self) + + else: + pairs = self.get_pairs(state) + rate = pairs[0].get_rate(state) + for pair in pairs[1:]: + rate *= pair.get_rate(state) + return rate + + def get_pairs(self, state) -> Union[Currency, Pair]: + """ + Get the list of pairs between self.base -> self.quote + """ + if isinstance(self.base, Fiat) and isinstance(self.quote, Fiat): + pairs = [Pair(CryptoAsset.CELO, self.base), + Pair(CryptoAsset.CELO, self.quote)] + elif isinstance(self.base, CryptoAsset) and isinstance(self.quote, CryptoAsset): + pairs = [Pair(self.base, Fiat.USD), Pair(self.quote, Fiat.USD)] + elif isinstance(self.base, Stable) and isinstance(self.quote, Stable): + end_pair = self.get_simulated_pair(state, match_base=False) + return [Pair(self.base, end_pair.quote), end_pair] + elif isinstance(self.base, CryptoAsset) and isinstance(self.quote, Fiat): + pairs = Pair(self.base, CryptoAsset.CELO).get_pairs( + state) + pairs.extend([Pair(CryptoAsset.CELO, self.quote)]) + elif isinstance(self.base, CryptoAsset) and isinstance(self.quote, Stable): + end_pair = self.get_simulated_pair(state, match_base=False) + pairs = [Pair(self.base, end_pair.quote), end_pair] + elif isinstance(self.base, Stable) and isinstance(self.quote, Fiat): + start_pair = self.get_simulated_pair(state) + pairs = [start_pair] + pairs.extend( + Pair(start_pair.quote, self.quote).get_pairs(state)) + else: + pairs = self.inverse.get_pairs(state) + return pairs + + def get_simulated_pair(self, state, match_base=True): + def match_reference(x): + return self.base if x else self.quote + market_prices = state['market_price'] + pair = [pair for pair in market_prices if pair.base == + match_reference(match_base)] + assert len( + pair) == 1, f'No or multiple pairs simulated for {match_reference(match_base)}' + return pair[0] diff --git a/model/utils/generator.py b/model/utils/generator.py index 06362fc..fdd0323 100644 --- a/model/utils/generator.py +++ b/model/utils/generator.py @@ -5,6 +5,7 @@ from abc import ABC, abstractmethod from typing import Callable, Dict, List, Type + def state_update_blocks(selector: str) -> Callable: """ Decorator used in Generators to specify that a method is meant @@ -16,6 +17,7 @@ def decorator(method): return method return decorator + # pylint: disable=too-few-public-methods,no-self-use class Generator(ABC): ''' @@ -75,6 +77,7 @@ def __cache_state_update_block_providers__(self): selector = method.__state_update_block_provider_selector__ self.__state_update_block_providers__[selector] = method + def generator_state_update_block(generator_class: Type[Generator], *selectors: List[str]): """ Returns a placeholder for one or more state update blocks which diff --git a/model/utils/price_impact_valuator.py b/model/utils/price_impact_valuator.py index 20ec99a..3fb7296 100644 --- a/model/utils/price_impact_valuator.py +++ b/model/utils/price_impact_valuator.py @@ -5,7 +5,8 @@ import numpy as np from model.system_parameters import Parameters -from model.types import Fiat, ImpactDelayType, Pair, PriceImpact +from model.types.base import Fiat, ImpactDelayType, PriceImpact +from model.types.pair import Pair PRICE_IMPACT_FUNCTION: Dict[PriceImpact, Callable] = { PriceImpact.ROOT_QUANTITY: diff --git a/model/utils/quantlib_wrapper.py b/model/utils/quantlib_wrapper.py index 33746a7..bd973c0 100644 --- a/model/utils/quantlib_wrapper.py +++ b/model/utils/quantlib_wrapper.py @@ -10,7 +10,7 @@ from experiments import simulation_configuration from model import constants -from model.types import MarketPriceConfig +from model.types.configs import MarketPriceConfig # raise numpy warnings as errors np.seterr(all='raise')