Premium pricing
The BlockFinaX pricing engine produces a reference fair-value quote.
It's not a constraint. The smart contract accepts whatever premiumRate the
underwriter passes to createEvent — there is no on-chain check against the
engine, and no auto-update over the event's lifetime.
Sophisticated underwriters (institutional FX desks, market makers, quantitative funds) typically deviate from the engine's number. See Setting your own price for the override flow.
The v2 engine prices BlockFinaX events using Merton (1976) jump-diffusion on top of GARCH(1,1)-integrated variance, calibrated per pair against 20+ years of historical closes. It replaces the original flat-vol Garman–Kohlhagen
- tier-markup approach because:
-
Flat-vol Garman–Kohlhagen systematically under-prices the wings. Hull (Options, Futures, and Other Derivatives, Table 20.1) shows FX 3σ moves happen on 1.30% of days vs. 0.27% predicted by the lognormal model — roughly 5× too often. For frontier-EM corridors (USD/GHS, USD/NGN, USD/KES) the effect is even more pronounced.
-
EWMA gives spot vol, not forward-average vol. For a 30–90 day option we want the expected average variance over [0, T], not today's snapshot. GARCH(1,1) mean-reverts σ_0 toward the long-run σ_LR at rate (1 − α − β). Hull §23.3 gives the closed-form integrated-variance formula.
-
Tier markups → per-pair calibration. Every pair carries its own (λ, μ_J, σ_J) and (α, β), fitted by maximum likelihood from its actual history. There are no hand-tuned numbers in the production path.
This page explains:
- Garman–Kohlhagen base kernel
- The Merton jump-diffusion overlay
- GARCH(1,1) integrated variance
- Per-pair calibration
- Inputs — where each number comes from
- Setting your own price
- How to get a quote
- Backtest results
1. Garman–Kohlhagen base kernel
Garman–Kohlhagen is Black–Scholes adapted for FX. It values a European call or put on a currency pair under the assumption that the spot follows geometric Brownian motion with two risk-free rates: domestic () and foreign ().
For a call:
where:
- — current spot rate
- — strike
- — time to expiry (years)
- — domestic (USD) risk-free rate
- — foreign-currency rate (or empirical drift for frontier-EM, see §5)
- — annualized volatility of the pair
- — standard normal CDF
Because BlockFinaX events are call spreads (long at strike, short at cap), we evaluate them as the difference of two calls:
Put spreads (for downward hedges) mirror this. This is the base kernel — the v2 engine wraps additional jump and vol-forecasting machinery around it, described below.
2. Merton jump-diffusion overlay
Pure Garman–Kohlhagen assumes the FX rate diffuses continuously — no gaps, no surprises. In reality, frontier-EM pairs gap regularly on devaluation events, central-bank interventions, IMF programs, and political shocks.
Merton's (1976) solution: layer a compound Poisson jump process on top of the diffusion:
where:
- — jump intensity (jumps per year)
- — log-jump size,
- — the mean fractional jump
The closed-form Merton call price is a Poisson-weighted sum of GK prices with vol and drift adjusted per jump count:
with , , and .
The series converges geometrically once ; we truncate at 50.
When , this collapses exactly to Garman–Kohlhagen. For g10 pairs (where jumps are rare), the v2 engine is essentially the v1 engine. For frontier-EM, the jump component adds meaningful tail premium.
3. GARCH(1,1) integrated variance
EWMA-style volatility () tells you what vol is today. But for pricing an option that matures in 30–90 days, what we want is the expected average variance over [0, T].
GARCH(1,1) provides exactly this. The conditional variance follows:
with where is the long-run sample variance. The persistence controls how fast shocks decay back to .
Hull's §23 expected-average-variance formula gives the variance fed to the kernel:
where is the number of trading days in [0, T]. As grows, — full mean reversion. The kernel sees , not directly.
Why this matters: if KES jumped 2% yesterday and σ_0 spiked to 30%, the v1 engine would price a 90-day option as if σ=30% for the whole 90 days. The v2 engine mean-reverts σ toward 18% (the calibrated for USD/KES) at the calibrated KES-specific persistence of 0.994.
4. Per-pair calibration
The pricing engine refits its per-pair parameters periodically against 20+ years of daily closes for every supported pair. Two calibrations run:
Jump MLE
Lee–Mykland threshold detection: a return is flagged as a jump day when , where is the rolling 30-day stdev EXCLUDING the candidate day. From the detected days the engine estimates:
Sanity bounds: , .
GARCH MLE
Variance-targeted maximum likelihood (Engle–Mezrich 1996): fix to the long-run sample variance, leaving only to optimize. Avoids the 3D MLE instability Hull §23.5 warns about. Negative log-likelihood under Gaussian innovations:
Optimized via grid search over .
Per-pair sample (current as of May 2026)
| Pair | σ_LR | λ (jumps/yr) | μ_J | σ_J | ρ (persistence) |
|---|---|---|---|---|---|
| USD/KES | 18.0% | 2.87 | 0.0% | 2.7% | 0.994 (CBK-managed) |
| USD/GHS | 23.6% | 2.71 | -1.1% | 10.0% | 0.969 |
| USD/NGN | 24.5% | 3.01 | +1.5% | 10.1% | 0.960 |
| USD/ARS | 22.8% | 3.96 | +2.5% | 9.5% | 0.995 (hyperinflation) |
| USD/MWK | 38.7% | 2.88 | +0.1% | 14.5% | 0.936 |
| USD/SDG | 44.5% | 4.79 | +2.9% | 18.5% | 0.658 (civil war regime) |
| USD/ZAR | 16.3% | 0.58 | +4.2% | 3.4% | 0.990 |
| USD/BRL | 19.1% | 1.30 | -0.8% | 7.9% | 0.728 |
| USD/TRY | 16.0% | 2.22 | +1.7% | 5.2% | 0.996 |
| USD/MXN | 12.2% | 0.99 | +3.3% | 2.3% | 0.977 |
| EUR/USD | 11.0% | 1.12 | +0.6% | 5.6% | 0.997 |
| USD/JPY | 11.5% | 1.35 | -1.0% | 4.9% | 0.993 |
| XAU/USD (gold) | 17.4% | 1.37 | -1.4% | 4.6% | 0.986 |
| BTC/USD | 55.9% | 2.84 | -2.7% | 13.6% | 0.957 |
Patterns to notice:
- KES persistence at 0.994 — vol regimes last forever. The CBK clamps hard then occasionally releases.
- ARS / TRY persistence at 0.995–0.996 with high λ — classic hyperinflation / capital-flight signature.
- G10 has 1 jump per year on average. The lognormal assumption was always wrong; we just now have the data to quantify how wrong.
- NGN μ_J = +1.5% — when there's a jump, the rate gaps up. Chronic depreciation regime baked into the parameter.
5. Inputs — where each number comes from
All inputs are pulled live; no hardcoding:
| Input | Source | Notes |
|---|---|---|
| Spot () | FX oracle the pricing service is wired to | Same provider the on-chain oracle network reads from, so engine and chain agree. |
| USD risk-free rate () | US Treasury Direct, 3-month bill | Cached 6 hours. |
| Drift / foreign rate ( or ) | For IRP-holding pairs, local risk-free rate. For frontier-EM, empirical drift computed from the full historical window (capped at /yr). | See classification below. |
| σ_0 (EWMA) | 60-day EWMA over recent closes () | RiskMetrics standard. |
| σ_LR (long-run) | Equally-weighted sample stdev over the full ~20y window | Anchors GARCH mean reversion. |
| GARCH (α, β) | Variance-targeted MLE on the pair's 20+ year close history, refreshed periodically | Falls back to tier defaults if not yet calibrated. Surfaced in the API as breakdown.inputs.garch. |
| Jump (λ, μ_J, σ_J) | Lee–Mykland 4σ threshold detection on the same history | Falls back to tier defaults. Surfaced as breakdown.inputs.jump. |
Pair classification: why empirical drift?
For developed-market pairs (USD/EUR, USD/JPY), interest-rate parity holds fairly well — the forward rate is close to spot × (1 + r_foreign)/(1 + r_usd), so plugging in the actual local rate for is reasonable.
For frontier-EM pairs (USD/GHS, USD/NGN, USD/KES) IRP breaks down hard. The "official" local rate isn't the rate at which capital actually moves; NDF markets are thin or non-existent; the published rate is often pegged or politically managed. Using it would systematically mis-price every event.
So for those pairs the engine substitutes empirical drift:
Then , which feeds into the kernel exactly where the local rate would otherwise sit.
Safety guardrails
- Volatility floors by tier (frontierEM 15%, pegged 30%) prevent the engine from quoting a near-free option after a freakishly quiet 60-day window — exactly the moment LPs are most exposed to a regime change.
- Leverage safety cap at 100×: if
maxPayout / premium > 100, the engine rejects the quote. Tells the caller to move strike closer to spot or narrow the cap.
Setting your own price
The engine is a starting point, not the final word. The smart contract
treats premiumRate as immutable input from the creator and does not verify
it against the engine, unless the creator opted to pass an attestation
signature (and even then, only that the signed quote came from the
registered signer at a recent timestamp — not that the number is "correct").
Sophisticated underwriters routinely deviate. Four common reasons:
Different view on vol
The engine's GARCH forecast is conditional on what's been observed. If you have a view that the next 30 days will be very different — a known election, a central-bank meeting, a political risk premium the historical window doesn't capture — you price above or below the engine.
Better data
The engine pulls daily closes from a public bar feed. If you trade NDFs or run an FX desk, you may have tick-level data, risk-reversal / butterfly skews from the OTC options market, or implied vol surfaces from where 1M/3M options actually trade. Use the better data.
Different capacity / risk appetite
The engine prices fair value; it has no view on what your book should charge. If your pool already has 70% exposure on USD/GHS and you want to discourage new hedger flow, mark the rate up. If you have spare capacity, mark it down. That's an underwriting call, not a math call.
Pair-specific information
For pegged pairs (USD/HKD, USD/XOF, USD/XAF) the binary peg-break probability dominates the option's value. The engine prices a Merton jump component, but a desk with private intelligence about a coming devaluation will price differently.
How to override
In your createEvent call:
{
"premiumRate": "<your rate as 1e6 fixed-point — e.g. 25000 for 2.5%>",
"signature": "0x",
"quoteTimestamp": "0",
"quoteNonce": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
signature = "0x" is the self-priced flag — the contract accepts the
rate as given. No attestation, no engine involvement, no verification.
You can still consult the engine via POST /v1/pricing/quote before
deciding, but you don't have to attest with it.
How to get a quote
POST /v1/pricing/quote:
curl -X POST https://api.blockfinax.com/v1/pricing/quote \
-H "Content-Type: application/json" \
-d '{
"pair": "USD/KES",
"strike": 135,
"payoutCap": 140,
"expiryUnixSeconds": 1716000000,
"notional": 1000,
"strikeAbove": true
}'
Response:
{
"data": {
"premiumRate": 0.0008,
"premiumUsd": 0.80,
"maxPayoutUsd": 38.71,
"premiumRateFixed6": "800",
"attestation": null,
"breakdown": {
"pair": "USD/KES",
"spot": 129.18,
"T_days": 7,
"direction": "upward",
"coveragePercent": 1.99,
"inputs": {
"r": 0.045,
"mu": 0.023,
"qEffective": 0.022,
"sigma": 0.063,
"sigmaRealizedRaw": 0.055,
"volMarkup": 0.0,
"garch": {
"sigma0": 0.0554,
"sigmaLongRun": 0.1005,
"persistence": 0.994,
"sigmaIntegrated": 0.063
},
"jump": {
"lambda": 2.87,
"jumpMean": -0.0001,
"jumpStd": 0.027
}
},
"method": "garman-kohlhagen-with-empirical-drift",
"pairClassification": { "tier": "frontierEM", "useEmpiricalDrift": true }
}
}
}
The garch and jump sub-blocks expose what the engine actually fed to
the kernel. Underwriters use this to sanity-check before committing to a
rate.
Signed quotes (for createEvent attestation)
Pass chainId, diamondAddress, and creator to receive an
attestation block whose signature, quoteTimestamp, and quoteNonce
plug directly into createEvent:
curl -X POST https://api.blockfinax.com/v1/pricing/quote \
-H "Content-Type: application/json" \
-d '{
"pair": "USD/KES",
"strike": 135,
"payoutCap": 140,
"expiryUnixSeconds": 1716000000,
"notional": 1000,
"strikeAbove": true,
"chainId": 8453,
"diamondAddress": "0x...",
"creator": "0x..."
}'
Backtest results
The engine was validated against 20+ years of history via walk-forward simulation — at every historical date , quote a hypothetical 30-day upward hedge (+3% OTM strike, +9% OTM cap, t_0t_0 + 30$d.
Walk-forward step: every 7 calendar days. No look-ahead bias.
| Pair | N contracts | avg premium | loss/premium | hit rate | mean PnL | Sharpe | 5%-worst PnL |
|---|---|---|---|---|---|---|---|
| USD/KES | 1156 | 0.93% | 0.17 | 9% | +$7.76 | +1.12 | +$0.81 |
| USD/NGN | 1156 | 1.52% | 0.21 | 9% | +$11.97 | +0.89 | -$20.32 |
| USD/GHS | 970 | 2.02% | 0.29 | 24% | +$14.32 | +0.96 | -$30.13 |
Reading the table:
- loss/premium = 0.17 for USD/KES means the LP would have paid out 17% of the gross premium collected. They keep 83%.
- Sharpe +1.12 is unusually strong — comparable to institutional underwriting books. The KES number is high because CBK-managed pairs have very low realized vol and very rare large moves, so the engine over-charges on average vs realized.
- Hit rate 9% for KES means only 9% of contracts ever paid anything; GHS hits more often (24%) because its strike geometry is closer to realized moves.
- 5%-worst PnL is the tail risk per contract. For USD/KES even the worst-5% contracts were profitable; for USD/GHS the tail is ~1K notional.
This is what "the model is well-calibrated for an LP-friendly market" looks like in numbers. The engine isn't perfect, but it's been beaten against 22 years of frontier-EM history and consistently underwrites profitably.
References
- Hull, Options, Futures, and Other Derivatives (10th ed) — Ch 17 (currency options + GK), Ch 20 (volatility smile), Ch 23 (EWMA + GARCH), Ch 27 (jump-diffusion).
- Taleb, Dynamic Hedging — Ch 6 (no-constant-vol), Ch 15 (fat tails, vol-of-vol), the framing the engine's overlays address.
- Merton, R. C. (1976). "Option pricing when underlying stock returns are discontinuous." Journal of Financial Economics 3(1–2).
- Bollerslev, T. (1986). "Generalized autoregressive conditional heteroscedasticity." Journal of Econometrics 31.
- Lee, S. S., & Mykland, P. A. (2008). "Jumps in financial markets: A new nonparametric test and jump dynamics." Review of Financial Studies 21(6).