Skip to main content

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

ComponentTutorial 1Tutorial 2
Data sourceHistorical parquetLive quotes from mts1b-marketdata
SizingBacktest portfolio simLive order generation via mts1b-portfolio
RiskBacktest cost modelLive mts1b-riskengine enforcement
ExecutionSimulated fillsmts1b-oms + paper broker fills
ReportingStatic HTMLLive 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 (adds oms, oms-algos, riskengine, portfolio, brokers to 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:

  1. At each rebalance (monthly close), compute the factor on the latest universe panel from mts1b-datalake.
  2. Apply the sizer in mts1b-portfolio to convert ranking → target weights.
  3. Diff target weights against current positions (via mts1b-oms).
  4. Emit child orders for the diff, each tagged with strategy_id=paper_momentum_demo.
  5. 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