PyFortis
Config-driven risk control plane for trading systems: pre-trade limits, circuit breakers, risk metrics — deterministic, explainable, fail-closed. Define the rules in YAML. Get a verdict, and the exact repair that would make it pass.
60 seconds to a verdict
pip install pyfortis
pyfortis init # writes policies/risk_policy.yaml + a golden PolicyTest
1. The policy init wrote (policies/risk_policy.yaml):
api_version: pyfortis.io/v1
kind: RiskPolicy
metadata: { name: starter, namespace: examples/minimal, version: "0.1.0" }
spec:
mode: shadow # shadow | enforce — REQUIRED, no default
base_currency: USD
limits:
- { name: max_order_notional, type: order_notional, scope: order, params: { max: 50000 }, severity: CRITICAL }
- { name: fat_finger, type: price_collar, scope: order, params: { max_deviation_pct: 0.05, reference: last }, severity: CRITICAL }
circuit_breakers:
- name: daily_loss
type: daily_pnl
scope: portfolio
params: { max_loss_pct_nav: 0.02 }
trip: { action: halt_trading }
rearm: manual
rearm_roles: [risk_manager]
2. Run this Python:
from datetime import UTC, datetime
from decimal import Decimal
from pyfortis import Account, Order, RiskContext, RiskEngine, Side
engine = RiskEngine.from_yaml("policies/risk_policy.yaml")
context = RiskContext(
as_of=datetime.now(UTC),
account=Account(nav=Decimal("10000000")),
prices={"AAPL": Decimal("190.00")},
)
order = Order(order_id="o-1", symbol="AAPL", side=Side.BUY, quantity=Decimal("400"))
result = engine.evaluate(order, context)
print(result.verdict) # APPROVED_WITH_WARNING — shadow lets it through
print(result.shadow_verdict) # REJECTED — what enforce would have said
print(result.headroom) # 263 — the quantity that would pass
print(result.failed_checks[0].fix) # reduce quantity to <= 263
The starter policy ships in mode: shadow, so the breach is recorded rather
than blocked. Flip spec.mode to enforce and the same order comes back
REJECTED.
That's it. No server, no database, no migrations, no broker connection.
3. Prove it before you trust it:
pyfortis validate policies/risk_policy.yaml # would it enforce what it says?
pyfortis lint policies/risk_policy.yaml # is it wise?
pyfortis test policies/tests/*.policy_test.yaml # does it decide what you think?
validate imports the built-in registries, merges extends, and holds every
limit to its check type's params model — so a misspelt type or param is a
load-time error naming what it could have meant, not a rejection at 09:31.
What it does
- Pre-trade gating — price collars, notional and quantity caps, position and concentration limits, buying power, restricted lists, session windows, message rates, duplicate and self-trade prevention, short-sale locates, actor budgets, leverage.
- Post-trade assessment — exposure, liquidity, VaR-family metric limits, stress losses, and the same limit vocabulary against the actual book.
- Circuit breakers — stateful, scoped, with cooldown and role-gated re-arm: daily P&L, drawdown, volatility, reject storms, stale data, execution throttles, breach counts, consecutive losses.
- Governance — versioned policy manifests,
extendscomposition, packaged regulatory baselines, expiring approved overrides, andPolicyTestfiles that run in CI. - Agent safety — actor profiles that scale limits down, cap daily budgets, route to a human above a threshold, and treat a warning as a stop.
What it does not do
PyFortis is a control plane, not a trading system. It deliberately does not:
- place, amend, or cancel orders — it returns a verdict; your execution layer acts on it;
- hold the book — positions, prices, and P&L are passed in as a
RiskContextsnapshot; - generate signals or size positions — that is an optimiser's job;
- put a model in the decision path — every verdict is arithmetic over declared limits. Nothing in the gate is learned, sampled, or prompted.
Grow as you need
Each rung is optional. Take the lowest one that answers your question.
Rung 1 — Library pip install pyfortis ✅ release 1
RiskEngine.evaluate() · headroom() · assess() · pure, stateless, no infra
Rung 2 — Monitor pip install pyfortis ✅ release 1
RiskMonitor · in-memory book, activity windows, live breaker state, hooks
Rung 3 — Orchestrator pip install pyfortis ✅ release 1
RiskOrchestrator over RiskStore protocols · in-memory store
Persistent SQLite / Postgres / Mongo stores + decision log ◻ release 2
Rung 4 — Service (the [api] extra lands with it) ◻ release 2
FastAPI gate service on :8008 · pyfortis.cfg
MCP server · events and sinks · worker ◻ release 3
Next.js UI on :3008 ◻ release 4
Release 1 is the whole core: the engine, every check, calculator and breaker,
the config layer, the monitor, the orchestrator with the in-memory store, the
CLI, and the PolicyTest runner. The examples/ ladder walks the same rungs.
Concepts
| Concept | What it is |
|---|---|
| Policy | A versioned RiskPolicy manifest: limits, metrics, breakers, actors, escalation. The unit of review and deployment. |
| Limit | One named rule of a declared type, with params, a scope, a selector, and a severity. |
| Stage | When the limit runs: pre_trade (hypothetical post-fill book), post_trade (actual book), or both. |
| Breaker | Stateful kill-switch: trips, applies a trip.action, waits a cooldown, re-arms automatically or by a role-holding human. |
| Escalation | Severity → actions (log, notify, block_order, require_approval, cancel_open_orders, flatten_positions, halt_trading). |
| Actor profile | A posture for a human, agent, or system caller: limit_scale, require_approval_above, budgets, deny_on. |
| Headroom | The largest additional quantity that would still pass — a rejection you can act on. |
| Shadow vs enforce | shadow evaluates and records but never blocks (shadow_verdict holds what enforce would have said). enforce blocks. |
| Override | An approved, scoped, expiring replacement for one limit's params, with approved_by, reason, and a ticket. |
Configuration-Driven Engineering
The rules are data, not code:
- name: max_position_size
type: position
stage: both # gate the order AND audit the book
scope: instrument
params:
max_long: 10000
max_short: 5000
params_by: { key: symbol, values: { AAPL: { max_long: 15000 } } }
thresholds: { warn_at: 0.8 }
severity: CRITICAL
tags: [reg:rts6-art17]
A risk manager can read it. A reviewer can diff it. CI can test it. And
policies reference checks, calculators, breakers, and handlers by name —
never by import path — so a policy file can never make PyFortis execute
arbitrary code. Names bind through a registry or an operator-owned Catalog.
Compose instead of copying:
spec:
mode: enforce
extends:
- pyfortis://contrib/equity/us_cash_baseline@1.0.0 # regulatory baseline pack
- pyfortis://contrib/agents/ramp_up@1.0.0 # agent actor profiles
CLI
| Command | Purpose |
|---|---|
pyfortis init |
Scaffold a policy and a golden PolicyTest. |
pyfortis validate <paths> |
Envelope, schema, refs — and, under strict_mode, check types and their params. |
pyfortis lint <paths> |
Gaps, dead limits, risky defaults. --fail-on warning|error. |
pyfortis diff <a> <b> |
What changed — and --fail-on-loosening when it widens. |
pyfortis test <paths> |
Run PolicyTest cases (build the engine too). -k to filter. |
pyfortis schema <kind> |
JSON Schema for a manifest kind. |
pyfortis capabilities |
Registered checks, calculators, breakers, handlers. |
pyfortis gate --policy P --order-json J |
Evaluate one order; exit 0 only when the verdict passes. |
pyfortis headroom --policy P --symbol S --side buy |
How much is allowed? |
pyfortis assess --policy P --context-file F |
Post-trade report for a book. |
pyfortis migrate <paths> |
Rewrite a legacy config as a manifest. |
pyfortis version |
The installed version. |
Exit codes: 0 success · 1 your policy or your run needs fixing (that
includes a rejected gate, a breached assess and an unreadable file) · 2
argparse rejected your command line. Every read command takes --json, and
every path argument is a file — use a shell glob for "everything here".
Documentation
Full docs: https://optophi.github.io/pyfortis/
- Quickstart — 60 seconds to a verdict
- Why PyFortis · Concepts
- Tutorials — your first policy · agents and approvals · circuit breakers
- How-to — custom checks · compose with
extends· migrate a legacy config · policy tests in CI - Reference — policy schema · checks · calculators · breakers · escalation · CLI · wire contract · error codes · glossary
- Design — blueprint · core contracts (release 1 as shipped; baseline for release 2)
- Examples —
examples/, a runnable ladder from a single gate call to policy packs.
Status and roadmap
Release 1, "core truth", is the stateless core and the config layer. It is the entire list under rungs 1–3 above. What is not in release 1:
| Release | Adds |
|---|---|
| 2 — persistence and service | SQLite/Postgres/Mongo stores, breaker-state and override persistence, decision log, FastAPI gate service (:8008), pyfortis.cfg. |
| 3 — integration | MCP server, events and sinks, worker, and the sibling seams (pyoptima, pyactuator, pystator, pygubernator, pycustodian). |
| 4 — surface | Next.js UI (:3008): policy editor, decision explorer, breaker board, headroom inspector. |
Card-by-card detail: BACKLOG.md.
Development
git clone https://github.com/optophi/pyfortis && cd pyfortis
uv pip install -e ".[dev]" # or: pip install -e ".[dev]" — extras: metrics, docs, ci, all, dev
pytest
./scripts/ci.sh # lint + types + tests + docs, exactly as CI runs them
License
MIT — see LICENSE.
Links
- Repository: GitHub
- Issues: GitHub Issues
- Contributing: CONTRIBUTING.md · AGENTS.md · Security · Code of conduct
- Architecture: ARCHITECTURE.md
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyfortis-0.0.2.tar.gz.
File metadata
- Download URL: pyfortis-0.0.2.tar.gz
- Upload date:
- Size: 636.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
87e57599b28e9de2fa67eb363c52f11aae3c248cab76ec94e226f7ce8077e79b
|
|
| MD5 |
12accbce5fd194d3841a2cfee6a44b98
|
|
| BLAKE2b-256 |
20fbc0fc2023333c3de2aaf6a8847beb7ff47812fdc22860f66be967c14a8282
|
Provenance
The following attestation bundles were made for pyfortis-0.0.2.tar.gz:
Publisher:
publish.yml on optophi/pyfortis
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyfortis-0.0.2.tar.gz -
Subject digest:
87e57599b28e9de2fa67eb363c52f11aae3c248cab76ec94e226f7ce8077e79b - Sigstore transparency entry: 2539390609
- Sigstore integration time:
-
Permalink:
optophi/pyfortis@9c237620b3c8b5220a798594e613acf077e2c9ff -
Branch / Tag:
refs/tags/v0.0.2 - Owner: https://github.com/optophi
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9c237620b3c8b5220a798594e613acf077e2c9ff -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyfortis-0.0.2-py3-none-any.whl.
File metadata
- Download URL: pyfortis-0.0.2-py3-none-any.whl
- Upload date:
- Size: 315.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f5a67527be2e1c02751ec0f5dbd144b98bd82925d71d4f7cf8737a9171a2c07a
|
|
| MD5 |
72076e346c1f81dc947c09009b78edb3
|
|
| BLAKE2b-256 |
7e254953a927be949db9c5bfd49a3553df538d1480d930fc3b2a07a8447ad2a2
|
Provenance
The following attestation bundles were made for pyfortis-0.0.2-py3-none-any.whl:
Publisher:
publish.yml on optophi/pyfortis
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyfortis-0.0.2-py3-none-any.whl -
Subject digest:
f5a67527be2e1c02751ec0f5dbd144b98bd82925d71d4f7cf8737a9171a2c07a - Sigstore transparency entry: 2539390868
- Sigstore integration time:
-
Permalink:
optophi/pyfortis@9c237620b3c8b5220a798594e613acf077e2c9ff -
Branch / Tag:
refs/tags/v0.0.2 - Owner: https://github.com/optophi
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9c237620b3c8b5220a798594e613acf077e2c9ff -
Trigger Event:
push
-
Statement type: