Metadata-Version: 2.4
Name: pyfortis
Version: 0.0.2
Summary: Config-driven risk engine for trading systems
Author-email: Optophi <contact@optophi.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/optophi/pyfortis
Project-URL: Documentation, https://optophi.github.io/pyfortis/
Project-URL: Repository, https://github.com/optophi/pyfortis
Project-URL: Issues, https://github.com/optophi/pyfortis/issues
Project-URL: Changelog, https://github.com/optophi/pyfortis/blob/main/CHANGELOG.md
Project-URL: Contributing, https://github.com/optophi/pyfortis/blob/main/CONTRIBUTING.md
Keywords: risk,trading,limits,circuit-breaker,finance,pre-trade
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml<7.0,>=6.0
Requires-Dist: pydantic<3.0,>=2.7
Provides-Extra: metrics
Requires-Dist: numpy<3.0,>=1.24; extra == "metrics"
Requires-Dist: scipy<2.0,>=1.10; extra == "metrics"
Provides-Extra: docs
Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
Requires-Dist: mkdocs-material<10.0,>=9.5; extra == "docs"
Requires-Dist: mkdocstrings[python]<1.0,>=0.27; extra == "docs"
Requires-Dist: pymdown-extensions<12.0,>=10.0; extra == "docs"
Requires-Dist: mike<3.0,>=2.1; extra == "docs"
Provides-Extra: ci
Requires-Dist: pyfortis[docs,metrics]; extra == "ci"
Requires-Dist: pytest>=8.0; extra == "ci"
Requires-Dist: pytest-asyncio>=0.24; extra == "ci"
Requires-Dist: pytest-cov>=5.0; extra == "ci"
Requires-Dist: pytest-timeout>=2.3; extra == "ci"
Requires-Dist: hypothesis>=6.80; extra == "ci"
Requires-Dist: ruff<1.0,>=0.9; extra == "ci"
Requires-Dist: mypy>=1.11; extra == "ci"
Requires-Dist: types-PyYAML>=6.0; extra == "ci"
Provides-Extra: all
Requires-Dist: pyfortis[metrics]; extra == "all"
Provides-Extra: dev
Requires-Dist: pyfortis[ci,docs,metrics]; extra == "dev"
Requires-Dist: pre-commit>=3.8; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5.1; extra == "dev"
Requires-Dist: pip-audit>=2.7; extra == "dev"
Requires-Dist: hypothesis>=6.80; extra == "dev"
Dynamic: license-file

# 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+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Ruff](https://img.shields.io/badge/lint-ruff-261230.svg)](https://github.com/astral-sh/ruff)

---

## 60 seconds to a verdict

```bash
pip install pyfortis
pyfortis init          # writes policies/risk_policy.yaml + a golden PolicyTest
```

**1. The policy `init` wrote** (`policies/risk_policy.yaml`):

```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:**

```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:**

```bash
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.

```text
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:

```yaml
- 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:

```yaml
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](docs/getting-started/quickstart.md)** — 60 seconds to a verdict
- **[Why PyFortis](docs/getting-started/why-pyfortis.md)** · **[Concepts](docs/getting-started/concepts.md)**
- **Tutorials** — [your first policy](docs/tutorials/first-policy.md) · [agents and approvals](docs/tutorials/agents-and-approvals.md) · [circuit breakers](docs/tutorials/breakers.md)
- **How-to** — [custom checks](docs/how-to/extend-with-custom-checks.md) · [compose with `extends`](docs/how-to/compose-policies-with-extends.md) · [migrate a legacy config](docs/how-to/migrate-legacy-config.md) · [policy tests in CI](docs/how-to/run-policy-tests-in-ci.md)
- **Reference** — [policy schema](docs/reference/policy-schema.md) · [checks](docs/reference/checks.md) · [calculators](docs/reference/calculators.md) · [breakers](docs/reference/breakers.md) · [escalation](docs/reference/escalation.md) · [CLI](docs/reference/cli.md) · [wire contract](docs/reference/wire-contract.md) · [error codes](docs/reference/error-codes.md) · [glossary](docs/reference/glossary.md)
- **Design** — [blueprint](docs/design/blueprint.md) · [core contracts](docs/design/core-contracts.md) (release 1 as shipped; baseline for release 2)
- **Examples** — [`examples/`](examples/README.md), 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](BACKLOG.md).

## Development

```bash
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](LICENSE).

## Links

- **Repository**: [GitHub](https://github.com/optophi/pyfortis)
- **Issues**: [GitHub Issues](https://github.com/optophi/pyfortis/issues)
- **Contributing**: [CONTRIBUTING.md](CONTRIBUTING.md) · [AGENTS.md](AGENTS.md) · [Security](SECURITY.md) · [Code of conduct](CODE_OF_CONDUCT.md)
- **Architecture**: [ARCHITECTURE.md](ARCHITECTURE.md)
