Tier 2 · Internal Risk Management

Equity Option Pricing — Black-Scholes-Merton Family

M-131 · lifecycle: monitoring · RAT-131-v1.0.1

Intended Use

Equity Option Pricing — Black-Scholes-Merton Family Price equity options and warrants for VA hedge effectiveness, derivative valuation, and Greeks-based risk reporting.

Family-level model covering Black-Scholes-Merton closed-form pricing of EUROPEAN-exercise equity options (COND-004 / INV-008: scope constrained to European; the production FinView L1 engine has no early-exercise path). Outputs price plus Greeks (delta, gamma, vega, rho, theta). Primary consumer is the VA hedge program (M-110). Two implementations: production (FinView L1, European only) and the independent challenger (Gold Copy L1, which additionally supports American exercise via binomial/LSMC but is used only for effective-challenge of the European pricing, not for production).


Components

Inputs, processing, outputs

data sources
DS-001 · DS-004 · DS-006 · DS-031 · DS-057
assumptions
A-053, A-060, A-061, A-062, A-063
engines
finview.L1.equity_option_pricer
goldcopy.L1.option_pricer
contracts
option_pricing_v1
upstream
M-136 · M-137
dimensions
D2

Methodology & Mechanics

Methodology

M-131 prices vanilla equity options under the Black-Scholes-Merton (BSM) closed-form analytic. The engine is FinView-resident: finlib.derivatives.equity_option_pricer.EquityOptionPricer, bound to the registry id finview.L1.equity_option_pricer via the slim @engine decorator (Decision 023 binding-only governance). It is an L1 instrument pricer; at the portfolio level it is dispatched by finview.L2.derivative_analytics (DerivativeAnalytics._build_pricer, instrument_subtype == 'equity_option'), which aggregates fair value and Greeks by risk category.

For spot S, strike K, time-to-expiry T (years), risk-free rate r, continuous dividend yield q, and volatility σ, the engine forms

d1 = [ ln(S/K) + (r − q + ½σ²)·T ] / (σ·√T)
d2 = d1 − σ·√T

and prices the two flavours as

call = S·e^(−qT)·N(d1) − K·e^(−rT)·N(d2)
put  = K·e^(−rT)·N(−d2) − S·e^(−qT)·N(−d1)

where N(·) is the standard-normal CDF (scipy.stats.norm.cdf). This is the exact code in _d1_d2() / _bs_price() — there is no lattice, no Monte Carlo, and no numerical integration; the price is a direct evaluation of the BSM formula. The dividend term e^(−qT) makes this the Merton dividend-adjusted variant, so equity-index options with a carry yield are handled correctly.

compute_fair_value() scales the per-unit premium to a position: n_contracts = notional / spot (share-equivalent count) and fair_value = sign · n_contracts · price_per_unit, where sign = +1 for a long position and −1 for a short. The method returns fair_value, price_per_unit, n_contracts, and the five Greeks.

Greeks are computed in closed form (not by bumping), each in a documented convention:

Greek Formula (call shown) Convention in code
delta e^(−qT)·N(d1) per unit of spot; put uses −e^(−qT)·N(−d1)
gamma e^(−qT)·n(d1) / (S·σ·√T) second derivative wrt spot; sign-independent
vega S·e^(−qT)·n(d1)·√T / 100 per 1% (1 vol-point) move, not per unit vol
theta analytic θ / 365 per calendar day (drift + carry terms)
rho K·T·e^(−rT)·N(d2) / 100 per 1% rate move; put is negative

n(·) is the normal PDF. Directional sign is applied to delta, vega, theta, and rho (a short position flips them); gamma is reported unsigned. The scaling constants (/100 for vega/rho, /365 for theta) are baked into the engine, so consumers must read the Greeks in those units.


Key Assumptions

Key Assumptions and Their Justification

