Metadata-Version: 2.3
Name: fiqua
Version: 0.4.0
Summary: A quant-finance library on top of numanlib: equity option pricing (closed-form and PDE), Greeks, implied vol, and multi-instrument trade valuation.
Author: Fabio Nicotra
Author-email: Fabio Nicotra <nicotra.fabio@icloud.com>
Requires-Dist: numanlib[interactive]==0.6.0
Requires-Dist: scipy
Requires-Dist: requests ; extra == 'providers'
Requires-Dist: yfinance>=1.6.0 ; extra == 'providers'
Requires-Dist: plotly>=5.0.0 ; extra == 'viz'
Requires-Python: >=3.12
Provides-Extra: providers
Provides-Extra: viz
Description-Content-Type: text/markdown

# fiqua

[![Test](https://github.com/FabioNicotra/fiqua/actions/workflows/test.yml/badge.svg)](https://github.com/FabioNicotra/fiqua/actions/workflows/test.yml)

A small quant-finance library built on top of [`numanlib`](https://pypi.org/project/numanlib/):
equity option pricing (closed-form and PDE), Greeks, implied vol, and valuing a batch of trades
end to end.

It's a portfolio project. The point was to build something the way a real pricing library gets
built, in layers, rather than to cram in as much surface area as possible. Equities came first, and
the core is set up so a second asset class (rates, under a short-rate model) can be added later
without reshaping it. See [Scope](#scope).

## What's here

`Stock`, `EuropeanOption`, and `CompositeInstrument` for multi-leg payoffs like a straddle, priced
and summed as one thing. Two pricing engines for the same model: `BlackScholesEngine` (closed-form)
and `BlackScholesPDEEngine` (a finite-difference solve of the Black-Scholes PDE, via numanlib's
parabolic PDE solver), checked against each other on the same contract. Greeks come back analytic
where an engine has a closed form for them and by bump-and-revalue where it doesn't, with per-metric
routing if you want to force one method over the other. Implied vol is recovered from a market
option price with Newton/Secant root-finding, on either engine. Day-count conventions
(Actual/365, Actual/360, 30/360) turn calendar dates into year fractions.

There's also a full pipeline for booked trades: `Trade` (a deal) resolves to a `Position` (which
instrument this is), gets routed to a pricing engine, priced under a `Model`, and comes back as one
result per trade, batched through `CalculationEngine`. Nothing below `Trade` is hardcoded to
equities — it's all dispatched through registries that `fiqua.equities` populates on import.

One thing that's the same everywhere: an `Instrument` has no `.price()` method. You always go
through an engine — `engine.add([...])` to queue requests, `engine.run()` to price the whole batch
in one round trip and get back one `PricerResult` per request. If one request fails, it fails on its
own; it doesn't take the rest of the batch down with it.

## Example: pricing an option two ways

```python
from datetime import date

from fiqua.core import Metric, RateCurve, Tenor
from fiqua.equities import BlackScholesEngine, BlackScholesPDEEngine, EuropeanOption, MarketData, PricingRequest, Stock, StockQuote, model_registry

stock = Stock(symbol="ACME", currency="USD")
option = EuropeanOption(underlying=stock, strike=100.0, maturity=1.0, option_type="call")
rate_curve = RateCurve(curve_id="USD", points={Tenor.M1: 0.05, Tenor.Y1: 0.05, Tenor.Y10: 0.05}, as_of=date.today())
market = MarketData(quotes={"ACME": StockQuote(spot=100.0, volatility=0.2, currency="USD")}, rate_curves={"USD": rate_curve})

closed_form = BlackScholesEngine(market, model_registry)
pde = BlackScholesPDEEngine(market, model_registry)

for engine in (closed_form, pde):
    engine.add([PricingRequest(instrument=option, metrics=[Metric.PV, Metric.DELTA, Metric.VEGA])])
    print(engine.run()[0])
```

Same contract, same market snapshot, two different numerical methods. See
[`demo/european_option_payoff.ipynb`](demo/european_option_payoff.ipynb) for the full walkthrough,
including payoff diagrams and a look at the solved PDE grid.

## Example: booking trades and valuing a batch

A booking system won't hand you an `EuropeanOption` object. It hands you a `product_type`/`terms`
pair whose shape you don't know until you read it, plus a quantity. `CalculationEngine` takes that
the rest of the way:

```python
from datetime import date

from fiqua.core import CalculationEngine, Metric, RateCurve, Tenor, Trade, Valuation
from fiqua.equities import EQUITY_OPTION_PRODUCT_TYPE, MarketData, StockQuote

rate_curve = RateCurve(curve_id="USD", points={Tenor.M1: 0.05, Tenor.Y1: 0.05, Tenor.Y10: 0.05}, as_of=date.today())
market = MarketData(quotes={"ACME": StockQuote(spot=100.0, volatility=0.2, currency="USD")}, rate_curves={"USD": rate_curve})

call = Trade(
    trade_id="trade_001",
    product_type=EQUITY_OPTION_PRODUCT_TYPE,
    terms={"underlying": "ACME", "currency": "USD", "strike": 100.0, "maturity": 1.0, "option_type": "call"},
    quantity=10,
)

calc = CalculationEngine(market)
calc.add([Valuation(trade=call, metrics=[Metric.PV, Metric.VEGA, Metric.DELTA])])

for trade_id, result in calc.run().items():
    print(f"{trade_id}: {result}")
```

That's four separately testable steps under the hood: `ProductResolver` turns the trade into a
`Position`, `PricingEngineRegistry` picks the engine, `ModelRegistry` supplies the dynamics, and
`CalculationEngine` assembles the result. `fiqua.core` never imports an equities class to do any of
this; `fiqua.equities` registers itself onto the same shared registries at import time. See
[`demo/pricing_pipeline_walkthrough.ipynb`](demo/pricing_pipeline_walkthrough.ipynb) for the full thing,
including a multi-leg strategy booked as a single trade.

## Architecture

**Delegation.** A `PricingEngineRegistry` maps `(product_type, model_key)` to a default engine plus
optional named alternates. `BlackScholesEngine` and `BlackScholesPDEEngine` sit under the exact same
key, because they price the same product under the same dynamics and differ only in numerical
method. Routing can go finer than "pick an engine for this trade": `PricingRequest`/`Valuation`
carry `routing: Dict[Metric, MetricRoute]`, so within one trade PV can come from the closed-form
engine while delta gets read off a solved PDE grid, or any single metric can be forced onto
bump-and-revalue no matter which engine would otherwise supply a closed form for it. Metric, engine,
and numerical method are three separate knobs, not one flag standing in for all three.

`CalculationEngine.run()` doesn't loop over trades one at a time. It resolves every trade first,
splits each one's requested metrics across whichever engines their routes point to, runs a single
`add()`/`run()` per engine so each engine sees its whole share of the batch at once, then stitches
each trade's results back into one row (a trade fails whole if any engine contributing to it fails,
rather than coming back half-filled). An engine only gets built the first time a trade needs it, and
after that it's reused for the rest of the batch — one registration, one live instance.

**Caching.** Solving the Black-Scholes PDE by finite differences is the slow part of this library:
roughly 10 ms a solve, against about 5 µs for a closed-form price, and it's the one worth not
repeating. What actually identifies a solve is a `SolveSpec`: a frozen record of the mesh, the
dynamics, and the contract terms, built once before anything gets solved. The cache is nothing
fancier than `Dict[SolveSpec, SolvedGrid]`, bounded to 512 entries (about 88 MB) with LRU eviction.
Every input the solver reads is in that key, so a change to the market or the solver settings just
produces a different spec rather than handing back a grid solved under the old one — there's no
invalidation logic because there's nothing to invalidate, only keys that stop matching. On a batch
that reprices the same contract repeatedly this collapses twelve solves at 119 ms down to one at
10 ms; on a batch of twelve distinct strikes there's nothing to share and the lookup costs nothing
extra. A `CalculationEngine` keeps this cache alive as long as it keeps the engines it built, and
its `clear_cache()` clears it across all of them at once.

## Scope

`fiqua.core` doesn't assume equities. `Instrument`, `PricingEngine`, `Model`, `Trade`,
`CalculationEngine`, and the registries wiring them together are all asset-class-agnostic; anything
that only makes sense for a stock or option (spot, volatility, `BlackScholesModel` itself) lives one
layer up, in `fiqua.equities`. Equities is just the first asset class built here, not the only one
the core is meant to hold — a second one, interest-rate products under a short-rate model, is on the
roadmap and would be the real test of whether that boundary holds up.

## Docs

Most non-obvious decisions have a writeup under [`docs/`](docs/), explaining why something is shaped
the way it is rather than just restating what it does:

| Doc | Covers |
| --- | --- |
| [`docs/architecture.md`](docs/architecture.md) | The framework: instruments, trades and resolution, pricing engines, the Model/Engine split, results and metrics, registries, per-metric routing, the batch orchestrator, market data and providers, date/rate primitives |
| [`docs/models.md`](docs/models.md) | The quant/numerical content of the equities asset class: Black-Scholes closed form, dividend yield, Greeks by bump-and-reprice, implied volatility, the PDE mesh, PDE grid access, the solve cache |
| [`docs/quant_library_architecture.md`](docs/quant_library_architecture.md) | The roadmap-level framing the two above implement |
| [`docs/repr-design.md`](docs/repr-design.md) | The `__repr__` convention every `src/` class follows |
| [`docs/core-layout.md`](docs/core-layout.md) | The `fiqua.core` file/folder layout |

The reasoning behind everything above is also cross-referenced from [`CLAUDE.md`](CLAUDE.md).

## Relationship to numanlib

numanlib owns the generic numerical methods: the PDE solver, root finding, interpolation. fiqua
owns the financial concepts built on top of them: what an option is, what market data it needs, how
a Greek is defined, how a batch of trades gets valued.

## Installation

```bash
pip install fiqua
```

## Development

```bash
uv sync
uv run pytest
```

The notebooks under [`demo/`](demo/) double as executable documentation. Each one walks through a
slice of the library end to end and is kept runnable against the current code.
