# Engine Docs

## Purpose

`fuggers_py.engine` now covers both the original synchronous research router surface and the missing reactive orchestration pieces that are useful for analytics and engine-style workflows:

- named curve construction
- single-instrument and batch pricing
- calculation-graph dependency tracking
- async market-data fanout and dirty-node propagation
- deterministic schedulers and throttling
- builder-based engine composition

The design stays Pythonic and testable. The old synchronous pricing path remains available and does not require any async setup.

## Main modules

- `fuggers_py.engine.curve_builder`
  - `CurveBuilder`
  - `ForwardRateCurve`
- `fuggers_py.engine.pricing_router`
  - `PricingRouter`
  - `PricingInput`
  - `BatchPricingResult`
- `fuggers_py.engine.funding_pricing_router`
  - `FundingPricingRouter`
  - `RepoPricingResult`
- `fuggers_py.engine.rates_pricing_router`
  - `RatesPricingRouter`
  - `RoutedFraPricingResult`
- `fuggers_py.engine.calc_graph`
  - `CalculationGraph`
  - `NodeId`
  - `NodeValue`
  - `ShardConfig`
- `fuggers_py.engine.market_data_listener`
  - `MarketDataPublisher`
  - `MarketDataListener`
  - market-data update dataclasses
- `fuggers_py.engine.scheduler`
  - `IntervalScheduler`
  - `EodScheduler`
  - `ThrottleManager`
  - `CronScheduler` when `croniter` is installed
- `fuggers_py.engine.reactive`
  - `ReactiveEngine`
- `fuggers_py.engine.builder`
  - `PricingEngineBuilder`
  - `PricingEngine`
- `fuggers_py.engine.config`
  - `EngineConfig`
- `fuggers_py.engine.coordination`
  - worker-pool and work-item coordination records
- `fuggers_py.engine.etf_pricing`
  - `EtfPricer`
- `fuggers_py.engine.portfolio_analytics`
  - `PortfolioAnalyzer`
- `fuggers_py.engine.errors`
  - engine-specific exception classes

## Synchronous workflows

The existing synchronous router remains the default research entry point:

```python
from decimal import Decimal

from fuggers_py.bonds import FixedBondBuilder
from fuggers_py.core import Date, Frequency
from fuggers_py.curves import DiscountCurveBuilder
from fuggers_py.engine import PricingRouter
from fuggers_py.data import (
    AnalyticsCurves,
    InstrumentId,
    PricingSpec,
)

settlement = Date.from_ymd(2026, 1, 15)
bond = (
    FixedBondBuilder.new()
    .with_issue_date(Date.from_ymd(2024, 1, 15))
    .with_maturity_date(Date.from_ymd(2031, 1, 15))
    .with_coupon_rate(Decimal("0.0450"))
    .with_frequency(Frequency.SEMI_ANNUAL)
    .build()
)
discount_curve = (
    DiscountCurveBuilder(reference_date=settlement)
    .add_zero_rate(1.0, Decimal("0.0350"))
    .add_zero_rate(5.0, Decimal("0.0385"))
    .build()
)
curves = AnalyticsCurves(discount_curve=discount_curve)
router = PricingRouter()
output = router.price(
    instrument=bond,
    settlement_date=settlement,
    instrument_id=InstrumentId("SCENARIO-A-FIXED"),
    market_price=Decimal("101.25"),
    pricing_spec=PricingSpec(compute_spreads=True),
    curves=curves,
)
```

Batch pricing uses the same route logic and isolates failures:

```python
from decimal import Decimal

from fuggers_py.engine import PricingInput

batch = router.price_batch(
    [
        PricingInput(instrument=bond, settlement_date=settlement, curves=curves, instrument_id="A", market_price=Decimal("101.25")),
        PricingInput(instrument=bond, settlement_date=settlement, curves=curves, instrument_id="B", market_price=Decimal("99.75")),
    ]
)
```

Funding-specific repo workflows stay separate from the bond router. `FundingPricingRouter`
prices `RepoTrade` objects directly and can enrich them with `RepoCurve` zero and forward
rates without widening the main bond-pricing path.

Tradable rates products follow the same pattern. `RatesPricingRouter` prices
`FixedFloatSwap`, `Ois`, `Fra`, and `BasisSwap` through a separate route that
consumes `AnalyticsCurves` and leaves `PricingRouter` focused on bonds.

## Reactive workflows

The reactive layer composes a calculation graph, market-data listener, schedulers, and the existing router.

Typical flow:

1. Build a `PricingEngine` with providers and optional schedulers.
2. Register pricing nodes with curve and quote dependencies.
3. Publish `QuoteUpdate`, `CurveInputUpdate`, `IndexFixingUpdate`, or other market-data events.
4. Call `process_once()` in tests or short-lived research flows.
5. Subscribe to node updates when you need fanout.

The calculation graph remains deterministic:

- dirty-node propagation is explicit
- cached values track revisions
- shard ownership is stable for a given `ShardConfig`

## Scheduling

Schedulers publish `NodeUpdate` events into async queues:

- `IntervalScheduler` for repeating intraday work
- `EodScheduler` for end-of-day style triggers
- `ThrottleManager` to suppress overly frequent updates
- `CronScheduler` for cron expressions when installed via the optional engine extra

Correctness notes:

- scheduler batches now share a single timestamp per emitted tick rather than
  creating slightly different timestamps node-by-node
- `CronScheduler` validates cron expressions at construction time and raises
  `SchedulerError` for missing optional support, empty expressions, and invalid
  cron syntax
- valid 5-field cron expressions such as `0 * * * *` are accepted when
  `croniter` is installed
- `EodScheduler` and `CronScheduler` validate that their polling intervals are
  strictly positive
- the reactive engine now unsubscribes relay queues from the owning publisher on
  shutdown and clears its internal inbound queue so start/stop/restart cycles do
  not leak subscriptions

Install the optional cron dependency with:

```bash
pip install "fuggers-py[engine]"
```

Minimal cron example:

```python
from fuggers_py.engine import CronScheduler, NodeId

scheduler = CronScheduler(
    [NodeId("curve:usd")],
    expression="0 * * * *",
    poll_interval_seconds=1.0,
)
```

The scheduler computes its first valid next-run time during construction, so
misconfigured cron expressions fail immediately instead of surfacing later from
`tick()` with raw `croniter` exceptions.

## Example usage

Reactive example:

```python
import asyncio

from fuggers_py.core import Date
from fuggers_py.data import MarketDataProvider, ReferenceDataProvider
from fuggers_py.engine import PricingEngineBuilder

async def main() -> None:
    reactive = (
        PricingEngineBuilder.new()
        .with_market_data_provider(MarketDataProvider())
        .with_reference_data_provider(ReferenceDataProvider())
        .with_settlement_date(Date.from_ymd(2026, 1, 15))
        .build_reactive()
    )
    await reactive.start()
    await reactive.stop()


asyncio.run(main())
```

Runnable example scripts:

- [examples/07_engine/01_curve_builder_and_batch_pricing_router.py](../examples/07_engine/01_curve_builder_and_batch_pricing_router.py)
- [examples/07_engine/02_reactive_market_data_listener_and_scheduling.py](../examples/07_engine/02_reactive_market_data_listener_and_scheduling.py)
- [examples/07_engine/03_end_to_end_research_pipeline.py](../examples/07_engine/03_end_to_end_research_pipeline.py)
