Skip to content

AI Traders

Learning objectives

After reading this page you will understand:

  • What pm-ai-trader and pm-ai-swarm are and when to use them
  • The four personality profiles and how they differ
  • How to launch a single bot or a swarm of bots
  • How bots manage risk (position limits, reject breaker)
  • How to configure your engine to allow AI gateway IDs
  • How to set up a realistic classroom simulation

Prerequisites: Running the Engine — the engine must be running before AI traders can connect. Configuration — AI traders connect as regular gateways; their IDs must be listed in engine_config.yaml (or the engine must be in unrestricted mode).


What are the AI trader processes?

EduMatcher ships two processes that generate autonomous order flow:

Process Command Purpose
pm-ai-trader poetry run pm-ai-trader A single autonomous bot — connects as a gateway, watches the book, submits limit orders
pm-ai-swarm poetry run pm-ai-swarm A swarm launcher — spawns N bots in one command, distributes profiles and symbols across them

They are designed to simulate realistic human order behaviour so that a classroom exchange has live activity even when students are not yet trading. Each bot behaves differently — some trade frequently in small sizes, others occasionally in large blocks — giving the order book variety and depth.


How a bot works

Each bot is a fully independent participant. It connects to the engine via ZeroMQ, authenticates as a gateway, requests the symbol list, then enters a decision loop:

flowchart TD
    CONN[Connect to engine\nPUSH :5555 / SUB :5556]
    AUTH[Send gateway_connect\nwait for gateway_auth ack]
    SYM[Request symbol list\nwait for system.symbols]
    LOOP{Decision loop\nevery decision_interval_ms}
    PAUSE{Risk pause\nactive?}
    STALE{Market data\nfresh?}
    PICK[Pick a symbol\ncheck position limit]
    PRICE[Calculate order price\nbest_bid / best_ask ± offset]
    SIDE[Choose side\ncross_probability → BUY or SELL]
    SUBMIT[Submit LIMIT DAY order]
    EVENTS[Handle events\nfill, ack, reject, book, trade]
    REJECT{Reject count\n≥ max_rejects\nin window?}
    BREAKER[Reject breaker trips\npause for cooldown_sec]

    CONN --> AUTH --> SYM --> LOOP
    LOOP --> PAUSE
    PAUSE -->|yes, wait| LOOP
    PAUSE -->|no| STALE
    STALE -->|stale or no data| LOOP
    STALE -->|fresh| PICK
    PICK --> PRICE --> SIDE --> SUBMIT
    SUBMIT --> EVENTS
    EVENTS --> REJECT
    REJECT -->|yes| BREAKER --> LOOP
    REJECT -->|no| LOOP

All orders are LIMIT DAY orders at prices derived from the current book. AI traders never submit market orders, FOK, or IOC — only resting limit orders that contribute to book depth.


Personality profiles

A profile controls how the bot trades: how often, how large, and how aggressively it crosses the spread. The four built-in profiles are:

Profile Decision interval Order size Cross probability Offset Size distribution Character
aggressive 250 ms 20–120 35% 0 ticks balanced Frequent trader; crosses the spread often; medium sizes
cautious 900 ms 10–60 5% 2 ticks away balanced Slow, patient; rarely crosses; small passive orders
many-small 180 ms 1–25 18% 1 tick away small-heavy High-frequency tiny orders; generates many executions
few-large 1400 ms 150–700 12% 1 tick away block-heavy Infrequent institutional-style block orders

cross_probability is the probability that the bot places a marketable limit order (price ≥ best ask for a buy, price \(\leq\) best bid for a sell) rather than a passive limit order beyond the spread.

passive_offset_ticks is how many ticks inside the spread the passive order is placed. Offset = 0 means posting at the best bid/ask; offset = 2 means posting 2 ticks behind the best price.

Size distributions

flowchart LR
    A["balanced\n(uniform random between min and max)"]
    B["small-heavy\n(quadratic — skews toward min)"]
    C["block-heavy\n(quadratic — skews toward max)"]

Risk management

Each bot has two built-in safety mechanisms:

Position limit

The bot tracks its own position per symbol (net quantity of shares held). Once |position| reaches max_position (default: 1000), it stops adding to the position in that direction. A long bot that is at +1000 will only submit sell orders; a short bot at −1000 will only submit buy orders.

Reject breaker

If the engine rejects max_rejects orders within reject_window_sec, the bot pauses for reject_cooldown_sec before submitting again. This prevents a misconfigured bot from flooding the engine with invalid orders.

[AI:AI01 14:30:05] reject breaker tripped; pausing submissions for 5.0s

Launching a single bot

poetry run pm-ai-trader --id AI01 --profile aggressive

Common options:

Option Default Description
--id required Gateway ID — must match an entry in engine_config.yaml or engine must be unrestricted
--profile cautious One of: aggressive, cautious, many-small, few-large
--symbols all Comma-separated list of symbols this bot watches; default is all symbols from engine
--seed 1 Random seed — same seed produces identical order sequence (useful for reproducibility)
--duration 0 (forever) Stop automatically after this many seconds
--run-id autogenerated Optional run identifier; a value is generated automatically if omitted
--max-position 1000 Absolute position limit per symbol
--max-rejects 25 Reject breaker threshold within the window
--reject-window 10.0 Rolling window in seconds for reject counting
--reject-cooldown 5.0 Pause duration in seconds after reject breaker trips
--stale-data 4.0 Seconds before market data is considered too old to trade on
--log-level WARNING Explicit level: CRITICAL, ERROR, WARNING, INFO, DEBUG
-v, --verbose off Increase verbosity (-v enables bot debug prints, -vv sets DEBUG)
-q, --quiet off Reduce output to warnings/errors

Example with all options:

poetry run pm-ai-trader \
  --id AI01 \
  --profile many-small \
  --symbols AAPL,MSFT \
  --seed 42 \
  --duration 120 \
  --max-position 500

The bot prints a summary on exit:

[AI:AI01 14:32:01] stopped submitted=284 acked=281 rejected=3 fills=68

Launching a swarm

pm-ai-swarm starts N bots simultaneously, distributing profiles and symbols round-robin:

poetry run pm-ai-swarm --count 10 --duration 60

The swarm assigns gateway IDs AI01 through AI10 automatically and, when --profiles is not given, cycles through all four profiles in alphabetical order: aggressive, cautious, few-large, many-small.

Swarm options

Option Default Description
--count 10 Number of bots to launch
--prefix AI Gateway ID prefix (e.g. BOTBOT01, BOT02, …)
--start-index 1 Starting index for gateway IDs
--profiles all Comma-separated profile cycle, e.g. aggressive,cautious
--symbols all from config Comma-separated symbols to trade
--config engine_config.yaml Path to config file used to discover symbols
--seed-base 1000 Seeds are seed_base + i for bot i
--duration 60.0 Seconds each bot runs; 0 = run until Ctrl-C
--max-position 1000 Position limit per bot per symbol
--python current Python Path to Python executable
--log-level WARNING Logging level for swarm launcher; also forwarded to child bots
-v, --verbose off Increase verbosity (-vINFO, -vvDEBUG); forwarded to child bots
-q, --quiet off Reduce output to warnings/errors; forwarded to child bots
flowchart LR
    SWARM["pm-ai-swarm\n--count 4"]
    BOT1["AI01\nprofile=aggressive\nsymbol=AAPL"]
    BOT2["AI02\nprofile=cautious\nsymbol=MSFT"]
    BOT3["AI03\nprofile=few-large\nsymbol=AAPL"]
    BOT4["AI04\nprofile=many-small\nsymbol=MSFT"]
    ENG["pm-engine"]

    SWARM --> BOT1 & BOT2 & BOT3 & BOT4
    BOT1 & BOT2 & BOT3 & BOT4 --> ENG

(Profile order shown is the default alphabetical cycle. Passing an explicit --profiles list uses that order verbatim instead.)

The swarm waits for all bots to finish (or until Ctrl-C), then exits. Each bot's output is interleaved in the terminal.

Graceful shutdown

Pressing Ctrl-C sends SIGINT to the swarm process, which forwards it to all child bots. Each bot cancels pending orders, prints its summary line, and disconnects cleanly. A second Ctrl-C force-kills immediately.


Configuring gateway IDs

Unrestricted mode (no config file)

If you start the engine with no engine_config.yaml, any gateway ID can connect and trade any symbol. This is the easiest way to test:

poetry run pm-engine               # unrestricted
poetry run pm-ai-swarm --count 5

With a config file

Add the bot gateway IDs to the gateways: section. AI traders are ordinary TRADER participants:

symbols:
  AAPL:
    tick_decimals: 2
  MSFT:
    tick_decimals: 2

