Deterministic, pure-functional capital protection evaluator for quantitative trading. Designed to run directly inside order-routing hot paths,
verdict-riskverifies trading signals against risk parameters held in memory. The pure evaluation path does no I/O; measured single-call latency is in the low tens of microseconds on commodity hardware (see Performance).
Traditional trade risk systems suffer execution drift, concurrency race conditions, network-induced latency spikes. verdict-risk solves these problems by splitting risk evaluation into a pure, mathematical computation layer separate from state management and network I/O.
| Layer | Responsibility | Latency Budget |
|---|---|---|
| Pure Math (this package) | Drawdown gates, position limits, cluster exposure cap, Kelly sizing | target < 50 µs (measured p99 ≈ 23 µs, see below) |
| State Management | Redis/Postgres persistence, audit logging | < 1 ms |
| Network I/O | Broker APIs, market data feeds | Variable |
| Feature | Description |
|---|---|
| Low-allocation hot path | msgspec structs with gc=False for positions/decisions; about one small allocation (the returned RiskDecision) per call |
| Deterministic gates | Same inputs → same outputs, always |
| Microsecond-scale latency | Pure Python hot path, no locks or I/O; reproducible with verdict-risk-benchmark |
| Stateless gates | Drawdown, position, cluster cap, Kelly — no external deps |
| Stateful desk controls | Daily loss limits, sector exposure, factor models (optional Redis) |
| OpenTelemetry native | Spans, metrics, logs for every evaluation |
| Property-based testing | Hypothesis fuzzing + formal verification of mathematical properties |
┌─────────────────────────────────────────────────────────────────────────────┐
│ VERDICT RISK │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ STATELESS RISK GATES (Pure Math) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Drawdown │ │ Position │ │ Cluster │ │ Kelly │ │ │
│ │ │ Gate │ │ Limit Gate │ │ Cap Gate │ │ Sizing │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ STATEFUL DESK CONTROLS (Optional) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Daily Loss │ │ Sector │ │ Factor │ │ Paper │ │ │
│ │ │ Limit │ │ Exposure │ │ Model │ │ Execution │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
The package is not published to PyPI yet. Install from source:
git clone https://github.com/mrnicholasbcarter-code/verdict-risk.git
cd verdict-risk
uv sync --extra dev # or: pip install -e ".[dev]"
uv run verdict-risk-benchmark --iterations 1000Requires Python 3.10+ (3.11 recommended).
The distribution name in pyproject.toml is llm-gate-risk; the Python import
namespace is trade_risk_engine. See the package boundary policy
before relying on a future verdict_risk import.
from trade_risk_engine import Position, RiskAuthority, RiskContext
decision = RiskAuthority.evaluate_trade(
ctx=RiskContext(max_daily_drawdown_pct=0.10),
daily_realized_pnl=-5_000,
equity=100_000,
target_family="BTC-USD",
proposed_cost=1_000,
open_positions=[
Position(
ticker="BTC-USD-1",
family="BTC-USD",
cost_basis=500,
current_value=500,
is_resolved=False,
)
],
)
assert decision.approvedfrom trade_risk_engine import KillSwitch, RiskAuthority
authority = RiskAuthority(kill_switch=KillSwitch())
# Stateful controls are opt-in and remain local to the authority.trade_risk_engine.load_risk_config() loads and validates an operational
desk-controls file, defaulting to ~/.verdict/risk_config.yaml:
# ~/.verdict/risk_config.yaml
drawdown:
max_daily_drawdown_pct: 0.05
concentration:
max_correlated_exposure: 200.0
max_cluster_usd: 20.0
expected_value:
min_expected_value: 0.01
consecutive_losses:
max_consecutive_losses: 5
time_window_seconds: 3600.0
kelly:
conservative_fraction: 0.25load_risk_config() returns a flat dict. The keys
max_daily_drawdown_pct, max_correlated_exposure, min_expected_value,
consecutive_loss_limit, and consecutive_loss_window_minutes are
RiskContext-compatible (consecutive_loss_window_minutes is derived from
time_window_seconds / 60). max_cluster_usd and kelly_conservative_fraction
are not RiskContext fields — they feed ClusterCapContext and
kelly_fraction() respectively. Missing files, invalid YAML, wrong-typed
fields, and out-of-range values each raise (FileNotFoundError,
yaml.YAMLError, TypeError, ValueError); unknown top-level keys only warn.
Every evaluation emits OpenTelemetry spans:
from opentelemetry import trace
from trade_risk_engine import RiskAuthority, RiskContext
tracer = trace.get_tracer("verdict-risk")
with tracer.start_as_current_span("risk.evaluation") as span:
result = RiskAuthority.evaluate_trade(
ctx=RiskContext(max_daily_drawdown_pct=0.10),
daily_realized_pnl=-5_000,
equity=100_000,
target_family="BTC-USD",
proposed_cost=1_000,
open_positions=[],
)
span.set_attribute("risk.approved", result.approved)
span.set_attribute("risk.reason", result.reason_code)# Run tests with property-based fuzzing
pytest tests/ -v --hypothesis-show-statistics
# Benchmarks
python -m trade_risk_engine.benchmark --iterations 1000 --warmup-iterations 100| Property | Test |
|---|---|
| Drawdown gate monotonicity | drawdown(a) >= drawdown(b) if a <= a) |
| Kelly optimality | f* = (bp - q)/b matches analytic solution (tests/test_kelly.py) |
| Cluster cap threshold correctness | approve at/under cap, reject over cap (tests/test_cluster_cap.py) |
| Position limit idempotence | gate(x); gate(x) == gate(x) |
Measured with the bundled benchmark, which times RiskAuthority.evaluate_trade
(the full stateless gate path) end to end:
uv run verdict-risk-benchmark --iterations 20000 --warmup-iterations 2000| Path | p50 | p95 | p99 |
|---|---|---|---|
RiskAuthority.evaluate_trade |
9.6 µs | 9.9 µs | 23.3 µs |
Measured 2026-09-24, CPython 3.13, Linux x86-64 workstation. Figures are
descriptive, not a guarantee: rerun on your hardware before using them as an
operational bound. Allocation, measured with tracemalloc over 1,000 warm calls,
is about 49 bytes (one object, the returned RiskDecision) per call. Individual
gates (cluster cap, Kelly sizing) are not benchmarked separately.
- Verdict Core: https://github.com/mrnicholasbcarter-code/verdict-core
- Verdict Backtest: https://github.com/mrnicholasbcarter-code/verdict-backtest
- RuVector: https://github.com/ruvnet/ruvector
- Ruflo: https://github.com/ruvnet/claude-flow
Verdict Edge is on the roadmap; no such repository exists yet, so the prior link (404) has been removed rather than fixed.
- Ported the coin-cluster exposure cap from
kalshi-trader/v40/risk.pyas a statelessClusterCapContext/evaluate_cluster_cap()gate, wired intoRiskAuthority.evaluate_trade/evaluate_with_stateas an optional kwarg. - Ported Kelly-criterion sizing from
kalshi-trader/v40/portfolio/kelly.pyastrade_risk_engine.kelly_fraction(), generalized with aconservative_fractionparameter (default0.25). - Added
trade_risk_engine.load_risk_config(), a validated YAML loader for~/.verdict/risk_config.yamldesk controls. - Corrected the GitHub org on the Verdict Core / Verdict Backtest links
(previously pointed at a non-existent
verdict/org) and removed the dead Verdict Edge link. - Removed the "Correlation gate" / "Kelly sizing" rows from the performance
benchmark table — no real benchmark had been run for either, and the
numbers were fabricated. They will be re-added once
trade_risk_engine.benchmarkactually measures them. - Replaced the "Correlation gate symmetry" math-property claim (which never applied to a cluster cap) with a cluster-cap threshold-correctness property, and confirmed the Kelly-optimality property against the ported formula.
MIT — see LICENSE