mts1b-riskengine — public API surface
7-gate pre-trade pipeline + synthetic stops + broker-exit reconciler + drawdown halt.
gRPC interface (primary)
service RiskEngine {
rpc CheckOrder(Order) returns (RiskDecision);
rpc GetEnvelope(EnvelopeRequest) returns (RiskEnvelope);
rpc UpdateEnvelope(RiskEnvelope) returns (RiskEnvelope);
rpc RequestHalt(HaltRequest) returns (HaltResponse);
rpc ResumeHalt(ResumeRequest) returns (HaltStatus);
}
OMS calls CheckOrder synchronously for every order; p99 ≤ 5ms.
Python client
from mts1b_riskengine import RiskEngineClient
re = RiskEngineClient(grpc_addr="localhost:50052")
# Check an order (does NOT submit)
decision = await re.check_order(order, envelope)
# RiskDecision(approved=True/False, rejecting_gate=str, code=str, reason=str, ...)
# Read envelope
env = await re.get_envelope(fund_id="paper-momentum")
# Update envelope (gated; rejected if loosening during halt)
new_env = env.model_copy(update={"max_position_pct": 0.07,
"version": env.version + 1})
applied = await re.update_envelope(new_env)
The 7 gates
| # | Gate | Check |
|---|---|---|
| 1 | idempotency | Order.idempotency_key not seen in 5-min window |
| 2 | schema | pydantic validates |
| 3 | static | broker / order_type / asset_class allowed; notional ≤ max |
| 4 | position_risk | resulting position pct, gross/net exposure, sector |
| 5 | drawdown_halt | fund not in halt state |
| 6 | short_side | shorting enabled; borrow fee acceptable |
| 7 | cro_veto (optional) | LLM CRO persona doesn't veto |
Each gate publishes outcome to NATS:
mts.v1.risk.gate.passed → dict with order_id, gate, latency_ms
mts.v1.risk.gate.failed → OrderRejection
NATS subjects
Subscribed
| Subject | Why |
|---|---|
mts.v1.oms.orders.created | observe new orders for stats |
mts.v1.oms.fills.created | update position store for next gate check |
mts.v1.treasury.nav.updated | recompute drawdown halt conditions |
Published
| Subject | Payload |
|---|---|
mts.v1.risk.envelope.updated | RiskEnvelope |
mts.v1.risk.gate.passed | dict with order_id, gate, latency |
mts.v1.risk.gate.failed | OrderRejection |
mts.v1.risk.halt.requested | HaltRequest (auto-halts) |
Synthetic stops
# Configuration
synthetic_stop:
enabled: true
poll_interval_s: 5 # crypto
poll_interval_equities_s: 60
Worker polls quotes every N seconds. When position price crosses fill_price × (1 ± synthetic_stop_pct), the worker emits a closing order via OMS (same path, same gates).
Honors stops even on broker outage or after-hours gap.
Broker-exit reconciler
broker_exit_reconciler:
enabled: true
interval_s: 120
alert_threshold: 1 # alert on any discrepancy
Every 120s: compare OMS positions ↔ broker positions. Differences indicate:
- Lost fill
- Broker-side reconciliation lag
- Manual broker trade (off-system)
- Cancellation race
Alerts go to Telegram via mts1b-platform.messaging. Auto-reconcile (configurable) updates OMS to match broker.
Drawdown halt — the kill switch
Triggers:
| Trigger | Severity |
|---|---|
daily_loss_halt_pct breached | FUND_HALT |
weekly_loss_halt_pct breached | FUND_HALT |
monthly_loss_halt_pct breached | FUND_HALT |
Manual mts cmd halt | FIRM_HALT |
news_spike watchdog | FIRM_HALT |
Per-strategy drift < -2.0σ | STRATEGY_HALT |
Halts are sticky — only operator can resume:
mts cmd resume <fund_id> # requires confirmation: type RESUME
mts cmd resume <strategy_id>
mts cmd resume # firm-wide; requires 2FA
CLI
# Show envelope
mts mts1b-riskengine envelope show --fund-id paper-momentum
# Tighten envelope (loosening during halt is rejected)
mts mts1b-riskengine envelope set --fund-id paper-momentum \
--max-position-pct 0.07 \
--daily-loss-halt-pct 0.02
# List recent rejections
mts mts1b-riskengine rejections list --fund-id paper-momentum --hours 24
# Force reconciliation
mts mts1b-riskengine reconcile --fund-id paper-momentum --force
# Halt status
mts mts1b-riskengine halts list
See also
- Concept — risk envelopes
foundation.risk— RiskEnvelope schemarepos/riskengine