Home / Libraries / ML4T Live / Docs
ML4T Live
ML4T Live Documentation
Production trading with broker integrations
Skip to content

Risk Controls

SafeBroker is the pre-trade control layer around a live broker. It validates each order before submission, persists risk state across restarts, and keeps the kill switch sticky until you clear it.

What Is Enforced Today

At order submission time, SafeBroker currently enforces:

  • position and exposure caps
  • per-order notional and share limits
  • order-rate limiting and duplicate-order suppression
  • asset allow/block lists
  • price-deviation checks for priced orders
  • stale-data rejection via max_data_staleness_seconds
  • daily-loss rejection via max_daily_loss
  • drawdown-triggered kill-switch activation via max_drawdown_pct
  • shadow-mode execution through VirtualPortfolio

The market-price checks now use the latest cached market snapshot from the engine instead of a placeholder fallback for first-entry trades.

LiveRiskConfig

from ml4t.live import LiveRiskConfig

config = LiveRiskConfig(
    max_position_value=25_000,
    max_position_shares=1_000,
    max_total_exposure=100_000,
    max_positions=10,
    max_order_value=5_000,
    max_order_shares=250,
    max_orders_per_minute=5,
    max_daily_loss=2_000,
    max_drawdown_pct=0.10,
    max_price_deviation_pct=0.05,
    max_data_staleness_seconds=60,
    dedup_window_seconds=1.0,
    allowed_assets={"SPY", "QQQ"},
    blocked_assets={"GME"},
    shadow_mode=True,
    kill_switch_enabled=False,
    fail_on_reconciliation_mismatch=True,
    state_file=".ml4t_risk_state.json",
    journal_file=".ml4t_execution_journal.jsonl",
)

Control Groups

Position Limits

  • max_position_value
  • max_position_shares
  • max_total_exposure
  • max_positions

Order Limits

  • max_order_value
  • max_order_shares
  • max_orders_per_minute
  • dedup_window_seconds

Loss And Safety Limits

  • max_daily_loss
  • max_drawdown_pct
  • max_price_deviation_pct
  • max_data_staleness_seconds
  • allowed_assets
  • blocked_assets

Execution And Persistence

  • shadow_mode
  • kill_switch_enabled
  • fail_on_reconciliation_mismatch
  • state_file
  • journal_file

Persisted State

The risk state file persists more than just the kill switch. It now captures:

  • current trading date and daily counters
  • daily-loss baseline through session_start_equity
  • persisted position and pending-order snapshots from the last clean disconnect
  • kill-switch state and reason

That persisted snapshot is used again on the next SafeBroker.connect() to generate the startup reconciliation report. If fail_on_reconciliation_mismatch=True, a non-clean reconciliation raises ReconciliationMismatchError and blocks startup outside shadow mode.

Execution Journal

SafeBroker also writes a JSONL execution journal. By default it lives next to the risk-state file and includes:

  • order submissions and shadow fills
  • manual or automatic kill-switch events
  • startup reconciliation outcomes
  • engine health transitions and recovery attempts when the broker is used through LiveEngine

Set journal_file explicitly when you want the journal stored elsewhere.

Shadow Mode

Shadow mode is the recommended first deployment step:

safe_broker = SafeBroker(broker, LiveRiskConfig(shadow_mode=True))

In shadow mode:

  • all normal risk checks still run
  • orders are marked filled virtually
  • VirtualPortfolio tracks positions and cash locally
  • no real broker order is submitted

Kill Switch

The kill switch can be activated manually or by a loss breach:

safe_broker.enable_kill_switch("manual halt")
safe_broker.disable_kill_switch()

Kill-switch state is persisted in state_file, so it survives process restarts until it is manually cleared.

SafeBroker Usage

from ml4t.live import IBBroker, LiveRiskConfig, SafeBroker

raw_broker = IBBroker(port=7497)
safe_broker = SafeBroker(
    raw_broker,
    LiveRiskConfig(
        shadow_mode=True,
        max_position_value=10_000,
        max_order_value=2_500,
        max_daily_loss=500,
        max_data_staleness_seconds=60,
    ),
)

Inside Strategy.on_data(...), order placement stays synchronous because LiveEngine passes a thread-safe broker wrapper to the strategy:

def on_data(self, timestamp, data, context, broker):
    if broker.get_position("AAPL") is None:
        broker.submit_order("AAPL", 10)

    # Pending orders can also be updated through the sync wrapper.
    # broker.replace_order("ML4T-1", limit_price=189.5)

Errors

Risk violations raise RiskLimitError:

from ml4t.live import RiskLimitError

try:
    await safe_broker.submit_order_async("AAPL", 10_000)
except RiskLimitError as exc:
    print(f"blocked: {exc}")
  1. Start in shadow mode.
  2. Validate order flow, virtual positions, and runtime health.
  3. Move to paper credentials with conservative limits.
  4. Check ml4t-live status and startup reconciliation before going live.
  5. Increase size only after repeated clean starts and expected fills.