Adding multiple rounds
In the previous example, our six participants had just one round to trade. We're now going to give them four rounds, with a change in private values and costs halfway through to keep things interesting.
Create an experiment.yaml file
Download the complete files and extract them into your experiment folder, keeping the examples/ directory structure.
Create or edit this stage's files in examples/tutorials/multiple_rounds.
Setting protocol.periods to 4 repeats the trading period four times.
We've also added a second set of schedules, a shift_period of 3, and a hook to restore each agent's starting cash and units before each round.
Create a new file called experiment.yaml in examples/tutorials/multiple_rounds, and copy the contents of the snippet below into it.
The highlighted lines show what has changed since the previous example.
variables:
buyer_values: ['100', '80', '60']
seller_costs: ['40', '60', '90']
shifted_buyer_values: ['120', '100', '80']
shifted_seller_costs: ['60', '80', '110']
shift_period: 3
buyer_endowment:
cash: '150'
units:
asset: 0
seller_endowment:
cash: '0'
units:
asset: 1
experiment:
name: multiple_rounds
seed: 42
output_dir: results
protocol:
period_duration_seconds: 30
periods: 4
hooks:
period_start:
- type: examples.tutorials.multiple_rounds.hooks.ResetEndowments
params:
buyer_endowment: ${variables.buyer_endowment}
seller_endowment: ${variables.seller_endowment}
assets:
- id: asset
name: Good
markets:
- id: asset
price_min: '0'
price_max: '150'
agents:
- id: buyer
n: 3
lm:
provider: openai
model: gpt-5.4-mini
temperature: null
reasoning_effort: none
max_tokens: 4096
prompt:
instructions:
content: |
You are a buyer. You can trade one unit per round.
Your private value appears in each market update.
Your earnings are your value minus the purchase price.
If you do not trade, your earnings are zero.
Maximise your earnings. Do not buy above your value. Do not sell.
context_providers:
- type: examples.tutorials.multiple_rounds.providers.PrivateValueProvider
params:
buyer_values: ${variables.buyer_values}
seller_costs: ${variables.seller_costs}
shifted_buyer_values: ${variables.shifted_buyer_values}
shifted_seller_costs: ${variables.shifted_seller_costs}
shift_period: ${variables.shift_period}
- type: snapshot_period_timing
- type: snapshot_order_book
- type: snapshot_own_orders
- type: snapshot_own_holdings
- type: snapshot_last_action_outcome
initial_holdings: ${variables.buyer_endowment}
trading_constraints:
allowed_sides: [bid]
max_units:
asset: 1
max_order_quantity: 1
max_outstanding_orders: 1
- id: seller
n: 3
lm:
provider: openai
model: gpt-5.4-mini
temperature: null
reasoning_effort: none
max_tokens: 4096
prompt:
instructions:
content: |
You are a seller. You can trade one unit per round.
Your private cost appears in each market update.
Your earnings are your sale price minus your cost.
If you do not trade, your earnings are zero.
Maximise your earnings. Do not sell below your cost. Do not buy.
context_providers:
- type: examples.tutorials.multiple_rounds.providers.PrivateValueProvider
params:
buyer_values: ${variables.buyer_values}
seller_costs: ${variables.seller_costs}
shifted_buyer_values: ${variables.shifted_buyer_values}
shifted_seller_costs: ${variables.shifted_seller_costs}
shift_period: ${variables.shift_period}
- type: snapshot_period_timing
- type: snapshot_order_book
- type: snapshot_own_orders
- type: snapshot_own_holdings
- type: snapshot_last_action_outcome
initial_holdings: ${variables.seller_endowment}
trading_constraints:
allowed_sides: [ask]
max_order_quantity: 1
max_outstanding_orders: 1
The buyer_endowment and seller_endowment variables now supply both the initial holdings and the hook's parameters.
This gives us one place to set the accounts that agents start with each round.
Write a context provider
Our provider still gives each agent its own private value or cost. This time, it also checks the current round to decide which schedule to use.
Create a file called providers.py alongside experiment.yaml, and copy in the following code:
import aigora
class PrivateValueProvider:
def __init__(
self,
buyer_values,
seller_costs,
shifted_buyer_values,
shifted_seller_costs,
shift_period,
):
self.shift_period = shift_period
self.values = {}
for number, values in enumerate(
zip(buyer_values, shifted_buyer_values), start=1
):
self.values[f"buyer_{number}"] = values
for number, costs in enumerate(
zip(seller_costs, shifted_seller_costs), start=1
):
self.values[f"seller_{number}"] = costs
def get_context(self, context: aigora.ProviderContext) -> str:
agent_id = context.agent.agent_id
original, shifted = self.values[agent_id]
value = shifted if context.experiment.period >= self.shift_period else original
label = "value" if agent_id.startswith("buyer_") else "cost"
return f"Your private {label} for this round is {value}."
The constructor pairs each participant's original and shifted values using zip.
Before shift_period, the provider returns the original value; from that round onward, it returns the shifted value.
So buyer_1 sees 100 in rounds 1 and 2, then 120 in rounds 3 and 4.
Each agent still sees only its own entry.
Write a hook
A seller who sold its unit in round 1 would otherwise have nothing left to sell in round 2. A buyer who bought a unit would already have reached its holding limit. To give everyone a fresh opportunity to trade, we'll reset their accounts before each round.
A hook is a class that runs at the start or end of a period.
Create a file called hooks.py alongside experiment.yaml, and copy in the following code:
import aigora
class ResetEndowments:
def __init__(self, buyer_endowment, seller_endowment):
self.buyer = buyer_endowment
self.seller = seller_endowment
def on_period_start(self, context: aigora.PeriodStartContext) -> None:
for agent_id, agent in context.agents.items():
endowment = self.buyer if agent_id.startswith("buyer_") else self.seller
agent.holdings = aigora.Holdings(**endowment)
Aigora calls on_period_start before trading begins in every round, including round 1.
Our hook restores each agent's starting cash and units while keeping its conversation history, so the agents can draw on their earlier experience.
Since cash resets each round, calculate earnings from trade prices and private values or costs for that round.
Run the experiment
Create run.py alongside the other files:
As before, asyncio.run(...) starts the asynchronous call in a terminal script.
Run it from your experiment folder (the folder containing examples/):
When the run finishes, trades.csv labels each trade with its period, and holdings.csv records the restored accounts at the start of each round.
The agent traces in traces/ show the change in private information from round 3.
We now have a market that runs over several rounds and changes along the way. In the next example, we'll run it under a few different conditions.
Download the complete files if you want a separate copy of this stage.