Skip to main content

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.

Python 3.11+ License: MIT Ruff


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, extends composition, packaged regulatory baselines, expiring approved overrides, and PolicyTest files 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 RiskContext snapshot;
  • 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/

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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyfortis-0.0.2.tar.gz (636.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyfortis-0.0.2-py3-none-any.whl (315.6 kB view details)

Uploaded Python 3

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

Hashes for pyfortis-0.0.2.tar.gz
Algorithm Hash digest
SHA256 87e57599b28e9de2fa67eb363c52f11aae3c248cab76ec94e226f7ce8077e79b
MD5 12accbce5fd194d3841a2cfee6a44b98
BLAKE2b-256 20fbc0fc2023333c3de2aaf6a8847beb7ff47812fdc22860f66be967c14a8282

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyfortis-0.0.2.tar.gz:

Publisher: publish.yml on optophi/pyfortis

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pyfortis-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f5a67527be2e1c02751ec0f5dbd144b98bd82925d71d4f7cf8737a9171a2c07a
MD5 72076e346c1f81dc947c09009b78edb3
BLAKE2b-256 7e254953a927be949db9c5bfd49a3553df538d1480d930fc3b2a07a8447ad2a2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyfortis-0.0.2-py3-none-any.whl:

Publisher: publish.yml on optophi/pyfortis

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.0.2 This release

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page