gateways:
  alf:
    - id: AI01
      description: AI bot 1
    - id: AI02
      description: AI bot 2
    - id: AI03
      description: AI bot 3
    # ... as many as --count
    - id: ST01
      description: Student 1
    - id: ST02
      description: Student 2

Using a range pattern

If you use the swarm default prefix AI and --count 10, the IDs will be AI01 through AI10. Add all ten to your config. With --start-index 1 (default) and --prefix AI the IDs are zero-padded to two digits.


Classroom demo setup

A typical classroom simulation uses a swarm of bots to provide realistic background order flow while students trade alongside them. The bots generate price discovery, spread variation, and occasional large moves that students must react to.

# engine_config.yaml
sessions_enabled: true   # run opening and closing auctions

symbols:
  AAPL:
    tick_decimals: 2
    last_buy_price: 149.90
    last_sell_price: 150.10
  MSFT:
    tick_decimals: 2
    last_buy_price: 415.00
    last_sell_price: 415.50
  TSLA:
    tick_decimals: 2
    last_buy_price: 250.00
    last_sell_price: 250.50

gateways:
  alf:
    # Instructor / operator
    - id: OPS01
      description: Operator console
      role: ADMIN

    # Market maker (optional, adds liquidity — see pm-mm-bot below).
    # Gateway ID follows pm-mm-bot's MM_<SYMBOL>_<nn> convention; see
    # [Market-Maker Bot](100-mm-bot.md#gateway-identity-convention).
    - id: MM_AAPL_01
      description: Market maker (AAPL)
      role: MARKET_MAKER
      quote_refresh_policy: INACTIVATE_ON_ANY_FILL
      enforce_mm_obligation: true
      mm_max_spread_ticks: 20
      mm_min_qty: 100

    # AI bots (30 bots, IDs AI01–AI30)
    - id: AI01
      description: AI bot 1
    - id: AI02
      description: AI bot 2
    # ... repeat through AI30

    # Students (adjust count for your class size)
    - id: ST01
      description: Student 1
    - id: ST02
      description: Student 2
    # ...

Launch sequence

# Terminal 1: matching engine
poetry run pm-engine

# Terminal 2: session scheduler (opens at 09:30, closes at 16:00)
poetry run pm-scheduler

# Terminal 3: clearing and stats
poetry run pm-clearing &
poetry run pm-stats &

# Terminal 4: market maker for AAPL (optional — omit if you skipped the
# MM_AAPL_01 gateway entry above)
poetry run pm-mm-bot --symbol AAPL

# Terminal 5: AI swarm (30 bots, all 4 profiles, run for 30 minutes)
poetry run pm-ai-swarm \
  --count 30 \
  --duration 1800 \
  --profiles aggressive,cautious,many-small,few-large

# Students connect individually
poetry run pm-alf-console --id ST01

For a quick demo without scheduling, use the launch script:

./tools/launch_all.sh

Understanding bot output

Each bot prefixes every log line with its gateway ID and time. At default verbosity the bot logs milestones and a startup summary of its own configuration, but not individual order submissions:

[AI:AI01 14:30:00] starting: profile=aggressive symbols=all duration=120s run_id=botrun-...
[AI:AI01 14:30:00] authenticated
[AI:AI01 14:30:01] reject breaker tripped; pausing submissions for 5.0s
[AI:AI01 14:30:06] reject breaker cooldown ended; resuming submissions
[AI:AI01 14:31:59] stopped submitted=312 acked=308 rejected=4 (PRICE_OUTSIDE_COLLAR=3, STALE_SYMBOL=1) fills=72

The rejected count typically reflects orders at prices outside the collar or submitted during a halt; a count of 3–5 per 300 submissions is normal. If the count is very high, check that the bot's gateway IDs are configured in the engine. The stopped line's parenthetical breaks the rejects down by reason, taken directly from the engine's order.ack rejection payloads.

With --verbose (-v), the bot additionally logs every order submission, fill, individual rejection, and reasons for skipping a trading decision (stale market data, position limit reached):

[AI:AI01 14:30:01] order SUBMIT BUY 45@149.97 AAPL
[AI:AI01 14:30:01] order REJECTED: PRICE_OUTSIDE_COLLAR
[AI:AI01 14:30:05] fill: BUY 45@149.97 AAPL pos=45
[AI:AI01 14:30:12] position limit reached on AAPL (pos=1000); forcing SELL
[AI:AI01 14:30:18] skip AAPL: stale market data (4.2s > 4.0s)

See also