Running a simulation¶
This tutorial runs a simulation from Python, prints and saves the market prices, reads the results from the
simulator after the run, and plots them.
The complete script is tutorial_first_simulation.py.
It needs matplotlib for the plot, which is not installed with PAMS
(pip install matplotlib).
The configuration¶
A simulation is described by a configuration: its markets, its agents and its sessions. This tutorial uses the Minimal configuration of Quick start, written as a Python dict:
CONFIG: Dict[str, Any] = {
"simulation": {
"markets": ["Market"],
"agents": ["FCNAgents"],
"sessions": [
{
"sessionName": "warmup",
"iterationSteps": 100,
"withOrderPlacement": True,
"withOrderExecution": False,
"withPrint": True,
},
{
"sessionName": "main",
"iterationSteps": 500,
"withOrderPlacement": True,
"withOrderExecution": True,
"withPrint": True,
},
],
},
"Market": {"class": "Market", "tickSize": 0.00001, "marketPrice": 300.0},
"FCNAgents": {
"class": "FCNAgent",
"numAgents": 100,
"markets": ["Market"],
"assetVolume": 50,
"cashAmount": 10000,
"fundamentalWeight": {"expon": [1.0]},
"chartWeight": {"expon": [0.0]},
"noiseWeight": {"expon": [1.0]},
"noiseScale": 0.001,
"timeWindowSize": [100, 200],
"orderMargin": [0.0, 0.1],
},
}
It creates one market and 100 FCNAgent agents.
The warmup session (steps 0 to 99) only fills the order book, and the main session (steps 100 to 599)
also executes the orders.
Every key is explained in JSON configuration.
The runners accept the configuration as a dict, as the path of a JSON file, or as an open JSON file, so the same
configuration saved as config.json can be run with SequentialRunner(settings="config.json", ...).
Running it¶
SequentialRunner builds the simulation from the configuration and runs it when main()
is called:
def print_prices(seed: int) -> None:
"""Run the simulation and print the market price at every step.
Args:
seed (int): seed of the random number generator.
"""
runner = SequentialRunner(
settings=CONFIG, prng=random.Random(seed), logger=MarketStepPrintLogger()
)
runner.main()
prngis the random number generator of the whole simulation. The simulator and every market, agent, session and event get their own generator seeded from it, so a run is fully determined by the seed: the same seed gives the same prices. Withoutprng, the runner usesrandom.Random(), and every run is different.loggerreceives the logs of the simulation.MarketStepPrintLoggerprints one line per market at the end of every step. A runner has at most one logger.
print_prices(seed=42) prints:
0 0 0 Market 300.0 300.0
0 1 0 Market 300.0 300.0
0 2 0 Market 300.0 300.0
...
0 99 0 Market 300.0 300.0
1 100 0 Market 298.11165 300.0
1 101 0 Market 298.11165 300.0
1 102 0 Market 298.85386 300.0
...
1 599 0 Market 297.40149 300.0
# INITIALIZATION TIME 0.0029998
# EXECUTION TIME 0.0955184
The columns are the session ID (the position of the session in sessions: 0 for warmup), the step,
the market ID, the market name, the market price and the fundamental price.
The market price stays at 300 while orders are not executed, and starts to move at step 100.
The fundamental price stays at 300 because the market has no fundamentalVolatility
(see Markets).
The last two lines, printed by main(), are the time taken to build and to run the simulation, in seconds.
They change from run to run.
You will also see this warning:
UserWarning: order price does not accord to the tick size. price will be modified
It is normal: FCN agents compute prices that are not multiples of tickSize, and the market rounds them
(buy orders down, sell orders up).
Saving the prices¶
To use the prices in Python instead of printing them, pass a MarketStepSaver:
def run_simulation(seed: int) -> Tuple[SequentialRunner, MarketStepSaver]:
"""Run the simulation and keep the prices of every step in memory.
Args:
seed (int): seed of the random number generator.
Returns:
Tuple[SequentialRunner, MarketStepSaver]: the runner after the run and the
saver holding the prices.
"""
saver = MarketStepSaver()
runner = SequentialRunner(settings=CONFIG, prng=random.Random(seed), logger=saver)
runner.main()
return runner, saver
After the run, saver.market_step_logs is a list with one dict per market and step, in the order of the
steps:
>>> saver.market_step_logs[100]
{'session_id': 1, 'market_time': 100, 'market_id': 0, 'market_name': 'Market', 'market_price': 298.11165, 'fundamental_price': 300.0}
To record other data, such as every trade, write your own logger (see Writing a logger).
Reading the results¶
After main() has returned, runner.simulator still holds every market, agent and session, and the markets
keep the history of their prices and volumes:
def summarize(runner: SequentialRunner) -> Dict[str, float]:
"""Read a few results of the main session from the simulator.
Args:
runner (SequentialRunner): the runner after ``main()`` has returned.
Returns:
Dict[str, float]: the last, lowest and highest market prices of the main
session, the number of shares traded in it, and the number of shares held by
all the agents at the end.
"""
market = runner.simulator.name2market["Market"]
main_session = runner.simulator.name2session["main"]
start = main_session.session_start_time
end = start + main_session.iteration_steps
prices = market.get_market_prices(times=range(start, end))
volumes = market.get_executed_volumes(times=range(start, end))
agents = runner.simulator.agents
return {
"last_price": prices[-1],
"min_price": min(prices),
"max_price": max(prices),
"total_volume": sum(volumes),
"total_shares": sum(
agent.get_asset_volume(market_id=market.market_id) for agent in agents
),
}
print(summarize(runner)) prints:
{'last_price': 297.40149, 'min_price': 289.48107000000005, 'max_price': 314.08524, 'total_volume': 205, 'total_shares': 5000}
The shares change hands but their total does not change: the 100 agents started with 50 shares each.
The simulator finds objects by name or ID:
name2market,name2agentandname2sessionby instance name ("Market","FCNAgents-0","main"), andid2marketandid2agentby ID;agents_group_name2agentgives the agents of a group ("FCNAgents");markets,agentsandsessionslist all of them.
The history methods of Market take the steps to read in times.
Without times they return every step up to the current time of the market, which is 600 after this run
(one step after the last one), so use each session’s session_start_time and iteration_steps to select
it, as above. Useful methods include:
Method |
Returns |
|---|---|
|
Market prices and fundamental prices. |
|
Traded volume, and the sum of price times volume, of each step. |
|
Price of the last execution and mid price at each step. They can be |
|
Volume-weighted average price from step 0 to |
|
Best prices in the order book now ( |
The methods ending in s also have a form for one step, such as get_market_price(time), where time
defaults to the current step. Asking for a step after the current one raises
AssertionError: Cannot refer the future parameters.
An agent’s holdings are given by get_cash_amount() and get_asset_volume(market_id).
Plotting¶
The saved prices can be plotted with matplotlib:
def plot_prices(saver: MarketStepSaver, path: str) -> None:
"""Plot the market price and the fundamental price of the main session.
Args:
saver (MarketStepSaver): the saver used as the logger of the run.
path (str): path of the image file to write.
"""
logs: List[Dict[str, Any]] = [
log for log in saver.market_step_logs if log["session_id"] == 1
]
times = [log["market_time"] for log in logs]
fig, ax = plt.subplots()
ax.plot(times, [log["market_price"] for log in logs], label="market price")
ax.plot(
times, [log["fundamental_price"] for log in logs], label="fundamental price"
)
ax.set_xlabel("step")
ax.set_ylabel("price")
ax.legend()
fig.savefig(path)
plt.close(fig)
plot_prices(saver, path="prices.png") keeps the main session (session ID 1) and writes:
Running the script¶
Running the complete script with python tutorial_first_simulation.py does all of the above with seed 42 and
writes prices.png to the current directory.
Next, Writing an agent adds your own trading strategy to this simulation.