diff --git a/docs/source/user_guide/config.rst b/docs/source/user_guide/config.rst index 1467a0f6..c2907b70 100644 --- a/docs/source/user_guide/config.rst +++ b/docs/source/user_guide/config.rst @@ -40,8 +40,8 @@ Json config "target": "SpotMarket-1", "triggerTime": 0, "priceChangeRate": -0.1, - "shockTimeLength": 2, - "enabled": true + "shockTimeLength": int (Optional, default: 1), + "enabled": bool (Optional, default: True) }, "Market": { "class": "string required", diff --git a/pams/agents/arbitrage_agent.py b/pams/agents/arbitrage_agent.py index 84216aa9..d2f074ef 100644 --- a/pams/agents/arbitrage_agent.py +++ b/pams/agents/arbitrage_agent.py @@ -45,7 +45,7 @@ def setup( # type: ignore settings: Dict[str, Any], accessible_markets_ids: List[int], *args, - **kwargs + **kwargs, ) -> None: """agent setup. Usually be called from simulator/runner automatically. @@ -96,7 +96,7 @@ def _submit_orders(self, market: Market) -> List[Union[Order, Cancel]]: market_price: float = index.get_market_price() if len(set(map(lambda x: x.outstanding_shares, spots))) > 1: - raise AssertionError( + raise NotImplementedError( "currently, the components must have the same outstanding shares" ) @@ -168,3 +168,15 @@ def submit_orders(self, markets: List[Market]) -> List[Union[Order, Cancel]]: for market in markets: orders.extend(self._submit_orders(market=market)) return orders + + def __repr__(self) -> str: + """string representation of FCN agent class. + + Returns: + str: string representation of this class. + """ + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} | id={self.agent_id}, rnd={self.prng}, " + f"order_volume={self.order_volume}, order_threshold_price={self.order_threshold_price}, " + f"order_time_length={self.order_time_length}>" + ) diff --git a/pams/events/base.py b/pams/events/base.py index 2149618d..4de2582b 100644 --- a/pams/events/base.py +++ b/pams/events/base.py @@ -6,6 +6,9 @@ from typing import List from typing import Optional from typing import Type +from typing import cast + +from pams.market import Market class EventHook: @@ -59,16 +62,29 @@ def __init__( raise ValueError( "specific_class and specific_instance are not supported except for market" ) - self.specific_class: Optional[Type] = ( - specific_class.__class__ if specific_class is not None else None - ) + else: + if hook_type == "market": + if specific_class is not None and not issubclass( + specific_class, Market + ): + raise ValueError("specific_class and hook_type is incompatible") + if specific_instance is not None and not isinstance( + specific_instance, Market + ): + raise ValueError( + "specific_instance and hook_type is incompatible" + ) + else: + raise AssertionError + specific_class = cast(Optional[Type], specific_class) + self.specific_class: Optional[Type] = specific_class self.specific_instance: Optional[object] = specific_instance def __repr__(self) -> str: return ( f"<{self.__class__.__module__}.{self.__class__.__name__} | hook_type={self.hook_type}, " - f"is_before={self.is_before}, time={self.time}, event={self.event}, specific_class={self.specific_class}, " - f"specific_instance={self.specific_instance}>" + f"is_before={self.is_before}, time={self.time}, specific_class={self.specific_class}, " + f"specific_instance={self.specific_instance}, event={self.event}>" ) diff --git a/pams/events/fundamental_price_shock.py b/pams/events/fundamental_price_shock.py index 8ebd47d1..a7ae14b4 100644 --- a/pams/events/fundamental_price_shock.py +++ b/pams/events/fundamental_price_shock.py @@ -54,6 +54,8 @@ def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None: # type: ign self.target_market_name = settings["target"] if "triggerTime" not in settings: raise ValueError("triggerTime is required for FundamentalPriceShock") + if not isinstance(settings["triggerTime"], int): + raise ValueError("triggerTime have to be int") self.trigger_time = self.session.session_start_time + settings["triggerTime"] if "priceChangeRate" not in settings: raise ValueError("priceChangeRate is required for FundamentalPriceShock") @@ -61,6 +63,8 @@ def setup(self, settings: Dict[str, Any], *args, **kwargs) -> None: # type: ign if "enabled" in settings: self.is_enabled = settings["enabled"] if "shockTimeLength" in settings: + if not isinstance(settings["shockTimeLength"], int): + raise ValueError("shockTimeLength have to be int") self.shock_time_length = settings["shockTimeLength"] self.target_market = self.simulator.name2market[self.target_market_name] diff --git a/pams/session.py b/pams/session.py index c0930fb4..ed5f77e8 100644 --- a/pams/session.py +++ b/pams/session.py @@ -59,7 +59,7 @@ def __repr__(self) -> str: f"iteration_steps={self.iteration_steps}, session_start_time={self.session_start_time}, " f"max_normal_orders={self.max_normal_orders}, max_high_frequency_orders={self.max_high_frequency_orders}, " f"with_order_placement={self.with_order_placement}, with_order_execution={self.with_order_execution}, " - f"high_frequency_submission_rate={self.high_frequency_submission_rate}, with_print={self.with_print}," + f"high_frequency_submission_rate={self.high_frequency_submission_rate}, with_print={self.with_print}, " f"logger={self.logger.__str__()}>" ) diff --git a/pams/version.py b/pams/version.py index 4ae81f3d..311f216e 100644 --- a/pams/version.py +++ b/pams/version.py @@ -1 +1 @@ -__version__ = "0.0.13" +__version__ = "0.0.14" diff --git a/pyproject.toml b/pyproject.toml index d6cac819..ef96b1fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pams" -version = "0.0.13" +version = "0.0.14" description = "PAMS: Platform for Artificial Market Simulations" authors = ["Masanori HIRANO "] license = "MIT" diff --git a/tests/pams/agents/test_arbitrage_agent.py b/tests/pams/agents/test_arbitrage_agent.py index f07e99c4..526e6f03 100644 --- a/tests/pams/agents/test_arbitrage_agent.py +++ b/tests/pams/agents/test_arbitrage_agent.py @@ -108,6 +108,32 @@ def test_setup(self) -> None: } agent.setup(settings=settings5, accessible_markets_ids=[0, 1, 2]) assert agent.order_time_length == 1 + agent = ArbitrageAgent( + agent_id=1, + prng=random.Random(42), + simulator=sim, + name="test_agent", + logger=logger, + ) + settings6 = {"assetVolume": 50, "cashAmount": 10000, "orderVolume": 1} + with pytest.raises(ValueError): + agent.setup(settings=settings6, accessible_markets_ids=[0, 1, 2]) + agent = ArbitrageAgent( + agent_id=1, + prng=random.Random(42), + simulator=sim, + name="test_agent", + logger=logger, + ) + settings5 = { + "assetVolume": 50, + "cashAmount": 10000, + "orderVolume": 1, + "orderThresholdPrice": 0.1, + "orderTimeLength": 10.1, + } + with pytest.raises(ValueError): + agent.setup(settings=settings5, accessible_markets_ids=[0, 1, 2]) @pytest.mark.parametrize("index_price", [350.0, 350.05, 349.95, 351, 349]) @pytest.mark.parametrize("order_volume", [1, 2]) @@ -205,3 +231,37 @@ def test_submit_orders(self, index_price: float, order_volume: int) -> None: assert not order1.is_buy assert not order2.is_buy assert index_order.is_buy + index_market._is_running = False + orders = agent.submit_orders(markets=[market1, market2, index_market]) + assert len(orders) == 0 + index_market._is_running = True + agent2 = ArbitrageAgent( + agent_id=1, prng=_prng, simulator=sim, name="test_agent", logger=logger + ) + agent2.setup(settings=settings1, accessible_markets_ids=[0, 1]) + orders = agent2.submit_orders(markets=[market1, market2, index_market]) + assert len(orders) == 0 + market1.outstanding_shares = 1000 + with pytest.raises(NotImplementedError): + agent.submit_orders(markets=[market1, market2, index_market]) + + def test__repr__(self) -> None: + sim = Simulator(prng=random.Random(4)) + logger = Logger() + _prng = random.Random(42) + agent = ArbitrageAgent( + agent_id=1, prng=_prng, simulator=sim, name="test_agent", logger=logger + ) + settings1 = { + "assetVolume": 50, + "cashAmount": 10000, + "orderVolume": 2, + "orderThresholdPrice": 0.1, + "orderTimeLength": 10, + } + agent.setup(settings=settings1, accessible_markets_ids=[0, 1, 2]) + assert ( + str(agent) + == f"" + ) diff --git a/tests/pams/events/__init__.py b/tests/pams/events/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/pams/events/test_base.py b/tests/pams/events/test_base.py new file mode 100644 index 00000000..4402f84e --- /dev/null +++ b/tests/pams/events/test_base.py @@ -0,0 +1,176 @@ +import random +from typing import List +from typing import Optional +from typing import Type + +import pytest + +from pams import Market +from pams import Session +from pams import Simulator +from pams.events import EventABC +from pams.events import EventHook +from pams.events import FundamentalPriceShock +from pams.logs import Logger +from tests.pams.agents.test_base import DummyAgent + + +class TestEventHook: + @pytest.mark.parametrize( + "hook_type", ["order", "cancel", "execution", "session", "market", "dummy"] + ) + @pytest.mark.parametrize("is_before", [True, False]) + @pytest.mark.parametrize("specific_class", [None, Market, DummyAgent]) + @pytest.mark.parametrize("specific_instance_name", [None, "market", "agent"]) + def test( + self, + hook_type: str, + is_before: bool, + specific_class: Optional[Type], + specific_instance_name: Optional[str], + ) -> None: + sim = Simulator(prng=random.Random(4)) + logger = Logger() + session = Session( + session_id=0, + prng=random.Random(42), + session_start_time=0, + simulator=sim, + name="session0", + logger=logger, + ) + session_setting = { + "sessionName": 0, + "iterationSteps": 500, + "withOrderPlacement": True, + "withOrderExecution": True, + "withPrint": True, + "maxNormalOrders": 1, + "events": ["FundamentalPriceShock"], + } + session.setup(settings=session_setting) + sim._add_session(session=session) + market = Market( + market_id=0, + prng=random.Random(42), + simulator=sim, + name="market1", + logger=logger, + ) + settings_market = { + "tickSize": 0.01, + "marketPrice": 300.0, + "outstandingShares": 2000, + } + market.setup(settings=settings_market) + agent = DummyAgent( + agent_id=1, + prng=random.Random(42), + simulator=sim, + name="test_agent", + logger=logger, + ) + settings_agent = {"assetVolume": 50, "cashAmount": 10000} + agent.setup(settings=settings_agent, accessible_markets_ids=[0]) + sim._add_agent(agent=agent) + + event = FundamentalPriceShock( + event_id=0, + prng=random.Random(42), + session=session, + simulator=sim, + name="event0", + ) + specific_instance: Optional[object] + if specific_instance_name is None: + specific_instance = None + elif specific_instance_name == "market": + specific_instance = market + elif specific_instance_name == "agent": + specific_instance = agent + else: + raise NotImplementedError + if ( + (hook_type == "execution" and is_before) + or hook_type == "dummy" + or (hook_type != "market" and specific_class is not None) + or (hook_type != "market" and specific_instance is not None) + or specific_class == DummyAgent + or specific_instance_name == "agent" + ): + with pytest.raises(ValueError): + EventHook( + event=event, + hook_type=hook_type, + is_before=is_before, + time=[1, 3], + specific_class=specific_class, + specific_instance=specific_instance, + ) + return + else: + event_hook = EventHook( + event=event, + hook_type=hook_type, + is_before=is_before, + time=[1, 3], + specific_class=specific_class, + specific_instance=specific_instance, + ) + sim._add_event(event_hook=event_hook) + assert event_hook.event == event + assert event_hook.hook_type == hook_type + assert event_hook.is_before == is_before + assert event_hook.time == [1, 3] + assert event_hook.specific_class == specific_class + assert event_hook.specific_instance == specific_instance + assert ( + str(event_hook) + == f"" + ) + + +class DummyEvent(EventABC): + def hook_registration(self) -> List[EventHook]: + return [] + + +class TestEventABC: + def test__init__(self) -> EventABC: + sim = Simulator(prng=random.Random(4)) + logger = Logger() + session = Session( + session_id=0, + prng=random.Random(42), + session_start_time=0, + simulator=sim, + name="session0", + logger=logger, + ) + session_setting = { + "sessionName": 0, + "iterationSteps": 500, + "withOrderPlacement": True, + "withOrderExecution": True, + "withPrint": True, + "maxNormalOrders": 1, + "events": ["FundamentalPriceShock"], + } + session.setup(settings=session_setting) + _prng = random.Random(42) + event = DummyEvent( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + assert event.event_id == 1 + assert event.prng == _prng + assert event.simulator == sim + assert event.name == "event" + assert event.session == session + assert ( + str(event) + == f"" + ) + event.setup(settings={}) + assert event.hook_registration() == [] + return event diff --git a/tests/pams/events/test_fundamental_price_shock.py b/tests/pams/events/test_fundamental_price_shock.py new file mode 100644 index 00000000..758be542 --- /dev/null +++ b/tests/pams/events/test_fundamental_price_shock.py @@ -0,0 +1,307 @@ +import random + +import pytest + +from pams import Market +from pams import Session +from pams import Simulator +from pams.events import EventABC +from pams.events import FundamentalPriceShock +from pams.logs import Logger +from tests.pams.events.test_base import TestEventABC + + +class TestFundamentalPriceShock(TestEventABC): + def test__init__(self) -> EventABC: + sim = Simulator(prng=random.Random(4)) + logger = Logger() + session = Session( + session_id=0, + prng=random.Random(42), + session_start_time=0, + simulator=sim, + name="session0", + logger=logger, + ) + session_setting = { + "sessionName": 0, + "iterationSteps": 500, + "withOrderPlacement": True, + "withOrderExecution": True, + "withPrint": True, + "maxNormalOrders": 1, + "events": ["FundamentalPriceShock"], + } + session.setup(settings=session_setting) + market = Market( + market_id=0, + prng=random.Random(42), + simulator=sim, + name="market1", + logger=logger, + ) + settings_market = { + "tickSize": 0.01, + "marketPrice": 300.0, + "outstandingShares": 2000, + } + market.setup(settings=settings_market) + sim._add_market(market=market) + _prng = random.Random(42) + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + assert event.is_enabled + assert event.shock_time_length == 1 + setting1 = { + "target": "market1", + "triggerTime": 0, + "priceChangeRate": -0.1, + "shockTimeLength": 2, + "enabled": False, + } + event.setup(settings=setting1) + assert event.target_market_name == "market1" + assert event.trigger_time == 0 + assert event.price_change_rate == -0.1 + assert not event.is_enabled + assert event.shock_time_length == 2 + assert event.target_market == market + + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + setting2 = { + "triggerDays": 100, + "target": "market1", + "triggerTime": 0, + "priceChangeRate": -0.1, + "shockTimeLength": 2, + "enabled": False, + } + with pytest.raises(ValueError): + event.setup(settings=setting2) + + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + setting3 = { + "triggerTime": 0, + "priceChangeRate": -0.1, + "shockTimeLength": 2, + "enabled": False, + } + with pytest.raises(ValueError): + event.setup(settings=setting3) + + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + setting4 = { + "target": "market1", + "priceChangeRate": -0.1, + "shockTimeLength": 2, + "enabled": False, + } + with pytest.raises(ValueError): + event.setup(settings=setting4) + + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + setting5 = { + "target": "market1", + "triggerTime": 0, + "shockTimeLength": 2, + "enabled": False, + } + with pytest.raises(ValueError): + event.setup(settings=setting5) + + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + setting6 = { + "target": "market1", + "triggerTime": 0, + "priceChangeRate": -0.1, + "shockTimeLength": 2, + } + event.setup(settings=setting6) + assert event.is_enabled + + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + setting7 = {"target": "market1", "triggerTime": 0, "priceChangeRate": -0.1} + event.setup(settings=setting7) + assert event.shock_time_length == 1 + + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + setting8 = { + "triggerDays": 100, + "target": "market1", + "triggerTime": 0.1, + "priceChangeRate": -0.1, + "shockTimeLength": 2, + "enabled": False, + } + with pytest.raises(ValueError): + event.setup(settings=setting8) + + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + setting9 = { + "triggerDays": 100, + "target": "market1", + "triggerTime": 0, + "priceChangeRate": -0.1, + "shockTimeLength": 2.1, + "enabled": False, + } + with pytest.raises(ValueError): + event.setup(settings=setting9) + return event + + def test_hook_registration(self) -> None: + sim = Simulator(prng=random.Random(4)) + logger = Logger() + session = Session( + session_id=0, + prng=random.Random(42), + session_start_time=0, + simulator=sim, + name="session0", + logger=logger, + ) + session_setting = { + "sessionName": 0, + "iterationSteps": 500, + "withOrderPlacement": True, + "withOrderExecution": True, + "withPrint": True, + "maxNormalOrders": 1, + "events": ["FundamentalPriceShock"], + } + session.setup(settings=session_setting) + market = Market( + market_id=0, + prng=random.Random(42), + simulator=sim, + name="market1", + logger=logger, + ) + settings_market = { + "tickSize": 0.01, + "marketPrice": 300.0, + "outstandingShares": 2000, + } + market.setup(settings=settings_market) + sim._add_market(market=market) + _prng = random.Random(42) + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + setting1 = { + "target": "market1", + "triggerTime": 0, + "priceChangeRate": -0.1, + "shockTimeLength": 2, + "enabled": True, + } + event.setup(settings=setting1) + event_hooks = event.hook_registration() + assert len(event_hooks) == 1 + event_hook = event_hooks[0] + assert event_hook.event == event + assert event_hook.hook_type == "market" + assert event_hook.is_before + assert event_hook.time == [0, 1] + assert event_hook.specific_instance == market + assert event_hook.specific_class is None + + setting2 = { + "target": "market1", + "triggerTime": 0, + "priceChangeRate": -0.1, + "shockTimeLength": 2, + "enabled": False, + } + event.setup(settings=setting2) + event_hooks = event.hook_registration() + assert len(event_hooks) == 0 + + def test_hooked_before_step_for_market(self) -> None: + sim = Simulator(prng=random.Random(4)) + logger = Logger() + session = Session( + session_id=0, + prng=random.Random(42), + session_start_time=0, + simulator=sim, + name="session0", + logger=logger, + ) + session_setting = { + "sessionName": 0, + "iterationSteps": 500, + "withOrderPlacement": True, + "withOrderExecution": True, + "withPrint": True, + "maxNormalOrders": 1, + "events": ["FundamentalPriceShock"], + } + session.setup(settings=session_setting) + market = Market( + market_id=0, + prng=random.Random(42), + simulator=sim, + name="market1", + logger=logger, + ) + settings_market = { + "tickSize": 0.01, + "marketPrice": 300.0, + "outstandingShares": 2000, + } + market.setup(settings=settings_market) + sim._add_market(market=market) + sim.fundamentals.add_market( + market_id=0, initial=300.0, drift=0.0, volatility=0.0, start_at=0 + ) + _prng = random.Random(42) + event = FundamentalPriceShock( + event_id=1, prng=_prng, session=session, simulator=sim, name="event" + ) + setting1 = { + "target": "market1", + "triggerTime": 0, + "priceChangeRate": -0.1, + "shockTimeLength": 2, + "enabled": True, + } + event.setup(settings=setting1) + with pytest.raises(AssertionError): + event.hooked_before_step_for_market(simulator=sim, market=market) + market._update_time(next_fundamental_price=300.0) + event.hooked_before_step_for_market(simulator=sim, market=market) + assert market.get_fundamental_price() == 300.0 * 0.9 + + market2 = Market( + market_id=1, + prng=random.Random(42), + simulator=sim, + name="market2", + logger=logger, + ) + settings_market = { + "tickSize": 0.01, + "marketPrice": 300.0, + "outstandingShares": 2000, + } + market.setup(settings=settings_market) + with pytest.raises(AssertionError): + event.hooked_before_step_for_market(simulator=sim, market=market2)