diff --git a/notebooks/vAMM.ipynb b/notebooks/vAMM.ipynb new file mode 100644 index 000000000..0b5171e9a --- /dev/null +++ b/notebooks/vAMM.ipynb @@ -0,0 +1,355 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "id": "94be4268-65d7-49b6-8c37-40628437dc58", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy\n", + "from typing import Optional, Tuple, List\n", + "from collections import namedtuple\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "MMOrder = namedtuple(\"MMOrder\", [\"size\", \"price\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d91a510a-3fc5-46f7-a77e-971a4c1e3394", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "class CFMMarketMaker:\n", + "\n", + " def __init__(\n", + " self,\n", + " initial_price: float = 100,\n", + " price_width_below: float = 0.05,\n", + " price_width_above: float = 0.05,\n", + " margin_usage_at_bound_above: float = 0.8,\n", + " margin_usage_at_bound_below: float = 0.8,\n", + " volume_per_side: float = 10,\n", + " num_levels: int = 25,\n", + " tick_spacing: float = 1,\n", + " ):\n", + "\n", + " self.base_price = initial_price\n", + " self.upper_price = (1 + price_width_above) * initial_price\n", + " self.lower_price = (1 - price_width_below) * initial_price\n", + "\n", + " self.base_price_sqrt = initial_price**0.5\n", + " self.upper_price_sqrt = self.upper_price**0.5\n", + " self.lower_price_sqrt = self.lower_price**0.5\n", + "\n", + " self.lower_liq_factor = 1 / (self.base_price_sqrt - self.lower_price_sqrt)\n", + " self.upper_liq_factor = 1 / (self.upper_price_sqrt - self.base_price_sqrt)\n", + "\n", + " self.margin_usage_at_bound_above = margin_usage_at_bound_above\n", + " self.margin_usage_at_bound_below = margin_usage_at_bound_below\n", + "\n", + " self.tick_spacing = tick_spacing\n", + " self.num_levels = num_levels\n", + " self.volume_per_side = volume_per_side\n", + "\n", + "\n", + " def initialise(\n", + " self,\n", + " vega,\n", + " ):\n", + " risk_factors = vega.get_risk_factors(self.market_id)\n", + " self.short_factor, self.long_factor = risk_factors.short, risk_factors.long\n", + "\n", + " def _quantity_for_move(\n", + " self,\n", + " start_price_sqrt,\n", + " end_price_sqrt,\n", + " range_upper_price_sqrt,\n", + " liquidity_factor,\n", + " ) -> Optional[float]:\n", + " if liquidity_factor == 0:\n", + " return None\n", + " start_fut_pos = (\n", + " liquidity_factor\n", + " * (range_upper_price_sqrt - start_price_sqrt)\n", + " / (start_price_sqrt * range_upper_price_sqrt)\n", + " )\n", + " end_fut_pos = (\n", + " liquidity_factor\n", + " * (range_upper_price_sqrt - end_price_sqrt)\n", + " / (end_price_sqrt * range_upper_price_sqrt)\n", + " )\n", + "\n", + " return abs(start_fut_pos - end_fut_pos)\n", + "\n", + " def _generate_shape(\n", + " self, bid_price_depth: float, ask_price_depth: float\n", + " ) -> Tuple[List[MMOrder], List[MMOrder]]:\n", + " return self._generate_shape_calcs(\n", + " balance=sum(\n", + " a.balance\n", + " for a in self.vega.get_accounts_from_stream(\n", + " key_name=self.key_name,\n", + " wallet_name=self.wallet_name,\n", + " market_id=self.market_id,\n", + " )\n", + " ),\n", + " average_entry=(\n", + " self.vega.positions_by_market(\n", + " wallet_name=self.wallet_name,\n", + " market_id=self.market_id,\n", + " key_name=self.key_name,\n", + " ).average_entry_price\n", + " if self.current_position != 0\n", + " else 0\n", + " ),\n", + " position=self.current_position,\n", + " )\n", + "\n", + " def _generate_shape_calcs(\n", + " self,\n", + " balance: float,\n", + " average_entry: float,\n", + " position: float,\n", + " ) -> Tuple[List[MMOrder], List[MMOrder]]:\n", + " volume_at_upper = (\n", + " self.margin_usage_at_bound_above\n", + " * (balance / self.short_factor)\n", + " / self.upper_price\n", + " )\n", + " upper_L = (\n", + " volume_at_upper\n", + " * self.upper_price_sqrt\n", + " * self.base_price_sqrt\n", + " / (self.upper_price_sqrt - self.base_price_sqrt)\n", + " )\n", + " \n", + " lower_L = (\n", + " self.margin_usage_at_bound_below\n", + " * (balance / self.long_factor)\n", + " * self.lower_liq_factor\n", + " )\n", + " \n", + "\n", + " if position > 0:\n", + " L = lower_L\n", + " usd_total = (\n", + " self.margin_usage_at_bound_below * balance / self.long_factor\n", + " )\n", + " lower_bound = self.lower_price_sqrt\n", + " upper_bound = self.base_price_sqrt\n", + " virt_x = abs(position) + L / upper_bound\n", + " virt_y = (usd_total - abs(position) * average_entry) + L * lower_bound\n", + " else:\n", + " L = upper_L\n", + " lower_bound = self.base_price_sqrt\n", + " upper_bound = self.upper_price_sqrt\n", + " virt_x = (volume_at_upper + position) + L / upper_bound\n", + " virt_y = (abs(position) * average_entry) + L * lower_bound\n", + " if L == 0:\n", + " ref_price = self.base_price\n", + " else:\n", + " ref_price = virt_y / virt_x\n", + "\n", + " return self._calculate_price_levels(\n", + " ref_price=ref_price,\n", + " balance=balance,\n", + " upper_L=upper_L,\n", + " lower_L=lower_L,\n", + " position=position,\n", + " )\n", + "\n", + " def _calculate_liq_val(\n", + " self, margin_frac: float, balance: float, risk_factor: float, liq_factor: float\n", + " ) -> float:\n", + " return margin_frac * (balance / risk_factor) * liq_factor\n", + "\n", + " def _calculate_price_levels(\n", + " self,\n", + " ref_price: float,\n", + " balance: float,\n", + " upper_L: float,\n", + " lower_L: float,\n", + " position: float,\n", + " ) -> Tuple[List[MMOrder], List[MMOrder]]:\n", + " if ref_price == 0:\n", + " ref_price = self.curr_price\n", + "\n", + " agg_bids = []\n", + " agg_asks = []\n", + "\n", + " pos = position\n", + "\n", + " for i in range(1, self.num_levels):\n", + " pre_price_sqrt = (ref_price + (i - 1) * self.tick_spacing) ** 0.5\n", + " price = ref_price + i * self.tick_spacing\n", + "\n", + " if price > self.upper_price or price < self.lower_price:\n", + " continue\n", + "\n", + " volume = self._quantity_for_move(\n", + " pre_price_sqrt,\n", + " price**0.5,\n", + " self.upper_price_sqrt if pos < 0 else self.base_price_sqrt,\n", + " upper_L if pos < 0 else lower_L,\n", + " )\n", + " if volume is not None:\n", + " if pos > 0 and pos - volume < 0:\n", + " volume = pos\n", + " agg_asks.append(MMOrder(volume, price))\n", + " pos -= volume\n", + "\n", + " pos = position\n", + " for i in range(1, self.num_levels):\n", + " pre_price_sqrt = (ref_price - (i - 1) * self.tick_spacing) ** 0.5\n", + " price = ref_price - i * self.tick_spacing\n", + "\n", + " if price > self.upper_price or price < self.lower_price:\n", + " continue\n", + "\n", + " volume = self._quantity_for_move(\n", + " pre_price_sqrt,\n", + " price**0.5,\n", + " self.upper_price_sqrt if pos < 0 else self.base_price_sqrt,\n", + " upper_L if pos < 0 else lower_L,\n", + " )\n", + " if volume is not None:\n", + " if pos < 0 and pos + volume > 0:\n", + " volume = pos\n", + " agg_bids.append(MMOrder(volume, price))\n", + " pos += volume\n", + "\n", + " return agg_bids, agg_asks" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pos would be 1549.2223120340511\n" + ] + }, + { + "data": { + "image/png": "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IuKtFi+Gtm1teGneHNavV3gR+R8FGBERbzRtGvz1r3DsGNxyC3z6qd1VWkQABRgREe9SUACjR8N999k/Dxxoz8RERTldmYhX0RgYERFvcfIk9O9vtwMAu97LY49BUJCzdYl4IQUYERFv8PPPdluAdesgNBTeeAP+9jenqxLxWgowIiJO27XLTovetQuqVYN58+DGG52uSsSrKcCIiDhp3To7WPfQIWjYEJYsgaZNna5KxOtpEK+IiFM++MBzQ8b16xVeRIpIAUZEpLwZA888A7ffDjk50KULrF6tDRlFikEBRkSkPOXlwbBhdqo02D/PmweVKztbl4iP0RgYEZHycvQo9OkDixbZqdHPPgvDh2uatEgJKMCIiJSHAwegc2e7t1HFivD223Z3aREpEQUYEZGytm0b3HorpKdDrVqwcCFce63TVYn4NI2BEREpSytXwvXX2/ByySV2ppHCi8h5U4ARESkrb74JHTpAdrYNMSkpcPHFTlcl4hcUYERESpsxMH48DBgAubnQqxd88gnUrOl0ZSJ+QwFGRKQ0nToFd98N48bZ4zFj4N137cBdESk1GsQrIlJasrPtzKIVKyA4GKZOhb//3emqRPySAoyISGlIT7czjbZts4vSzZ5tj0WkTCjAiIicry+/hE6dICMD6taFxYvhyiudrkrEr2kMjIhISR0+DK++Cm3b2vDSrJmdJq3wIlLmdAZGRKQkPv7Ybsboctnj+Hi7u3RUlLN1iQQInYERESmuN96w41tcLmjSBP75T1iyROFFpBwVO8CsXbuWLl26UK9ePYKCgpg/f77H43fddRdBQUEetw4dOni0OXz4MP369SMyMpJq1aoxaNAgjh496tFm8+bNtGnThooVKxIbG8ukSZOK3zsRkdJkjJ0ePWgQ5OfD3/4GmzfDo49CaKjT1YkElGIHmGPHjnHFFVfw0ksvnbNNhw4dOHDggPs2a9Ysj8f79evHtm3bWL58OYsWLWLt2rXce++97sddLhft27enYcOGpKamMnnyZJ588kleffXV4pYrIlI6cnPt+i7jx9vjxx6zK+2GhTlbl0iAKvYYmI4dO9KxY8ffbRMeHk5MTMxZH9u+fTvJycl88cUXXH311QC88MIL3HrrrTzzzDPUq1ePd955h1OnTvHGG28QFhbG5ZdfTlpaGs8995xH0BERKRcuF/TsCcuXQ0iI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OJ/+n03r1q354Ycf3JchfbX/RRETE8PBgwc97svLy+Pw4cM+83fA+fT/bFq3bk1eXh579+4Fyqf/CjCnyc3NJTc3l+Bgz7clJCSEgoICwP7lHhoayooVK9yP79y5k/T0dOLi4gCIi4tjy5YtHr/gy5cvJzIykqZNm5ZDT0quKO/B2aSlpQFQt25dwLffg0JRUVHUrl2bb7/9lk2bNnHbbbcB/v87UOhc/T8bX/r8jTEMGzaMefPmsXLlyt+c5i6tzzcuLs7jOQrbFD6HU0qj/2eTlpZG9erV3f/69tX+F0VcXBxZWVmkpqa671u5ciUFBQXuMB8XF8fatWvJzc11t1m+fDlNmjRx9PJRafT/bNLS0ggODnZfWiuX/pfacGAfceTIEfPVV1+Zr776ygDmueeeM1999ZX5/vvvjTF2dsXll19uVq1aZb777jszffp0U7FiRTN16lT3cwwZMsQ0aNDArFy50mzatMnExcWZuLg49+OFUyzbt29v0tLSTHJysqldu7ZXTCE15vzfg127dpnx48ebTZs2mT179piPPvrINGrUyLRt29b9Gt78HvxR/2fPnm1WrVpldu/ebebPn28aNmxounfv7vEcvvw7cL799/XPf+jQoSYqKsqsXr3aYwro8ePH3W1K4/MtnEY8evRos337dvPSSy95xTTi0uj/ggULzGuvvWa2bNlivv32WzN16lQTERFhxo4d627jy/0/cOCA+eqrr8xrr71mALN27Vrz1VdfmUOHDrnbdOjQwVx55ZVmw4YN5rPPPjOXXHKJxzTqrKwsEx0dbe68806zdetW895775mIiAjHp1GXRv/XrVtnpkyZYtLS0szu3bvN22+/bWrXrm369+/vfo7y6H/ABZjCaWFn3gYMGGCMsR/cXXfdZerVq2cqVqxomjRpYp599llTUFDgfo4TJ06Y++67z1SvXt1ERESYbt26mQMHDni8zt69e03Hjh1NpUqVTK1atcyoUaNMbm5ueXb1nM73PUhPTzdt27Y1NWrUMOHh4aZx48Zm9OjRHuuAGOO978Ef9f/555839evXN6GhoaZBgwbm8ccf/83UX1/+HTjf/vv653+2vvO/NU0Kldbnu2rVKtOyZUsTFhZmGjVq5PEaTimN/i9dutS0bNnSVKlSxVSuXNlcccUV5uWXXzb5+fker+Wr/R83btwftjl06JDp06ePqVKliomMjDQDBw40R44c8Xitr7/+2txwww0mPDzcXHDBBWbixInl1MtzK43+p6ammtatW5uoqChTsWJFc9lll5l//etf5uTJkx6vVdb9D/pfh0RERER8hsbAiIiIiM9RgBERERGfowAjIiIiPkcBRkRERHyOAoyIiIj4HAUYERER8TkKMCIiIuJzFGBERETE5yjAiIiIiM9RgBERERGfowAjIiIiPkcBRkRERHzO/weU6MUeqx66RAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "mm = CFMMarketMaker(\n", + " initial_price=2000,\n", + " price_width_above=0.1,\n", + " price_width_below=0.1,\n", + " margin_usage_at_bound_above=0.8,\n", + " margin_usage_at_bound_below=0.8,\n", + " num_levels=300,\n", + " tick_spacing=1,\n", + ")\n", + "\n", + "balance = 100_000\n", + "\n", + "mm.short_factor = 0.02\n", + "mm.long_factor = 0.02\n", + "\n", + "volume_at_upper = (\n", + " mm.margin_usage_at_bound_above * (balance / mm.short_factor) / mm.upper_price\n", + ")\n", + "upper_L = (\n", + " volume_at_upper\n", + " * mm.upper_price_sqrt\n", + " * mm.base_price_sqrt\n", + " / (mm.upper_price_sqrt - mm.base_price_sqrt)\n", + ")\n", + "\n", + "lower_L = (\n", + " mm.margin_usage_at_bound_below\n", + " * (balance / mm.long_factor)\n", + " * mm.lower_liq_factor\n", + ")\n", + "\n", + "to_price = 1850\n", + "pos = mm._quantity_for_move(\n", + " mm.base_price_sqrt,\n", + " to_price**0.5,\n", + " mm.base_price_sqrt if to_price < mm.base_price else mm.upper_price_sqrt,\n", + " lower_L,\n", + ")\n", + "\n", + "bids, asks = mm._generate_shape_calcs(\n", + " balance=balance, average_entry=to_price, position=pos\n", + ")\n", + "\n", + "x = []\n", + "y = []\n", + "\n", + "cumsum = 0\n", + "for bid in bids:\n", + " x.append(bid.price)\n", + " cumsum += bid.size\n", + " y.append(cumsum)\n", + "\n", + "plt.plot(x, y, color=\"blue\")\n", + "x = []\n", + "y = []\n", + "\n", + "cumsum = 0\n", + "for ask in asks:\n", + " x.append(ask.price)\n", + " cumsum += ask.size\n", + " y.append(cumsum)\n", + "plt.plot(x, y, color=\"red\")\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/vega_sim/api/data.py b/vega_sim/api/data.py index a57f39246..8b2516fd1 100644 --- a/vega_sim/api/data.py +++ b/vega_sim/api/data.py @@ -95,6 +95,8 @@ class Order: "market_id", "asset", "timestamp", + "margin_mode", + "margin_factor", ], ) @@ -915,6 +917,7 @@ def _trade_from_proto( def _margin_level_from_proto( margin_level: vega_protos.vega.MarginLevels, decimal_spec: DecimalSpec ) -> MarginLevels: + print(margin_level) return MarginLevels( maintenance_margin=num_from_padded_int( margin_level.maintenance_margin, decimal_spec.asset_decimals @@ -932,6 +935,8 @@ def _margin_level_from_proto( market_id=margin_level.market_id, asset=margin_level.asset, timestamp=margin_level.timestamp, + margin_mode=margin_level.margin_mode, + margin_factor=float(margin_level.margin_factor), ) diff --git a/vega_sim/api/data_raw.py b/vega_sim/api/data_raw.py index b25d5d12b..7307075e3 100644 --- a/vega_sim/api/data_raw.py +++ b/vega_sim/api/data_raw.py @@ -627,12 +627,14 @@ def list_ledger_entries( base_request.date_range.CopyFrom( data_node_protos_v2.trading_data.DateRange( start_timestamp=( - from_datetime.timestamp() * 1e9 + int(from_datetime.timestamp() * 1e9) if from_datetime is not None else None ), end_timestamp=( - to_datetime.timestamp() * 1e9 if to_datetime is not None else None + int(to_datetime.timestamp() * 1e9) + if to_datetime is not None + else None ), ) ) diff --git a/vega_sim/api/market.py b/vega_sim/api/market.py index 2b56e47b5..4f1c8c7fc 100644 --- a/vega_sim/api/market.py +++ b/vega_sim/api/market.py @@ -64,7 +64,6 @@ """ -import copy import functools import logging from typing import Optional, Union diff --git a/vega_sim/devops/registry.py b/vega_sim/devops/registry.py index 66f33c697..795a1daa8 100644 --- a/vega_sim/devops/registry.py +++ b/vega_sim/devops/registry.py @@ -4,23 +4,21 @@ """ -from vega_sim.devops.scenario import DevOpsScenario - -from vega_sim.scenario.common.agents import ( - ArbitrageLiquidityProvider, - ExponentialShapedMarketMaker, -) - from vega_sim.devops.classes import ( + AuctionTraderArgs, MarketMakerArgs, MarketManagerArgs, - AuctionTraderArgs, RandomTraderArgs, SensitiveTraderArgs, SimulationArgs, ) - +from vega_sim.devops.scenario import DevOpsScenario +from vega_sim.scenario.common.agents import ( + ArbitrageLiquidityProvider, + ExponentialShapedMarketMaker, +) from vega_sim.scenario.common.utils.price_process import Granularity, LivePrice +from vega_sim.scenario.constant_function_market.agents import CFMV3MarketMaker SCENARIOS = { "ETHUSD": lambda: DevOpsScenario( @@ -247,4 +245,62 @@ state_update_freq=10, tag="agent", ), + "amm_market_maker_ethusd": lambda: CFMV3MarketMaker( + wallet_name=None, + key_name=None, + market_name=None, + asset_name=None, + num_steps=0, + tick_spacing=0.005, + num_levels=15, + initial_asset_mint=0, + commitment_amount=0, + price_width_above=2.0, + price_width_below=0.98, + max_loss_at_bound_above=0.7, + max_loss_at_bound_below=0.7, + initial_price=0.1674, + base_balance=100_000, + ), + # "amm_market_maker_kepusd_prod": lambda: CFMV3MarketMaker( + # wallet_name=None, + # key_name=None, + # market_name=None, + # asset_name=None, + # num_steps=0, + # tick_spacing=0.001, + # num_levels=13, + # initial_asset_mint=0, + # commitment_amount=500, + # bound_perc=0.17, + # fee_amount=0.001, + # price_width_above=0.3, + # price_width_below=0.4, + # margin_multiple_at_lower=0.1, + # margin_multiple_at_upper=0.1, + # initial_price=0.1381, + # base_balance=3000, + # position_offset=2600, + # ), + "amm_market_maker_hlpusd_prod": lambda: CFMV3MarketMaker( + wallet_name=None, + key_name=None, + market_name=None, + asset_name=None, + num_steps=0, + tick_spacing=0.5, + num_levels=13, + initial_asset_mint=0, + commitment_amount=500, + bound_perc=0.17, + fee_amount=0.001, + price_width_above=4, + price_width_below=0.99, + margin_multiple_at_lower=0.5, + margin_multiple_at_upper=0.5, + initial_price=4, + base_balance=3000, + position_offset=0, + order_validity_length=60, + ), } diff --git a/vega_sim/devops/run_agent.py b/vega_sim/devops/run_agent.py index 23ae6b049..8b45f6ec6 100644 --- a/vega_sim/devops/run_agent.py +++ b/vega_sim/devops/run_agent.py @@ -109,15 +109,16 @@ def main(): with VegaServiceNetwork( network=Network[args.network], run_with_console=args.console, + run_with_wallet=True, ) as vega: if agent is not None: - agent.asset_name = vega.data_cache.asset_from_feed( - asset_id=vega.market_info( - vega.find_market_id( - name=agent.market_name, raise_on_missing=True - ) - ).tradable_instrument.instrument.future.settlement_asset - ).details.symbol + market_info = vega.market_info( + vega.find_market_id(name=agent.market_name, raise_on_missing=True) + ) + agent.asset_name = ( + market_info.tradable_instrument.instrument.perpetual.settlement_asset + or market_info.tradable_instrument.instrument.future.settlement_asset + ) scenario.run_iteration( vega=vega, diff --git a/vega_sim/devops/scenario.py b/vega_sim/devops/scenario.py index 9b5f1b57b..b5381dae9 100644 --- a/vega_sim/devops/scenario.py +++ b/vega_sim/devops/scenario.py @@ -131,7 +131,7 @@ def configure_agents( random_state=random_state ) else: - self.price_process = get_live_price(product=self.binance_code) + self.price_process = None # get_live_price(product=self.binance_code) if self.scenario_wallet.market_creator_agent is None: raise ValueError( @@ -270,7 +270,7 @@ def configure_agents( if kwargs.get("network", Network.FAIRGROUND) == Network.NULLCHAIN: kwargs["agent"].price_process_generator = iter(self.price_process) - kwargs["agent"].order_validity_length = self.step_length_seconds + kwargs["agent"].order_validity_length = 10 * self.step_length_seconds agents.append(kwargs.get("agent", None)) diff --git a/vega_sim/environment/environment.py b/vega_sim/environment/environment.py index a881e5429..d83fecc91 100644 --- a/vega_sim/environment/environment.py +++ b/vega_sim/environment/environment.py @@ -544,6 +544,7 @@ def _run( def step(self, vega: VegaServiceNetwork) -> None: t = time.time() + print("Stepping") state = self.state_func(vega) logging.debug(f"Get state took {time.time() - t} seconds.") for agent in ( @@ -551,6 +552,7 @@ def step(self, vega: VegaServiceNetwork) -> None: if self.random_agent_ordering else self.agents ): + agent.step(state) try: agent.step(state) except Exception as e: diff --git a/vega_sim/local_data_cache.py b/vega_sim/local_data_cache.py index f57102953..306b78013 100644 --- a/vega_sim/local_data_cache.py +++ b/vega_sim/local_data_cache.py @@ -318,7 +318,8 @@ def start_live_feeds( market_ids=market_ids, party_ids=party_ids, ) - self.initialise_transfer_monitoring() + if start_high_load_feeds: + self.initialise_transfer_monitoring() self.initialise_market_data( market_ids, ) @@ -360,7 +361,10 @@ def initialise_assets(self): self._asset_from_feed[asset.id] = asset def initialise_accounts(self): - base_accounts = data.list_accounts(data_client=self._trading_data_client) + base_accounts = data.list_accounts( + data_client=self._trading_data_client, + pub_key="683a68eab083f765a93d342bb15e2fed41a9d8f9a61212b6af1e11e883620125", + ) with self.account_lock: for account in base_accounts: @@ -447,6 +451,7 @@ def initialise_market_data( def initialise_transfer_monitoring( self, ): + return base_transfers = [] base_transfers.extend( @@ -458,6 +463,7 @@ def initialise_transfer_monitoring( self._transfer_state_from_feed.setdefault(t.party_to, {})[t.id] = t def initialise_network_parameters(self): + return base_network_parameters = data.list_network_parameters( data_client=self._trading_data_client ) diff --git a/vega_sim/network_service.py b/vega_sim/network_service.py index fa4a1026f..deadf7cc1 100644 --- a/vega_sim/network_service.py +++ b/vega_sim/network_service.py @@ -117,6 +117,7 @@ def manage_vega_processes( if run_with_wallet: vega_wallet_path = environ.get("VEGA_WALLET_PATH", "vegawallet") + vegaWalletProcess = _popen_process( popen_args=[ vega_wallet_path, @@ -198,6 +199,7 @@ def _find_network_config_toml( ), ] ) + for search_path in search_paths: full_path = path.join( search_path, diff --git a/vega_sim/scenario/common/agents.py b/vega_sim/scenario/common/agents.py index 605bb298e..b8fe6bc6e 100644 --- a/vega_sim/scenario/common/agents.py +++ b/vega_sim/scenario/common/agents.py @@ -18,6 +18,7 @@ import time from collections import namedtuple +from dataclasses import dataclass from enum import Enum from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union @@ -61,7 +62,15 @@ class MarketHistoryData: AUCTION2_WALLET = WalletConfig("AUCTION2", "AUCTION2pass") ITOrder = namedtuple("ITOrder", ["side", "size"]) -MMOrder = namedtuple("MMOrder", ["size", "price"]) + + +@dataclass +class MMOrder: + size: float + price: float + side: vega_protos.Side + time_in_force: vega_protos.Order.TimeInForce = vega_protos.Order.TIME_IN_FORCE_GTT + LiquidityProvision = namedtuple("LiquidityProvision", ["amount", "fee"]) @@ -255,6 +264,24 @@ def initialise( ) self.vega.wait_fn(5) + def _execution_price(self, volume: float, side: vega_protos.Side): + book_depth = self.vega.market_depth(self.market_id) + remaining_volume = volume + vwap = None + + levels = book_depth.buys if side == vega_protos.SIDE_SELL else book_depth.sells + idx = 0 + + while remaining_volume > 0: + if len(levels) > idx: + if vwap is None: + vwap = 0 + vwap += levels[idx].price * min(levels[idx].volume, remaining_volume) + remaining_volume -= levels[idx].volume + else: + break + return vwap + def step(self, vega_state: VegaState): self.curr_price = next(self.price_process_generator) @@ -263,13 +290,29 @@ def step(self, vega_state: VegaState): buy_vol = self.random_state.poisson(self.buy_intensity) * self.base_order_size sell_vol = self.random_state.poisson(self.sell_intensity) * self.base_order_size - best_bid, best_ask = self.vega.best_prices(self.market_id) + # best_bid, best_ask = self.vega.best_prices(self.market_id) + buy_vwap = self._execution_price(buy_vol, vega_protos.SIDE_BUY) + sell_vwap = self._execution_price(buy_vol, vega_protos.SIDE_SELL) - will_buy = self.random_state.rand() < np.exp( - -1 * self.probability_decay * max([best_ask - self.curr_price, 0]) + will_buy = ( + ( + self.random_state.rand() + < np.exp( + -1 * self.probability_decay * max([buy_vwap - self.curr_price, 0]) + ) + ) + if buy_vwap is not None + else False ) - will_sell = self.random_state.rand() < np.exp( - -1 * self.probability_decay * max([self.curr_price - best_bid, 0]) + will_sell = ( + ( + self.random_state.rand() + < np.exp( + -1 * self.probability_decay * max([self.curr_price - sell_vwap, 0]) + ) + ) + if sell_vwap is not None + else False ) if buy_first and will_buy: @@ -314,6 +357,125 @@ def place_order(self, vega_state: VegaState, volume: float, side: vega_protos.Si ) +class ArbitrageTrader(StateAgentWithWallet): + NAME_BASE = "arbitrage_trader" + + def __init__( + self, + key_name: str, + market_name: str, + asset_name: str, + price_process_generator: Iterable[float], + initial_asset_mint: float = 1000000, + buy_intensity: float = 1, + sell_intensity: float = 1, + spread_offset: float = 0.01, + tag: str = "", + random_state: Optional[np.random.RandomState] = None, + base_order_size: float = 1, + wallet_name: str = None, + ): + super().__init__(wallet_name=wallet_name, key_name=key_name, tag=tag) + self.initial_asset_mint = initial_asset_mint + self.buy_intensity = buy_intensity + self.sell_intensity = sell_intensity + self.market_name = market_name + self.asset_name = asset_name + self.random_state = ( + random_state if random_state is not None else np.random.RandomState() + ) + self.base_order_size = base_order_size + self.price_process_generator = price_process_generator + self.spread_offset = spread_offset + + def initialise( + self, + vega: Union[VegaServiceNull, VegaServiceNetwork], + create_key: bool = True, + mint_key: bool = True, + ): + # Initialise wallet + super().initialise(vega=vega, create_key=create_key) + # Get market id + self.market_id = self.vega.find_market_id(name=self.market_name) + + # Get asset id + self.asset_id = self.vega.find_asset_id(symbol=self.asset_name) + if mint_key: + # Top up asset + self.vega.mint( + key_name=self.key_name, + asset=self.asset_id, + amount=self.initial_asset_mint, + wallet_name=self.wallet_name, + ) + self.vega.wait_fn(5) + + def step(self, vega_state: VegaState): + self.curr_price = next(self.price_process_generator) + + position = self.vega.positions_by_market( + wallet_name=self.wallet_name, + market_id=self.market_id, + key_name=self.key_name, + ) + + self.current_position = int(position.open_volume) if position is not None else 0 + + if abs(self.current_position) > 0: + self.place_order( + vega_state=vega_state, + volume=abs(self.current_position), + side=( + vega_protos.SIDE_BUY + if self.current_position < 0 + else vega_protos.SIDE_SELL + ), + price=self.curr_price, + ) + + buy_vol = self.random_state.poisson(self.buy_intensity) * self.base_order_size + sell_vol = self.random_state.poisson(self.sell_intensity) * self.base_order_size + + self.place_order( + vega_state=vega_state, + volume=buy_vol, + side=vega_protos.SIDE_BUY, + price=self.curr_price * (1 - self.spread_offset), + ) + + self.place_order( + vega_state=vega_state, + volume=sell_vol, + side=vega_protos.SIDE_SELL, + price=self.curr_price * (1 + self.spread_offset), + ) + + def place_order( + self, vega_state: VegaState, volume: float, side: vega_protos.Side, price: float + ): + if ( + ( + vega_state.market_state[self.market_id].trading_mode + == markets_protos.Market.TradingMode.TRADING_MODE_CONTINUOUS + ) + and vega_state.market_state[self.market_id].state + == markets_protos.Market.State.STATE_ACTIVE + and volume != 0 + ): + self.vega.submit_order( + trading_key=self.key_name, + market_id=self.market_id, + order_type="TYPE_LIMIT", + side=side, + volume=volume, + price=price, + time_in_force="TIME_IN_FORCE_IOC", + wait=False, + trading_wallet=self.wallet_name, + ) + + class PriceSensitiveLimitOrderTrader(StateAgentWithWallet): NAME_BASE = "price_sensitive_lo_trader" @@ -1053,14 +1215,14 @@ class ShapedMarketMaker(StateAgentWithWallet): def __init__( self, key_name: str, - price_process_generator: Iterable[float], + price_process_generator: Optional[Iterable[float]], best_price_offset_fn: Callable[[float, int], Tuple[float, float]], shape_fn: Callable[ [ float, float, ], - Tuple[List[MMOrder], List[MMOrder]], + List[MMOrder], ], liquidity_commitment_fn: Optional[ Callable[[Optional[VegaState]], Optional[LiquidityProvision]] @@ -1080,6 +1242,7 @@ def __init__( max_order_size: float = 10000, order_validity_length: Optional[float] = None, auto_top_up: bool = False, + move_orders_at_once: bool = True, ): super().__init__(wallet_name=wallet_name, key_name=key_name, tag=tag) self.price_process_generator = price_process_generator @@ -1111,6 +1274,7 @@ def __init__( self.auto_top_up = auto_top_up self.mint_key = False + self.move_orders_at_once = move_orders_at_once self.order_validity_length = order_validity_length self.supplied_amount = ( @@ -1162,8 +1326,9 @@ def initialise( def step(self, vega_state: VegaState): self.current_step += 1 - self.prev_price = self.curr_price - self.curr_price = next(self.price_process_generator) + if self.price_process_generator is not None: + self.prev_price = self.curr_price + self.curr_price = next(self.price_process_generator) self._update_state(current_step=self.current_step) @@ -1174,19 +1339,18 @@ def step(self, vega_state: VegaState): key_name=self.key_name, ) - current_position = ( - int(position.open_volume) if position is not None and position else 0 + self.current_position = ( + float(position.open_volume) if position is not None and position else 0 ) + self.bid_depth, self.ask_depth = self.best_price_offset_fn( - current_position, self.current_step + self.current_position, self.current_step ) if (self.bid_depth is None) or (self.ask_depth is None): return - new_buy_shape, new_sell_shape = self.shape_fn(self.bid_depth, self.ask_depth) - scaled_buy_shape, scaled_sell_shape = self._scale_orders( - buy_shape=new_buy_shape, sell_shape=new_sell_shape - ) + new_shape = self.shape_fn(self.bid_depth, self.ask_depth) + scaled_shape = self._scale_orders(shape=new_shape) curr_buy_orders, curr_sell_orders = [], [] @@ -1219,33 +1383,50 @@ def step(self, vega_state: VegaState): else: curr_sell_orders.append(order) - # We want to first make the spread wider by moving the side which is in the - # direction of the move (e.g. if price falls, the bids) - first_side = ( - ( - vega_protos.SIDE_BUY - if scaled_sell_shape[0].price < curr_buy_orders[0].price - else vega_protos.SIDE_SELL + try: + min_sell = min( + o.price for o in scaled_shape if o.side == vega_protos.SIDE_SELL ) - if (scaled_sell_shape != []) and (curr_buy_orders != []) - else vega_protos.SIDE_BUY - ) - if first_side == vega_protos.SIDE_BUY: + except ValueError: + min_sell = None + + if not self.move_orders_at_once: + # We want to first make the spread wider by moving the side which is in the + # direction of the move (e.g. if price falls, the bids) + first_side = ( + ( + vega_protos.SIDE_BUY + if min_sell < curr_buy_orders[0].price + else vega_protos.SIDE_SELL + ) + if (min_sell is not None) and (curr_buy_orders != []) + else vega_protos.SIDE_BUY + ) + if first_side == vega_protos.SIDE_BUY: + self._move_side( + curr_buy_orders, + scaled_shape, + only_side=vega_protos.SIDE_BUY, + cancel_and_replace=False, + ) self._move_side( - vega_protos.SIDE_BUY, - curr_buy_orders, - scaled_buy_shape, + curr_sell_orders, + scaled_shape, + only_side=vega_protos.SIDE_SELL, + cancel_and_replace=False, ) - self._move_side( - vega_protos.SIDE_SELL, - curr_sell_orders, - scaled_sell_shape, - ) - if first_side == vega_protos.SIDE_SELL: + if first_side == vega_protos.SIDE_SELL: + self._move_side( + curr_buy_orders, + scaled_shape, + only_side=vega_protos.SIDE_BUY, + cancel_and_replace=False, + ) + else: self._move_side( - vega_protos.SIDE_BUY, - curr_buy_orders, - scaled_buy_shape, + orders, + scaled_shape, + cancel_and_replace=True, ) if ( @@ -1284,9 +1465,17 @@ def step(self, vega_state: VegaState): def _scale_orders( self, - buy_shape: List[MMOrder], - sell_shape: List[MMOrder], + shape: List[MMOrder], ): + buy_shape = [] + sell_shape = [] + + for o in shape: + if o.side == vega_protos.SIDE_BUY: + buy_shape.append(o) + else: + sell_shape.append(o) + buy_scaling_factor = ( self.safety_factor * self.supplied_amount * self.stake_to_ccy_volume ) / self._calculate_liquidity( @@ -1302,7 +1491,9 @@ def _scale_orders( # Scale the shapes scaled_buy_shape = [ MMOrder( - min([order.size * buy_scaling_factor, self.max_order_size]), order.price + min([order.size * buy_scaling_factor, self.max_order_size]), + order.price, + vega_protos.SIDE_BUY, ) for order in buy_shape ] @@ -1310,11 +1501,12 @@ def _scale_orders( MMOrder( min([order.size * sell_scaling_factor, self.max_order_size]), order.price, + vega_protos.SIDE_SELL, ) for order in sell_shape ] - return scaled_buy_shape, scaled_sell_shape + return scaled_buy_shape + scaled_sell_shape def _calculate_liquidity( self, @@ -1332,9 +1524,10 @@ def _calculate_liquidity( def _move_side( self, - side: vega_protos.Side, - orders: List[Order], + existing_orders: List[Order], new_shape: List[MMOrder], + cancel_and_replace: bool = True, + only_side: Optional[vega_protos.Side] = None, ) -> None: amendments = [] submissions = [] @@ -1345,19 +1538,19 @@ def _move_side( if self.order_validity_length is not None else None ) + if only_side is not None: + new_shape = [o for o in new_shape if o.side == only_side] for i, order in enumerate(new_shape): - if i < len(orders): - order_to_amend = orders[i] + if i < len(existing_orders) and not cancel_and_replace: + order_to_amend = existing_orders[i] transaction = self.vega.build_order_amendment( market_id=self.market_id, order_id=order_to_amend.id, price=order.price, time_in_force=( - "TIME_IN_FORCE_GTT" - if self.order_validity_length is not None - else "TIME_IN_FORCE_GTC" + order.time_in_force ), size_delta=order.size - order_to_amend.remaining, expires_at=expires_at, @@ -1371,27 +1564,31 @@ def _move_side( price=order.price, size=order.size, order_type="TYPE_LIMIT", - time_in_force=( - "TIME_IN_FORCE_GTT" - if self.order_validity_length is not None - else "TIME_IN_FORCE_GTC" - ), - side=side, + time_in_force=order.time_in_force, + side=order.side, expires_at=expires_at, + post_only=True, ) - submissions.append(transaction) + if transaction is not None: + submissions.append(transaction) - if len(orders) > len(new_shape): - for order in orders[len(new_shape) :]: + if not cancel_and_replace and len(existing_orders) > len(new_shape): + for order in existing_orders[len(new_shape) :]: transaction = self.vega.build_order_cancellation( order_id=order.id, market_id=self.market_id, ) cancellations.append(transaction) + if cancel_and_replace: + cancellations.append( + self.vega.build_order_cancellation( + market_id=self.market_id, order_id=None + ) + ) - if submissions is not []: + if not all(val is [] for val in (submissions, amendments, cancellations)): self.vega.submit_instructions( wallet_name=self.wallet_name, key_name=self.key_name, @@ -1566,7 +1763,7 @@ def _optimal_strategy( def _generate_shape( self, bid_price_depth: float, ask_price_depth: float - ) -> Tuple[List[MMOrder], List[MMOrder]]: + ) -> List[MMOrder]: bid_orders = self._calculate_price_volume_levels( bid_price_depth, vega_protos.Side.SIDE_BUY ) @@ -1575,7 +1772,7 @@ def _generate_shape( ) self.curr_bids = bid_orders self.curr_asks = ask_orders - return bid_orders, ask_orders + return bid_orders + ask_orders def _calculate_price_volume_levels( self, @@ -1597,7 +1794,12 @@ def _calculate_price_volume_levels( ) level_price[level_price < 1 / 10**self.mdp] = 1 / 10**self.mdp - return [MMOrder(vol, price) for vol, price in zip(level_vol, level_price)] + return [ + MMOrder( + vol, price, vega_protos.SIDE_BUY if is_buy else vega_protos.SIDE_SELL + ) + for vol, price in zip(level_vol, level_price) + ] class HedgedMarketMaker(ExponentialShapedMarketMaker): @@ -2872,9 +3074,10 @@ def step(self, vega_state: VegaState): self.seen_trades.add(trade.id) market_trades.setdefault(market.id, []).append(trade) + accounts = self.vega.get_accounts_from_stream() + positions = self.vega.list_all_positions() - accounts = self.vega.list_accounts() self.states.append( MarketHistoryData( at_time=start_time, @@ -3484,7 +3687,8 @@ def __init__( if (not is_referrer) and (referrer_key_name is None): raise ValueError( - "ReferralWrapper must either designate the agent as a referrer or specify a referrer key name." + "ReferralWrapper must either designate the agent as a referrer or" + " specify a referrer key name." ) self.is_referrer = is_referrer diff --git a/vega_sim/scenario/common/utils/price_process.py b/vega_sim/scenario/common/utils/price_process.py index 1ed4fa952..cb19bbcfe 100644 --- a/vega_sim/scenario/common/utils/price_process.py +++ b/vega_sim/scenario/common/utils/price_process.py @@ -54,11 +54,16 @@ def random_walk( # Simulate external midprice for i in range(1, len(S)): S[i] = S[i - 1] + drift + sigma * dW[i] - + # if S[i] > starting_price and S[i] > S[i - 1]: + # S[i] = S[i - 1] + 0.8 * (S[i] - S[i - 1]) + # if S[i] < starting_price and S[i] < S[i - 1]: + # S[i] = S[i - 1] - 0.8 * abs(S[i] - S[i - 1]) # market decimal place if decimal_precision: S = np.round(S, decimal_precision) + S = S - S.mean() + # If random state is passed then error if it generates a negative price # Otherwise retry with a new seed diff --git a/vega_sim/scenario/constant_function_market/__init__.py b/vega_sim/scenario/constant_function_market/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vega_sim/scenario/constant_function_market/agents.py b/vega_sim/scenario/constant_function_market/agents.py new file mode 100644 index 000000000..e1ebc63cd --- /dev/null +++ b/vega_sim/scenario/constant_function_market/agents.py @@ -0,0 +1,976 @@ +from typing import Iterable, List, Optional, Tuple, Union +from logging import getLogger + +import numpy as np + +from vega_sim.proto.vega import vega as vega_protos +from vega_sim.scenario.common.agents import ( + LiquidityProvision, + MMOrder, + ShapedMarketMaker, + VegaState, +) + +logger = getLogger(__name__) + + +def _price_for_size( + side: vega_protos.Side, + position: float, + ref_price: float, + k_scaling_large: float, + k_scaling_small: float, + trade_size: float, +) -> float: + if side == vega_protos.Side.SIDE_SELL: + k_scaling = k_scaling_small if position >= 0 else k_scaling_large + + virtual_sell = (k_scaling / ref_price) ** 0.5 + virtual_buy = (k_scaling * ref_price) ** 0.5 + + return (virtual_buy + trade_size * ref_price) / virtual_sell + elif side == vega_protos.Side.SIDE_BUY: + k_scaling = k_scaling_small if position <= 0 else k_scaling_large + + virtual_sell = (k_scaling / ref_price) ** 0.5 + virtual_buy = (k_scaling * ref_price) ** 0.5 + + return virtual_buy / (virtual_sell + trade_size) + + +def _build_price_levels( + position: float, + ref_price: float, + min_trade_unit: float, + num_steps: int, + tick_spacing: float, + k_scaling_large: float, + k_scaling_small: float, +): + buy_prices = [] + sell_prices = [] + buy_price = ref_price + sell_price = ref_price + for i in range(0, num_steps): + buy_price = _price_for_size( + side=vega_protos.Side.SIDE_BUY, + position=position + i * min_trade_unit, + ref_price=buy_price, + k_scaling_large=k_scaling_large, + k_scaling_small=k_scaling_small, + trade_size=min_trade_unit, + ) + buy_prices.append(buy_price) + + sell_price = _price_for_size( + vega_protos.Side.SIDE_SELL, + position=position - i * min_trade_unit, + ref_price=sell_price, + k_scaling_large=k_scaling_large, + k_scaling_small=k_scaling_small, + trade_size=min_trade_unit, + ) + sell_prices.append(sell_price) + + return sorted(buy_prices, reverse=True), sorted(sell_prices, reverse=False) + + +def _aggregate_price_levels( + prices: List[float], + side: vega_protos.Side, + tick_spacing: float, + starting_price: float, + min_trade_unit: float, + max_levels: Optional[int] = None, +) -> List[MMOrder]: + shifter = 1 if side == vega_protos.Side.SIDE_SELL else -1 + outputs = [] + + current_price_level = starting_price + shifter * tick_spacing + price_level_vol = 0 + + while len(prices) > 0: + price, prices = prices[0], prices[1:] + + if shifter * price <= shifter * current_price_level: + price_level_vol += min_trade_unit + else: + current_price_level += shifter * tick_spacing + + if price_level_vol > 0: + outputs.append( + MMOrder( + size=price_level_vol, + price=current_price_level, + ) + ) + price_level_vol = 0 + if max_levels is not None and len(outputs) == max_levels: + break + if price_level_vol > 0: + outputs.append( + MMOrder( + size=price_level_vol, + price=current_price_level, + ) + ) + + return outputs + + +class CFMMarketMaker(ShapedMarketMaker): + NAME_BASE = "cfm_market_maker" + + def __init__( + self, + key_name: str, + num_steps: int, + initial_asset_mint: float = 1000000, + market_name: str = None, + asset_name: str = None, + commitment_amount: float = 6000, + supplied_amount: Optional[float] = None, + market_decimal_places: int = 5, + fee_amount: float = 0.001, + k_scaling_large: float = 2, + k_scaling_small: float = 1, + min_trade_unit: float = 0.01, + initial_price: float = 100, + volume_per_side: float = 10, + num_levels: int = 25, + tick_spacing: float = 1, + asset_decimal_places: int = 0, + tag: str = "", + wallet_name: str = None, + orders_from_stream: Optional[bool] = True, + state_update_freq: Optional[int] = None, + order_validity_length: Optional[float] = None, + price_process_generator: Optional[Iterable[float]] = None, + ): + super().__init__( + wallet_name=wallet_name, + initial_asset_mint=initial_asset_mint, + market_name=market_name, + asset_name=asset_name, + commitment_amount=commitment_amount, + supplied_amount=supplied_amount, + market_decimal_places=market_decimal_places, + asset_decimal_places=asset_decimal_places, + tag=tag, + shape_fn=self._generate_shape, + best_price_offset_fn=lambda *args, **kwargs: (0, 0), + liquidity_commitment_fn=self._liq_provis, + key_name=key_name, + orders_from_stream=orders_from_stream, + state_update_freq=state_update_freq, + order_validity_length=order_validity_length, + price_process_generator=price_process_generator, + ) + if k_scaling_large < k_scaling_small: + raise Exception("k_scaling_large should be larger than k_scaling_small") + self.k_scaling_large = k_scaling_large + self.k_scaling_small = k_scaling_small + self.tick_spacing = tick_spacing + self.num_levels = num_levels + self.fee_amount = fee_amount + self.volume_per_side = volume_per_side + + self.num_steps = num_steps + self.min_trade_unit = min_trade_unit + + self.curr_bids, self.curr_asks = None, None + + def _liq_provis(self, state: VegaState) -> LiquidityProvision: + return LiquidityProvision( + amount=self.commitment_amount, + fee=self.fee_amount, + ) + + def _scale_orders( + self, + buy_shape: List[MMOrder], + sell_shape: List[MMOrder], + ): + return (buy_shape, sell_shape) + + def _generate_shape( + self, bid_price_depth: float, ask_price_depth: float + ) -> Tuple[List[MMOrder], List[MMOrder]]: + # ref_price = self.curr_price + ref_price = self.vega.last_trade_price(market_id=self.market_id) + + if ref_price == 0: + ref_price = self.curr_price + + bid_orders, ask_orders = _build_price_levels( + position=self.current_position, + ref_price=ref_price, + min_trade_unit=self.min_trade_unit, + num_steps=int(self.volume_per_side / self.min_trade_unit), + tick_spacing=self.tick_spacing, + k_scaling_large=self.k_scaling_large, + k_scaling_small=self.k_scaling_small, + ) + agg_bids = _aggregate_price_levels( + bid_orders, + side=vega_protos.Side.SIDE_BUY, + tick_spacing=self.tick_spacing, + starting_price=ref_price, + min_trade_unit=self.min_trade_unit, + max_levels=self.num_levels, + ) + agg_asks = _aggregate_price_levels( + ask_orders, + side=vega_protos.Side.SIDE_SELL, + tick_spacing=self.tick_spacing, + starting_price=ref_price, + min_trade_unit=self.min_trade_unit, + max_levels=self.num_levels, + ) + self.curr_bids = agg_bids + self.curr_asks = agg_asks + + return agg_bids, agg_asks + + +class CFMV3MarketMaker(ShapedMarketMaker): + NAME_BASE = "cfm_v3_market_maker" + + def __init__( + self, + key_name: str, + num_steps: int, + initial_price: float = 100, + price_width_below: float = 0.05, + price_width_above: float = 0.05, + margin_multiple_at_lower: float = 0.8, + margin_multiple_at_upper: float = 0.8, + base_balance: float = 100, + initial_asset_mint: float = 1000000, + market_name: str = None, + asset_name: str = None, + commitment_amount: float = 6000, + supplied_amount: Optional[float] = None, + market_decimal_places: int = 5, + fee_amount: float = 0.001, + volume_per_side: float = 10, + num_levels: int = 25, + tick_spacing: float = 1, + asset_decimal_places: int = 0, + bound_perc: float = 0.2, + tag: str = "", + wallet_name: str = None, + orders_from_stream: Optional[bool] = True, + state_update_freq: Optional[int] = None, + order_validity_length: Optional[float] = 5, + price_process_generator: Optional[Iterable[float]] = None, + use_last_price_as_ref: bool = False, + position_offset: float = 0, + ): + super().__init__( + wallet_name=wallet_name, + initial_asset_mint=initial_asset_mint, + market_name=market_name, + asset_name=asset_name, + commitment_amount=commitment_amount, + supplied_amount=supplied_amount, + market_decimal_places=market_decimal_places, + asset_decimal_places=asset_decimal_places, + tag=tag, + shape_fn=self._generate_shape, + best_price_offset_fn=lambda *args, **kwargs: (0, 0), + liquidity_commitment_fn=self._liq_provis, + key_name=key_name, + orders_from_stream=orders_from_stream, + state_update_freq=state_update_freq, + order_validity_length=order_validity_length, + price_process_generator=price_process_generator, + ) + self.base_price = initial_price + self.upper_price = (1 + price_width_above) * initial_price + self.lower_price = (1 - price_width_below) * initial_price + + self.base_price_sqrt = initial_price**0.5 + self.upper_price_sqrt = self.upper_price**0.5 + self.lower_price_sqrt = self.lower_price**0.5 + + self.lower_liq_factor = 1 / (self.base_price_sqrt - self.lower_price_sqrt) + self.upper_liq_factor = 1 / (self.upper_price_sqrt - self.base_price_sqrt) + + self.base_balance = base_balance + # self.max_loss_at_bound_above = max_loss_at_bound_above + # self.max_loss_at_bound_below = max_loss_at_bound_below + + self.margin_multiple_at_lower = margin_multiple_at_lower + self.margin_multiple_at_upper = margin_multiple_at_upper + + self.tick_spacing = tick_spacing + self.num_levels = num_levels + self.fee_amount = fee_amount + self.volume_per_side = volume_per_side + + self.curr_bids, self.curr_asks = None, None + self.use_last_price_as_ref = use_last_price_as_ref + self.bound_perc = bound_perc + self.position_offset = position_offset + + def initialise( + self, + vega, + create_key: bool = True, + mint_key: bool = True, + ): + super().initialise(vega=vega, create_key=create_key, mint_key=mint_key) + + logger.info( + f"Quoting for key {self.vega.wallet.public_key(self.key_name, self.wallet_name)}" + ) + risk_factors = vega.get_risk_factors(self.market_id) + self.short_factor, self.long_factor = risk_factors.short, risk_factors.long + + def _liq_provis(self, state: VegaState) -> LiquidityProvision: + return LiquidityProvision( + amount=self.commitment_amount, + fee=self.fee_amount, + ) + + def _scale_orders( + self, + shape: List[MMOrder], + ): + return shape + + def _quantity_for_move( + self, + start_price_sqrt, + end_price_sqrt, + range_upper_price_sqrt, + liquidity_factor, + ) -> Optional[float]: + if liquidity_factor == 0: + return None + start_fut_pos = ( + liquidity_factor + * (range_upper_price_sqrt - start_price_sqrt) + / (start_price_sqrt * range_upper_price_sqrt) + ) + end_fut_pos = ( + liquidity_factor + * (range_upper_price_sqrt - end_price_sqrt) + / (end_price_sqrt * range_upper_price_sqrt) + ) + + return abs(start_fut_pos - end_fut_pos) + + def _add_orders_at_bounds( + self, + bound_perc: float, + existing_orders: List[MMOrder], + ) -> List[MMOrder]: + existing_buys = [] + existing_sells = [] + + for o in existing_orders: + if o.side == vega_protos.SIDE_BUY: + existing_buys.append(o) + else: + existing_sells.append(o) + best_bid, best_ask = self.vega.best_prices(self.market_id) + + best_ask = min( + min([x.price for x in existing_sells]), + best_ask if best_ask != 0.0 else 999999999, + ) + best_bid = max(max([x.price for x in existing_buys]), best_bid) + + mid = (best_ask + best_bid) / 2 + lower = mid * (1 - bound_perc) + upper = mid * (1 + bound_perc) + + market_data = self.vega.market_info(self.market_id) + scaler = 10**market_data.decimal_places + buy_vol = sum( + a.price * scaler * round(a.size / scaler) + for a in existing_buys + if a.price >= lower + ) + sell_vol = sum( + a.price * scaler * round(a.size / scaler) + for a in existing_sells + if a.price <= upper + ) + + required_vol = self.commitment_amount * 24 + + buy_orders = ( + [ + MMOrder( + (required_vol - buy_vol) / lower, + lower, + vega_protos.SIDE_BUY, + time_in_force=vega_protos.Order.TIME_IN_FORCE_GFN, + ) + ] + if buy_vol < required_vol + else [] + ) + + sell_orders = ( + [ + MMOrder( + (required_vol - sell_vol) / upper, + upper, + vega_protos.SIDE_SELL, + time_in_force=vega_protos.Order.TIME_IN_FORCE_GFN, + ) + ] + if sell_vol < required_vol + else [] + ) + + return buy_orders + sell_orders + + def _generate_shape( + self, bid_price_depth: float, ask_price_depth: float + ) -> Tuple[List[MMOrder], List[MMOrder]]: + base_calcs = self._generate_shape_calcs( + balance=sum( + a.balance + for a in self.vega.get_accounts_from_stream( + key_name=self.key_name, + wallet_name=self.wallet_name, + market_id=self.market_id, + ) + ), + position=self.current_position + self.position_offset, + ) + additional_price_bounds = self._add_orders_at_bounds( + bound_perc=self.bound_perc, existing_orders=base_calcs + ) + return base_calcs + additional_price_bounds + + def _generate_shape_calcs( + self, + balance: float, + position: float, + ) -> Tuple[List[MMOrder], List[MMOrder]]: + if balance == 0: + print("No funds, no orders") + return ([], []) + unit_upper_L = ( + self.upper_price_sqrt + * self.base_price_sqrt + / (self.upper_price_sqrt - self.base_price_sqrt) + ) + + unit_lower_L = ( + self.lower_price_sqrt + * self.base_price_sqrt + / (self.base_price_sqrt - self.lower_price_sqrt) + ) + aep_lower = ( + -1 + * unit_lower_L + * self.base_price_sqrt + * ((unit_lower_L / (unit_lower_L + self.base_price_sqrt)) - 1) + ) + aep_upper = ( + -1 + * unit_upper_L + * self.upper_price_sqrt + * ((unit_upper_L / (unit_upper_L + self.upper_price_sqrt)) - 1) + ) + + # volume_at_lower = ( + # self.base_balance + # * self.max_loss_at_bound_below + # / (aep_lower - self.lower_price) + # ) + # volume_at_upper = ( + # self.base_balance + # * self.max_loss_at_bound_above + # / (self.upper_price - aep_upper) + # ) + + volume_at_lower = ( + self.margin_multiple_at_lower + * balance + / ( + self.lower_price * (1 - self.margin_multiple_at_lower) + + self.margin_multiple_at_lower * aep_lower + ) + ) + volume_at_upper = ( + self.margin_multiple_at_upper + * balance + / ( + self.upper_price * (1 + self.margin_multiple_at_upper) + - self.margin_multiple_at_upper * aep_upper + ) + ) + + upper_L = ( + volume_at_upper + * self.upper_price_sqrt + * self.base_price_sqrt + / (self.upper_price_sqrt - self.base_price_sqrt) + ) + + lower_L = ( + volume_at_lower + * self.lower_price_sqrt + * self.base_price_sqrt + / (self.base_price_sqrt - self.lower_price_sqrt) + ) + + if position > 0: + L = lower_L + upper_bound = self.base_price_sqrt + rt_ref_price = upper_bound / (position * upper_bound / L + 1) + else: + L = upper_L + upper_bound = self.upper_price_sqrt + rt_ref_price = upper_bound / ( + (volume_at_upper + position) * upper_bound / L + 1 + ) + + print(f"At position {position} quoting around fair price {rt_ref_price ** 2}") + return self._calculate_price_levels( + ref_price=rt_ref_price**2, + upper_L=upper_L, + lower_L=lower_L, + position=position, + ) + + def _calculate_liq_val( + self, margin_frac: float, balance: float, risk_factor: float, liq_factor: float + ) -> float: + return margin_frac * (balance / risk_factor) * liq_factor + + def _calculate_price_levels( + self, + ref_price: float, + upper_L: float, + lower_L: float, + position: float, + ) -> Tuple[List[MMOrder], List[MMOrder]]: + if ref_price == 0: + ref_price = self.curr_price + + agg_bids = [] + agg_asks = [] + + pos = position + + for i in range(1, self.num_levels): + pre_price_sqrt = (ref_price + (i - 1) * self.tick_spacing) ** 0.5 + price = ref_price + i * self.tick_spacing + + if price > self.upper_price or price < self.lower_price: + continue + + volume = self._quantity_for_move( + pre_price_sqrt, + price**0.5, + self.upper_price_sqrt if pos <= 0 else self.base_price_sqrt, + upper_L if pos <= 0 else lower_L, + ) + + if volume is not None: + if pos > 0 and pos - volume < 0: + volume = pos + agg_asks.append(MMOrder(volume, price, vega_protos.SIDE_SELL)) + pos -= volume + + pos = position + for i in range(1, self.num_levels): + pre_price_sqrt = (ref_price - (i - 1) * self.tick_spacing) ** 0.5 + price = ref_price - i * self.tick_spacing + + if price > self.upper_price or price < self.lower_price: + continue + + volume = abs( + self._quantity_for_move( + pre_price_sqrt, + price**0.5, + self.upper_price_sqrt if pos < 0 else self.base_price_sqrt, + upper_L if pos < 0 else lower_L, + ) + ) + if volume is not None: + if pos < 0 and pos + volume >= 0: + volume = abs(pos) + agg_bids.append(MMOrder(volume, price, vega_protos.SIDE_BUY)) + pos += volume + + self.curr_bids = agg_bids + self.curr_asks = agg_asks + + return agg_bids + agg_asks + + +class CFMV3LastTradeMarketMaker(ShapedMarketMaker): + NAME_BASE = "cfm_v3_market_maker" + + def __init__( + self, + key_name: str, + num_steps: int, + initial_price: float = 100, + price_width_below: float = 0.05, + price_width_above: float = 0.05, + margin_usage_at_bound_above: float = 0.8, + margin_usage_at_bound_below: float = 0.8, + initial_asset_mint: float = 1000000, + market_name: str = None, + asset_name: str = None, + commitment_amount: float = 6000, + supplied_amount: Optional[float] = None, + market_decimal_places: int = 5, + fee_amount: float = 0.001, + volume_per_side: float = 10, + num_levels: int = 25, + tick_spacing: float = 1, + asset_decimal_places: int = 0, + tag: str = "", + wallet_name: str = None, + orders_from_stream: Optional[bool] = True, + state_update_freq: Optional[int] = None, + order_validity_length: Optional[float] = None, + price_process_generator: Optional[Iterable[float]] = None, + use_last_price_as_ref: bool = False, + ): + super().__init__( + wallet_name=wallet_name, + initial_asset_mint=initial_asset_mint, + market_name=market_name, + asset_name=asset_name, + commitment_amount=commitment_amount, + supplied_amount=supplied_amount, + market_decimal_places=market_decimal_places, + asset_decimal_places=asset_decimal_places, + tag=tag, + shape_fn=self._generate_shape, + best_price_offset_fn=lambda *args, **kwargs: (0, 0), + liquidity_commitment_fn=self._liq_provis, + key_name=key_name, + orders_from_stream=orders_from_stream, + state_update_freq=state_update_freq, + order_validity_length=order_validity_length, + price_process_generator=price_process_generator, + ) + self.base_price = initial_price + self.upper_price = (1 + price_width_above) * initial_price + self.lower_price = (1 - price_width_below) * initial_price + + self.base_price_sqrt = initial_price**0.5 + self.upper_price_sqrt = self.upper_price**0.5 + self.lower_price_sqrt = self.lower_price**0.5 + + self.lower_liq_factor = 1 / (self.base_price_sqrt - self.lower_price_sqrt) + self.upper_liq_factor = 1 / (self.upper_price_sqrt - self.base_price_sqrt) + + self.margin_usage_at_bound_above = margin_usage_at_bound_above + self.margin_usage_at_bound_below = margin_usage_at_bound_below + + self.tick_spacing = tick_spacing + self.num_levels = num_levels + self.fee_amount = fee_amount + self.volume_per_side = volume_per_side + + self.curr_bids, self.curr_asks = None, None + self.use_last_price_as_ref = use_last_price_as_ref + + def initialise( + self, + vega, + create_key: bool = True, + mint_key: bool = True, + ): + super().initialise(vega=vega, create_key=create_key, mint_key=mint_key) + + risk_factors = vega.get_risk_factors(self.market_id) + self.short_factor, self.long_factor = risk_factors.short, risk_factors.long + + def _liq_provis(self, state: VegaState) -> LiquidityProvision: + return LiquidityProvision( + amount=self.commitment_amount, + fee=self.fee_amount, + ) + + def _scale_orders( + self, + buy_shape: List[MMOrder], + sell_shape: List[MMOrder], + ): + return (buy_shape, sell_shape) + + def _quantity_for_move( + self, + start_price_sqrt, + end_price_sqrt, + range_upper_price_sqrt, + liquidity_factor, + ) -> Optional[float]: + if liquidity_factor == 0: + return None + start_fut_pos = ( + liquidity_factor + * (range_upper_price_sqrt - start_price_sqrt) + / (start_price_sqrt * range_upper_price_sqrt) + ) + end_fut_pos = ( + liquidity_factor + * (range_upper_price_sqrt - end_price_sqrt) + / (end_price_sqrt * range_upper_price_sqrt) + ) + + return abs(start_fut_pos - end_fut_pos) + + def _generate_shape( + self, bid_price_depth: float, ask_price_depth: float + ) -> Tuple[List[MMOrder], List[MMOrder]]: + balance = sum( + a.balance + for a in self.vega.get_accounts_from_stream( + key_name=self.key_name, + wallet_name=self.wallet_name, + market_id=self.market_id, + ) + ) + + upper_L = self._calculate_liq_val( + self.margin_usage_at_bound_above, + balance, + self.short_factor, + self.upper_liq_factor, + ) + lower_L = self._calculate_liq_val( + self.margin_usage_at_bound_below, + balance, + self.long_factor, + self.lower_liq_factor, + ) + if self.use_last_price_as_ref: + ref_price = self.vega.last_trade_price(market_id=self.market_id) + else: + average_entry = ( + self.vega.positions_by_market( + wallet_name=self.wallet_name, + market_id=self.market_id, + key_name=self.key_name, + ).average_entry_price + if self.current_position + self.position_offset != 0 + else 0 + ) + if self.current_position > 0: + L = lower_L + lower_bound = self.lower_price_sqrt + upper_bound = self.base_price + usd_total = ( + self.margin_usage_at_bound_below * balance / self.long_factor + ) + else: + L = upper_L + lower_bound = self.base_price + upper_bound = self.upper_price_sqrt + usd_total = ( + self.margin_usage_at_bound_above * balance / self.short_factor + ) + if L == 0: + ref_price = self.base_price + else: + virt_x = self.current_position + upper_bound / L + virt_y = ( + usd_total - self.current_position * average_entry + ) + L * lower_bound + ref_price = virt_y / virt_x + + return self._calculate_price_levels( + ref_price=ref_price, balance=balance, upper_L=upper_L, lower_L=lower_L + ) + + def _calculate_liq_val( + self, margin_frac: float, balance: float, risk_factor: float, liq_factor: float + ) -> float: + return margin_frac * (balance / risk_factor) * liq_factor + + def _calculate_price_levels( + self, ref_price: float, balance: float, upper_L: float, lower_L: float + ) -> Tuple[List[MMOrder], List[MMOrder]]: + if ref_price == 0: + ref_price = self.curr_price + + agg_bids = [] + agg_asks = [] + + for i in range(1, self.num_levels): + pre_price_sqrt = (ref_price + (i - 1) * self.tick_spacing) ** 0.5 + price = ref_price + i * self.tick_spacing + + if price > self.upper_price or price < self.lower_price: + continue + + volume = self._quantity_for_move( + pre_price_sqrt, + price**0.5, + self.upper_price_sqrt, + upper_L if price > self.base_price else lower_L, + ) + if volume is not None: + agg_asks.append(MMOrder(volume, price)) + + for i in range(1, self.num_levels): + pre_price_sqrt = (ref_price - (i - 1) * self.tick_spacing) ** 0.5 + price = ref_price - i * self.tick_spacing + + if price > self.upper_price or price < self.lower_price: + continue + + volume = self._quantity_for_move( + pre_price_sqrt, + price**0.5, + self.upper_price_sqrt, + upper_L if price > self.base_price else lower_L, + ) + + if volume is not None: + agg_bids.append(MMOrder(volume, price)) + + self.curr_bids = agg_bids + self.curr_asks = agg_asks + + return agg_bids, agg_asks + + +# if __name__ == "__main__": +# import matplotlib.pyplot as plt + +# buy_pri1, sell_pri1 = _build_price_levels( +# position=0, +# ref_price=100, +# min_trade_unit=0.002, +# num_steps=1, +# tick_spacing=0.01, +# k_scaling_small=1e6, +# k_scaling_large=1e6, +# ) + +# print(f"Spread Bid @ {buy_pri1[0]} - Ask @ {sell_pri1[0]}") +# bids, asks = _build_price_levels( +# position=-1, +# ref_price=100, +# min_trade_unit=0.01, +# num_steps=1000, +# tick_spacing=0.01, +# k_scaling_small=7e6, +# k_scaling_large=100e6, +# ) + +# x = [] +# y = [] + +# cumsum = 0 +# for bid in bids: +# x.append(bid.price) +# cumsum += bid.size +# y.append(cumsum) + +# plt.plot(x, y, color="blue") +# x = [] +# y = [] + +# cumsum = 0 +# for ask in asks: +# x.append(ask.price) +# cumsum += ask.size +# y.append(cumsum) +# plt.plot(x, y, color="red") +# plt.show() + +if __name__ == "__main__": + import matplotlib.pyplot as plt + + mm = CFMV3MarketMaker( + "fawfa", + num_steps=12, + initial_price=2000, + price_width_above=10, + price_width_below=0.9, + margin_usage_at_bound_above=0.8, + margin_usage_at_bound_below=0.8, + initial_asset_mint=100_000, + market_name="MKT", + num_levels=2000, + tick_spacing=1, + ) + balance = 1000 + + mm.short_factor = 0.02 + mm.long_factor = 0.02 + + volume_at_upper = ( + mm.margin_usage_at_bound_above * (balance / mm.short_factor) / mm.upper_price + ) + upper_L = ( + volume_at_upper + * mm.upper_price_sqrt + * mm.base_price_sqrt + / (mm.upper_price_sqrt - mm.base_price_sqrt) + ) + + lower_L = ( + mm.margin_usage_at_bound_below + * (balance / mm.long_factor) + * mm.lower_liq_factor + ) + + to_price = 2000 + pos = mm._quantity_for_move( + mm.base_price_sqrt, + to_price**0.5, + mm.base_price_sqrt if to_price < mm.base_price else mm.upper_price_sqrt, + lower_L, + ) + print(f"pos would be {pos}") + bids, asks = mm._generate_shape_calcs(balance=balance, position=pos) + + # x = [] + # y = [] + + # cumsum = 0 + # for bid in bids: + # x.append(bid.price) + # cumsum += bid.size + # y.append(cumsum) + + # plt.plot(x, y, color="blue") + x = [] + y = [] + + # cumsum = 0 + # for ask in asks: + # x.append(ask.price) + # cumsum += ask.size + # y.append(cumsum) + # plt.plot(x, y, color="red") + # plt.show() + + cumsum = 0 + for bid in bids: + x.append(bid.price) + cumsum += bid.size + y.append(bid.size) + + plt.bar(x, y, color="blue") + x = [] + y = [] + + cumsum = 0 + for ask in asks: + x.append(ask.price) + cumsum += ask.size + y.append(ask.size) + plt.bar(x, y, color="red") + plt.show() diff --git a/vega_sim/scenario/constant_function_market/scenario.py b/vega_sim/scenario/constant_function_market/scenario.py new file mode 100644 index 000000000..cf951b0ce --- /dev/null +++ b/vega_sim/scenario/constant_function_market/scenario.py @@ -0,0 +1,487 @@ +import argparse +import datetime +import logging +from dataclasses import dataclass +from typing import Dict, List, Optional + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +import vega_sim.proto.vega as vega_protos +from vega_sim.api.market import MarketConfig +from vega_sim.environment.environment import MarketEnvironmentWithState +from vega_sim.null_service import VegaServiceNull +from vega_sim.scenario.common.agents import ( + ExponentialShapedMarketMaker, + LimitOrderTrader, + MarketOrderTrader, + ArbitrageTrader, + PriceSensitiveMarketOrderTrader, + StateAgent, + UncrossAuctionAgent, +) +from vega_sim.scenario.common.utils.price_process import ( + Granularity, + get_historic_price_series, + random_walk, +) +from vega_sim.scenario.configurable_market.agents import ConfigurableMarketManager +from vega_sim.scenario.constant_function_market.agents import ( + CFMMarketMaker, + CFMV3MarketMaker, +) +from vega_sim.scenario.constants import Network +from vega_sim.scenario.scenario import Scenario +from vega_sim.tools.scenario_plots import ( + account_and_margin_plots, + fuzz_plots, + price_comp_plots, + plot_run_outputs, + reward_plots, + sla_plot, +) + + +@dataclass +class MarketHistoryAdditionalData: + at_time: datetime.datetime + external_prices: Dict[str, float] + posn: float + + +@dataclass +class LedgerEntries: + entries: List[dict] + + +def additional_ledger_data_to_rows(data) -> List[Dict]: + results = [] + + if isinstance(data, LedgerEntries): + for ledger_entry in data.entries: + results.append( + { + "time": ledger_entry.timestamp, + "quantity": ledger_entry.quantity, + "transfer_type": ledger_entry.transfer_type, + "asset_id": ledger_entry.asset_id, + "from_account_type": ledger_entry.from_account_type, + "to_account_type": ledger_entry.to_account_type, + "from_account_party_id": ledger_entry.from_account_party_id, + "to_account_party_id": ledger_entry.to_account_party_id, + "from_account_market_id": ledger_entry.from_account_market_id, + "to_account_market_id": ledger_entry.to_account_market_id, + } + ) + return results + + +def state_extraction_fn(vega: VegaServiceNull, agents: dict): + at_time = vega.get_blockchain_time() + + external_prices = {} + + posn = 0 + for _, agent in agents.items(): + if isinstance(agent, (CFMMarketMaker, CFMV3MarketMaker)): + external_prices[agent.market_id] = agent.curr_price + vega_posn = vega.positions_by_market( + agent.key_name, agent.market_id, agent.wallet_name + ) + posn = vega_posn.open_volume if vega_posn else 0 + + return MarketHistoryAdditionalData( + at_time=at_time, external_prices=external_prices, posn=posn + ) + + +def final_extraction(vega: VegaServiceNull, agents: dict): + results = [] + for _, agent in agents.items(): + if isinstance(agent, (CFMMarketMaker, CFMV3MarketMaker)): + from_ledger_entries = vega.list_ledger_entries( + from_party_ids=[agent._public_key], + transfer_types=[14, 20, 30, 31, 32, 33, 34, 35], + ) + to_ledger_entries = vega.list_ledger_entries( + to_party_ids=[agent._public_key], + transfer_types=[14, 20, 30, 31, 32, 33, 34, 35], + ) + results = results + from_ledger_entries + to_ledger_entries + + return LedgerEntries(entries=results) + + +def additional_data_to_rows(data) -> List[pd.Series]: + results = [] + if isinstance(data, LedgerEntries): + return results + for market_id in data.external_prices.keys(): + results.append( + { + "time": data.at_time, + "market_id": market_id, + "external_price": data.external_prices.get(market_id, np.NaN), + "position": data.posn, + } + ) + return results + + +class CFMScenario(Scenario): + def __init__( + self, + num_steps: int = 60 * 24 * 30 * 3, + transactions_per_block: int = 4096, + block_length_seconds: float = 1, + step_length_seconds: Optional[float] = None, + market_config: Optional[dict] = None, + output: bool = True, + pause_every_n_steps: Optional[int] = None, + ): + super().__init__( + state_extraction_fn=lambda vega, agents: state_extraction_fn(vega, agents), + final_extraction_fn=lambda vega, agents: final_extraction(vega, agents), + additional_data_output_fns={ + "additional_data.csv": lambda data: additional_data_to_rows(data), + "ledger_entries.csv": lambda data: additional_ledger_data_to_rows(data), + }, + ) + + self.market_config = market_config + + self.pause_every_n_steps = pause_every_n_steps + self.num_steps = num_steps + self.step_length_seconds = ( + step_length_seconds + if step_length_seconds is not None + else block_length_seconds + ) + + self.block_length_seconds = block_length_seconds + self.transactions_per_block = transactions_per_block + + self.output = output + + def configure_agents( + self, + vega: VegaServiceNull, + tag: str, + random_state: Optional[np.random.RandomState], + **kwargs, + ) -> List[StateAgent]: + self.random_state = ( + random_state if random_state is not None else np.random.RandomState() + ) + + self.agents = [] + self.initial_asset_mint = 10e9 + + # Define the market and the asset: + market_name = "CFM:Perp" + asset_name = "USD" + asset_dp = 18 + + market_agents = {} + # Create fuzzed market config + market_config = MarketConfig() + + if self.market_config is not None: + for param in self.market_config: + market_config.set(param=self.market_config[param]) + + i_market = 0 + # Create fuzzed price process + # price_process = [1000] * self.num_steps + price_process = random_walk( + random_state=self.random_state, + starting_price=1000, + sigma=1, + num_steps=self.num_steps, + decimal_precision=int(market_config.decimal_places), + ) + # price_process = get_historic_price_series( + # product_id="ETH-USD", + # granularity=Granularity.MINUTE, + # start=str(datetime.datetime(2023, 10, 23, 10)), + # end=str( + # datetime.datetime(2023, 10, 23, 10) + datetime.timedelta(minutes=1000) + # ), + # ).values + # price_process = get_historic_price_series( + # product_id="ETH-USD", + # granularity=Granularity.MINUTE, + # start=str(datetime.datetime(2022, 11, 8)), + # end=str(datetime.datetime(2022, 11, 8) + datetime.timedelta(minutes=1000)), + # ).values + + # price_process = get_historic_price_series( + # product_id="ETH-USD", + # granularity=Granularity.MINUTE, + # start=str(datetime.datetime(2023, 7, 8)), + # end=str(datetime.datetime(2023, 7, 8) + datetime.timedelta(minutes=1000)), + # ).values + + # Create fuzzed market managers + market_agents["market_managers"] = [ + ConfigurableMarketManager( + proposal_wallet_name="MARKET_MANAGER", + proposal_key_name="PROPOSAL_KEY", + termination_wallet_name="MARKET_MANAGER", + termination_key_name="TERMINATION_KEY", + market_config=market_config, + market_name=market_name, + market_code=market_name, + asset_dp=asset_dp, + asset_name=asset_name, + settlement_price=price_process[-1], + tag="MARKET", + ) + ] + + market_agents["auction_traders"] = [ + UncrossAuctionAgent( + wallet_name="AUCTION_TRADERS", + key_name=f"MARKET_{str(i_market).zfill(3)}_{side}", + side=side, + initial_asset_mint=self.initial_asset_mint, + price_process=iter(price_process), + market_name=market_name, + asset_name=asset_name, + uncrossing_size=0.01, + tag=f"MARKET_{str(i_market).zfill(3)}_AGENT_{str(i_agent).zfill(3)}", + ) + for i_agent, side in enumerate(["SIDE_BUY", "SIDE_SELL"]) + ] + + # market_agents["market_makers_exp"] = [ + # ExponentialShapedMarketMaker( + # wallet_name="MARKET_MAKERS", + # key_name=f"MARKET_{str(i_market).zfill(3)}", + # price_process_generator=iter(price_process), + # initial_asset_mint=self.initial_asset_mint, + # market_name=market_name, + # asset_name=asset_name, + # commitment_amount=1e6, + # market_decimal_places=market_config.decimal_places, + # asset_decimal_places=asset_dp, + # num_steps=self.num_steps, + # kappa=2.4, + # tick_spacing=0.05, + # market_kappa=50, + # state_update_freq=10, + # tag=f"MARKET_{str(i_market).zfill(3)}", + # ) + # for i_market in range(6) + # ] + market_agents["market_makers"] = [ + # ExponentialShapedMarketMaker( + # wallet_name="MARKET_MAKERS", + # key_name=f"MARKET_{str(i_market).zfill(3)}", + # price_process_generator=iter(price_process), + # initial_asset_mint=self.initial_asset_mint, + # market_name=market_name, + # asset_name=asset_name, + # commitment_amount=1e6, + # market_decimal_places=market_config.decimal_places, + # asset_decimal_places=asset_dp, + # num_steps=self.num_steps, + # kappa=2.4, + # tick_spacing=0.05, + # market_kappa=50, + # state_update_freq=10, + # tag=f"MARKET_{str(i_market).zfill(3)}", + # ) + # CFMMarketMaker( + # key_name="CFM_MAKER", + # num_steps=self.num_steps, + # initial_asset_mint=self.initial_asset_mint, + # market_name=market_name, + # asset_name=asset_name, + # commitment_amount=10e6, + # market_decimal_places=market_config.decimal_places, + # fee_amount=0.001, + # k_scaling_large=600e6, + # k_scaling_small=5e6, + # min_trade_unit=0.01, + # initial_price=price_process[0], + # num_levels=20, + # volume_per_side=100, + # tick_spacing=0.5, + # asset_decimal_places=asset_dp, + # tag="MARKET_CFM", + # price_process_generator=iter(price_process), + # ) + CFMV3MarketMaker( + key_name="CFM_MAKER", + num_steps=self.num_steps, + initial_asset_mint=10_000, + market_name=market_name, + asset_name=asset_name, + commitment_amount=1_000, + market_decimal_places=market_config.decimal_places, + fee_amount=0.001, + # initial_price=max(price_process), + initial_price=price_process[0], + num_levels=200, + tick_spacing=0.1, + price_width_above=0.003, + price_width_below=0.003, + margin_usage_at_bound_above=0.8, + margin_usage_at_bound_below=0.8, + asset_decimal_places=asset_dp, + price_process_generator=iter(price_process), + tag=f"MARKET_CFM_{i}", + ) + for i in range(1) + ] + + # market_agents["price_sensitive_traders"] = [ + # PriceSensitiveMarketOrderTrader( + # key_name=f"PRICE_SENSITIVE_{str(i_agent).zfill(3)}", + # market_name=market_name, + # asset_name=asset_name, + # price_process_generator=iter(price_process), + # initial_asset_mint=self.initial_asset_mint, + # buy_intensity=5, + # sell_intensity=5, + # price_half_life=0.2, + # tag=f"SENSITIVE_AGENT_{str(i_agent).zfill(3)}", + # random_state=random_state, + # base_order_size=0.2, + # wallet_name="SENSITIVE_TRADERS", + # ) + # for i_agent in range(6) + # ] + + market_agents["arb_traders"] = [ + ArbitrageTrader( + key_name=f"PRICE_SENSITIVE_{str(i_agent).zfill(3)}", + market_name=market_name, + asset_name=asset_name, + price_process_generator=iter(price_process), + initial_asset_mint=self.initial_asset_mint, + buy_intensity=100, + sell_intensity=100, + spread_offset=0.0001, + tag=f"ARB_AGENT_{str(i_agent).zfill(3)}", + random_state=random_state, + base_order_size=1, + wallet_name="ARB_TRADERS", + ) + for i_agent in range(2) + ] + + # market_agents["random_traders"] = [ + # MarketOrderTrader( + # wallet_name="RANDOM_TRADERS", + # key_name=( + # f"MARKET_{str(i_market).zfill(3)}_AGENT_{str(i_agent).zfill(3)}" + # ), + # market_name=market_name, + # asset_name=asset_name, + # buy_intensity=10, + # sell_intensity=10, + # base_order_size=0.01, + # step_bias=0.5, + # tag=f"MARKET_{str(i_market).zfill(3)}_AGENT_{str(i_agent).zfill(3)}", + # random_state=random_state, + # ) + # for i_agent in range(20) + # # for i_agent in range(1) + # ] + + # for i_agent in range(1): + # market_agents["random_traders"].append( + # LimitOrderTrader( + # wallet_name=f"RANDOM_TRADERS", + # key_name=( + # f"LIMIT_{str(i_market).zfill(3)}_AGENT_{str(i_agent).zfill(3)}" + # ), + # market_name=market_name, + # asset_name=asset_name, + # time_in_force_opts={"TIME_IN_FORCE_GTT": 1}, + # buy_volume=0.1, + # sell_volume=0.1, + # buy_intensity=1, + # sell_intensity=1, + # submit_bias=1, + # cancel_bias=0, + # duration=120, + # price_process=price_process, + # spread=0, + # mean=-3, + # sigma=0.5, + # tag=( + # f"MARKET_{str(i_market).zfill(3)}_AGENT_{str(i_agent).zfill(3)}" + # ), + # ) + # ) + + for _, agent_list in market_agents.items(): + self.agents.extend(agent_list) + + return {agent.name(): agent for agent in self.agents} + + def configure_environment( + self, + vega: VegaServiceNull, + **kwargs, + ) -> MarketEnvironmentWithState: + return MarketEnvironmentWithState( + agents=list(self.agents.values()), + n_steps=self.num_steps, + random_agent_ordering=False, + transactions_per_block=self.transactions_per_block, + vega_service=vega, + step_length_seconds=self.step_length_seconds, + block_length_seconds=vega.seconds_per_block, + pause_every_n_steps=self.pause_every_n_steps, + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--debug", action="store_true") + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO if not args.debug else logging.DEBUG) + + scenario = CFMScenario( + num_steps=600, + step_length_seconds=10, + block_length_seconds=1, + transactions_per_block=4096, + # pause_every_n_steps=100, + ) + + with VegaServiceNull( + warn_on_raw_data_access=False, + run_with_console=False, + use_full_vega_wallet=False, + retain_log_files=True, + launch_graphql=False, + seconds_per_block=scenario.block_length_seconds, + transactions_per_block=scenario.transactions_per_block, + ) as vega: + scenario.run_iteration( + vega=vega, + pause_at_completion=False, + log_every_n_steps=10, + output_data=True, + ) + + figs = price_comp_plots() + for i, fig in enumerate(figs.values()): + fig.savefig(f"./cfm_plots/trading-{i}.jpg") + + account_fig = account_and_margin_plots( + agent_types=[CFMMarketMaker, CFMV3MarketMaker] + ) + account_fig.savefig("./cfm_plots/accounts.jpg") + plt.close(account_fig) + + fig = sla_plot() + fig.savefig("./cfm_plots/sla.jpg") + plt.close(fig) diff --git a/vega_sim/scenario/constants.py b/vega_sim/scenario/constants.py index 0fb04adfc..a6adda111 100644 --- a/vega_sim/scenario/constants.py +++ b/vega_sim/scenario/constants.py @@ -11,3 +11,4 @@ class Network(Enum): MAINNET_MIRROR = "vegawallet-mainnet-mirror" TESTNET2 = "testnet2" CAPSULE = "config" + MAINNET1 = "mainnet1" diff --git a/vega_sim/scenario/fuzzed_markets/agents.py b/vega_sim/scenario/fuzzed_markets/agents.py index c984fc9bf..87381d9d3 100644 --- a/vega_sim/scenario/fuzzed_markets/agents.py +++ b/vega_sim/scenario/fuzzed_markets/agents.py @@ -367,9 +367,6 @@ def step(self, vega_state): wait=False, ) except Exception as e: - import pdb - - pdb.set_trace() print(f"There was an error {e}") diff --git a/vega_sim/service.py b/vega_sim/service.py index d329ae683..2f14a4617 100644 --- a/vega_sim/service.py +++ b/vega_sim/service.py @@ -32,6 +32,7 @@ forward, statistics, num_to_padded_int, + num_from_padded_int, wait_for_core_catchup, wait_for_datanode_sync, ) @@ -1743,6 +1744,9 @@ def market_info( market_id=market_id, data_client=self.trading_data_client_v2 ) + def last_trade_price(self, market_id: str) -> float: + return self.market_data_from_feed(market_id=market_id).last_traded_price + @raw_data def market_data_from_feed( self, @@ -1842,7 +1846,7 @@ def price_bounds( """ Output the tightest price bounds in the current market. """ - market_data = self.get_latest_market_data( + market_data = self.market_data_from_feed( market_id=market_id, ) @@ -3049,6 +3053,13 @@ def list_ledger_entries( A list of all transfers matching the requested criteria """ + to_datetime = ( + to_datetime + if to_datetime is not None + else datetime.datetime.fromtimestamp( + (self.get_blockchain_time_from_feed() / 1e9) + 360 + ) + ) return data.list_ledger_entries( data_client=self.trading_data_client_v2, asset_id=asset_id, diff --git a/vega_sim/tools/scenario_plots.py b/vega_sim/tools/scenario_plots.py index b0b8f4aeb..69ae1ef66 100644 --- a/vega_sim/tools/scenario_plots.py +++ b/vega_sim/tools/scenario_plots.py @@ -78,6 +78,10 @@ def wrapped_fn(*args, **kwargs): "color": "c", "stack": True, }, + vega_protos.vega.TRANSFER_TYPE_MAKER_FEE_RECEIVE: { + "color": "y", + "stack": True, + }, } """ @@ -456,7 +460,49 @@ def plot_price_comparison( ax0.plot(external_price_series, linewidth=0.8, alpha=0.8) ax0.set_ylabel("PRICE") - ax0.legend(labels=["external price", "mark price"]) + ax0.autoscale(enable=True, axis="y") + ax0.legend(labels=["mark price", "external price"]) + + +def plot_position( + fig: Figure, + fuzzing_df: pd.DataFrame, + ss: Optional[SubplotSpec], +): + """Plots the external price and mark price along with their respective volatilities. + + Args: + fig (Figure): + Figure object to plot the data on. + fuzzing_df (pd.DataFrame): + DataFrame containing fuzzing data. + data_df (pd.DataFrame): + DataFrame containing market data. + ss (Optional[SubplotSpec]): + SubplotSpec object representing the position of the subplot on the figure. + """ + if ss is None: + gs = GridSpec( + nrows=1, + ncols=1, + ) + else: + gs = GridSpecFromSubplotSpec( + subplot_spec=ss, + nrows=1, + ncols=1, + ) + ax0 = fig.add_subplot(gs[0, 0]) + + ax0.set_title("Position", loc="left", fontsize=12, color=(0.3, 0.3, 0.3)) + + posn_series = fuzzing_df["position"].replace(0, np.nan) + + ax0.plot(posn_series) + ax0.set_ylim(ax0.get_ylim()) + + ax0.set_ylabel("Position") + ax0.autoscale(enable=True, axis="y") @log_plotting_function @@ -557,6 +603,53 @@ def plot_risky_close_outs( ax1r.ticklabel_format(axis="y", style="sci", scilimits=(0, 0)) +@log_plotting_function +def price_comp_plots(run_name: Optional[str] = None) -> Figure: + data_df = load_market_data_df(run_name=run_name) + fuzzing_df = load_fuzzing_df(run_name=run_name) + + market_chains = load_market_chain(run_name=run_name) + if not market_chains: + market_chains = { + market_id: [market_id] for market_id in data_df["market_id"].unique() + } + + figs = {} + for market_id, market_children in market_chains.items(): + market_data_df = _series_df_to_single_series( + data_df[data_df["market_id"].isin(market_children)], + market_series=market_children, + ) + market_fuzzing_df = _series_df_to_single_series( + fuzzing_df[fuzzing_df["market_id"].isin(market_children)], + market_series=market_children, + ) + + fig = plt.figure(figsize=[8, 10]) + fig.suptitle( + f"Price Comparison Plots", + fontsize=18, + fontweight="bold", + color=(0.2, 0.2, 0.2), + ) + fig.tight_layout() + + plt.rcParams.update({"font.size": 8}) + + gs = GridSpec(nrows=3, ncols=1, height_ratios=[2, 2, 3], hspace=0.3) + + plot_trading_mode(fig, ss=gs[0, 0], data_df=market_data_df) + plot_price_comparison( + fig, + ss=gs[1, 0], + data_df=market_data_df, + fuzzing_df=market_fuzzing_df, + ) + plot_position(fig, ss=gs[2, 0], fuzzing_df=market_fuzzing_df) + figs[market_id] = fig + return figs + + @log_plotting_function def fuzz_plots(run_name: Optional[str] = None) -> Figure: data_df = load_market_data_df(run_name=run_name) @@ -617,6 +710,92 @@ def fuzz_plots(run_name: Optional[str] = None) -> Figure: return figs +@log_plotting_function +def account_and_margin_plots( + run_name: Optional[str] = None, agent_types: Optional[list] = None +): + accounts_df = load_accounts_df(run_name=run_name) + agents_df = load_agents_df(run_name=run_name) + + fig = plt.figure(figsize=[8, 10]) + fig.suptitle( + f"Agent Account Plots", + fontsize=18, + fontweight="bold", + color=(0.2, 0.2, 0.2), + ) + fig.tight_layout() + + plt.rcParams.update({"font.size": 8}) + + agent_types = ( + agent_types + if agent_types is not None + else [ + FuzzingAgent, + FuzzyLiquidityProvider, + RiskyMarketOrderTrader, + RiskySimpleLiquidityProvider, + ] + ) + + gs = GridSpec(nrows=len(agent_types) * 2, ncols=1, hspace=0.5) + + axs: list[plt.Axes] = [] + + for i, agent_type in enumerate(agent_types): + tot_plt = fig.add_subplot(gs[2 * i, 0]) + margin_plt = fig.add_subplot(gs[2 * i + 1, 0]) + pos_plt = fig.add_subplot(gs[2 * i + 1, 0]) + + tot_plt.set_title( + f"Total Account Balance: {agent_type.__name__}", + loc="left", + fontsize=12, + color=(0.3, 0.3, 0.3), + ) + margin_plt.set_title( + f"Margin Account Balance: {agent_type.__name__}", + loc="left", + fontsize=12, + color=(0.3, 0.3, 0.3), + ) + + margin_plt.set_title( + f"Position: {agent_type.__name__}", + loc="left", + fontsize=12, + color=(0.3, 0.3, 0.3), + ) + + agent_keys = agents_df["agent_key"][ + agents_df["agent_type"] == agent_type.__name__ + ].to_list() + for key in agent_keys: + totals = ( + accounts_df[accounts_df["party_id"] == key]["balance"] + .groupby(level=0) + .sum() + ) + margin = ( + accounts_df[ + (accounts_df["party_id"] == key) + & ( + accounts_df["type"] + == vega_protos.vega.AccountType.ACCOUNT_TYPE_MARGIN + ) + ]["balance"] + .groupby(level=0) + .sum() + ) + tot_plt.plot(totals) + margin_plt.plot(margin) + tot_plt.autoscale(enable=True, axis="y") + margin_plt.autoscale(enable=True, axis="y") + + return fig + + @log_plotting_function def account_plots(run_name: Optional[str] = None, agent_types: Optional[list] = None): accounts_df = load_accounts_df(run_name=run_name) @@ -633,12 +812,16 @@ def account_plots(run_name: Optional[str] = None, agent_types: Optional[list] = plt.rcParams.update({"font.size": 8}) - agent_types = [ - FuzzingAgent, - FuzzyLiquidityProvider, - RiskyMarketOrderTrader, - RiskySimpleLiquidityProvider, - ] + agent_types = ( + agent_types + if agent_types is not None + else [ + FuzzingAgent, + FuzzyLiquidityProvider, + RiskyMarketOrderTrader, + RiskySimpleLiquidityProvider, + ] + ) gs = GridSpec(nrows=len(agent_types), ncols=1, hspace=0.5) @@ -1217,9 +1400,9 @@ def determine_poi(row): ) axs[-1].set_ylabel("Amount [USD]") - axs[0].get_shared_x_axes().join(*[ax for ax in axs[0:]]) - axs[1].get_shared_y_axes().join(*[axs[1], axs[3]]) - axs[2].get_shared_y_axes().join(*[axs[2], axs[4]]) + # axs[0].get_shared_x_axes().join(*[ax for ax in axs[0:]]) + # axs[1].get_shared_y_axes().join(*[axs[1], axs[3]]) + # axs[2].get_shared_y_axes().join(*[axs[2], axs[4]]) return fig diff --git a/vega_sim/vegahome/config/node/config.toml b/vega_sim/vegahome/config/node/config.toml index 17ef756c5..441e2243f 100644 --- a/vega_sim/vegahome/config/node/config.toml +++ b/vega_sim/vegahome/config/node/config.toml @@ -64,7 +64,7 @@ UlimitNOFile = 8192 LogPriceLevelsDebug = false LogRemovedOrdersDebug = false [Execution.Risk] - Level = "Info" + Level = "Debug" LogMarginUpdate = true [Execution.Position] Level = "Info" diff --git a/vega_sim/vegahome/genesis.json b/vega_sim/vegahome/genesis.json index 66b174634..b18d5fd31 100644 --- a/vega_sim/vegahome/genesis.json +++ b/vega_sim/vegahome/genesis.json @@ -150,9 +150,9 @@ "market.liquidity.providersFeeCalculationTimeStep": "1s", "market.liquidity.sla.nonPerformanceBondPenaltyMax": "0.5", "market.liquidity.sla.nonPerformanceBondPenaltySlope": "2", - "market.liquidity.stakeToCcyVolume": "1.0", + "market.liquidity.stakeToCcyVolume": "0.001", "market.liquidity.targetstake.triggering.ratio": "0.25", - "market.liquidityProvision.minLpStakeQuantumMultiple": "5000", + "market.liquidityProvision.minLpStakeQuantumMultiple": "50", "market.liquidityProvision.shapes.maxSize": "100", "market.margin.scalingFactors": "{\"search_level\": 1.1, \"initial_margin\": 1.5, \"collateral_release\": 1.7}", "market.monitor.price.defaultParameters": "{\"triggers\": [{\"auction_extension\": 300, \"horizon\": 43200, \"probability\": \"0.9999999\"}] }", @@ -189,7 +189,7 @@ "spam.pow.numberOfPastBlocks": "100", "spam.pow.numberOfTxPerBlock": "1000", "spam.protection.delegation.min.tokens": "100000000000000000", - "spam.protection.max.batchSize": "30", + "spam.protection.max.batchSize": "200", "spam.protection.max.delegations": "360", "spam.protection.max.proposals": "300", "spam.protection.max.votes": "300",