From f5d24d7cc4479ad7a976f649501760219ed2fdda Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 23 Jun 2023 17:16:18 +0900 Subject: [PATCH 01/32] add main and config --- samples/market_share/__init__.py | 0 samples/market_share/config-01.json | 60 ++++++++++++++++++++++++ samples/market_share/config-02.json | 72 +++++++++++++++++++++++++++++ samples/market_share/main.py | 30 ++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 samples/market_share/__init__.py create mode 100644 samples/market_share/config-01.json create mode 100644 samples/market_share/config-02.json create mode 100644 samples/market_share/main.py diff --git a/samples/market_share/__init__.py b/samples/market_share/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/samples/market_share/config-01.json b/samples/market_share/config-01.json new file mode 100644 index 00000000..a007f1d5 --- /dev/null +++ b/samples/market_share/config-01.json @@ -0,0 +1,60 @@ +{ + "simulation": { + "markets": ["Market-A", "Market-B"], + "agents": ["MarketShareFCNAgents"], + "sessions": [ + { "sessionName": 0, + "iterationSteps": 100, + "withOrderPlacement": true, + "withOrderExecution": false, + "withPrint": true + }, + { "sessionName": 1, + "iterationSteps": 2000, + "withOrderPlacement": true, + "withOrderExecution": true, + "withPrint": true, + "maxHifreqOrders": 1 + } + ] + }, + + "Market-A": { + "class": "Market", + "tickSize": 5.0, "MEMO": "0.05% of 10,000 YEN", + "marketPrice": 300.0, + "outstandingShares": 25000, + + "MEMO": "Required only here", + "tradeVolume": 90 + }, + + "Market-B": { + "class": "Market", + "tickSize": 1.0, "MEMO": "0.01% of 10,000 YEN", + "marketPrice": 300.0, + "outstandingShares": 25000, + + "MEMO": "Required only here", + "tradeVolume": 10 + }, + + "MarketShareFCNAgents": { + "class": "MarketShareFCNAgent", + "numAgents": 100, + + "MEMO": "Agent class", + "markets": ["Market-A", "Market-B"], + "assetVolume": 50, + "cashAmount": 10000, + + "MEMO": "FCNAgent class", + "fundamentalWeight": {"expon": [1.0]}, + "chartWeight": {"expon": [0.2]}, + "noiseWeight": {"expon": [1.0]}, + "noiseScale": 0.0001, + "timeWindowSize": [100, 200], + "orderMargin": [0.0, 0.1], + "marginType": "normal" + } +} diff --git a/samples/market_share/config-02.json b/samples/market_share/config-02.json new file mode 100644 index 00000000..adac03ef --- /dev/null +++ b/samples/market_share/config-02.json @@ -0,0 +1,72 @@ +{ + "simulation": { + "markets": ["Market-A", "Market-B"], + "agents": ["MarketShareFCNAgents", "MarketMakerAgent"], + "sessions": [ + { "sessionName": 0, + "iterationSteps": 100, + "withOrderPlacement": true, + "withOrderExecution": false, + "withPrint": true + }, + { "sessionName": 1, + "iterationSteps": 2000, + "withOrderPlacement": true, + "withOrderExecution": true, + "withPrint": true, + "maxHifreqOrders": 1 + } + ] + }, + + "Market-A": { + "class": "Market", + "tickSize": 0.00001, + "marketPrice": 300.0, + "outstandingShares": 25000, + + "MEMO": "Required only here", + "tradeVolume": 90 + }, + + "Market-B": { + "class": "Market", + "tickSize": 0.00001, + "marketPrice": 300.0, + "outstandingShares": 25000, + + "MEMO": "Required only here", + "tradeVolume": 10 + }, + + "MarketShareFCNAgents": { + "class": "MarketShareFCNAgent", + "numAgents": 100, + + "MEMO": "Agent class", + "markets": ["Market-A", "Market-B"], + "assetVolume": 50, + "cashAmount": 10000, + + "MEMO": "FCNAgent class", + "fundamentalWeight": {"expon": [1.0]}, + "chartWeight": {"expon": [0.0]}, + "noiseWeight": {"expon": [1.0]}, + "noiseScale": 0.001, + "timeWindowSize": [100, 200], + "orderMargin": [0.0, 0.1] + }, + + "MarketMakerAgent": { + "class": "MarketMakerAgent", + "numAgents": 1, + + "markets": ["Market-B"], + "assetVolume": 50, + "cashAmount": 10000, + + "targetMarket": "Market-B", + "netInterestSpread": 0.02, + "orderTimeLength": 2 + } +} diff --git a/samples/market_share/main.py b/samples/market_share/main.py new file mode 100644 index 00000000..bcb91110 --- /dev/null +++ b/samples/market_share/main.py @@ -0,0 +1,30 @@ +import argparse +import random +from typing import Optional + +from pams.logs.market_step_loggers import MarketStepPrintLogger +from pams.runners.sequential import SequentialRunner + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--config", "-c", type=str, required=True, help="config.json file" + ) + parser.add_argument( + "--seed", "-s", type=int, default=None, help="simulation random seed" + ) + args = parser.parse_args() + config: str = args.config + seed: Optional[int] = args.seed + + runner = SequentialRunner( + settings=config, + prng=random.Random(seed) if seed is not None else None, + logger=MarketStepPrintLogger(), + ) + runner.main() + + +if __name__ == "__main__": + main() From 20015abbb72fdff4a2118f3cda7b345abaeb9381 Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 23 Jun 2023 18:29:00 +0900 Subject: [PATCH 02/32] add base functions --- .../market_share/market_share_fcn_agent.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 samples/market_share/market_share_fcn_agent.py diff --git a/samples/market_share/market_share_fcn_agent.py b/samples/market_share/market_share_fcn_agent.py new file mode 100644 index 00000000..77419a5e --- /dev/null +++ b/samples/market_share/market_share_fcn_agent.py @@ -0,0 +1,66 @@ +import random +from typing import Any +from typing import Dict +from typing import List +from typing import Union + +from pams.agents.fcn_agent import FCNAgent +from pams.market import Market +from pams.order import Cancel, Order + + +class MarketShareFCNAgent(FCNAgent): + """Market Share FCN Agent class + + This class inherits from the :class:`pams.agents.FCNAgent` class. + """ + def setup( + self, settings: Dict[str, Any], + accessible_markets_ids: List[int] + ) -> None: + """agent setup. Usually be called from simulator/runner automatically. + + Args: + settings (Dict[str, Any]): agent configuration. + This must include the parameters "d" and "DarkPoolMarket". + This can include the parameters "fundamentalWeight", "chartWeight", + "noiseWeight", "noiseScale", "timeWindowSize", "orderMargin", "marginType", + and "meanReversionTime". + accessible_markets_ids (List[int]): list of market IDs. len(accessible_markets_ids) must be 2. + + Returns: + None + """ + if len(accessible_markets_ids) != 2: + raise ValueError("length of accessible_markets_ids is not 2.") + super().setup(settings=settings, accessible_markets_ids=accessible_markets_ids) + + def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: + """submit orders based on FCN-based calculation. + + Create limit order to litMarket first, and then change the order destination to DarkPoolMarket with probability 1-d. + .. seealso:: + - :func:`pams.agents.Agent.submit_orders` + """ + filter_markets: List[Market] = self.filter_markets(markets) + weights: List[float] = [] + for market in filter_markets: + weights.append(float(self.get_sum_trade_volume(market))) + if len(filter_markets) == 0: + raise RuntimeError("filter_markets in MarketShareFCNAgent is empty.") + return self.submit_orders_by_market(random.choice(filter_markets)) + + def filter_markets(self, markets: List[Market]) -> List[Market]: + a: List[Market] = [] + for market in markets: + if self.is_market_accessible(market_id=market.market_id): + a.append(market) + return a + + def get_sum_trade_volume(self, market: Market) -> int: + t: int = market.get_time() + time_window_size: int = min(t, self.time_window_size) + volume: int = 0 + for d in range(1, time_window_size + 1): + volume += market.get_executed_volume(t - d) + return volume From 2219228783aa5ee091ee87e4d981b2d85cec87cc Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 23 Jun 2023 18:30:19 +0900 Subject: [PATCH 03/32] fix docstring --- samples/market_share/market_share_fcn_agent.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/samples/market_share/market_share_fcn_agent.py b/samples/market_share/market_share_fcn_agent.py index 77419a5e..76b8458b 100644 --- a/samples/market_share/market_share_fcn_agent.py +++ b/samples/market_share/market_share_fcn_agent.py @@ -21,12 +21,8 @@ def setup( """agent setup. Usually be called from simulator/runner automatically. Args: - settings (Dict[str, Any]): agent configuration. - This must include the parameters "d" and "DarkPoolMarket". - This can include the parameters "fundamentalWeight", "chartWeight", - "noiseWeight", "noiseScale", "timeWindowSize", "orderMargin", "marginType", - and "meanReversionTime". - accessible_markets_ids (List[int]): list of market IDs. len(accessible_markets_ids) must be 2. + settings (Dict[str, Any]): agent configuration. See also :func:`pams.agents.FCNAgent.setup`. + accessible_markets_ids (List[int]): list of market IDs. Length of accessible_markets_ids must be 2. Returns: None From b1e209909d1a0eaaa250847ef7ac9ce59cf92060 Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 23 Jun 2023 18:30:51 +0900 Subject: [PATCH 04/32] format --- samples/market_share/market_share_fcn_agent.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/market_share/market_share_fcn_agent.py b/samples/market_share/market_share_fcn_agent.py index 76b8458b..1b3d78fc 100644 --- a/samples/market_share/market_share_fcn_agent.py +++ b/samples/market_share/market_share_fcn_agent.py @@ -6,7 +6,8 @@ from pams.agents.fcn_agent import FCNAgent from pams.market import Market -from pams.order import Cancel, Order +from pams.order import Cancel +from pams.order import Order class MarketShareFCNAgent(FCNAgent): @@ -14,9 +15,9 @@ class MarketShareFCNAgent(FCNAgent): This class inherits from the :class:`pams.agents.FCNAgent` class. """ + def setup( - self, settings: Dict[str, Any], - accessible_markets_ids: List[int] + self, settings: Dict[str, Any], accessible_markets_ids: List[int] ) -> None: """agent setup. Usually be called from simulator/runner automatically. From e4f3185e92efc549d1b7674860989fd17b912e72 Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 23 Jun 2023 18:39:37 +0900 Subject: [PATCH 05/32] for test --- pams/agents/__init__.py | 1 + {samples/market_share => pams/agents}/market_share_fcn_agent.py | 0 samples/market_share/{config-02.json => config-mm.json} | 0 samples/market_share/{config-01.json => config.json} | 0 4 files changed, 1 insertion(+) rename {samples/market_share => pams/agents}/market_share_fcn_agent.py (100%) rename samples/market_share/{config-02.json => config-mm.json} (100%) rename samples/market_share/{config-01.json => config.json} (100%) diff --git a/pams/agents/__init__.py b/pams/agents/__init__.py index 49c7dc44..b065e0fe 100644 --- a/pams/agents/__init__.py +++ b/pams/agents/__init__.py @@ -2,4 +2,5 @@ from .base import Agent from .fcn_agent import FCNAgent from .high_frequency_agent import HighFrequencyAgent +from .market_share_fcn_agent import MarketShareFCNAgent from .test_agent import TestAgent diff --git a/samples/market_share/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py similarity index 100% rename from samples/market_share/market_share_fcn_agent.py rename to pams/agents/market_share_fcn_agent.py diff --git a/samples/market_share/config-02.json b/samples/market_share/config-mm.json similarity index 100% rename from samples/market_share/config-02.json rename to samples/market_share/config-mm.json diff --git a/samples/market_share/config-01.json b/samples/market_share/config.json similarity index 100% rename from samples/market_share/config-01.json rename to samples/market_share/config.json From 3b9c174fd47ed37411d140d1a6d5bb1d4086a025 Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 23 Jun 2023 18:42:53 +0900 Subject: [PATCH 06/32] fix to solve error --- pams/agents/market_share_fcn_agent.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pams/agents/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py index 1b3d78fc..f8ddd10e 100644 --- a/pams/agents/market_share_fcn_agent.py +++ b/pams/agents/market_share_fcn_agent.py @@ -17,7 +17,11 @@ class MarketShareFCNAgent(FCNAgent): """ def setup( - self, settings: Dict[str, Any], accessible_markets_ids: List[int] + self, + settings: Dict[str, Any], + accessible_markets_ids: List[int], + *args: Any, + **kwargs: Any, ) -> None: """agent setup. Usually be called from simulator/runner automatically. From 399cf03bb18fed2f458104ceb93592caac776b10 Mon Sep 17 00:00:00 2001 From: bonochof Date: Sat, 24 Jun 2023 00:10:38 +0900 Subject: [PATCH 07/32] add market_maker_agent --- pams/agents/__init__.py | 1 + pams/agents/market_maker_agent.py | 80 +++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 pams/agents/market_maker_agent.py diff --git a/pams/agents/__init__.py b/pams/agents/__init__.py index b065e0fe..873aef42 100644 --- a/pams/agents/__init__.py +++ b/pams/agents/__init__.py @@ -3,4 +3,5 @@ from .fcn_agent import FCNAgent from .high_frequency_agent import HighFrequencyAgent from .market_share_fcn_agent import MarketShareFCNAgent +from .market_maker_agent import MarketMakerAgent from .test_agent import TestAgent diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py new file mode 100644 index 00000000..44613b02 --- /dev/null +++ b/pams/agents/market_maker_agent.py @@ -0,0 +1,80 @@ +import random +from typing import Any +from typing import Dict +from typing import List +from typing import Union + +from pams.agents.high_frequency_agent import HighFrequencyAgent +from pams.market import Market +from pams.order import Cancel +from pams.order import Order +from pams.utils.json_random import JsonRandom + + +class MarketMakerAgent(HighFrequencyAgent): + """Market Maker Agent class + + This class inherits from the :class:`pams.agents.Agent` class. + """ + + target_market: Market + net_interest_spread: float + order_time_length: int + + def setup( + self, + settings: Dict[str, Any], + accessible_markets_ids: List[int], + *args: Any, + **kwargs: Any, + ) -> None: + """agent setup. Usually be called from simulator/runner automatically. + + Args: + settings (Dict[str, Any]): agent configuration. + This must include the parameters "targetMarket" and "netInterestSpread". + This can include the parameters "orderTimeLength". + accessible_markets_ids (List[int]): list of market IDs. Length of accessible_markets_ids must be 2. + + Returns: + None + """ + if "targetMarket" not in settings: + raise ValueError("targetMarket is required for MarketMakerAgent.") + self.target_market = self.simulator.name2market[settings["targetMarket"]] + if "netInterestSpread" not in settings: + raise ValueError("netInterestSpread is required for MarketMakerAgent.") + json_random: JsonRandom = JsonRandom(prng=self.prng) + self.net_interest_spread = json_random.random(json_value=settings["netInterestSpread"]) + self.order_time_length = json_random.random(json_value=settings["orderTimeLength"] if "orderTimeLength" in settings else "2") + super().setup(settings=settings, accessible_markets_ids=accessible_markets_ids) + + def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: + """submit orders based on FCN-based calculation. + + Create limit order to litMarket first, and then change the order destination to DarkPoolMarket with probability 1-d. + .. seealso:: + - :func:`pams.agents.Agent.submit_orders` + """ + filter_markets: List[Market] = self.filter_markets(markets) + weights: List[float] = [] + for market in filter_markets: + weights.append(float(self.get_sum_trade_volume(market))) + if len(filter_markets) == 0: + raise RuntimeError("filter_markets in MarketShareFCNAgent is empty.") + return self.submit_orders_by_market(random.choice(filter_markets)) + + def filter_markets(self, markets: List[Market]) -> List[Market]: + a: List[Market] = [] + for market in markets: + if self.is_market_accessible(market_id=market.market_id): + a.append(market) + return a + + def get_sum_trade_volume(self, market: Market) -> int: + t: int = market.get_time() + time_window_size: int = min(t, self.time_window_size) + volume: int = 0 + for d in range(1, time_window_size + 1): + volume += market.get_executed_volume(t - d) + return volume From d4e251f72b55a02198eac54094ec8adb80e7d2ea Mon Sep 17 00:00:00 2001 From: bonochof Date: Sat, 24 Jun 2023 00:33:47 +0900 Subject: [PATCH 08/32] add MMAgent dynamics --- pams/agents/market_maker_agent.py | 63 ++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py index 44613b02..06f996ee 100644 --- a/pams/agents/market_maker_agent.py +++ b/pams/agents/market_maker_agent.py @@ -6,7 +6,7 @@ from pams.agents.high_frequency_agent import HighFrequencyAgent from pams.market import Market -from pams.order import Cancel +from pams.order import LIMIT_ORDER, Cancel from pams.order import Order from pams.utils.json_random import JsonRandom @@ -50,31 +50,48 @@ def setup( super().setup(settings=settings, accessible_markets_ids=accessible_markets_ids) def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: - """submit orders based on FCN-based calculation. + """submit orders. - Create limit order to litMarket first, and then change the order destination to DarkPoolMarket with probability 1-d. .. seealso:: - :func:`pams.agents.Agent.submit_orders` """ - filter_markets: List[Market] = self.filter_markets(markets) - weights: List[float] = [] - for market in filter_markets: - weights.append(float(self.get_sum_trade_volume(market))) - if len(filter_markets) == 0: - raise RuntimeError("filter_markets in MarketShareFCNAgent is empty.") - return self.submit_orders_by_market(random.choice(filter_markets)) + base_price: float = self.get_base_price(markets) + if base_price != float('inf'): + base_price = self.target_market.get_market_price() + orders: List[Order] = [] + price_margin: float = self.target_market.get_fundamental_price() * self.net_interest_spread * 0.5 + order_volume: int = 1 + orders.append( + Order( + agent_id=self.agent_id, + market_id=self.target_market.market_id, + is_buy=True, + kind=LIMIT_ORDER, + volume=order_volume, + price=base_price - price_margin, + ttl=self.order_time_length, + ) + ) + orders.append( + Order( + agent_id=self.agent_id, + market_id=self.target_market.market_id, + is_buy=False, + kind=LIMIT_ORDER, + volume=order_volume, + price=base_price + price_margin, + ttl=self.order_time_length, + ) + ) + return orders - def filter_markets(self, markets: List[Market]) -> List[Market]: - a: List[Market] = [] + def get_base_price(self, markets: List[Market]) -> float: + max_buy: float = -float('inf') for market in markets: - if self.is_market_accessible(market_id=market.market_id): - a.append(market) - return a - - def get_sum_trade_volume(self, market: Market) -> int: - t: int = market.get_time() - time_window_size: int = min(t, self.time_window_size) - volume: int = 0 - for d in range(1, time_window_size + 1): - volume += market.get_executed_volume(t - d) - return volume + if self.is_market_accessible(market.market_id) and market.get_best_buy_price() is float: + max_buy = max(max_buy, market.get_best_buy_price()) + min_sell: float = float('inf') + for market in markets: + if self.is_market_accessible(market.market_id) and market.get_best_sell_price() is float: + min_sell = min(min_sell, market.get_best_sell_price()) + return (max_buy + min_sell) / 2.0 From cca9808cb25a41fcfb72130456b7c2f7a7972109 Mon Sep 17 00:00:00 2001 From: bonochof Date: Sat, 24 Jun 2023 00:34:39 +0900 Subject: [PATCH 09/32] format --- pams/agents/__init__.py | 2 +- pams/agents/market_maker_agent.py | 33 ++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/pams/agents/__init__.py b/pams/agents/__init__.py index 873aef42..bf4c5743 100644 --- a/pams/agents/__init__.py +++ b/pams/agents/__init__.py @@ -2,6 +2,6 @@ from .base import Agent from .fcn_agent import FCNAgent from .high_frequency_agent import HighFrequencyAgent -from .market_share_fcn_agent import MarketShareFCNAgent from .market_maker_agent import MarketMakerAgent +from .market_share_fcn_agent import MarketShareFCNAgent from .test_agent import TestAgent diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py index 06f996ee..29d3fc66 100644 --- a/pams/agents/market_maker_agent.py +++ b/pams/agents/market_maker_agent.py @@ -6,7 +6,8 @@ from pams.agents.high_frequency_agent import HighFrequencyAgent from pams.market import Market -from pams.order import LIMIT_ORDER, Cancel +from pams.order import LIMIT_ORDER +from pams.order import Cancel from pams.order import Order from pams.utils.json_random import JsonRandom @@ -45,8 +46,14 @@ def setup( if "netInterestSpread" not in settings: raise ValueError("netInterestSpread is required for MarketMakerAgent.") json_random: JsonRandom = JsonRandom(prng=self.prng) - self.net_interest_spread = json_random.random(json_value=settings["netInterestSpread"]) - self.order_time_length = json_random.random(json_value=settings["orderTimeLength"] if "orderTimeLength" in settings else "2") + self.net_interest_spread = json_random.random( + json_value=settings["netInterestSpread"] + ) + self.order_time_length = json_random.random( + json_value=settings["orderTimeLength"] + if "orderTimeLength" in settings + else "2" + ) super().setup(settings=settings, accessible_markets_ids=accessible_markets_ids) def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: @@ -56,10 +63,12 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: - :func:`pams.agents.Agent.submit_orders` """ base_price: float = self.get_base_price(markets) - if base_price != float('inf'): + if base_price != float("inf"): base_price = self.target_market.get_market_price() orders: List[Order] = [] - price_margin: float = self.target_market.get_fundamental_price() * self.net_interest_spread * 0.5 + price_margin: float = ( + self.target_market.get_fundamental_price() * self.net_interest_spread * 0.5 + ) order_volume: int = 1 orders.append( Order( @@ -86,12 +95,18 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: return orders def get_base_price(self, markets: List[Market]) -> float: - max_buy: float = -float('inf') + max_buy: float = -float("inf") for market in markets: - if self.is_market_accessible(market.market_id) and market.get_best_buy_price() is float: + if ( + self.is_market_accessible(market.market_id) + and market.get_best_buy_price() is float + ): max_buy = max(max_buy, market.get_best_buy_price()) - min_sell: float = float('inf') + min_sell: float = float("inf") for market in markets: - if self.is_market_accessible(market.market_id) and market.get_best_sell_price() is float: + if ( + self.is_market_accessible(market.market_id) + and market.get_best_sell_price() is float + ): min_sell = min(min_sell, market.get_best_sell_price()) return (max_buy + min_sell) / 2.0 From 08df84eb78282480d4bc559497910a89e2f060d9 Mon Sep 17 00:00:00 2001 From: bonochof Date: Sat, 24 Jun 2023 00:37:14 +0900 Subject: [PATCH 10/32] fix warning --- pams/agents/market_maker_agent.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py index 29d3fc66..c9401c3c 100644 --- a/pams/agents/market_maker_agent.py +++ b/pams/agents/market_maker_agent.py @@ -1,4 +1,3 @@ -import random from typing import Any from typing import Dict from typing import List @@ -29,13 +28,13 @@ def setup( *args: Any, **kwargs: Any, ) -> None: - """agent setup. Usually be called from simulator/runner automatically. + """agent setup. Usually be called from simulator/runner automatically. Args: settings (Dict[str, Any]): agent configuration. This must include the parameters "targetMarket" and "netInterestSpread". This can include the parameters "orderTimeLength". - accessible_markets_ids (List[int]): list of market IDs. Length of accessible_markets_ids must be 2. + accessible_markets_ids (List[int]): list of market IDs. Returns: None From 06b1ef55ce836680de8368634920950c72c83890 Mon Sep 17 00:00:00 2001 From: bonochof Date: Sat, 24 Jun 2023 00:49:10 +0900 Subject: [PATCH 11/32] fix error --- pams/agents/market_maker_agent.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py index c9401c3c..424c4bd9 100644 --- a/pams/agents/market_maker_agent.py +++ b/pams/agents/market_maker_agent.py @@ -48,10 +48,10 @@ def setup( self.net_interest_spread = json_random.random( json_value=settings["netInterestSpread"] ) - self.order_time_length = json_random.random( - json_value=settings["orderTimeLength"] + self.order_time_length = ( + int(json_random.random(json_value=settings["orderTimeLength"])) if "orderTimeLength" in settings - else "2" + else 2 ) super().setup(settings=settings, accessible_markets_ids=accessible_markets_ids) @@ -61,10 +61,10 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: .. seealso:: - :func:`pams.agents.Agent.submit_orders` """ + orders: List[Union[Order, Cancel]] = [] base_price: float = self.get_base_price(markets) if base_price != float("inf"): base_price = self.target_market.get_market_price() - orders: List[Order] = [] price_margin: float = ( self.target_market.get_fundamental_price() * self.net_interest_spread * 0.5 ) @@ -98,14 +98,14 @@ def get_base_price(self, markets: List[Market]) -> float: for market in markets: if ( self.is_market_accessible(market.market_id) - and market.get_best_buy_price() is float + and market.get_best_buy_price() is not None ): - max_buy = max(max_buy, market.get_best_buy_price()) + max_buy = max(max_buy, market.get_best_buy_price()) # type: ignore # NOQA min_sell: float = float("inf") for market in markets: if ( self.is_market_accessible(market.market_id) - and market.get_best_sell_price() is float + and market.get_best_sell_price() is not None ): - min_sell = min(min_sell, market.get_best_sell_price()) + min_sell = min(min_sell, market.get_best_sell_price()) # type: ignore # NOQA return (max_buy + min_sell) / 2.0 From bf86f91e84be3c1e321731e63652c347db45e25c Mon Sep 17 00:00:00 2001 From: bonochof Date: Sat, 24 Jun 2023 00:49:51 +0900 Subject: [PATCH 12/32] fix docstring --- pams/agents/market_share_fcn_agent.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pams/agents/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py index f8ddd10e..46521e55 100644 --- a/pams/agents/market_share_fcn_agent.py +++ b/pams/agents/market_share_fcn_agent.py @@ -39,9 +39,8 @@ def setup( def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: """submit orders based on FCN-based calculation. - Create limit order to litMarket first, and then change the order destination to DarkPoolMarket with probability 1-d. .. seealso:: - - :func:`pams.agents.Agent.submit_orders` + - :func:`pams.agents.FCNAgent.submit_orders` """ filter_markets: List[Market] = self.filter_markets(markets) weights: List[float] = [] From 56c89f91073f91ffd5db6ac4e717f31467199170 Mon Sep 17 00:00:00 2001 From: bonochof Date: Sat, 24 Jun 2023 01:25:23 +0900 Subject: [PATCH 13/32] add MarketShare and MarketMaker agents to test --- tests/pams/utils/test_class_finder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/pams/utils/test_class_finder.py b/tests/pams/utils/test_class_finder.py index a5f1476e..e4847fb1 100644 --- a/tests/pams/utils/test_class_finder.py +++ b/tests/pams/utils/test_class_finder.py @@ -37,6 +37,8 @@ def test_find_class() -> None: find_class(name="HighFrequencyAgent") find_class(name="FCNAgent") find_class(name="ArbitrageAgent") + find_class(name="MarketShareFCNAgent") + find_class(name="MarketMakerAgent") find_class(name="JsonRandom") class DummyAgent(FCNAgent): From c000ea0621c27d8223b3378512cd3979b582008b Mon Sep 17 00:00:00 2001 From: bonochof Date: Sat, 24 Jun 2023 13:50:52 +0900 Subject: [PATCH 14/32] add roulette --- pams/agents/market_share_fcn_agent.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pams/agents/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py index 46521e55..af4fcaee 100644 --- a/pams/agents/market_share_fcn_agent.py +++ b/pams/agents/market_share_fcn_agent.py @@ -1,4 +1,3 @@ -import random from typing import Any from typing import Dict from typing import List @@ -43,12 +42,13 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: - :func:`pams.agents.FCNAgent.submit_orders` """ filter_markets: List[Market] = self.filter_markets(markets) + if len(filter_markets) == 0: + raise RuntimeError("filter_markets in MarketShareFCNAgent is empty.") weights: List[float] = [] for market in filter_markets: weights.append(float(self.get_sum_trade_volume(market))) - if len(filter_markets) == 0: - raise RuntimeError("filter_markets in MarketShareFCNAgent is empty.") - return self.submit_orders_by_market(random.choice(filter_markets)) + k: int = self.roulette(weights=weights) + return self.submit_orders_by_market(filter_markets[k]) def filter_markets(self, markets: List[Market]) -> List[Market]: a: List[Market] = [] @@ -64,3 +64,14 @@ def get_sum_trade_volume(self, market: Market) -> int: for d in range(1, time_window_size + 1): volume += market.get_executed_volume(t - d) return volume + + def roulette(self, weights: List[float]) -> int: + size: int = len(weights) + total: float = sum(weights) + d: float = total * self.get_prng().random() + w: float = 0.0 + for i in range(size): + w += weights[i] + if d <= w: + return i + return size - 1 From 2f89a18125628d32903307bba9abd70ff005990b Mon Sep 17 00:00:00 2001 From: bonochof Date: Sat, 24 Jun 2023 14:20:03 +0900 Subject: [PATCH 15/32] add docs --- pams/agents/market_maker_agent.py | 14 +++++++++--- pams/agents/market_share_fcn_agent.py | 32 +++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py index 424c4bd9..94c3bb89 100644 --- a/pams/agents/market_maker_agent.py +++ b/pams/agents/market_maker_agent.py @@ -62,7 +62,7 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: - :func:`pams.agents.Agent.submit_orders` """ orders: List[Union[Order, Cancel]] = [] - base_price: float = self.get_base_price(markets) + base_price: float = self.get_base_price(markets=markets) if base_price != float("inf"): base_price = self.target_market.get_market_price() price_margin: float = ( @@ -94,17 +94,25 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: return orders def get_base_price(self, markets: List[Market]) -> float: + """get base price of markets. + + Args: + markets (List[:class:`pams.Market`]): markets. + + Returns: + float: average of the max and min prices. + """ max_buy: float = -float("inf") for market in markets: if ( - self.is_market_accessible(market.market_id) + self.is_market_accessible(market_id=market.market_id) and market.get_best_buy_price() is not None ): max_buy = max(max_buy, market.get_best_buy_price()) # type: ignore # NOQA min_sell: float = float("inf") for market in markets: if ( - self.is_market_accessible(market.market_id) + self.is_market_accessible(market_id=market.market_id) and market.get_best_sell_price() is not None ): min_sell = min(min_sell, market.get_best_sell_price()) # type: ignore # NOQA diff --git a/pams/agents/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py index af4fcaee..53a1dfe1 100644 --- a/pams/agents/market_share_fcn_agent.py +++ b/pams/agents/market_share_fcn_agent.py @@ -22,7 +22,7 @@ def setup( *args: Any, **kwargs: Any, ) -> None: - """agent setup. Usually be called from simulator/runner automatically. + """agent setup. Usually be called from simulator/runner automatically. Args: settings (Dict[str, Any]): agent configuration. See also :func:`pams.agents.FCNAgent.setup`. @@ -41,16 +41,24 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: .. seealso:: - :func:`pams.agents.FCNAgent.submit_orders` """ - filter_markets: List[Market] = self.filter_markets(markets) + filter_markets: List[Market] = self.filter_markets(markets=markets) if len(filter_markets) == 0: raise RuntimeError("filter_markets in MarketShareFCNAgent is empty.") weights: List[float] = [] for market in filter_markets: - weights.append(float(self.get_sum_trade_volume(market))) + weights.append(float(self.get_sum_trade_volume(market=market))) k: int = self.roulette(weights=weights) - return self.submit_orders_by_market(filter_markets[k]) + return self.submit_orders_by_market(market=filter_markets[k]) def filter_markets(self, markets: List[Market]) -> List[Market]: + """filter markets by accessibility. + + Args: + markets (List[:class:`pams.Market`]): markets before filtering. + + Returns: + List[:class:`pams.Market`]: markets after filtering. + """ a: List[Market] = [] for market in markets: if self.is_market_accessible(market_id=market.market_id): @@ -58,6 +66,14 @@ def filter_markets(self, markets: List[Market]) -> List[Market]: return a def get_sum_trade_volume(self, market: Market) -> int: + """get sum of trade volume. + + Args: + market (:class:`pams.Market`): trading market. + + Returns: + int: total trade volume. + """ t: int = market.get_time() time_window_size: int = min(t, self.time_window_size) volume: int = 0 @@ -66,6 +82,14 @@ def get_sum_trade_volume(self, market: Market) -> int: return volume def roulette(self, weights: List[float]) -> int: + """weighted roulette. Randomly get the index of the list. + + Args: + weights (List[float]): rouleted list. + + Returns: + int: index of the list. + """ size: int = len(weights) total: float = sum(weights) d: float = total * self.get_prng().random() From 8565ddf6a16aa4936322ff3aab467dc700847037 Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 30 Jun 2023 18:18:59 +0900 Subject: [PATCH 16/32] add setting tradeVolume parameter --- pams/market.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pams/market.py b/pams/market.py index 323b865a..ba4ed26f 100644 --- a/pams/market.py +++ b/pams/market.py @@ -103,6 +103,10 @@ def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None: # type: ign self._market_prices = [float(settings["fundamentalPrice"])] else: raise ValueError("fundamentalPrice or marketPrice is required for market") + if "tradeVolume" in settings: + if not isinstance(settings["tradeVolume"], int): + raise ValueError("tradeVolume must be int") + self._executed_volumes = [int(settings["tradeVolume"])] def _extract_sequential_data_by_time( self, From fe7cf4546bfb9c3d10795763138bfc046a8d958a Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 30 Jun 2023 18:20:40 +0900 Subject: [PATCH 17/32] fix to use random.choices --- pams/agents/market_share_fcn_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pams/agents/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py index 53a1dfe1..0ef52a49 100644 --- a/pams/agents/market_share_fcn_agent.py +++ b/pams/agents/market_share_fcn_agent.py @@ -48,7 +48,7 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: for market in filter_markets: weights.append(float(self.get_sum_trade_volume(market=market))) k: int = self.roulette(weights=weights) - return self.submit_orders_by_market(market=filter_markets[k]) + return self.submit_orders_by_market(market=self.get_prng().choices(filter_markets, weights=weights)[0]) def filter_markets(self, markets: List[Market]) -> List[Market]: """filter markets by accessibility. From 3df41561108b0d0c83214b6095996371fedc08c5 Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 30 Jun 2023 18:21:52 +0900 Subject: [PATCH 18/32] fix cond for None --- pams/agents/market_maker_agent.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py index 94c3bb89..fe8d288c 100644 --- a/pams/agents/market_maker_agent.py +++ b/pams/agents/market_maker_agent.py @@ -63,7 +63,7 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: """ orders: List[Union[Order, Cancel]] = [] base_price: float = self.get_base_price(markets=markets) - if base_price != float("inf"): + if base_price is None: base_price = self.target_market.get_market_price() price_margin: float = ( self.target_market.get_fundamental_price() * self.net_interest_spread * 0.5 @@ -116,4 +116,6 @@ def get_base_price(self, markets: List[Market]) -> float: and market.get_best_sell_price() is not None ): min_sell = min(min_sell, market.get_best_sell_price()) # type: ignore # NOQA + if max_buy == -float("inf") or min_sell == float("inf"): + return None return (max_buy + min_sell) / 2.0 From 0b22c8abfad2cc729c4bfe37d0f0b91744eb8213 Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 30 Jun 2023 18:30:09 +0900 Subject: [PATCH 19/32] fix error --- pams/agents/market_maker_agent.py | 7 ++++--- pams/agents/market_share_fcn_agent.py | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py index fe8d288c..943e017d 100644 --- a/pams/agents/market_maker_agent.py +++ b/pams/agents/market_maker_agent.py @@ -1,6 +1,7 @@ from typing import Any from typing import Dict from typing import List +from typing import Optional from typing import Union from pams.agents.high_frequency_agent import HighFrequencyAgent @@ -62,7 +63,7 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: - :func:`pams.agents.Agent.submit_orders` """ orders: List[Union[Order, Cancel]] = [] - base_price: float = self.get_base_price(markets=markets) + base_price: Optional[float] = self.get_base_price(markets=markets) if base_price is None: base_price = self.target_market.get_market_price() price_margin: float = ( @@ -93,14 +94,14 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: ) return orders - def get_base_price(self, markets: List[Market]) -> float: + def get_base_price(self, markets: List[Market]) -> Optional[float]: """get base price of markets. Args: markets (List[:class:`pams.Market`]): markets. Returns: - float: average of the max and min prices. + float, Optional: average of the max and min prices. """ max_buy: float = -float("inf") for market in markets: diff --git a/pams/agents/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py index 0ef52a49..621dd504 100644 --- a/pams/agents/market_share_fcn_agent.py +++ b/pams/agents/market_share_fcn_agent.py @@ -47,8 +47,10 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: weights: List[float] = [] for market in filter_markets: weights.append(float(self.get_sum_trade_volume(market=market))) - k: int = self.roulette(weights=weights) - return self.submit_orders_by_market(market=self.get_prng().choices(filter_markets, weights=weights)[0]) + k: int = self.roulette(weights=weights) # NOQA + return self.submit_orders_by_market( + market=self.get_prng().choices(filter_markets, weights=weights)[0] + ) def filter_markets(self, markets: List[Market]) -> List[Market]: """filter markets by accessibility. From 5aee3ae03f7bc611cdcae5afbc7337d1cc50e99e Mon Sep 17 00:00:00 2001 From: bonochof Date: Fri, 30 Jun 2023 22:03:53 +0900 Subject: [PATCH 20/32] add documentation --- pams/market.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pams/market.py b/pams/market.py index ba4ed26f..828312c7 100644 --- a/pams/market.py +++ b/pams/market.py @@ -85,7 +85,7 @@ def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None: # type: ign Args: settings (Dict[str, Any]): market configuration. Usually, automatically set from json config of simulator. This must include the parameters "tickSize" and either "marketPrice" or "fundamentalPrice". - This can include the parameter "outstandingShares". + This can include the parameter "outstandingShares" and "tradeVolume". Returns: None From 7aa2faeea3cde2faa47fbc7c4dbb3f86423ca697 Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Thu, 17 Aug 2023 08:55:31 +0200 Subject: [PATCH 21/32] refactor market share FCN --- pams/agents/market_share_fcn_agent.py | 74 ++++----------------------- 1 file changed, 10 insertions(+), 64 deletions(-) diff --git a/pams/agents/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py index 621dd504..bfe5161a 100644 --- a/pams/agents/market_share_fcn_agent.py +++ b/pams/agents/market_share_fcn_agent.py @@ -1,5 +1,3 @@ -from typing import Any -from typing import Dict from typing import List from typing import Union @@ -12,61 +10,30 @@ class MarketShareFCNAgent(FCNAgent): """Market Share FCN Agent class + This agent submits orders based on market shares. This class inherits from the :class:`pams.agents.FCNAgent` class. """ - def setup( - self, - settings: Dict[str, Any], - accessible_markets_ids: List[int], - *args: Any, - **kwargs: Any, - ) -> None: - """agent setup. Usually be called from simulator/runner automatically. - - Args: - settings (Dict[str, Any]): agent configuration. See also :func:`pams.agents.FCNAgent.setup`. - accessible_markets_ids (List[int]): list of market IDs. Length of accessible_markets_ids must be 2. - - Returns: - None - """ - if len(accessible_markets_ids) != 2: - raise ValueError("length of accessible_markets_ids is not 2.") - super().setup(settings=settings, accessible_markets_ids=accessible_markets_ids) - def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: - """submit orders based on FCN-based calculation. + """submit orders based on FCN-based calculation and market shares. .. seealso:: - :func:`pams.agents.FCNAgent.submit_orders` """ - filter_markets: List[Market] = self.filter_markets(markets=markets) + filter_markets: List[Market] = [ + market + for market in markets + if self.is_market_accessible(market_id=market.market_id) + ] if len(filter_markets) == 0: raise RuntimeError("filter_markets in MarketShareFCNAgent is empty.") weights: List[float] = [] for market in filter_markets: - weights.append(float(self.get_sum_trade_volume(market=market))) - k: int = self.roulette(weights=weights) # NOQA - return self.submit_orders_by_market( + weights.append(float(self.get_sum_trade_volume(market=market)) + 1e-10) + return super().submit_orders_by_market( market=self.get_prng().choices(filter_markets, weights=weights)[0] ) - def filter_markets(self, markets: List[Market]) -> List[Market]: - """filter markets by accessibility. - - Args: - markets (List[:class:`pams.Market`]): markets before filtering. - - Returns: - List[:class:`pams.Market`]: markets after filtering. - """ - a: List[Market] = [] - for market in markets: - if self.is_market_accessible(market_id=market.market_id): - a.append(market) - return a - def get_sum_trade_volume(self, market: Market) -> int: """get sum of trade volume. @@ -78,26 +45,5 @@ def get_sum_trade_volume(self, market: Market) -> int: """ t: int = market.get_time() time_window_size: int = min(t, self.time_window_size) - volume: int = 0 - for d in range(1, time_window_size + 1): - volume += market.get_executed_volume(t - d) + volume: int = sum(market.get_executed_volumes(range(1, time_window_size + 1))) return volume - - def roulette(self, weights: List[float]) -> int: - """weighted roulette. Randomly get the index of the list. - - Args: - weights (List[float]): rouleted list. - - Returns: - int: index of the list. - """ - size: int = len(weights) - total: float = sum(weights) - d: float = total * self.get_prng().random() - w: float = 0.0 - for i in range(size): - w += weights[i] - if d <= w: - return i - return size - 1 From cd0b1818d5d0a6ebf6d4fdc9c57a82865a9a04f3 Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Thu, 17 Aug 2023 09:09:34 +0200 Subject: [PATCH 22/32] added test --- pams/agents/market_share_fcn_agent.py | 2 +- .../agents/test_market_share_fcn_agent.py | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/pams/agents/test_market_share_fcn_agent.py diff --git a/pams/agents/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py index bfe5161a..b821fbb7 100644 --- a/pams/agents/market_share_fcn_agent.py +++ b/pams/agents/market_share_fcn_agent.py @@ -26,7 +26,7 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: if self.is_market_accessible(market_id=market.market_id) ] if len(filter_markets) == 0: - raise RuntimeError("filter_markets in MarketShareFCNAgent is empty.") + raise AssertionError("filter_markets in MarketShareFCNAgent is empty.") weights: List[float] = [] for market in filter_markets: weights.append(float(self.get_sum_trade_volume(market=market)) + 1e-10) diff --git a/tests/pams/agents/test_market_share_fcn_agent.py b/tests/pams/agents/test_market_share_fcn_agent.py new file mode 100644 index 00000000..59233306 --- /dev/null +++ b/tests/pams/agents/test_market_share_fcn_agent.py @@ -0,0 +1,63 @@ +import random +from typing import List +from typing import cast + +import pytest + +from pams import Market +from pams import Order +from pams import Simulator +from pams.agents import MarketShareFCNAgent +from tests.pams.agents.test_base import TestAgent + + +class TestMarketShareFCNAgent(TestAgent): + @pytest.mark.parametrize("seed", [1, 42, 100, 200]) + def test_submit_orders(self, seed: int) -> None: + sim = Simulator(prng=random.Random(seed + 1)) + _prng = random.Random(seed) + agent = MarketShareFCNAgent( + agent_id=1, prng=_prng, simulator=sim, name="test_agent" + ) + settings1 = { + "assetVolume": 50, + "cashAmount": 10000, + "fundamentalWeight": 1.0, + "chartWeight": 2.0, + "noiseWeight": 3.0, + "noiseScale": 0.001, + "timeWindowSize": 100, + "orderMargin": 0.1, + "marginType": "fixed", + "meanReversionTime": 200, + } + agent.setup(settings=settings1, accessible_markets_ids=[0, 1, 2]) + market1 = Market( + market_id=0, prng=random.Random(seed - 1), simulator=sim, name="market1" + ) + market1._update_time(next_fundamental_price=300.0) + market2 = Market( + market_id=1, prng=random.Random(seed - 1), simulator=sim, name="market2" + ) + market2._update_time(next_fundamental_price=300.0) + market3 = Market( + market_id=2, prng=random.Random(seed - 1), simulator=sim, name="market3" + ) + market3._update_time(next_fundamental_price=300.0) + market_share_test = [0, 0, 0] + for _ in range(1000): + orders = cast( + List[Order], agent.submit_orders(markets=[market1, market2, market3]) + ) + order = orders[0] + market_share_test[order.market_id] += 1 + assert market_share_test[0] > 300 + assert market_share_test[1] > 300 + assert market_share_test[2] > 300 + + agent2 = MarketShareFCNAgent( + agent_id=1, prng=_prng, simulator=sim, name="test_agent" + ) + agent2.setup(settings=settings1, accessible_markets_ids=[]) + with pytest.raises(AssertionError): + agent2.submit_orders(markets=[market1, market2, market3]) From caa478e3dd2e9860d7dd319bd9d3dbb68ece21a0 Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Thu, 17 Aug 2023 09:24:12 +0200 Subject: [PATCH 23/32] tradeVolume was moved --- pams/market.py | 4 ---- samples/market_share/config-mm.json | 4 ++-- samples/market_share/config.json | 4 ++-- samples/market_share/main.py | 13 +++++++++++++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/pams/market.py b/pams/market.py index 828312c7..3b9d7936 100644 --- a/pams/market.py +++ b/pams/market.py @@ -103,10 +103,6 @@ def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None: # type: ign self._market_prices = [float(settings["fundamentalPrice"])] else: raise ValueError("fundamentalPrice or marketPrice is required for market") - if "tradeVolume" in settings: - if not isinstance(settings["tradeVolume"], int): - raise ValueError("tradeVolume must be int") - self._executed_volumes = [int(settings["tradeVolume"])] def _extract_sequential_data_by_time( self, diff --git a/samples/market_share/config-mm.json b/samples/market_share/config-mm.json index adac03ef..98529fd6 100644 --- a/samples/market_share/config-mm.json +++ b/samples/market_share/config-mm.json @@ -20,7 +20,7 @@ }, "Market-A": { - "class": "Market", + "class": "ExtendedMarket", "tickSize": 0.00001, "marketPrice": 300.0, "outstandingShares": 25000, @@ -30,7 +30,7 @@ }, "Market-B": { - "class": "Market", + "class": "ExtendedMarket", "tickSize": 0.00001, "marketPrice": 300.0, "outstandingShares": 25000, diff --git a/samples/market_share/config.json b/samples/market_share/config.json index a007f1d5..bccf51ca 100644 --- a/samples/market_share/config.json +++ b/samples/market_share/config.json @@ -20,7 +20,7 @@ }, "Market-A": { - "class": "Market", + "class": "ExtendedMarket", "tickSize": 5.0, "MEMO": "0.05% of 10,000 YEN", "marketPrice": 300.0, "outstandingShares": 25000, @@ -30,7 +30,7 @@ }, "Market-B": { - "class": "Market", + "class": "ExtendedMarket", "tickSize": 1.0, "MEMO": "0.01% of 10,000 YEN", "marketPrice": 300.0, "outstandingShares": 25000, diff --git a/samples/market_share/main.py b/samples/market_share/main.py index bcb91110..1cae383b 100644 --- a/samples/market_share/main.py +++ b/samples/market_share/main.py @@ -1,11 +1,23 @@ import argparse import random +from typing import Any +from typing import Dict from typing import Optional +from pams import Market from pams.logs.market_step_loggers import MarketStepPrintLogger from pams.runners.sequential import SequentialRunner +class ExtendedMarket(Market): + def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None: + super(ExtendedMarket, self).setup(settings=settings, *args, **kwargs) + if "tradeVolume" in settings: + if not isinstance(settings["tradeVolume"], int): + raise ValueError("tradeVolume must be int") + self._executed_volumes = [int(settings["tradeVolume"])] + + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument( @@ -23,6 +35,7 @@ def main() -> None: prng=random.Random(seed) if seed is not None else None, logger=MarketStepPrintLogger(), ) + runner.class_register(cls=ExtendedMarket) runner.main() From 12f6c1d927e56f921f2fe6c9129845b160340fb5 Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Thu, 17 Aug 2023 09:35:06 +0200 Subject: [PATCH 24/32] fix docment --- docs/source/reference/agents.rst | 3 ++- docs/source/user_guide/config.rst | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/source/reference/agents.rst b/docs/source/reference/agents.rst index 16c25364..29a63d62 100644 --- a/docs/source/reference/agents.rst +++ b/docs/source/reference/agents.rst @@ -9,4 +9,5 @@ Agents agents.Agent agents.HighFrequencyAgent agents.FCNAgent - agents.ArbitrageAgent \ No newline at end of file + agents.ArbitrageAgent + agents.MarketShareFCNAgent diff --git a/docs/source/user_guide/config.rst b/docs/source/user_guide/config.rst index 25cd92af..2f907ef0 100644 --- a/docs/source/user_guide/config.rst +++ b/docs/source/user_guide/config.rst @@ -94,6 +94,10 @@ Json config "timeWindowSize": JsonRandomFormat, "orderMargin": JsonRandomFormat, "marginType": "fixed" or "normal" (Optional; default fixed) + }, + "MarketShareFCNAgents": { + "class": "MarketShareFCNAgent", + "extends": "FCNAgent" }, "ArbitrageAgent": { "class": "ArbitrageAgent", From 8900a94818f455cd92362ecff553da024887acd6 Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Thu, 17 Aug 2023 10:18:57 +0200 Subject: [PATCH 25/32] refactoring --- pams/agents/market_maker_agent.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py index 943e017d..57bd47f2 100644 --- a/pams/agents/market_maker_agent.py +++ b/pams/agents/market_maker_agent.py @@ -16,6 +16,8 @@ class MarketMakerAgent(HighFrequencyAgent): """Market Maker Agent class This class inherits from the :class:`pams.agents.Agent` class. + + # TODO: model specifies """ target_market: Market @@ -40,6 +42,7 @@ def setup( Returns: None """ + super().setup(settings=settings, accessible_markets_ids=accessible_markets_ids) if "targetMarket" not in settings: raise ValueError("targetMarket is required for MarketMakerAgent.") self.target_market = self.simulator.name2market[settings["targetMarket"]] @@ -54,7 +57,6 @@ def setup( if "orderTimeLength" in settings else 2 ) - super().setup(settings=settings, accessible_markets_ids=accessible_markets_ids) def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: """submit orders. @@ -105,18 +107,20 @@ def get_base_price(self, markets: List[Market]) -> Optional[float]: """ max_buy: float = -float("inf") for market in markets: + best_buy_price: Optional[float] = market.get_best_buy_price() if ( self.is_market_accessible(market_id=market.market_id) - and market.get_best_buy_price() is not None + and best_buy_price is not None ): - max_buy = max(max_buy, market.get_best_buy_price()) # type: ignore # NOQA + max_buy = max(max_buy, best_buy_price) min_sell: float = float("inf") for market in markets: + best_sell_price: Optional[float] = market.get_best_sell_price() if ( self.is_market_accessible(market_id=market.market_id) - and market.get_best_sell_price() is not None + and best_sell_price is not None ): - min_sell = min(min_sell, market.get_best_sell_price()) # type: ignore # NOQA + min_sell = min(min_sell, best_sell_price) if max_buy == -float("inf") or min_sell == float("inf"): return None return (max_buy + min_sell) / 2.0 From 259c16875f84659f802adb0e8e432cd602a4a5aa Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Thu, 17 Aug 2023 10:21:37 +0200 Subject: [PATCH 26/32] added input check --- pams/agents/market_maker_agent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py index 57bd47f2..b6dceaa6 100644 --- a/pams/agents/market_maker_agent.py +++ b/pams/agents/market_maker_agent.py @@ -45,6 +45,8 @@ def setup( super().setup(settings=settings, accessible_markets_ids=accessible_markets_ids) if "targetMarket" not in settings: raise ValueError("targetMarket is required for MarketMakerAgent.") + if not isinstance(settings["targetMarket"], str): + raise ValueError("targetMarket must be string") self.target_market = self.simulator.name2market[settings["targetMarket"]] if "netInterestSpread" not in settings: raise ValueError("netInterestSpread is required for MarketMakerAgent.") From 9c01b39e065882dd2716700b93dbbd45fc12d86d Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Thu, 17 Aug 2023 23:45:14 +0900 Subject: [PATCH 27/32] WIP --- tests/pams/agents/test_market_maker_agent.py | 99 ++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 tests/pams/agents/test_market_maker_agent.py diff --git a/tests/pams/agents/test_market_maker_agent.py b/tests/pams/agents/test_market_maker_agent.py new file mode 100644 index 00000000..21d87869 --- /dev/null +++ b/tests/pams/agents/test_market_maker_agent.py @@ -0,0 +1,99 @@ +import random + +import pytest + +from pams import Market +from pams import Simulator +from pams.agents import MarketMakerAgent +from tests.pams.agents.test_base import TestAgent + + +class TestMarketMakerAgent(TestAgent): + def test_setup(self) -> None: + sim = Simulator(prng=random.Random(4)) + market1 = Market( + market_id=0, prng=random.Random(42), simulator=sim, name="market1" + ) + settings_market = { + "tickSize": 0.01, + "marketPrice": 300.0, + "outstandingShares": 2000, + } + market1.setup(settings=settings_market) + market1._update_time(next_fundamental_price=300.0) + sim._add_market(market=market1) + agent = MarketMakerAgent( + agent_id=1, prng=random.Random(42), simulator=sim, name="test_agent" + ) + settings1 = { + "assetVolume": 50, + "cashAmount": 10000, + "targetMarket": "market1", + "netInterestSpread": 0.05, + "orderTimeLength": 3, + } + agent.setup(settings=settings1, accessible_markets_ids=[0]) + agent = MarketMakerAgent( + agent_id=1, prng=random.Random(42), simulator=sim, name="test_agent" + ) + settings2 = { + "assetVolume": 50, + "cashAmount": 10000, + "netInterestSpread": 0.05, + "orderTimeLength": 3, + } + with pytest.raises(ValueError): + agent.setup(settings=settings2, accessible_markets_ids=[0]) + agent = MarketMakerAgent( + agent_id=1, prng=random.Random(42), simulator=sim, name="test_agent" + ) + settings3 = { + "assetVolume": 50, + "cashAmount": 10000, + "targetMarket": 1, + "netInterestSpread": 0.05, + "orderTimeLength": 3, + } + with pytest.raises(ValueError): + agent.setup(settings=settings3, accessible_markets_ids=[0]) + agent = MarketMakerAgent( + agent_id=1, prng=random.Random(42), simulator=sim, name="test_agent" + ) + settings4 = { + "assetVolume": 50, + "cashAmount": 10000, + "targetMarket": "market1", + "orderTimeLength": 3, + } + with pytest.raises(ValueError): + agent.setup(settings=settings4, accessible_markets_ids=[0]) + + def test_submit_orders(self) -> None: + sim = Simulator(prng=random.Random(4)) + market1 = Market( + market_id=0, prng=random.Random(42), simulator=sim, name="market1" + ) + settings_market = { + "tickSize": 0.01, + "marketPrice": 300.0, + "outstandingShares": 2000, + } + market1.setup(settings=settings_market) + market1._update_time(next_fundamental_price=300.0) + sim._add_market(market=market1) + agent = MarketMakerAgent( + agent_id=1, prng=random.Random(42), simulator=sim, name="test_agent" + ) + settings1 = { + "assetVolume": 50, + "cashAmount": 10000, + "targetMarket": "market1", + "netInterestSpread": 0.05, + "orderTimeLength": 3, + } + agent.setup(settings=settings1, accessible_markets_ids=[0]) + orders = agent.submit_orders(markets=[market1]) + order_buy = list(filter(lambda x: x.is_buy, orders))[0] + order_sell = list(filter(lambda x: not x.is_buy, orders))[0] + assert order_buy.price == 300.0 * 0.975 + assert order_sell.price == 300.0 * 1.025 From 1ea08b1cd62f70186b369c40e9c27e0e1f4b465e Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Fri, 18 Aug 2023 00:53:24 +0900 Subject: [PATCH 28/32] update type --- samples/market_share/main.py | 4 ++-- tests/pams/agents/test_market_maker_agent.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/samples/market_share/main.py b/samples/market_share/main.py index 1cae383b..fd5ba818 100644 --- a/samples/market_share/main.py +++ b/samples/market_share/main.py @@ -10,8 +10,8 @@ class ExtendedMarket(Market): - def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None: - super(ExtendedMarket, self).setup(settings=settings, *args, **kwargs) + def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None: # type: ignore # NOQA + super(ExtendedMarket, self).setup(settings, *args, **kwargs) if "tradeVolume" in settings: if not isinstance(settings["tradeVolume"], int): raise ValueError("tradeVolume must be int") diff --git a/tests/pams/agents/test_market_maker_agent.py b/tests/pams/agents/test_market_maker_agent.py index 21d87869..987cce2f 100644 --- a/tests/pams/agents/test_market_maker_agent.py +++ b/tests/pams/agents/test_market_maker_agent.py @@ -1,8 +1,11 @@ import random +from typing import List +from typing import cast import pytest from pams import Market +from pams import Order from pams import Simulator from pams.agents import MarketMakerAgent from tests.pams.agents.test_base import TestAgent @@ -92,7 +95,9 @@ def test_submit_orders(self) -> None: "orderTimeLength": 3, } agent.setup(settings=settings1, accessible_markets_ids=[0]) - orders = agent.submit_orders(markets=[market1]) + orders = cast(List[Order], agent.submit_orders(markets=[market1])) + for order in orders: + assert isinstance(order, Order) order_buy = list(filter(lambda x: x.is_buy, orders))[0] order_sell = list(filter(lambda x: not x.is_buy, orders))[0] assert order_buy.price == 300.0 * 0.975 From 414f7ce0bfea221783621433b761daf0a635427b Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Fri, 18 Aug 2023 01:21:22 +0900 Subject: [PATCH 29/32] added docs --- pams/agents/market_maker_agent.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pams/agents/market_maker_agent.py b/pams/agents/market_maker_agent.py index b6dceaa6..11b7fe39 100644 --- a/pams/agents/market_maker_agent.py +++ b/pams/agents/market_maker_agent.py @@ -17,8 +17,11 @@ class MarketMakerAgent(HighFrequencyAgent): This class inherits from the :class:`pams.agents.Agent` class. - # TODO: model specifies - """ + This agent submits orders to the target market at the following price: + :math:`\left\{\max_i(P^b_i) + \min_i(P^a_i) \pm P_f \times \theta\right\} / 2` + where :math:`P^b_i` and :math:`P^a_i` are the best bid and ask prices of the :math:`i`-th target market, + and :math:`P_f` is the fundamental price of the target market. + """ # NOQA target_market: Market net_interest_spread: float From df6ba52804efc70b4d728e36be60838c79dcac2a Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Fri, 18 Aug 2023 01:45:21 +0900 Subject: [PATCH 30/32] added test adn doc --- docs/source/user_guide/config.rst | 7 ++++ tests/pams/agents/test_market_maker_agent.py | 39 ++++++++++++++++++++ tests/samples/test_market_share.py | 19 ++++++++++ 3 files changed, 65 insertions(+) create mode 100644 tests/samples/test_market_share.py diff --git a/docs/source/user_guide/config.rst b/docs/source/user_guide/config.rst index 2f907ef0..6c0b4e6b 100644 --- a/docs/source/user_guide/config.rst +++ b/docs/source/user_guide/config.rst @@ -106,4 +106,11 @@ Json config "orderThresholdPrice": float, "orderTimeLength": int (Optional, default 1), }, + "MarketMakerAgent": { + "class": "MarketMakerAgent", + "extends": "Agents", + "targetMarket": string required, + "netInterestSpread": float required, + "orderTimeLength": int optional; default 2, + } } diff --git a/tests/pams/agents/test_market_maker_agent.py b/tests/pams/agents/test_market_maker_agent.py index 987cce2f..d1594ed3 100644 --- a/tests/pams/agents/test_market_maker_agent.py +++ b/tests/pams/agents/test_market_maker_agent.py @@ -4,6 +4,7 @@ import pytest +from pams import LIMIT_ORDER from pams import Market from pams import Order from pams import Simulator @@ -102,3 +103,41 @@ def test_submit_orders(self) -> None: order_sell = list(filter(lambda x: not x.is_buy, orders))[0] assert order_buy.price == 300.0 * 0.975 assert order_sell.price == 300.0 * 1.025 + order = Order( + agent_id=0, + market_id=0, + is_buy=True, + kind=LIMIT_ORDER, + volume=1, + placed_at=None, + price=290.0, + order_id=None, + ttl=None, + ) + market1._add_order(order=order) + orders = cast(List[Order], agent.submit_orders(markets=[market1])) + for order in orders: + assert isinstance(order, Order) + order_buy = list(filter(lambda x: x.is_buy, orders))[0] + order_sell = list(filter(lambda x: not x.is_buy, orders))[0] + assert order_buy.price == 300.0 * 0.975 + assert order_sell.price == 300.0 * 1.025 + order = Order( + agent_id=0, + market_id=0, + is_buy=False, + kind=LIMIT_ORDER, + volume=1, + placed_at=None, + price=320.0, + order_id=None, + ttl=None, + ) + market1._add_order(order=order) + orders = cast(List[Order], agent.submit_orders(markets=[market1])) + for order in orders: + assert isinstance(order, Order) + order_buy = list(filter(lambda x: x.is_buy, orders))[0] + order_sell = list(filter(lambda x: not x.is_buy, orders))[0] + assert order_buy.price == 305.0 - 300.0 * 0.025 + assert order_sell.price == 305.0 + 300.0 * 0.025 diff --git a/tests/samples/test_market_share.py b/tests/samples/test_market_share.py new file mode 100644 index 00000000..f39740fa --- /dev/null +++ b/tests/samples/test_market_share.py @@ -0,0 +1,19 @@ +import os.path +from unittest import mock + +from samples.market_share.main import main + + +def test_market_share() -> None: + root_dir: str = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + + sample_dir = os.path.join(root_dir, "samples", "market_share") + with mock.patch( + "sys.argv", ["main.py", "--config", f"{sample_dir}/config.json", "--seed", "1"] + ): + main() + with mock.patch( + "sys.argv", + ["main.py", "--config", f"{sample_dir}/config-mm.json", "--seed", "1"], + ): + main() From 06d4834e56283898035e61de5e4af4094dfec7c8 Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Fri, 18 Aug 2023 02:02:56 +0900 Subject: [PATCH 31/32] added test --- tests/samples/test_market_share.py | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/samples/test_market_share.py b/tests/samples/test_market_share.py index f39740fa..cf9f34d7 100644 --- a/tests/samples/test_market_share.py +++ b/tests/samples/test_market_share.py @@ -1,7 +1,46 @@ import os.path +import random from unittest import mock +import pytest + +from pams import Simulator +from pams.logs import Logger +from samples.market_share.main import ExtendedMarket from samples.market_share.main import main +from tests.pams.test_market import TestMarket + + +class TestExtendedMarket(TestMarket): + base_class = ExtendedMarket + + def test_setup(self) -> None: + m = self.base_class( + market_id=0, + prng=random.Random(42), + logger=Logger(), + simulator=Simulator(prng=random.Random(42)), + name="test", + ) + m.setup( + settings={ + "tickSize": 0.001, + "outstandingShares": 100, + "marketPrice": 300.0, + "fundamentalPrice": 500.0, + "tradeVolume": 100, + } + ) + with pytest.raises(ValueError): + m.setup( + settings={ + "tickSize": 0.001, + "outstandingShares": 100, + "marketPrice": 300.0, + "fundamentalPrice": 500.0, + "tradeVolume": "error", + } + ) def test_market_share() -> None: From 3e47f3057576c4f4e6577fd044987eeeb13e5026 Mon Sep 17 00:00:00 2001 From: Masanori HIRANO Date: Wed, 30 Aug 2023 00:40:23 +0900 Subject: [PATCH 32/32] bug fix in calc sum executed volumes --- examples/fat_finger.ipynb | 2 +- pams/agents/market_share_fcn_agent.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/fat_finger.ipynb b/examples/fat_finger.ipynb index 890acb4e..f1b0b5c5 100644 --- a/examples/fat_finger.ipynb +++ b/examples/fat_finger.ipynb @@ -188,4 +188,4 @@ }, "nbformat": 4, "nbformat_minor": 1 -} \ No newline at end of file +} diff --git a/pams/agents/market_share_fcn_agent.py b/pams/agents/market_share_fcn_agent.py index b821fbb7..a946d911 100644 --- a/pams/agents/market_share_fcn_agent.py +++ b/pams/agents/market_share_fcn_agent.py @@ -44,6 +44,6 @@ def get_sum_trade_volume(self, market: Market) -> int: int: total trade volume. """ t: int = market.get_time() - time_window_size: int = min(t, self.time_window_size) - volume: int = sum(market.get_executed_volumes(range(1, time_window_size + 1))) + time_window_size: int = min(t + 1, self.time_window_size) + volume: int = sum(market.get_executed_volumes(range(0, time_window_size))) return volume