From 17457cf20f2461cec2acf3bc4e949b350d92100e Mon Sep 17 00:00:00 2001 From: Nadiem Sissouno Date: Tue, 5 Jul 2022 13:10:39 +0200 Subject: [PATCH 1/5] order type --- .../strategies/strategy_arbitrage_trader.py | 26 ++++------ model/entities/strategies/trader_strategy.py | 49 +++++++++---------- model/entities/trader.py | 13 +++-- model/types/base.py | 16 ++++++ 4 files changed, 53 insertions(+), 51 deletions(-) diff --git a/model/entities/strategies/strategy_arbitrage_trader.py b/model/entities/strategies/strategy_arbitrage_trader.py index 3e49181..204a0d9 100644 --- a/model/entities/strategies/strategy_arbitrage_trader.py +++ b/model/entities/strategies/strategy_arbitrage_trader.py @@ -108,9 +108,8 @@ def calculate(self, _params, prev_state): market_price, spread ) - else: - self.order = {"sell_amount": None} - self.sell_amount = self.order["sell_amount"] + + # self.sell_amount = self.order["sell_amount"] def sell_order_stable(self, buckets: MentoBuckets, market_price: float, spread: float): """ @@ -127,13 +126,9 @@ def sell_order_stable(self, buckets: MentoBuckets, market_price: float, spread: spread ) - self.order = { - "sell_amount": min( - sell_amount, - max_budget_stable, - ), - "sell_reserve_asset": False, - } + self.order.sell_amount = min(sell_amount, + max_budget_stable,) + self.order.sell_reserve_asset = False def sell_order_reserve_asset( self, @@ -154,15 +149,12 @@ def sell_order_reserve_asset( spread ) - self.order = { - "sell_amount": min( - sell_amount, - max_budget_celo, - ), - "sell_reserve_asset": True, - } + self.order.sell_amount = min(sell_amount, + max_budget_celo,) + self.order.sell_reserve_asset = True # pylint: disable=no-self-use + def optimal_sell_amount(self, bucket_sell, bucket_buy, price_buy_sell, spread): """ bucket_sell: bucket of asset that is sold diff --git a/model/entities/strategies/trader_strategy.py b/model/entities/strategies/trader_strategy.py index 50a1c69..dc002c9 100644 --- a/model/entities/strategies/trader_strategy.py +++ b/model/entities/strategies/trader_strategy.py @@ -13,7 +13,7 @@ from cvxpy import Maximize, Minimize, Problem, Variable import cvxpy -from model.types.base import MentoBuckets +from model.types.base import MentoBuckets, Order from model.types.pair import Pair from model.types.configs import MentoExchangeConfig if TYPE_CHECKING: @@ -28,6 +28,7 @@ class TraderStrategy: """ parent: "Trader" exchange_config: MentoExchangeConfig + order: Order def __init__(self, parent: "Trader", acting_frequency): self.parent = parent @@ -42,8 +43,11 @@ def __init__(self, parent: "Trader", acting_frequency): self.optimization_direction = None self.constraints = [] # TODO order vs sell_amount ??? - self.sell_amount = None - self.order = None + self.order = Order(asset=None, + sell_amount=None, + buy_amount=None, + sell_reserve_asset=None, + exchange=None) @property def reference_fiat(self): @@ -169,40 +173,31 @@ def minimise_price_impact(self, sell_amount, sell_reserve_asset, params): def trader_passes_step(self, _params, prev_state): return prev_state["timestep"] % self.acting_frequency != 0 - def return_optimal_trade(self, params, prev_state): + def create_order(self, params, prev_state): """ Returns the optimal action to be executed by actor """ - if self.trader_passes_step(params, prev_state): - # Actor not acting this timestep - trade = None - else: + # if self.trader_passes_step(params, prev_state): + # # Actor not acting this timestep + if not self.trader_passes_step(params, prev_state): self.optimize(params=params, prev_state=prev_state) - sell_amount = ( - self.variables["sell_amount"].value - if self.variables - else self.sell_amount + self.order.sell_amount = ( + self.variables["sell_amount"].value if self.variables else self.order.sell_amount ) - if sell_amount is None or sell_amount == 0: - trade = None + + if self.order.sell_amount is None or self.order.sell_amount == 0: + self.order.sell_amount = None else: - sell_reserve_asset = self.sell_reserve_asset(params, prev_state) - sell_amount = self.minimise_price_impact( - sell_amount, sell_reserve_asset, params) - buy_amount = self.mento.get_buy_amount( + self.order.asset = self.sell_reserve_asset(params, prev_state) + self.order.sell_amount = self.minimise_price_impact( + self.order.sell_amount, self.order.asset, params) + self.order.buy_amount = self.mento.get_buy_amount( exchange=self.parent.config.exchange, - sell_amount=sell_amount, - sell_reserve_asset=sell_reserve_asset, + sell_amount=self.order.sell_amount, + sell_reserve_asset=self.order.sell_reserve_asset, prev_state=prev_state, ) - trade = { - "sell_reserve_asset": sell_reserve_asset, - "sell_amount": sell_amount, - "buy_amount": buy_amount, - } - return trade - def market_price(self, prev_state) -> float: # TODO: Do we need to quote in equivalent Fiat for Stable? return ( diff --git a/model/entities/trader.py b/model/entities/trader.py index 44f1a27..dfcf4d8 100644 --- a/model/entities/trader.py +++ b/model/entities/trader.py @@ -54,22 +54,21 @@ def execute( """ Execute the agent's state change """ - order = self.strategy.return_optimal_trade(params, prev_state) - if order is None: + self.strategy.create_order(params, prev_state) + if self.strategy.order.sell_amount is None: return { "mento_buckets": prev_state["mento_buckets"], "floating_supply": prev_state["floating_supply"], "reserve_balance": prev_state["reserve_balance"], } - sell_amount = order["sell_amount"] - sell_reserve_asset = order["sell_reserve_asset"] - self.rebalance_portfolio(sell_amount, sell_reserve_asset, prev_state) + self.rebalance_portfolio(self.strategy.order.sell_amount, + self.strategy.order.sell_reserve_asset, prev_state) next_bucket, delta = self.mento.exchange( self.config.exchange, - sell_amount, - sell_reserve_asset, + self.strategy.order.sell_amount, + self.strategy.order.sell_reserve_asset, prev_state ) diff --git a/model/types/base.py b/model/types/base.py index d5c6a9f..97e90ce 100644 --- a/model/types/base.py +++ b/model/types/base.py @@ -79,3 +79,19 @@ class AggregationMethod(Enum): class OracleType(Enum): SINGLE_SOURCE = 'single_source' + + +class Exchange(SerializableEnum): + MENTO = 'mento' + GENERAL_MARKET = 'general_market' + +# pylint:disable = too-few-public-methods + + +class Order(): + def __init__(self, asset, sell_amount, buy_amount, sell_reserve_asset, exchange): + self.asset: Currency = asset + self.sell_amount: float = sell_amount + self.buy_amount: float = buy_amount + self.sell_reserve_asset: bool = sell_reserve_asset + self.exchange: Exchange = exchange From 8f19ade29078c93a1fbbbfbba6a47d0e8a9a3eba Mon Sep 17 00:00:00 2001 From: Nadiem Sissouno Date: Thu, 7 Jul 2022 11:53:25 +0200 Subject: [PATCH 2/5] WIP fix dependencies --- model/entities/account.py | 7 ++-- model/entities/strategies/trader_strategy.py | 6 ++- model/entities/trader.py | 20 ++-------- model/generators/mento.py | 41 +++++++++++++++++--- model/types/base.py | 13 +------ model/types/order.py | 23 +++++++++++ 6 files changed, 72 insertions(+), 38 deletions(-) create mode 100644 model/types/order.py diff --git a/model/entities/account.py b/model/entities/account.py index 64be74e..ec1419a 100644 --- a/model/entities/account.py +++ b/model/entities/account.py @@ -5,10 +5,11 @@ # pylint: disable=too-few-public-methods from typing import TYPE_CHECKING from uuid import UUID -from model.entities.balance import Balance if TYPE_CHECKING: + from model.entities.balance import Balance from model.generators.accounts import AccountGenerator + class Account(): """ Agent / Account Holder @@ -16,14 +17,14 @@ class Account(): parent: "AccountGenerator" account_id: UUID account_name: str - balance: Balance + balance: "Balance" def __init__( self, parent: "AccountGenerator", account_id: UUID, account_name: str, - balance: Balance + balance: "Balance" ): self.parent = parent self.account_id = account_id diff --git a/model/entities/strategies/trader_strategy.py b/model/entities/strategies/trader_strategy.py index dc002c9..6950289 100644 --- a/model/entities/strategies/trader_strategy.py +++ b/model/entities/strategies/trader_strategy.py @@ -13,8 +13,9 @@ from cvxpy import Maximize, Minimize, Problem, Variable import cvxpy -from model.types.base import MentoBuckets, Order +from model.types.base import MentoBuckets from model.types.pair import Pair +from model.types.order import Order from model.types.configs import MentoExchangeConfig if TYPE_CHECKING: from model.entities.trader import Trader @@ -43,7 +44,8 @@ def __init__(self, parent: "Trader", acting_frequency): self.optimization_direction = None self.constraints = [] # TODO order vs sell_amount ??? - self.order = Order(asset=None, + self.order = Order(account=self.parent, + asset=None, sell_amount=None, buy_amount=None, sell_reserve_asset=None, diff --git a/model/entities/trader.py b/model/entities/trader.py index dfcf4d8..785af4c 100644 --- a/model/entities/trader.py +++ b/model/entities/trader.py @@ -9,7 +9,8 @@ from model.generators.mento import MentoExchangeGenerator from model.entities import strategies -from model.entities.account import Account, Balance +from model.entities.account import Account +from model.entities.balance import Balance from model.types.pair import Pair from model.types.configs import MentoExchangeConfig, TraderConfig from model.utils.rng_provider import RNGProvider @@ -65,22 +66,7 @@ def execute( self.rebalance_portfolio(self.strategy.order.sell_amount, self.strategy.order.sell_reserve_asset, prev_state) - next_bucket, delta = self.mento.exchange( - self.config.exchange, - self.strategy.order.sell_amount, - self.strategy.order.sell_reserve_asset, - prev_state - ) - - self.balance += delta - reserve_delta = Balance({ - self.exchange_config.reserve_asset: - -1 * delta.get(self.exchange_config.reserve_asset), - }) - self.parent.reserve.balance += reserve_delta - - next_buckets = deepcopy(prev_state["mento_buckets"]) - next_buckets[self.config.exchange] = next_bucket + next_buckets = self.mento.process_order(self.strategy.order, prev_state) return { "mento_buckets": next_buckets, diff --git a/model/generators/mento.py b/model/generators/mento.py index 8dec9b3..f3572c7 100644 --- a/model/generators/mento.py +++ b/model/generators/mento.py @@ -4,17 +4,23 @@ Handles one or more mento instances """ -from typing import Any, Dict, Set +from copy import deepcopy +from typing import TYPE_CHECKING, Any, Dict, Set import numpy as np from model.constants import blocktime_seconds from model.entities.balance import Balance +from model.utils.generator_container import GeneratorContainer 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 +# if TYPE_CHECKING: +# from model.entities.balance import Balance +# from model.generators.accounts import AccountGenerator + # raise numpy warnings as errors np.seterr(all='raise') @@ -31,16 +37,22 @@ class MentoExchangeGenerator(Generator): """ configs: Dict[MentoExchange, MentoExchangeConfig] active_exchanges: Set[MentoExchange] + container: GeneratorContainer - def __init__(self, configs: Dict[Stable, MentoExchangeConfig], active_exchanges: Set[Stable]): + def __init__(self, configs: Dict[Stable, MentoExchangeConfig], + active_exchanges: Set[Stable], container: GeneratorContainer): + from model.generators.accounts import AccountGenerator self.configs = configs self.active_exchanges = active_exchanges + self.container = container + self.account_generator = self.container.get(AccountGenerator) - @classmethod - def from_parameters(cls, params, _initial_state, _container): + @ classmethod + def from_parameters(cls, params, _initial_state, container): return cls( params['mento_exchanges_config'], - set(params['mento_exchanges_active']) + set(params['mento_exchanges_active']), + container ) @state_update_blocks('bucket_update') @@ -171,3 +183,22 @@ def exchange(self, exchange: MentoExchange, sell_amount, sell_reserve_asset, pre }) return (next_bucket, delta) + + def process_order(self, order, prev_state): + next_bucket, delta = self.exchange( + order.account.config.exchange, + order.strategy.order.sell_amount, + order.strategy.order.sell_reserve_asset, + prev_state + ) + + order.account.balance += delta + reserve_delta = Balance({ + order.account.exchange_config.reserve_asset: + -1 * delta.get(order.account.exchange_config.reserve_asset), + }) + self.account_generator.reserve.balance += reserve_delta + + next_buckets = deepcopy(prev_state["mento_buckets"]) + next_buckets[order.config.exchange] = next_bucket + return next_buckets diff --git a/model/types/base.py b/model/types/base.py index 97e90ce..4bbbc1d 100644 --- a/model/types/base.py +++ b/model/types/base.py @@ -5,6 +5,8 @@ from typing import TypedDict, Union from enum import Enum +from model.entities.account import Account + class SerializableEnum(Enum): def __str__(self): @@ -84,14 +86,3 @@ class OracleType(Enum): class Exchange(SerializableEnum): MENTO = 'mento' GENERAL_MARKET = 'general_market' - -# pylint:disable = too-few-public-methods - - -class Order(): - def __init__(self, asset, sell_amount, buy_amount, sell_reserve_asset, exchange): - self.asset: Currency = asset - self.sell_amount: float = sell_amount - self.buy_amount: float = buy_amount - self.sell_reserve_asset: bool = sell_reserve_asset - self.exchange: Exchange = exchange diff --git a/model/types/order.py b/model/types/order.py new file mode 100644 index 0000000..f80502d --- /dev/null +++ b/model/types/order.py @@ -0,0 +1,23 @@ +""" +Provides a Order class +""" + + +from typing import TYPE_CHECKING +from model.types.base import Currency, Exchange + + +if TYPE_CHECKING: + from model.entities.account import Account + +# pylint:disable = too-few-public-methods + + +class Order(): + def __init__(self, account, asset, sell_amount, buy_amount, sell_reserve_asset, exchange): + self.account: Account = account + self.asset: Currency = asset + self.sell_amount: float = sell_amount + self.buy_amount: float = buy_amount + self.sell_reserve_asset: bool = sell_reserve_asset + self.exchange: Exchange = exchange From 7f81c789133938e1976d5198d2d1485e344c9125 Mon Sep 17 00:00:00 2001 From: boqdan Date: Thu, 7 Jul 2022 13:07:49 +0300 Subject: [PATCH 3/5] Fix circular container initialization --- model/generators/mento.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/model/generators/mento.py b/model/generators/mento.py index f3572c7..b757ab4 100644 --- a/model/generators/mento.py +++ b/model/generators/mento.py @@ -5,11 +5,13 @@ """ from copy import deepcopy +from functools import cached_property from typing import TYPE_CHECKING, Any, Dict, Set import numpy as np from model.constants import blocktime_seconds from model.entities.balance import Balance +from model.types.order import Order from model.utils.generator_container import GeneratorContainer from model.types.base import MentoBuckets, MentoExchange, Stable from model.types.pair import Pair @@ -41,19 +43,23 @@ class MentoExchangeGenerator(Generator): def __init__(self, configs: Dict[Stable, MentoExchangeConfig], active_exchanges: Set[Stable], container: GeneratorContainer): - from model.generators.accounts import AccountGenerator self.configs = configs self.active_exchanges = active_exchanges self.container = container - self.account_generator = self.container.get(AccountGenerator) + - @ classmethod + @classmethod def from_parameters(cls, params, _initial_state, container): return cls( params['mento_exchanges_config'], set(params['mento_exchanges_active']), container ) + + @cached_property + def account_generator(self): + from model.generators.accounts import AccountGenerator + return self.container.get(AccountGenerator) @state_update_blocks('bucket_update') def bucket_update(self): @@ -184,11 +190,11 @@ def exchange(self, exchange: MentoExchange, sell_amount, sell_reserve_asset, pre return (next_bucket, delta) - def process_order(self, order, prev_state): + def process_order(self, order: Order, prev_state): next_bucket, delta = self.exchange( order.account.config.exchange, - order.strategy.order.sell_amount, - order.strategy.order.sell_reserve_asset, + order.sell_amount, + order.sell_reserve_asset, prev_state ) @@ -200,5 +206,5 @@ def process_order(self, order, prev_state): self.account_generator.reserve.balance += reserve_delta next_buckets = deepcopy(prev_state["mento_buckets"]) - next_buckets[order.config.exchange] = next_bucket + next_buckets[order.exchange] = next_bucket return next_buckets From 83f3b43dfbff44504896b3907fadccb9c7e7af7e Mon Sep 17 00:00:00 2001 From: Nadiem Sissouno Date: Fri, 22 Jul 2022 10:47:17 +0200 Subject: [PATCH 4/5] adding pair to data_feed; changing impact config --- experiments/simulation_configuration.py | 6 +++--- model/system_parameters.py | 8 +++++--- model/types/base.py | 23 +++++++++++++++++++---- model/types/pair.py | 16 ++++++++++++++++ model/utils/data_feed.py | 6 +++++- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/experiments/simulation_configuration.py b/experiments/simulation_configuration.py index 5373fbf..09a77b2 100644 --- a/experiments/simulation_configuration.py +++ b/experiments/simulation_configuration.py @@ -4,10 +4,10 @@ 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 = 2 # number of days +SIMULATION_TIME_DAYS = 1 # 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 -MONTE_CARLO_RUNS = 2 # number of runs -DATA_SOURCE = 'historical' # 'mock' or 'historical' +MONTE_CARLO_RUNS = 1 # number of runs +DATA_SOURCE = 'mock' # 'mock' or 'historical' TOTAL_BLOCKS = (BLOCKS_PER_TIMESTEP * TIMESTEPS) + 1 diff --git a/model/system_parameters.py b/model/system_parameters.py index 7227366..e54c0ed 100644 --- a/model/system_parameters.py +++ b/model/system_parameters.py @@ -213,16 +213,18 @@ class InitParameters(TypedDict): ], impacted_assets=[[ Pair(CryptoAsset.CELO, Fiat.USD), + Pair(CryptoAsset.CELO, Fiat.EUR), + Pair(CryptoAsset.CELO, Fiat.BRL), Pair(Stable.CUSD, Fiat.USD), Pair(Stable.CEUR, Fiat.EUR), Pair(Stable.CREAL, Fiat.BRL), - Pair(CryptoAsset.BTC, Fiat.USD), - Pair(CryptoAsset.ETH, Fiat.USD), - Pair(CryptoAsset.DAI, Fiat.USD), + ]], variance_market_price=[{ Pair(CryptoAsset.CELO, Fiat.USD): 1, + Pair(CryptoAsset.CELO, Fiat.EUR): 1, + Pair(CryptoAsset.CELO, Fiat.BRL): 1, Pair(Stable.CUSD, Fiat.USD): 0.01, Pair(Stable.CEUR, Fiat.EUR): 0.01, Pair(Stable.CREAL, Fiat.BRL): 0.01, diff --git a/model/types/base.py b/model/types/base.py index d5c6a9f..7d25505 100644 --- a/model/types/base.py +++ b/model/types/base.py @@ -2,14 +2,29 @@ Various Python types used in the model """ from __future__ import annotations +from enum import Enum, EnumMeta from typing import TypedDict, Union -from enum import Enum class SerializableEnum(Enum): def __str__(self): return self.value +# pylint: disable=no-value-for-parameter + + +class MetaEnum(EnumMeta): + def __contains__(cls, item): + try: + cls(item) + except ValueError: + return False + return True + + +class ReflectiveSerializableEnum(SerializableEnum, metaclass=MetaEnum): + pass + class TraderType(Enum): """ @@ -20,7 +35,7 @@ class TraderType(Enum): MAX_TRADER = "SellMax" -class Stable(SerializableEnum): +class Stable(ReflectiveSerializableEnum): """ Celo Stable assets """ @@ -29,14 +44,14 @@ class Stable(SerializableEnum): CEUR = "ceur" -class CryptoAsset(SerializableEnum): +class CryptoAsset(ReflectiveSerializableEnum): CELO = "celo" ETH = "eth" BTC = "btc" DAI = "dai" -class Fiat(SerializableEnum): +class Fiat(ReflectiveSerializableEnum): USD = "usd" EUR = "eur" BRL = "brl" diff --git a/model/types/pair.py b/model/types/pair.py index a652a17..99ffb1e 100644 --- a/model/types/pair.py +++ b/model/types/pair.py @@ -47,6 +47,22 @@ def __str__(self): def inverse(self) -> Pair: return Pair(self.quote, self.base) + @classmethod + def parse_from_string(cls, string): + """ + Creating Pair from string 'baseccy_quoteccy' + """ + base, quote = string.split('_') + assert base not in Fiat, f'Wrong quote convention used for {string}' + assert quote in Fiat, f'Wrong quote convention used for {string}' + if base in CryptoAsset: + base = CryptoAsset(base) + elif base in Stable: + base = Stable(base) + quote = Fiat(quote) + + return Pair(base, quote) + def get_rate(self, state: StateVariables) -> float: """ Get the market rate for any pair as long as there's a path diff --git a/model/utils/data_feed.py b/model/utils/data_feed.py index cd15b2f..8f8a3a9 100644 --- a/model/utils/data_feed.py +++ b/model/utils/data_feed.py @@ -11,6 +11,7 @@ # pylint: disable = unused-import # pylint: disable = redefined-outer-name import data.mock_data # this is necessary to create mock data if not existent +from model.types.pair import Pair DATA_FOLDER = Path(__file__, "../../../data/").resolve() MOCK_DATA_FILE_NAME = "mock_logreturns.prq" @@ -36,7 +37,8 @@ def __init__(self, data_folder): self.data = np.array(self.historical_data) self.length = len(self.historical_data) - self.assets = list(self.historical_data.columns) + self.assets = [Pair.parse_from_string(name) + for name in self.historical_data.columns] def load_mock_data(self, data_file_name): """ @@ -59,8 +61,10 @@ def load_historical_data(self, data_file_name): raise NotImplementedError(f"File extension {file_extension} not supported") historical_log_returns = self.calculate_log_returns(historical_prices) + print(historical_log_returns) return historical_log_returns + # pylint: disable =no-self-use def calculate_log_returns(self, data_frame): """ calculates log returns out of a data frame with price time series in its columns From 0cf2b06d952343fb222df9f0855c22ef59e4ebbb Mon Sep 17 00:00:00 2001 From: Nadiem Sissouno Date: Fri, 22 Jul 2022 18:14:19 +0200 Subject: [PATCH 5/5] more refactoring (WIP) --- .../strategies/strategy_arbitrage_trader.py | 23 +++----- .../strategies/strategy_random_trader.py | 54 ++++++++++++------- model/entities/strategies/trader_strategy.py | 36 +++++++++---- model/entities/trader.py | 22 ++++---- model/generators/mento.py | 19 ++++--- model/system_parameters.py | 8 ++- model/types/order.py | 15 +++--- 7 files changed, 108 insertions(+), 69 deletions(-) diff --git a/model/entities/strategies/strategy_arbitrage_trader.py b/model/entities/strategies/strategy_arbitrage_trader.py index 204a0d9..c8562f7 100644 --- a/model/entities/strategies/strategy_arbitrage_trader.py +++ b/model/entities/strategies/strategy_arbitrage_trader.py @@ -1,17 +1,11 @@ """ Strategy: Arbitrage Trader """ -from enum import Enum import numpy as np from model.types.base import MentoBuckets from .trader_strategy import TraderStrategy - - -class TradingRegime(Enum): - SELL_STABLE = "SELL_STABLE" - SELL_RESERVE_ASSET = "SELL_RESERVE_ASSET" - PASS = "PASS" +from .trader_strategy import TradingRegime # pylint: disable=using-constant-test @@ -28,9 +22,9 @@ def __init__(self, parent, acting_frequency=1): def sell_reserve_asset(self, _params, prev_state): # Arb trade will sell CELO if CELO/USD > CELO/cUSD - return self.trading_regime(prev_state) == TradingRegime.SELL_RESERVE_ASSET + return self.determine_trading_regime(prev_state) == TradingRegime.SELL_RESERVE_ASSET - def trading_regime(self, prev_state) -> TradingRegime: + def determine_trading_regime(self, prev_state) -> TradingRegime: """ Indicates how the trader will act depending on the relation of mento price and market price @@ -63,7 +57,7 @@ def define_expressions(self, _params, prev_state): mento_buckets = self.mento_buckets(prev_state) spread = self.exchange_config.spread - if self.trading_regime(prev_state) == TradingRegime.SELL_STABLE: + if self.trading_regime == TradingRegime.SELL_STABLE: self.expressions["profit"] = ( -1 * self.variables["sell_amount"] * mento_buckets['reserve_asset'] @@ -72,7 +66,7 @@ def define_expressions(self, _params, prev_state): + (1 - spread) * self.variables["sell_amount"] + market_price * self.variables["sell_amount"] ) - elif self.trading_regime(prev_state) == TradingRegime.SELL_RESERVE_ASSET: + elif self.trading_regime == TradingRegime.SELL_RESERVE_ASSET: self.expressions["profit"] = ( -self.variables["sell_amount"] * mento_buckets['stable'] @@ -83,7 +77,7 @@ def define_expressions(self, _params, prev_state): ) def trader_passes_step(self, _params, prev_state): - return (self.trading_regime(prev_state) == "PASS") or \ + return (self.determine_trading_regime(prev_state) == "PASS") or \ (prev_state["timestep"] % self.acting_frequency != 0) # # pylint: disable=attribute-defined-outside-init @@ -94,15 +88,14 @@ def calculate(self, _params, prev_state): market_price = self.market_price(prev_state) mento_buckets = self.mento_buckets(prev_state) spread = self.exchange_config.spread - mento_price = mento_buckets['stable'] / mento_buckets['reserve_asset'] - if market_price * (1 - spread) > mento_price: + if self.trading_regime == TradingRegime.SELL_STABLE: self.sell_order_stable( mento_buckets, market_price, spread ) - elif market_price / (1 - spread) < mento_price: + elif self.trading_regime == TradingRegime.SELL_RESERVE_ASSET: self.sell_order_reserve_asset( mento_buckets, market_price, diff --git a/model/entities/strategies/strategy_random_trader.py b/model/entities/strategies/strategy_random_trader.py index 1fc7a39..c8cbd44 100644 --- a/model/entities/strategies/strategy_random_trader.py +++ b/model/entities/strategies/strategy_random_trader.py @@ -1,27 +1,30 @@ """ Strategy: Random Trader """ +from typing import List from cvxpy import Variable import numpy as np from experiments import simulation_configuration -from .trader_strategy import TraderStrategy +from model.entities.strategies.trader_strategy import TraderStrategy, TradingRegime +from model.types.order import Order + class RandomTrading(TraderStrategy): """ Random Trading """ + order_list: List[Order] def __init__(self, parent, acting_frequency=1): # The following is used to define the strategy and needs to be provided in subclass super().__init__(parent, acting_frequency) - self.generate_sell_amounts() - self.sell_amount = None self.rng = parent.rngp.get_rng("RandomTrader", self.parent.account_id) + self.order_list = self.generate_sell_amounts() def sell_reserve_asset(self, _params, prev_state): - return self.orders[prev_state["timestep"]]["sell_reserve_asset"] + return self.order_list["sell_reserve_asset"][prev_state["timestep"]] def define_variables(self): self.variables["sell_amount"] = Variable(pos=True) @@ -52,7 +55,7 @@ def define_constraints(self, params, prev_state): self.variables["sell_amount"] <= min( max_budget_reserve_asset, - self.orders[prev_state["timestep"]]["sell_amount"] + self.order_list["sell_amount"][prev_state["timestep"]] ) ) else: @@ -60,31 +63,44 @@ def define_constraints(self, params, prev_state): self.variables["sell_amount"] <= min( max_budget_stable, - self.orders[prev_state["timestep"]]["sell_amount"] + self.order_list["sell_amount"][prev_state["timestep"]] ) ) + def determine_trading_regime(self, prev_state) -> TradingRegime: + """ + Indicates how the trader will act depending on the relation of mento price + and market price + """ + sell_reserve_asset = self.order_list[ + 'sell_reserve_asset'][prev_state['timestep']] + regime = TradingRegime.PASS + if sell_reserve_asset: + regime = TradingRegime.SELL_RESERVE_ASSET + elif not sell_reserve_asset: + regime = TradingRegime.SELL_STABLE + return regime + def generate_sell_amounts( self, - blocks_per_timestep=simulation_configuration.BLOCKS_PER_TIMESTEP, - timesteps=simulation_configuration.TIMESTEPS, ): """ This function generates lognormal returns """ - # timesteps_per_year = constants.blocks_per_year // blocks_per_timestep - sample_size = timesteps * blocks_per_timestep + 1 - # TODO parametrise random params incl. seed - sell_gold = self.rng.binomial(1, 0.5, sample_size) - orders = np.vstack( - [sell_gold, np.abs(self.rng.normal(100, 5, size=sample_size))] - ) - self.orders = np.core.records.fromarrays( - orders, names=["sell_reserve_asset", "sell_amount"] - ) + sample_size = simulation_configuration.TOTAL_BLOCKS + 1 + + sell_reserve_asset = (self.rng.binomial(1, 0.5, sample_size) == 1) + sell_amount = np.abs(self.rng.normal(1000, 0, size=sample_size)) + + orders = {"sell_reserve_asset": sell_reserve_asset, + "sell_amount": sell_amount} + return orders def calculate(self, _params, prev_state): """ Calculates optimal trade if analytical solution is available """ - self.sell_amount = self.orders[prev_state["timestep"]]["sell_amount"] + sell_amounts = self.order_list["sell_amount"] + order_directions = self.order_list["sell_reserve_asset"] + self.order.sell_amount = sell_amounts[prev_state["timestep"]] + self.order.sell_reserve_asset = order_directions[prev_state["timestep"]] diff --git a/model/entities/strategies/trader_strategy.py b/model/entities/strategies/trader_strategy.py index 6950289..fc224f2 100644 --- a/model/entities/strategies/trader_strategy.py +++ b/model/entities/strategies/trader_strategy.py @@ -8,6 +8,7 @@ inside of solve() but the objective_function and the constraints should still be specified for completeness! """ +from enum import Enum from typing import TYPE_CHECKING import logging from cvxpy import Maximize, Minimize, Problem, Variable @@ -21,6 +22,12 @@ from model.entities.trader import Trader +class TradingRegime(Enum): + SELL_STABLE = "SELL_STABLE" + SELL_RESERVE_ASSET = "SELL_RESERVE_ASSET" + PASS = "PASS" + + class TraderStrategy: """ Base trader strategy class to solve a convex optimisation problem. @@ -29,6 +36,7 @@ class TraderStrategy: """ parent: "Trader" exchange_config: MentoExchangeConfig + trading_regime: TradingRegime order: Order def __init__(self, parent: "Trader", acting_frequency): @@ -43,13 +51,13 @@ def __init__(self, parent: "Trader", acting_frequency): self.objective_function = None self.optimization_direction = None self.constraints = [] - # TODO order vs sell_amount ??? - self.order = Order(account=self.parent, - asset=None, + self.trading_regime = None + self.order = Order(account_id=self.parent.account_id, + pair=None, sell_amount=None, buy_amount=None, sell_reserve_asset=None, - exchange=None) + exchange=self.parent.config.exchange) @property def reference_fiat(self): @@ -85,6 +93,12 @@ def define_expressions(self, _params, _prev_state): """ raise NotImplementedError("Subclasses must implement define_expressions()") + def determine_trading_regime(self, _prev_state) -> TradingRegime: + """ + Indicates how the trader will act depending on the state of the market + """ + raise NotImplementedError("Subclasses must implement trading_regime()") + def define_constraints(self, params, prev_state): """ Defines and returns the constraints under which the optimization is conducted @@ -173,15 +187,18 @@ def minimise_price_impact(self, sell_amount, sell_reserve_asset, params): return sell_amount_adjusted def trader_passes_step(self, _params, prev_state): + """ + evaluates whether trader passes current step due to acting frequency + """ return prev_state["timestep"] % self.acting_frequency != 0 def create_order(self, params, prev_state): """ Returns the optimal action to be executed by actor """ - # if self.trader_passes_step(params, prev_state): - # # Actor not acting this timestep - if not self.trader_passes_step(params, prev_state): + self.determine_trading_regime(prev_state) + if (not self.trader_passes_step(params, prev_state) + and self.trading_regime != TradingRegime.PASS): self.optimize(params=params, prev_state=prev_state) self.order.sell_amount = ( self.variables["sell_amount"].value if self.variables else self.order.sell_amount @@ -190,15 +207,16 @@ def create_order(self, params, prev_state): if self.order.sell_amount is None or self.order.sell_amount == 0: self.order.sell_amount = None else: - self.order.asset = self.sell_reserve_asset(params, prev_state) + #self.order.pair = self.sell_reserve_asset(params, prev_state) self.order.sell_amount = self.minimise_price_impact( - self.order.sell_amount, self.order.asset, params) + self.order.sell_amount, self.order.pair, params) self.order.buy_amount = self.mento.get_buy_amount( exchange=self.parent.config.exchange, sell_amount=self.order.sell_amount, sell_reserve_asset=self.order.sell_reserve_asset, prev_state=prev_state, ) + self.parent.order = self.order def market_price(self, prev_state) -> float: # TODO: Do we need to quote in equivalent Fiat for Stable? diff --git a/model/entities/trader.py b/model/entities/trader.py index 785af4c..143a99c 100644 --- a/model/entities/trader.py +++ b/model/entities/trader.py @@ -4,13 +4,13 @@ """ # pylint: disable=too-few-public-methods from typing import TYPE_CHECKING -from copy import deepcopy from uuid import UUID from model.generators.mento import MentoExchangeGenerator from model.entities import strategies from model.entities.account import Account from model.entities.balance import Balance +from model.types.order import Order from model.types.pair import Pair from model.types.configs import MentoExchangeConfig, TraderConfig from model.utils.rng_provider import RNGProvider @@ -28,6 +28,7 @@ class Trader(Account): exchange_config: MentoExchangeConfig mento: MentoExchangeGenerator rngp: RNGProvider + order: Order def __init__( self, @@ -41,11 +42,10 @@ def __init__( self.rngp = rngp self.mento = self.parent.container.get(MentoExchangeGenerator) self.config = config - self.exchange_config = self.mento.configs.get(self.config.exchange) - strategy_class = getattr(strategies, config.trader_type.value) assert strategy_class is not None, f"{config.trader_type.value} is not a strategy" self.strategy = strategy_class(self) + self.order = None def execute( self, @@ -56,17 +56,17 @@ def execute( Execute the agent's state change """ self.strategy.create_order(params, prev_state) - if self.strategy.order.sell_amount is None: + if self.order.sell_amount is None: return { "mento_buckets": prev_state["mento_buckets"], "floating_supply": prev_state["floating_supply"], "reserve_balance": prev_state["reserve_balance"], } - self.rebalance_portfolio(self.strategy.order.sell_amount, - self.strategy.order.sell_reserve_asset, prev_state) + self.rebalance_portfolio(self.order.sell_amount, + self.order.sell_reserve_asset, params, prev_state) - next_buckets = self.mento.process_order(self.strategy.order, prev_state) + next_buckets = self.mento.process_order(self.order, prev_state) return { "mento_buckets": next_buckets, @@ -74,16 +74,16 @@ def execute( "reserve_balance": self.parent.reserve.balance, } - def rebalance_portfolio(self, target_amount, target_is_reserve_asset, prev_state): + def rebalance_portfolio(self, target_amount, target_is_reserve_asset, params, prev_state): """ Sometimes the optimal trade might require selling more of an asset than the trader has in his portfolio, but the total value of the portfolio would cover it therefore they can rebalance and execute the trade. """ - reserve_asset = self.exchange_config.reserve_asset - stable = self.exchange_config.stable - reference_fiat = self.exchange_config.reference_fiat + reserve_asset = params['mento_exchanges_config'][self.config.exchange].reserve_asset + stable = params['mento_exchanges_config'][self.config.exchange].stable + reference_fiat = params['mento_exchanges_config'][self.config.exchange].reference_fiat # TODO: Should these be quoted in the specific # fiat of the stable? diff --git a/model/generators/mento.py b/model/generators/mento.py index b757ab4..7f3bdf7 100644 --- a/model/generators/mento.py +++ b/model/generators/mento.py @@ -6,7 +6,7 @@ from copy import deepcopy from functools import cached_property -from typing import TYPE_CHECKING, Any, Dict, Set +from typing import Any, Dict, Set import numpy as np from model.constants import blocktime_seconds @@ -46,7 +46,6 @@ def __init__(self, configs: Dict[Stable, MentoExchangeConfig], self.configs = configs self.active_exchanges = active_exchanges self.container = container - @classmethod def from_parameters(cls, params, _initial_state, container): @@ -55,7 +54,7 @@ def from_parameters(cls, params, _initial_state, container): set(params['mento_exchanges_active']), container ) - + @cached_property def account_generator(self): from model.generators.accounts import AccountGenerator @@ -164,6 +163,7 @@ def exchange(self, exchange: MentoExchange, sell_amount, sell_reserve_asset, pre """ Update the simulation state with a trade between the reserve currency and stable """ + config = self.configs.get(exchange) assert config is not None @@ -191,17 +191,22 @@ def exchange(self, exchange: MentoExchange, sell_amount, sell_reserve_asset, pre return (next_bucket, delta) def process_order(self, order: Order, prev_state): + """ + wrapper function responsible for performing exchange and balancing accounts + """ next_bucket, delta = self.exchange( - order.account.config.exchange, + order.exchange, order.sell_amount, order.sell_reserve_asset, prev_state ) + config = self.configs.get(order.exchange) + account = self.account_generator.accounts_by_id.get(order.account_id) + account.balance += delta - order.account.balance += delta reserve_delta = Balance({ - order.account.exchange_config.reserve_asset: - -1 * delta.get(order.account.exchange_config.reserve_asset), + config.reserve_asset: + -1 * delta.get(config.reserve_asset), }) self.account_generator.reserve.balance += reserve_delta diff --git a/model/system_parameters.py b/model/system_parameters.py index e54c0ed..9f02f75 100644 --- a/model/system_parameters.py +++ b/model/system_parameters.py @@ -243,10 +243,16 @@ class InitParameters(TypedDict): ), TraderConfig( trader_type=TraderType.ARBITRAGE_TRADER, - count=2, + count=1, balance=Balance({CryptoAsset.CELO: 500000, Stable.CEUR: 1000000}), exchange=MentoExchange.CEUR_CELO ), + # TraderConfig( + # trader_type=TraderType.RANDOM_TRADER, + # count=1, + # balance=Balance({CryptoAsset.CELO: 500000, Stable.CEUR: 1000000}), + # exchange=MentoExchange.CEUR_CELO + # ), ] ], diff --git a/model/types/order.py b/model/types/order.py index f80502d..dd45e59 100644 --- a/model/types/order.py +++ b/model/types/order.py @@ -3,20 +3,21 @@ """ -from typing import TYPE_CHECKING -from model.types.base import Currency, Exchange +#from typing import TYPE_CHECKING +from model.types.base import Account, Exchange +from model.types.pair import Pair -if TYPE_CHECKING: - from model.entities.account import Account +# if TYPE_CHECKING: +# from model.entities.account import Account # pylint:disable = too-few-public-methods class Order(): - def __init__(self, account, asset, sell_amount, buy_amount, sell_reserve_asset, exchange): - self.account: Account = account - self.asset: Currency = asset + def __init__(self, account_id, pair, sell_amount, buy_amount, sell_reserve_asset, exchange): + self.account_id: Account = account_id + self.pair: Pair = pair self.sell_amount: float = sell_amount self.buy_amount: float = buy_amount self.sell_reserve_asset: bool = sell_reserve_asset