Date closed: 2026-05-27
Tag: pquantlib-phase3-complete @ aacc2c2
Predecessor: pquantlib-phase2-complete @ b5d2519
Test count: 922 → 1284/0/0 (+362). pyright + ruff clean.
Design spec: phase3-design.md. Plan: phase3-plan.md.
| Cluster | Mode | Commits | Tests added | Coverage |
|---|---|---|---|---|
| L3-A pilot | sequential, 6 stages | 7 | +115 | Settings.evaluation_date observable wired; 4 retroactive L1/L2 cleanups (Schedule null-effective-date fallback, TermStructure moving-mode, RelativeDateBootstrapHelper, SmileSection floating-mode); Payoff hierarchy (PlainVanilla + Cash/AssetOrNothing + Gap + SuperFund + SuperShare); Exercise hierarchy (European/American/Bermudan); Instrument + PricingEngine + GenericEngine[ArgsT, ResultsT]; BlackFormula family (lognormal + bachelier + implied-vol + Black-vega); Option + OneAssetOption + cross-cluster Protocols (Instrument / PricingEngine / StochasticProcess) |
| L3-B bonds | parallel | 6 | +81 | Bond abstract + FixedRateBond + ZeroCouponBond + FloatingRateBond + AmortizingFixedRateBond + Callability + CallabilitySchedule + DiscountingBondEngine + BondForward; extended CashFlows with leg-walking helpers |
| L3-C swaps + carry-over closures | parallel | 4 | +41 | Swap abstract + FixedVsFloatingSwap + VanillaSwap + OvernightIndexedSwap + ZeroCouponSwap + make_vanilla_swap + make_ois + DiscountingSwapEngine. Closed L2-C carry-overs: SwapRateHelper.implied_quote, OISRateHelper.implied_quote, SwapIndex.forecast_fixing/underlying_swap. Added par-coupons forecast to IborCouponPricer |
| L3-D options + processes | parallel | 4 | +97 | StochasticProcess + StochasticProcess1D + EulerDiscretization + GeneralizedBlackScholesProcess + BlackScholesProcess + BlackProcess + BlackScholesMertonProcess; VanillaOption + EuropeanOption; BlackCalculator helper; AnalyticEuropeanEngine + BinomialVanillaEngine (CRR/JarrowRudd/Tian/LeisenReimer via direct numpy backward induction) |
| L3-E forwards + FRAs | parallel | 4 | +28 | Forward abstract + ForwardTypePayoff + Position enum + FxForward + DiscountingFwdEngine + ForwardRateAgreement. Closed L2-C FraRateHelper(useIndexedCoupon=True). Modified LazyObject.calculate for bootstrap recursion |
| Post-merge alignment | sequential | 1 | 0 | YieldTermStructureProtocol.discount param name unified to t across Protocol + 3 test-mock classes |
| Total | ~26 | +362 | ~50 classes |
L3-A pilot was tagged separately as pquantlib-phase3-l3-A-complete @ e72bcdf mid-phase; that tag is preserved as the predecessor for parallel cluster dispatch.
All 4 parallel clusters significantly exceeded their test targets — L3-B +81 vs +30 target, L3-D +97 vs +43 target, L3-C +41 vs +35 target, L3-E +28 vs +20 target. Subagents added richer behavioral coverage than the floor estimates.
Same fan-out pattern as Phases 1 + 2, now proven across three phases:
- Wall-clock: L3-A pilot ~50 min (largest pilot to date — Settings observable wiring + 14 classes across 6 stages). L3-B/C/D/E in parallel ~50 min wall-clock for ~36 classes across 4 disjoint topic areas (bonds / swaps / equity options + processes / forwards).
- Three Protocols (Instrument / PricingEngine / StochasticProcess) defined in L3-A glued the 4 parallel clusters at merge time. The pattern continues to scale.
- No probe rebuild per worktree: subagents continued using the main worktree's already-built libQuantLib via one-off
clang++linking. - One real alignment surprise: parameter-name mismatch between L3-C's concrete YieldTermStructure (
t) and L3-E's slimmed Protocol comment (which still saidarg). Caught by pyright in 8 test-mock locations; resolved in a singlealign(...)commit.
Three issues caught + resolved at merge:
YieldTermStructureProtocol.discountparameter name drifted between subagents (L3-C renamed concretearg→t; L3-E slimmed the Protocol but its docstring + 3 test-mock classes still usedarg). Resolved by unifying toteverywhere; structural typing requires positional-or-keyword arg names to match.- L3-B's
BondForwardPositionplaceholder vs L3-E'sPositionenum — no actual conflict at merge. L3-B inlined position values directly intoBondForwardrather than importing from a not-yet-existent module; L3-E ported the canonicalPositionenum. Future BondForward refactor can adopt L3-E's enum without breaking changes. CMakeLists.txt— 4 paralleladd_executableadditions for cluster probes, stacked at merge.
The Protocol-glue pattern continues to deliver:
- L3-B's
DiscountingBondEnginetakes a concreteYieldTermStructure(because it needszero_rate()/forward_rate()which the slim Protocol can't offer — documented). - L3-C's
DiscountingSwapEnginetakesYieldTermStructuredirectly (same reason). - L3-D's
AnalyticEuropeanEnginetakes aGeneralizedBlackScholesProcess(concrete) rather thanStochasticProcessProtocol(Protocol can't structurally narrow to multi-Devolvesignature). - L3-E's
DiscountingFwdEnginetakesYieldTermStructureProtocol(only needsdiscount()).
Lesson: Protocols work for narrow consumers; concrete bases work for rich consumers. Both patterns coexist cleanly.
In addition to Phase 1+2's:
ObservableSettingsmulti-inheritsSingleton + Observablewith an_observable_settings_initializedguard to preventObservable.__init__running twice (Singleton metaclass caches the instance).Settings.evaluationDate()global replaced byObservableSettings().evaluation_date_or_today()— explicit None means "today".
GenericEngine[ArgsT, ResultsT]PEP 695 generic with typed bounds (PricingEngineArguments,PricingEngineResults).BinomialVanillaEnginecollapses C++template<class T>tree hierarchy into a single class parameterized byTreeBuilderenum (CRR / JarrowRudd / Tian / LeisenReimer) + direct numpy backward induction. Bypasses the entireTree/Lattice/DiscretizedAsset/DiscretizedVanillaOptionclass hierarchy.BlackCalculatorreplaces the C++ Visitor pattern (BlackCalculator::Calculator) withisinstancedispatch.- 1-D scalar overloads on StochasticProcess renamed
drift_1d/diffusion_1d/expectation_1d/variance_1d/std_deviation_1d/evolve_1dto coexist with multi-Ddrift(t, NDArray) -> NDArraysignatures. - EulerDiscretization uses
typing.overloadto satisfy both multi-D and 1-D abstract discretization bases with one body (C++ achieves via multiple-inheritance + name-overloading). YieldTermStructureProtocolis slim — onlydiscount(); concreteYieldTermStructureextends withzero_rate()/forward_rate()(richer signatures that can't structurally narrow).
Forward→ForwardTypePayoff(replaces C++Position::Typecasting).PositionIntEnum for Long/Short forward position.Bond.yield_rate(Pythonyieldis a reserved keyword — same trailing-underscore pattern asyield_subpackage from Phase 2).make_vanilla_swap/make_oisfree functions with kwargs (Pythonic replacement for C++MakeVanillaSwap/MakeOISBuilders).Exercise/EarlyExerciseabstract-by-convention (no@abstractmethod) — matches C++ public constructors with empty defaultdates_.
IborCouponPricerdefaults to par-coupons forecast (C++Settings::usingAtParCoupons=trueanalog). Fixed a 1.8e-5 rel drift in 5y swap NPV caught by L3-C testing.LazyObject.calculatesets_calculated=TrueBEFORE invoking_perform_calculations(C++ parity) with rollback on exception — supports bootstrap recursionForward.forward_value → calculate → perform_calculations → forward_value.GeneralizedBlackScholesProcess.evolve_1dbypasses the StochasticProcess1D.evolve_1d default in the Euler-fallback branch (which routes throughexpectation_1d→ "not implemented"); calls discretization drift directly to match C++ semantics.Bond.yield_rateBrent solver at LOOSE tier (1e-8) rather than NewtonSafe with derivative — easier in Python without templated function-with-derivative.
- C++
shared_ptr<PlainVanillaPayoff>overloads ofblackFormula*not ported — Python callers unwrap themselves. - C++
bachelierBlackFormulaImpliedVolChoi(approximation) carved out; only the exact Jaeckel 2017 variant ported.
All intentionally not ported. Each lands either as Phase 4/5 work or a dedicated follow-up.
Asian / Barrier / Basket / Cliquet / Lookback / Quanto / DoubleBarrier / ComplexChooser / Compound / HolderExtensible / Variance options.
Swaption, NonStandardSwaption, FloatFloatSwaption, MakeSwaption, CapFloor, MakeCapFloor.
CDS / CreditDefaultSwap / MakeCDS / ConvertibleBond.
BMA swap, Float/Float swap, NonStandardSwap, MultipleResetsSwap, EquityTotalReturnSwap. RateAveraging.Simple for OIS.
- All Heston / Bates / GJR-GARCH / Hull-White / G2 / CEV / SABR engines under
pricingengines/vanilla/. - All MC engines (Monte Carlo).
- All FD (finite-difference) engines.
VanillaOption.implied_volatility(depends on FdBlackScholesVanillaEngine).
HestonProcess, BatesProcess, GJRGARCHProcess, G2Process, Hull-White, CIR, Vasicek.
BTP, CmsRateBond, CpiBond, AmortizingCmsRateBond, AmortizingFloatingRateBond.
Tree, BlackScholesLattice, DiscretizedAsset, DiscretizedVanillaOption, TimeGrid plumbing; Joshi4 / AdditiveEQP / Trigeorgis trees. (BinomialVanillaEngine ported via direct numpy backward induction, bypassing the hierarchy.)
- Full
GaussianOrthogonalPolynomialhierarchy. - SobolRsg / Burley2020SobolRsg low-discrepancy.
- LM / BFGS / Simplex / ConjugateGradient / SimulatedAnnealing optimizers.
- 8+ cubic-spline variants.
- QR / Eigenvalue / SVD / SparseMatrix utilities.
- Full GammaFunction.
- All inflation termstructures + indexes + cashflows.
- All credit termstructures.
- ZABR / SABR / XABR vol; capfloor / optionlet / swaption vol.
- 35 specialty ibors beyond the 8 must-port.
- Advanced curve construction (FittedBondDiscountCurve / MultiCurve / GlobalBootstrap / spline-fitting variants); PiecewiseYieldCurve full bootstrap.
- Settings.evaluation_date wiring in L3-A unblocked everything as planned. 4 retroactive L1/L2 cleanups landed in the same commit. The "defer the observable, plan to wire in the next phase that needs it" strategy works well.
- Test-floor estimates underestimate by 2× for instrument + engine ports. Phase 3 targeted +204 tests; landed +362 (+77% over). Instruments + engines naturally compose into more behavioral assertions per class than termstructure/index ports.
- Protocol parameter names matter for structural typing. The post-merge
arg→talignment cost ~10 minutes and demonstrated that Protocols are positional-or-keyword-strict in pyright. For Phase 4: lock the Protocol parameter-name convention in the pilot and tell subagents not to rename. - Subagents make conservative scope choices that pay off. L3-D's BinomialVanillaEngine bypassed the entire Tree/Lattice/DiscretizedAsset class hierarchy with direct numpy backward induction — cleaner than porting the C++ template machinery, still bit-exact convergence to the analytical solution.
- L2 carry-overs all closed cleanly. L3-C completed SwapRateHelper / OISRateHelper / SwapIndex; L3-E completed FraRateHelper(useIndexedCoupon=True). The deferred-with-clear-unblocker pattern works.
- Cross-cluster type duplication still happens. L3-B and L3-E independently invented
Position-like enums (BondForwardPosition vs Position). L3-B's was inlined so no merge conflict, but the pre-pilot canonical-type discipline from Phase 2 should be extended to Phase 4 — port commonly-needed enums up front.
Sister-project anchor: jquantlib/docs/migration/phase2-L4-models-plan.md. Expected scope: short-rate models + Heston + Hull-White + G2 + their calibration engines + the full optimizer suite (LM / BFGS / Simplex from Phase 1 carve-outs).
Phase 4 will unblock:
- Swaption pricing engines + CapFloor pricing engines (instruments already ported in carve-out; engines need short-rate models).
- BlackIborCouponPricer cap/floor branches (need OptionletVolatilityStructure).
- VanillaOption.implied_volatility (needs FdBlackScholesVanillaEngine).
- All Heston / Bates / GJR-GARCH equity option engines.
Phase 4 should also retroactively close Phase 1's optimizer carve-outs (LM / BFGS / Simplex / ConjugateGradient) — they're needed for model calibration.