diff --git a/pams/logs/__init__.py b/pams/logs/__init__.py index 2f4826b2..08c62832 100644 --- a/pams/logs/__init__.py +++ b/pams/logs/__init__.py @@ -1,5 +1,6 @@ from .base import CancelLog from .base import ExecutionLog +from .base import ExpirationLog from .base import Log from .base import Logger from .base import MarketStepBeginLog diff --git a/pams/logs/base.py b/pams/logs/base.py index 134dbbdd..6b722330 100644 --- a/pams/logs/base.py +++ b/pams/logs/base.py @@ -120,6 +120,51 @@ def __init__( # TODO: Type validation +class ExpirationLog(Log): + """Expiration type log class. + + This log is usually generated when an order is expired on markets. + """ + + def __init__( + self, + order_id: Optional[int], + market_id: int, + time: int, + order_time: Optional[int], + agent_id: int, + is_buy: bool, + kind: OrderKind, + volume: int, + price: Optional[float] = None, + ttl: Optional[int] = None, + ): + """initialize + + Args: + order_id (int): order ID. + market_id (int): market ID. + time (int): time. + order_time (int): time to order. + agent_id (int): agent ID. + is_buy (bool): whether it is a buy order or not. + kind (:class:`pams.order.OrderKind`): kind of order. + volume (int): order volume. + price (float, Optional): order price. + ttl (int, Optional): time to order expiration. + """ + self.order_id: Optional[int] = order_id + self.market_id: int = market_id + self.time: int = time + self.order_time: Optional[int] = order_time + self.agent_id: int = agent_id + self.is_buy: bool = is_buy + self.kind: OrderKind = kind + self.price: Optional[float] = price + self.volume: int = volume + self.ttl: Optional[int] = ttl + + class ExecutionLog(Log): """Execution type log class. @@ -352,6 +397,8 @@ def process(self, logs: List["Log"]) -> None: self.process_order_log(log=log) elif isinstance(log, CancelLog): self.process_cancel_log(log=log) + elif isinstance(log, ExpirationLog): + self.process_expiration_log(log=log) elif isinstance(log, ExecutionLog): self.process_execution_log(log=log) elif isinstance(log, SimulationBeginLog): @@ -390,6 +437,17 @@ def process_cancel_log(self, log: "CancelLog") -> None: """ pass + def process_expiration_log(self, log: "ExpirationLog") -> None: + """process expiration log. Called from :func:`process`. + + Args: + log (:class:`pams.logs.ExpirationLog`]): expiration log + + Returns: + None + """ + pass + def process_execution_log(self, log: "ExecutionLog") -> None: """process execution log. Called from :func:`process`. diff --git a/pams/market.py b/pams/market.py index 3b9d7936..57a7f509 100644 --- a/pams/market.py +++ b/pams/market.py @@ -14,6 +14,7 @@ from .logs.base import CancelLog from .logs.base import ExecutionLog +from .logs.base import ExpirationLog from .logs.base import Log from .logs.base import Logger from .logs.base import OrderLog @@ -551,8 +552,14 @@ def _set_time(self, time: int, next_fundamental_price: float) -> None: None """ self.time = time - self.buy_order_book._set_time(time) - self.sell_order_book._set_time(time) + logs: List[ExpirationLog] = self.buy_order_book._set_time(time) + if self.logger is not None: + for log in logs: + log.read_and_write(logger=self.logger) + logs_: List[ExpirationLog] = self.sell_order_book._set_time(time) + if self.logger is not None: + for log_ in logs_: + log_.read_and_write(logger=self.logger) self._fill_until(time=time) self._fundamental_prices[self.time] = next_fundamental_price if self.time > 0: @@ -599,8 +606,14 @@ def _update_time(self, next_fundamental_price: float) -> None: None """ self.time += 1 - self.buy_order_book._set_time(self.time) - self.sell_order_book._set_time(self.time) + logs: List[ExpirationLog] = self.buy_order_book._set_time(self.time) + if self.logger is not None: + for log in logs: + log.read_and_write(logger=self.logger) + logs_: List[ExpirationLog] = self.sell_order_book._set_time(self.time) + if self.logger is not None: + for log_ in logs_: + log_.read_and_write(logger=self.logger) self._fill_until(time=self.time) self._fundamental_prices[self.time] = next_fundamental_price if self.time > 0: diff --git a/pams/order_book.py b/pams/order_book.py index 8f3413d5..341ab56e 100644 --- a/pams/order_book.py +++ b/pams/order_book.py @@ -3,6 +3,7 @@ from typing import List from typing import Optional +from .logs.base import ExpirationLog from .order import Cancel from .order import Order @@ -122,8 +123,12 @@ def change_order_volume(self, order: Order, delta: int) -> None: if order.volume < 0: raise AssertionError - def _check_expired_orders(self) -> None: - """check and delete expired orders. (Internal Method)""" + def _check_expired_orders(self) -> List[ExpirationLog]: + """check and delete expired orders. (Internal Method) + + Returns: + List[ExpirationLog]: the list of expiration logs. + """ delete_orders: List[Order] = sum( [value for key, value in self.expire_time_list.items() if key < self.time], [], @@ -131,26 +136,42 @@ def _check_expired_orders(self) -> None: delete_keys: List[int] = [ key for key, value in self.expire_time_list.items() if key < self.time ] + logs: List[ExpirationLog] = [] if len(delete_orders) == 0: - return + return logs # TODO: think better sorting in the following 3 lines for delete_order in delete_orders: + log: ExpirationLog = ExpirationLog( + order_id=delete_order.order_id, + market_id=delete_order.market_id, + time=self.time, + order_time=delete_order.placed_at, + agent_id=delete_order.agent_id, + is_buy=delete_order.is_buy, + kind=delete_order.kind, + volume=delete_order.volume, + price=delete_order.price, + ttl=delete_order.ttl, + ) + logs.append(log) self.priority_queue.remove(delete_order) heapq.heapify(self.priority_queue) for key in delete_keys: self.expire_time_list.pop(key) + return logs - def _set_time(self, time: int) -> None: + def _set_time(self, time: int) -> List[ExpirationLog]: """set time step. (Usually, it is called from market.) Args: time (int): time step. Returns: - None + List[ExpirationLog]: the list of expiration logs. """ self.time = time - self._check_expired_orders() + logs: List[ExpirationLog] = self._check_expired_orders() + return logs def _update_time(self) -> None: """update time. (Usually, it is called from market.) diff --git a/tests/pams/logs/test_base.py b/tests/pams/logs/test_base.py index dd3f0fd7..7410a8b9 100644 --- a/tests/pams/logs/test_base.py +++ b/tests/pams/logs/test_base.py @@ -10,6 +10,7 @@ from pams import Simulator from pams.logs import CancelLog from pams.logs import ExecutionLog +from pams.logs import ExpirationLog from pams.logs import Log from pams.logs import Logger from pams.logs import MarketStepBeginLog @@ -140,6 +141,53 @@ def test__init__(self) -> None: assert log.ttl is None +class TestExpirationLog: + def test__init__(self) -> None: + log = ExpirationLog( + order_id=2, + market_id=3, + time=10, + order_time=8, + agent_id=4, + is_buy=True, + kind=LIMIT_ORDER, + volume=10, + price=100.0, + ttl=2, + ) + assert log.order_id == 2 + assert log.market_id == 3 + assert log.time == 10 + assert log.order_time == 8 + assert log.agent_id == 4 + assert log.is_buy is True + assert log.kind == LIMIT_ORDER + assert log.volume == 10 + assert log.price == 100.0 + assert log.ttl == 2 + + log = ExpirationLog( + order_id=2, + market_id=3, + time=10, + order_time=8, + agent_id=4, + is_buy=True, + kind=MARKET_ORDER, + volume=10, + ) + assert log.order_id == 2 + assert log.market_id == 3 + assert log.time == 10 + assert log.order_time == 8 + assert log.agent_id == 4 + assert log.is_buy is True + assert log.kind == MARKET_ORDER + assert log.volume == 10 + assert log.price is None + assert log.ttl is None + + class TestExecutionLog: def test___init__(self) -> None: log = ExecutionLog( @@ -329,6 +377,7 @@ def __init__(self) -> None: super().__init__() self.n_order_log = 0 self.n_cancel_log = 0 + self.n_expiration_log = 0 self.n_execution_log = 0 self.n_simulation_begin_log = 0 self.n_simulation_end_log = 0 @@ -343,6 +392,9 @@ def process_order_log(self, log: OrderLog) -> None: def process_cancel_log(self, log: CancelLog) -> None: self.n_cancel_log += 1 + def process_expiration_log(self, log: ExpirationLog) -> None: + self.n_expiration_log += 1 + def process_execution_log(self, log: ExecutionLog) -> None: self.n_execution_log += 1 @@ -400,6 +452,19 @@ def process_market_step_end_log(self, log: MarketStepEndLog) -> None: ttl=9, ) logger.write(log=cancel_log) + expiration_log = ExpirationLog( + order_id=1, + market_id=2, + time=5, + order_time=3, + agent_id=4, + is_buy=True, + kind=LIMIT_ORDER, + volume=6, + price=7.0, + ttl=2, + ) + logger.write(log=expiration_log) execution_log = ExecutionLog( market_id=1, time=2, @@ -435,6 +500,7 @@ def process_market_step_end_log(self, log: MarketStepEndLog) -> None: assert logger.n_order_log == 1 assert logger.n_cancel_log == 1 + assert logger.n_expiration_log == 1 assert logger.n_execution_log == 1 assert logger.n_simulation_begin_log == 1 assert logger.n_simulation_end_log == 1 @@ -481,6 +547,19 @@ def test_process2(self) -> None: ttl=9, ) logger.write(log=cancel_log) + expiration_log = ExpirationLog( + order_id=1, + market_id=2, + time=5, + order_time=3, + agent_id=4, + is_buy=True, + kind=LIMIT_ORDER, + volume=6, + price=7.0, + ttl=2, + ) + logger.write(log=expiration_log) execution_log = ExecutionLog( market_id=1, time=2,