[ engine · safety ]

Compliance gate

Every rebalance plan passes through the compliance agent before any broker call. Hard / soft / warn vetos. Approve / Override / Reject buttons. Every decision lands in an immutable JSONL ledger. This is what makes LLM-driven trading safer than a vanilla 'AI trading bot.'

What it is · how it works · why it matters

[ what ]

A deterministic veto layer between the optimizer and the broker — every rebalance plan passes through hard/soft/warn rules before any broker call.

[ how ]

ComplianceBusAgent on the A2A bus. Hard vetos block submit (position cap, restricted list, wash-sale). Soft vetos require PM ack (turnover, sector concentration). Warns are informational. PM clicks Approve / Override / Reject — all three audit to immutable JSONL.

[ why ]

All veto rules are deterministic Python — they execute regardless of what the LLM proposes. Hard vetos cannot be bypassed; soft vetos require explicit PM acknowledgment; every decision lands in the immutable audit ledger.

Overview

The compliance agent runs as ComplianceBusAgent on the A2A bus (and is also directly callable from the rebalance modal). It receives RebalanceConstructed events and emits either RebalanceCleared or RebalanceBlocked. Source: src/traderspace/bus/agents/compliance.py.

Three veto levels

levelbehaviorexample
HardBlock submit entirely. Cannot be approved.Single-name exceeds max_position_pct; restricted-list name; wash-sale violation
SoftSurface warning; PM must explicitly acknowledge.Turnover > 20%; sector concentration above target
WarnLog to audit but don't gate.Low diversification; high portfolio realized vol

Built-in rules

ruleleveldefault param
max_position_pcthard0.25 (25%)
restricted_listhardempty (set per tenant)
wash_sale_window_dayshard30
turnover_pct_warnsoft0.20 (20% per rebalance)
sector_concentration_maxsoft0.45 (45% in one sector)
min_diversificationwarn4 non-zero positions
realized_vol_targetwarn0.22 (22%)

Approve / Override / Reject

Three buttons in the rebalance modal:

All three write to data/audit/rebalance_decisions.jsonl — append-only, never edited. This file is the source of truth for the DPO flywheel; never delete it.

PM pin — hold a position without trading it

Sometimes the PM agrees with the plan as a whole but disagrees with a single proposed trade — “I like my NVDA position where it is, don't touch it.” The dashboard's rebalance card lets you express that directly: every row in the Order preview has an × (Hold) button. Clicking it pins that name at its current position.

What that does, mechanically:

This is distinct from the three compliance levels above: pin is a PM-side soft override on individual orders, not a rule-driven veto on the plan. It coexists with compliance — if the rest of the plan still passes hard vetos, Approve will submit only the un-pinned orders. The compliance ledger will reflect what was actually submitted, and pinned rows show up in the plan payload so post-hoc review can see exactly which names the PM held back.

The chip strip also exposes a ↻ backtest w/o these button, which POSTs /api/backtest/run with the same name list as exclude: [...]. In backtest context there's no live position to “hold,” so exclude there has the natural meaning of “drop these from the sleeve universe entirely” — a way to ask “what would my sleeve do if these names weren't in it?” without editing the YAML.

Configuring the rules

Per-tenant overrides live in configs/compliance/<tenant_id>.yaml. Example:

max_position_pct: 0.15           # tighter than default
restricted_list:
  - "TSLA"
  - "GME"
turnover_pct_warn: 0.10           # tighter warn
sector_concentration_max: 0.35

The compliance agent merges defaults + per-tenant + per-sleeve YAML configs at solve time.

Reading the verdict

{
  "verdict": "blocked",
  "hard": [
    {"rule": "max_position_pct", "symbol": "NVDA", "value": 0.31, "limit": 0.25}
  ],
  "soft": [
    {"rule": "turnover_pct_warn", "value": 0.27, "limit": 0.20}
  ],
  "warn": [],
  "summary": "1 hard veto, 1 soft warning. Cannot submit; lower NVDA or raise cap."
}

Hard veto means cannot submit. Soft warnings need PM ack. Warns are informational.

Adding a rule

Edit src/traderspace/bus/agents/compliance.py. Each rule is a function that takes (plan, config) and returns a list of finding dicts. Add to the appropriate HARD / SOFT / WARN tuple.

def _check_max_options_notional(plan, cfg):
    notional = sum(o.notional for o in plan.options)
    if notional > cfg.get("max_options_notional", 0.0):
        return [{"rule": "max_options_notional", "value": notional, "limit": cfg["max_options_notional"]}]
    return []
HARD += (_check_max_options_notional,)

REST surface

VerbPathPurpose
POST/api/compliance/checkBody: {plan, sleeve_id}. Returns verdict + findings.
GET/api/compliance/configMerged config for the current tenant + sleeve.
GET/api/audit/decisions?since=ISODecision ledger.
NVTrader v0.1.18 · docs ·⚠ Not financial advice ·Docs home ·App