Assumption Value in canonical run Source / derivation Justification
Volatility σ 0.20 (caller-supplied) book / market_data['implied_vol_surface'] at runtime BSM treats σ as a single constant flat vol. The engine consumes whatever the caller passes; DerivativeAnalytics._get_market_inputs tags equity-option vol as source: 'Book', source_type: 'book' — i.e. a position-level input, not a live surface lookup in the current build.
Risk-free rate r 0.04 caller-supplied; DerivativeAnalytics default risk_free_rate=0.04 Flat continuously-compounded discount rate. There is no term-structure pull from the SOFR curve for options today — a single scalar r is used.
Dividend yield q 0.0 (ATM canonical) caller-supplied; defaults to 0.0 Continuous-dividend Merton adjustment. The test_ql_validation index option uses q = 1.3% (S&P-style carry); zero is correct for a non-dividend single name / the canonical demonstration.
Exercise style European hard-coded (no early-exercise branch) The closed form is exact only for European exercise. The engine has no American/early-exercise path (no lattice, no Bjerksund-Stensland approximation) — see Limitations.
Underlying dynamics GBM, constant σ, r, q BSM model assumption Lognormal spot with constant parameters. No skew/smile, no stochastic vol, no jumps — the standard BSM idealisation.
Position scaling n_contracts = notional / spot compute_fair_value() Treats notional as a cash amount converted to share-equivalents at spot; long/short sign applied to fair value and first-order Greeks.

Prose. The load-bearing assumption is flat constant volatility: BSM collapses the entire implied-vol surface to one number, so the engine prices on-strike but cannot reproduce a market smile across strikes without the caller supplying a strike-specific σ. In the present build the vol and spot are book/position inputs (source_type: 'book'), which is the honest state — there is a vol_surface.py module in finlib (0% covered in the test run) but the equity-option pricer does not consume it. The rate is a flat scalar rather than a discount-curve pull, acceptable for short-dated single-name options but a known simplification for longer tenors where the curve shape matters.


Output Snapshot

Output Snapshot

Deterministic single-option run of EquityOptionPricer (finview.L1.equity_option_pricer), reproducible and requiring no live data (BV-032 immune — every input is a literal). Run via python scripts/model_snapshots.py M-131; the underlying analytics are asserted by tests/test_equity_option_pricer.py (6 tests, all passing), cross-checked against the independent Gold Copy challenger goldcopy.L1.option_pricer (reconciliation in modelling/validation_evidence/M-131/v1.0.0/, max relative diff 0.00e+00 within 2%), and supplementarily against QuantLib in tests/test_ql_validation.py::TestOptionsValidation (skipped when un-installed).

Input: canonical ATM European call · S = 100 · K = 100 · T = 1.0y · r = 4% · σ = 20% · q = 0 · notional = 100 · long.

output value meaning
price_per_unit (call premium) 9.925054 BSM call value per unit of spot
fair_value 9.925054 n_contracts = notional/spot = 1, so equals premium
delta 0.617911 e^(−qT)·N(d1) — ITM-leaning at ATM because of positive drift
gamma 0.019069 curvature of value in spot
vega (per 1% vol) 0.381388 premium change for a +1 vol-point move
theta (per day) −0.016133 daily time decay (long option loses value with time)
rho (per 1% rate) 0.518661 premium change for a +1% rate move
put-call parity residual 0.00e+00 (C − P) − (S·e^(−qT) − K·e^(−rT)) to machine precision

The premium 9.925054 matches the textbook BSM closed form to six decimals (independently recomputed from scipy.stats.norm); the well-known r = 5% ATM value 10.4506 also reproduces, confirming the implementation. The delta exceeds 0.50 at-the-money because the positive risk-neutral drift (r − q + ½σ²) pushes d1 > 0. Put-call parity holds to machine zero, the strongest internal closed-form invariant the engine can satisfy. The identical table is produced by both the FinView venv and the InsModel venv (the snapshot inserts [redacted] on sys.path, mirroring the M-040 cross-repo pattern).

Captured 2026-06-04 · deterministic, no live data.


Limitations

