Writing a logger

A logger receives the logs of a simulation: the orders, cancels and executions, and the beginning and end of the simulation, of each session and of each market step. The built-in MarketStepPrintLogger and MarketStepSaver only record the market prices at each step (see Running a simulation). This tutorial writes a logger that prints a summary of each session and saves every trade to a CSV file. The complete script is tutorial_custom_logger.py.

The logger

A logger inherits from Logger and overrides the methods of the logs it needs. The others do nothing:

Method

Log

process_simulation_begin_log, process_simulation_end_log

SimulationBeginLog, SimulationEndLog

process_session_begin_log, process_session_end_log

SessionBeginLog, SessionEndLog (log.session)

process_market_step_begin_log, process_market_step_end_log

MarketStepBeginLog, MarketStepEndLog (log.session, log.market), for each market at each step

process_order_log

OrderLog, for each order a market has received

process_cancel_log

CancelLog, for each cancel order a market has received

process_expiration_log

ExpirationLog, for each order removed from the order book because of its ttl

process_execution_log

ExecutionLog, for each execution

The order, cancel, expiration and execution logs give the IDs of the market and of the agents. The other logs give the simulator (log.simulator).

class TradeLogger(Logger):
    """Print a summary of each session and save every execution to a CSV file."""

    def __init__(self, path: str) -> None:
        """Initialize the logger.

        Args:
            path (str): path of the CSV file written at the end of the simulation.

        """
        super().__init__()
        self.path: str = path
        self.executions: List[ExecutionLog] = []
        self.n_orders: int = 0
        self.volume: int = 0

    def process_session_begin_log(self, log: SessionBeginLog) -> None:
        """Reset the counters at the beginning of each session.

        Args:
            log (SessionBeginLog): the log of the session that begins.

        """
        self.n_orders = 0
        self.volume = 0

    def process_order_log(self, log: OrderLog) -> None:
        """Count the orders accepted by the markets.

        Args:
            log (OrderLog): the accepted order.

        """
        self.n_orders += 1

    def process_execution_log(self, log: ExecutionLog) -> None:
        """Keep the execution and add its volume to the volume of the session.

        Args:
            log (ExecutionLog): the execution.

        """
        self.executions.append(log)
        self.volume += log.volume

    def process_session_end_log(self, log: SessionEndLog) -> None:
        """Print the counters at the end of each session.

        Args:
            log (SessionEndLog): the log of the session that ends.

        """
        print(
            f"{log.session.name}: {self.n_orders} orders, "
            f"{self.volume} shares traded"
        )

    def process_simulation_end_log(self, log: SimulationEndLog) -> None:
        """Write all the executions to the CSV file.

        Args:
            log (SimulationEndLog): the log of the end of the simulation.

        """
        with open(self.path, "w", newline="", encoding="utf-8") as fp:
            writer = csv.writer(fp)
            writer.writerow(["time", "market", "price", "volume", "buyer", "seller"])
            for execution in self.executions:
                writer.writerow(
                    [
                        execution.time,
                        self.simulator.id2market[execution.market_id].name,
                        execution.price,
                        execution.volume,
                        self.simulator.id2agent[execution.buy_agent_id].name,
                        self.simulator.id2agent[execution.sell_agent_id].name,
                    ]
                )


  • __init__ must call super().__init__().

  • The runner sets self.simulator when the runner is created, before the simulation starts. The logger uses it to find the names of the market and of the agents from their IDs.

  • An ExecutionLog gives the step (time), the market, the buyer and the seller, their order IDs, the price and the volume of one execution.

  • The logger keeps the executions in memory and writes the file at the end of the simulation. This also keeps it usable with the process runner, which cannot copy a logger that holds an open file (see Parallel runners).

When the logs are processed

The market step logs are processed at once, during the step. So, in process_market_step_end_log, the market is at the end of that step, which is how MarketStepSaver reads its prices.

The order, cancel, expiration and execution logs are kept by the logger until it receives the next session or simulation log, at the beginning or at the end of a session or of the simulation. They are then processed in the order they happened, before that log. In process_execution_log, the market and the agents are therefore already at the end of the session: use the fields of the log, not the current state of the market or the agents.

In this logger, the orders and executions of a session are counted when the session ends, just before process_session_end_log prints them.

Using the logger

A runner takes one logger:

def run_simulation(seed: int, path: str) -> SequentialRunner:
    """Run the simulation with the trade logger.

    Args:
        seed (int): seed of the random number generator.
        path (str): path of the CSV file to write.

    Returns:
        SequentialRunner: the runner after the run.

    """
    runner = SequentialRunner(
        settings=CONFIG, prng=random.Random(seed), logger=TradeLogger(path=path)
    )
    runner.main()
    return runner


To record several things, handle them all in one logger. run_simulation(seed=42, path="trades.csv") prints the summaries of the two sessions, then the two time lines of main():

warmup: 100 orders, 0 shares traded
main: 500 orders, 205 shares traded
# INITIALIZATION TIME 0.0019999
# EXECUTION TIME 0.0909915

In each step, one FCN agent places one order (maxNormalOrders is 1), and orders are only executed in the main session. trades.csv starts with:

time,market,price,volume,buyer,seller
100,Market,298.11165,1,FCNAgents-16,FCNAgents-66
100,Market,298.11165,1,FCNAgents-91,FCNAgents-66
100,Market,298.11165,1,FCNAgents-87,FCNAgents-55

At step 100, the first step of main, the orders placed during warmup start to be executed.