Tutorial 2: Paper trading
This tutorial takes the strategy from Tutorial 1 and runs it live against a paper broker, with the full risk-gate + OMS chain in the middle.
Time: ~20 minutes setup + a market-open window to see real fills.
Prerequisites: Tutorial 1 completed; you have mts1b-deploy installed and your 8 v1 services running.
What changes vs Tutorial 1
| Component | Tutorial 1 | Tutorial 2 |
|---|---|---|
| Data source | Historical parquet | Live quotes from mts1b-marketdata |
| Sizing | Backtest portfolio sim | Live order generation via mts1b-portfolio |
| Risk | Backtest cost model | Live mts1b-riskengine enforcement |
| Execution | Simulated fills | mts1b-oms + paper broker fills |
| Reporting | Static HTML | Live grafana + Telegram alerts |
The strategy code is unchanged. That's the entire point of mts1b-foundation-mediated boundaries.
Step 1 — Switch to paper-trading profile
mts1b-deploy menuconfig
Change:
- Profile:
paper-trading(addsoms,oms-algos,riskengine,portfolio,brokersto the 8 v1 services) - Asset classes:
equities
Save. Then:
mts1b-deploy install --config mts1b.config # idempotent; only installs the deltas
mts1b-deploy status
# 12/12 services healthy
Step 2 — Create a fund
mts mts1b-treasury fund create \
--fund-id paper-momentum-demo \
--nav 100000 \
--base-currency USD \
--broker paper
The paper broker is an in-process simulator. It accepts orders, marks them filled at the next quote, charges a configurable per-side cost.
Step 3 — Define a risk envelope
mts mts1b-riskengine envelope set \
--fund-id paper-momentum-demo \
--max-gross-exposure 1.0 \
--max-position-pct 0.25 \
--daily-loss-halt-pct 0.03 \
--max-order-notional 30000 \
--allowed-brokers paper \
--allowed-order-types market,limit \
--enable-shorting false
The envelope is published to mts.v1.risk.envelope.updated. mts1b-oms reloads in real time.
Step 4 — Subscribe the strategy to the live data feed
mts mts1b-research strategy register \
--strategy-id paper_momentum_demo \
--fund-id paper-momentum-demo \
--factor f_momentum_12_1 \
--params '{"h_long": 252, "h_skip": 21}' \
--universe equities-spy-qqq-iwm-gld-tlt \
--rebal monthly \
--sizing kelly_voltarget_12 \
--enabled true
mts1b-research will now:
- At each rebalance (monthly close), compute the factor on the latest universe panel from
mts1b-datalake. - Apply the sizer in
mts1b-portfolioto convert ranking → target weights. - Diff target weights against current positions (via
mts1b-oms). - Emit child orders for the diff, each tagged with
strategy_id=paper_momentum_demo. - Each order passes through all 7 risk gates before reaching the paper broker.
Step 5 — Watch it tick
# Subscribe to all NATS events for this fund
mts mts1b-platform tail \
--subject "mts.v1.>" \
--filter "fund_id=paper-momentum-demo"
Sample output during a rebalance:
[2026-05-26 16:00:01.243] mts.v1.research.signals.published {signal_id: sig-abc, factor: f_momentum_12_1, weights: {SPY:0.4, QQQ:0.6}}
[2026-05-26 16:00:01.291] mts.v1.portfolio.targets.computed {targets: [{SPY: 40000}, {QQQ: 60000}]}
[2026-05-26 16:00:01.328] mts.v1.oms.orders.created {order_id: ord-1, symbol: SPY, side: buy, qty: 80}
[2026-05-26 16:00:01.329] mts.v1.oms.orders.created {order_id: ord-2, symbol: QQQ, side: buy, qty: 130}
[2026-05-26 16:00:01.412] mts.v1.risk.gate.passed {order_id: ord-1, gates: [static, position, drawdown, short]}
[2026-05-26 16:00:01.418] mts.v1.risk.gate.passed {order_id: ord-2, gates: [static, position, drawdown, short]}
[2026-05-26 16:00:01.502] mts.v1.brokers.paper.orders.routed {order_id: ord-1}
[2026-05-26 16:00:01.503] mts.v1.brokers.paper.orders.routed {order_id: ord-2}
[2026-05-26 16:00:02.001] mts.v1.brokers.paper.fills.raw {order_id: ord-1, fill_qty: 80, fill_price: 500.12}
[2026-05-26 16:00:02.002] mts.v1.brokers.paper.fills.raw {order_id: ord-2, fill_qty: 130, fill_price: 461.87}
[2026-05-26 16:00:02.045] mts.v1.oms.fills.created {order_id: ord-1, fill: ...}
[2026-05-26 16:00:02.046] mts.v1.oms.fills.created {order_id: ord-2, fill: ...}
[2026-05-26 16:00:02.087] mts.v1.treasury.nav.paper-momentum-demo {nav: 100012.50, change: +0.012%}
Step 6 — Inspect positions
mts mts1b-oms positions list --fund-id paper-momentum-demo
fund=paper-momentum-demo nav=$100,012.50
symbol qty avg_price market_price unrealized_pl pct_nav
SPY 80 500.12 501.45 +$106.40 40.1%
QQQ 130 461.87 462.20 +$42.90 60.1%
gross_exposure: 100.2%
net_exposure: 100.2%
cash: $0.20
Step 7 — Watch the risk gates in action
Try an oversized order:
mts mts1b-oms order submit \
--fund-id paper-momentum-demo \
--symbol SPY --side buy --qty 200 --order-type market
Expected:
✗ Order REJECTED by riskengine
gate: max_order_notional
code: ORDER_NOTIONAL_EXCEEDED
reason: 200 × $500 = $100,000 > envelope max_order_notional ($30,000)
envelope_id: env-...
The order never reaches the broker. mts.v1.risk.gate.failed is emitted to NATS, audited by mts1b-operations.
Step 8 — Trigger the drawdown halt (optional)
To verify the halt works, temporarily lower the threshold:
mts mts1b-riskengine envelope set \
--fund-id paper-momentum-demo \
--daily-loss-halt-pct 0.001 # 0.1% halt
Then send a market order in size, wait for it to fill, then place another. The second should be blocked:
✗ Order REJECTED by riskengine
gate: drawdown_halt
code: DAILY_LOSS_HALT
reason: daily P/L -0.15% ≤ -0.10% halt threshold
envelope_id: env-...
Resume with mts cmd resume paper-momentum-demo.
Step 9 — Live monitoring
Grafana dashboard:
mts1b-deploy open grafana
# opens http://localhost:3000 → Fund Overview → paper-momentum-demo
Panels: NAV curve, drawdown, exposure breakdown, order-flow throughput, rejection-rate by gate.
Step 10 — Tear down
mts mts1b-treasury fund close --fund-id paper-momentum-demo --reason "demo done"
Closes all positions at next market and archives the fund.
What's next
- Tutorial 3: Custom strategy — write your own factor + walk-forward validate
- Concept: Risk envelopes — the gate hierarchy in depth
mts1b-omsrepo spec — state machine internals