Limitations and Known Gaps

  1. European exercise only — scope constrained (COND-004 / INV-008, remediated 2026-06-06). The production FinView engine has a single closed-form path; there is no binomial/trinomial lattice and no analytic American approximation (Barone-Adesi-Whaley / Bjerksund-Stensland). Any American equity option (notably deep-ITM puts and dividend-paying calls where early exercise has value) would be mispriced if routed here. Remediation: rather than building an American path into the production engine, M-131's scope has been formally constrained to European exercise — "American" has been removed from the model description in both the registry entry and this card's Description/Intended Use, and American equity options are declared out of scope (see Description scope note). This closes INV-008 by the scope-constraint route (charter §2.2; no fabrication). The Gold Copy challenger goldcopy.L1.option_pricer does implement American exercise (binomial / LSMC), but that capability is used only for effective-challenge, not for production pricing; if American production pricing is later required, it is a separate development-flow build (new engine capability), not a documentation fix.
  2. Flat constant volatility — no smile/skew. A single scalar σ is used; the engine does not consume an implied-vol surface. finlib/vol_surface.py exists but is not wired into the equity-option pricer (0% test coverage in the snapshot run). Off-ATM strikes priced with an ATM vol will diverge from market.
  3. Flat scalar risk-free rate — no discount-curve pull. r is a single number, not a tenor-interpolated rate off the bootstrapped SOFR/Treasury curve that the bond/IRS pricers use. Acceptable for short-dated options; a known simplification for longer maturities.
  4. Vol and spot are book/position inputs, not live market. Per DerivativeAnalytics._get_market_inputs, equity-option market inputs are tagged source: 'Book' / source_type: 'book'. There is no live spot or live implied-vol feed in the present build, so portfolio valuation inherits the book's mark, not an independent market mark.
  5. Constant continuous dividend yield only. Discrete cash dividends and borrow/repo financing spreads are not modelled — only a single continuous q. Single-name options with lumpy ex-dividend dates carry basis error.
  6. No QuantLib at runtime, and not installed in either venv. QuantLib is a supplementary test-only challenger (@pytest.mark.quantlib, skipped if absent) and is not installed in the FinView or InsModel venvs at capture time, so the QuantLib cross-validation in test_ql_validation.py does not run in the default suite. This is no longer the operative cross-engine gap: the Gold Copy challenger goldcopy.L1.option_pricer reconciliation (6 cases, 0.00e+00 max relative price diff, within the registry's 2% tolerance — run 2026-06-06, COND-002) is the operative cross-engine challenge, with the put-call-parity invariant and closed-form recomputation as additional internal checks.

Status of formerly-tracked items (updated 2026-06-06, RAT-131 remediation). - INV-008 (American exercise) — remediated by scope constraint to European (COND-004): "American" removed from registry + card; American options out of scope. Re-opens only as a separate development-flow build if American production pricing is later required. - Cross-engine challenge — remediated by the documented finview-vs-goldcopy reconciliation (COND-002/003); the Gold Copy challenger is the challenger of record. QuantLib remains a supplementary skipped check. - Tier-2 validation + lifecycle — registry last_validated_on stamped 2026-06-06, documentation_pack flags reconciled, validation-evidence pack assembled at modelling/validation_evidence/M-131/v1.0.0/ (COND-001). Independent 2L effective-challenge sign-off is pending 2L re-review. - Still tracked (genuine future work, not blockers): wire the DS-057 implied-vol surface into the pricer so off-ATM strikes price on a smile (Limitations #2/#4); define a packaged / versioned interface + CI drift gate for the FinView pricing library consumed cross-repo (INV-032, shared with M-040 / M-130–137).


Validation Evidence

Validation Packet

Check Mechanism Status
Closed-form recomputation Snapshot premium 9.925054 reproduced from an independent scipy.stats.norm BSM evaluation; matches to 6 dp. Textbook r = 5% ATM value 10.4506 also reproduced. Pass
Put-call parity invariant Snapshot computes (C − P) − (S·e^(−qT) − K·e^(−rT)); residual = 0.00e+00. Asserted in test_put_call_parity (tol < 0.01). Pass (machine zero)
Delta bounds test_delta_bounds: call delta ∈ [0, 1], put delta ∈ [−1, 0]. Pass
Vega monotonicity test_vol_sensitivity: higher σ → higher premium. Pass
Positive premium / sign test_positive_premium, test_short_direction: premiums > 0; short fair value < 0. Pass
Greeks present test_greeks_present: delta, gamma, vega, theta returned. Pass
Full unit suite tests/test_equity_option_pricer.py — 6 tests. 6 passed
Cross-engine challenger (Gold Copy — challenger of record) goldcopy.L1.option_pricer (ecosystem/gold_copy/instruments/option_pricer.py) is the registry-declared cross_check for finview.L1.equity_option_pricer (engine_registry, tolerance {price: 2%}). It is an independent first-principles BSM re-implementation that imports only gold_copy.* (no FinView/InsModel). Reconciliation run 2026-06-06 (COND-002): 6 option cases (ATM call/put, OTM call/put with dividend, long-dated high-vol, short-dated ITM) — max relative price diff 0.00e+00, all within the 2% tolerance; all five Greeks also reconcile to machine zero on the common convention scale. Reproducible via the validation-evidence pack. Pass (within 2%; machine zero)
Cross-engine challenger (QuantLib — supplementary) tests/test_ql_validation.py::TestOptionsValidation prices the same options with ql.AnalyticEuropeanEngine (BlackScholesMertonProcess); tolerances OPTION_PRICE_TOL = 5%, GREEK_TOL = 5% (delta). Test-only dependency, marked @pytest.mark.quantlib, skipped — QuantLib not installed in either venv at capture. Supplementary only; the Gold Copy reconciliation above is the operative cross-engine challenge. Not run (skipped)

Challenger of record (COND-003, corrected 2026-06-06). The challenger of record is the Gold Copy engine goldcopy.L1.option_pricer, which the engine_registry binds as M-131's cross_check (tolerance {price: 2%}). It is an independent BSM re-implementation living in the gold_copy repo (not in FinView), import-isolated from FinView/InsModel. A prior version of this card stated "there is no gold_copy / Gold Copy challenger engine" for equity options and named QuantLib as the challenger of record — that was incorrect and understated the registry-declared independent challenger; it is corrected here. The Gold Copy engine genuinely exists, and the documented finview-vs-goldcopy reconciliation (6 cases, 0.00e+00 max relative diff, well within 2%) is now on file as the operative cross-engine challenge — upgrading M-131 from "internal-invariant validated" to "cross-engine validated." QuantLib's AnalyticEuropeanEngine remains a supplementary test-only challenger (skipped when un-installed); the put-call-parity invariant and independent closed-form recomputation remain in force as additional internal checks.


References

References

  • Black, F. & Scholes, M. (1973), "The Pricing of Options and Corporate Liabilities," Journal of Political Economy 81(3) — the closed-form d1/d2 call/put formulae.
  • Merton, R. C. (1973), "Theory of Rational Option Pricing," Bell Journal of Economics — the continuous-dividend e^(−qT) adjustment implemented here.
  • Engine source: [redacted] (EquityOptionPricer, finview.L1.equity_option_pricer).
  • Portfolio dispatch: [redacted] (DerivativeAnalytics, finview.L2.derivative_analytics).
  • Unit tests: [redacted] (6 tests).
  • Cross-engine (QuantLib) validation: [redacted]::TestOptionsValidation (@pytest.mark.quantlib).
  • Governance binding: [redacted] (slim @engine decorator, Decision 023).
  • Snapshot driver pattern: [redacted] (snap_M_040 is the FinView-resident cross-repo precedent).

Findings: - INV-008 (2L) — European-only closed form; American equity options would be mispriced if routed here. Remediated 2026-06-06 by scope constraint to European exercise (COND-004): American removed from registry + card scope, American options declared out of scope. See Limitation #1 and the Description scope note. Re-opens only as a separate development-flow build if American production pricing is required.


Change Log

Change Log

Card change history. Code-side change history lives in git log of the component files.

  • 2026-05-08 — stub created from registry data per Decision 023 Phase 5 / B-07.
  • 2026-06-04 — Tier-2 hand-authoring of Methodology, Key Assumptions and Their Justification, Limitations and Known Gaps, Validation Packet, and References from the engine source (finlib/derivatives/equity_option_pricer.py) + DerivativeAnalytics dispatch. Stub marker advanced to `. Output Snapshot captured the same day (python scripts/model_snapshots.py M-131`, deterministic, no live data).
  • 2026-06-05 — card stamped in modelling/model_doc_stamps.yaml (fingerprint 3de198425154bf81, engine watermark 4e65d861fdbc).
  • 2026-06-06 — documentation-accuracy pass against the engine source. Added missing Standards Coverage section (ASOP 56 / SR 26-2 / internal); added DS-057 (options IV surface, PL-07, planned) to Components to match the registry entry while preserving the "not yet wired" posture; cross-referenced 2L finding INV-008 (open) under Limitation #1 + References; consolidated the gated/ratification items (INV-008, DS-057 wiring, QuantLib challenger, Tier-2 sign-off + lifecycle, INV-032) into a transparency note. No model outputs, validation results, or back-test numbers were fabricated or changed.
  • 2026-06-06 (RAT-131-v1.0.0 1L remediation, Decision 053 §2.2) — remediated COND-001..004 with real, reproducible evidence only:
  • COND-003 — corrected the challenger of record: the prior "no gold_copy challenger engine" note was wrong; goldcopy.L1.option_pricer is the registry-declared independent challenger and is now named as such in the Validation Packet.
  • COND-002 — ran and documented the finview-vs-goldcopy cross-engine reconciliation (6 cases: ATM call/put, OTM ±dividend, long-dated high-vol, short-dated ITM) → max relative price diff 0.00e+00, all five Greeks reconcile to machine zero on the common convention scale, all within the registry's 2% tolerance. Assembled the validation-evidence pack modelling/validation_evidence/M-131/v1.0.0/.
  • COND-004 / INV-008 — constrained M-131 scope to European exercise; removed "American" from the registry description and the card Description/Limitations; declared American options out of scope (closed by scope constraint, not a build).
  • COND-001 — reconciled the registry documentation_pack flags (model_card/validation_evidence/change_log → present), stamped lifecycle.last_validated_on: 2026-06-06. No model outputs invented; QuantLib remains honestly marked skipped. Ready for 2L re-review.

2L Inventory Review

Open findings (2)

Independent 2nd-line review (INV-2026-06) — implemented capability vs registered scope. Each carries a recommended fix and is tracked in insightalm-mrm until closed.

HIGH INV-029 · P5 · validation-gap

Validation evidence + change logs missing across most of the inventory

Only M-001/M-020/M-050 carried full documentation packs before this pass. Most models record validation_evidence: missing and change_log: missing with peer_review: pending. Gold tests freeze behaviour but many assert only structural invariants (e.g. reserve>0), not correctness against external truth. The flagship T0-vs-10-K match is circular (BV-032).

Recommendation: For each Tier-1 model: produce a validation-evidence pack (back-test vs disclosed results once BV-032 re-calibration lands, sensitivity suite, challenger comparison), a change log, and a 2L ratification. Sequence behind BV-032 (firm-data) for anything needing 10-K reconciliation.

LOW INV-032 · cross-repo · cross-repo

FinView-resident pricing/CECL engines reached cross-repo with no drift gate

M-040 (CECL) and M-130-137 (asset pricing) are FinView-resident, consumed by InsModel only via sys.path insertion. No packaged interface or drift gate — a finlib signature change silently breaks the InsModel snapshot harness / consumers.

Recommendation: Define a packaged interface (or a versioned contract) for the FinView pricing library and a CI drift gate, so cross-repo consumers fail loudly on signature change. Tie to D041 ownership.


Validation Coverage

Per-tier expectations

Per MRM Framework §10.2 + §10.3, this model's regulatory_frameworks tag list activates the following overlays:

asop_56 sr_26_2 internal
component tier-2 expectation status
Registry entry required present
Model card (§10.5 doc pack) required present
Validation evidence required present
Change log required present
Independent effective challenge (2L) required attested

Ratification

Ratified — RAT-131-v1.0.1

Latest ratification on file: RAT-131-v1.0.1. Authored by 2L (mrm-peer-reviewer) per Decision 028 charter §5 Pattern A.