Skip to content

Measure geometric average-strike Asian times from the valuation date - #2983

Open
Jackmeson1 wants to merge 1 commit into
lballabio:masterfrom
Jackmeson1:fix/geometric-average-strike-valuation-date
Open

Jackmeson1 wants to merge 1 commit into
lballabio:masterfrom
Jackmeson1:fix/geometric-average-strike-valuation-date

Conversation

@Jackmeson1

Copy link
Copy Markdown
Contributor

AnalyticDiscreteGeometricAverageStrikeAsianEngine measures the fixing times and the residual time from the first
fixing date instead of the valuation date (analytic_discr_geom_av_strike.cpp:57-76 on 70f240f29). With flat curves
sharing the valuation date and day counter, a delayed first fixing causes the price to omit the dividend discount
factor D_q(0, t1). The engine also mixes clocks: fixing times use the volatility day counter, the residual time the
risk-free one, and the drift multiplies zero rates (each in its own day counter) by those times. Finally, the engine
divides by the effective standard deviation without handling zero variance; when all fixings are at maturity, this
produces NaN even though the payoff is identically zero.

This PR selects fixings on or after the risk-free curve's reference date, as
AnalyticDiscreteGeometricAveragePriceAsianEngine does. It measures fixing and exercise times using the volatility
day counter from the volatility reference date, and obtains forwards from discount factors at each fixing and
exercise date (ln(D_q/D_r)). The call price is the same two-lognormal formula, D_r(T)·(F_T·N(d1) - E[G]·N(d2)), now
evaluated with BlackCalculator. The variance of ln(S_T/G) is accumulated from the nonnegative time increments
between fixings (weight (i/N)^2 after the i-th fixing, 1 after the last), instead of as
Var[ln S_T] + Var[ln G] - 2 Cov[ln S_T, ln G], whose subtraction can leave a rounding residue; when it is exactly
zero the engine returns the discounted intrinsic value D_r(T)·max(±(F_T - E[G]), 0).

With flat curves sharing today as their reference date, a common day counter, the first fixing today, and positive
effective variance, the price is unchanged. On non-flat curves the forward to each fixing now comes from the curves at
that date instead of the zero rates to maturity. The volatility is still read once at the exercise date, and
pastFixings must still be 0.

Flat curves, spot 100, r = 6%, q = 3%, 20% volatility, Actual/360, maturity in 360 days. References were calculated
independently in Python; the forward-start base values are reconstructed from those references and the MSVC
fail-before differences.

Case 70f240f29 this PR Reference
Ten fixings every 36 days, call (existing test) 4.97109 4.95620 4.95620159631, joint lognormal
One fixing after 180 days, call 6.27617569337 6.18273561059 at-the-money forward-start call
One fixing after 180 days, put 4.80953508792 4.73793043905 at-the-money forward-start put
Every fixing at maturity NaN 0 payoff is identically 0

For the ten-fixing case, t1 = 0.1 and the base/reference ratio is exp(0.03·0.1) = 1.0030045. For both forward-start
cases, t1 = 0.5 and the ratio is exp(0.03·0.5) = 1.0151131.

Tests (test-suite/asianoptions.cpp):

  • testAnalyticDiscreteGeometricAverageStrike: the reference changes from 4.97109, which encodes the defect, to
    4.95620, with the joint-lognormal moments in a comment (a Monte Carlo estimate agrees within its error; it is not
    used as the reference).
  • ...ForwardStart: one fixing halfway to maturity equals S·D_q(t1) times an at-the-money call/put on [t1, T],
    computed with blackFormula, within 1e-12.
  • ...AtMaturity: one, three or seven fixings at maturity, for maturities of 157 and 360 days, give a finite 0 within
    1e-12, call and put. An earlier, cancellation-based draft of this patch left about 1.5e-7 in price for each option
    type in the 157-day, seven-fixing case.
  • ...DayCounters: the risk-free, dividend and volatility curves switch independently between Actual/360 and
    Actual/365 (Fixed) with rates scaled by 365/360 and volatility by sqrt(365/360), so every discount factor and
    variance is unchanged; the price must agree within 1e-8 percent (1e-10 relative) in all eight combinations.
  • ...FirstFixingToday: with the first fixing today, the price equals the previous formula's, computed independently
    with blackFormula, within 1e-12. This one passes before and after the change.
  • ...ZeroVolatility: with zero or 1e-14 volatility and positive, zero and negative carry, the price agrees with the
    discounted deterministic payoff within 1e-12.
  • ...SmallVolatility: one fixing today and 1e-9 volatility give an at-the-money option worth about 3.9e-8, which
    must match blackFormula within 1e-2 percent (1e-4 relative) for both calls and puts and not be clamped to zero.
    This one passes before and after the change.
  • ...NonFlatRates: a risk-free curve with the same 6% zero rate to maturity as a flat curve but 1% to the first
    fixing; at zero volatility the price must equal the discounted payoff with G the geometric mean of the dated
    forwards, within 1e-12.

With the engine from 70f240f29, the updated and added tests produce 46 failed assertions in my Windows MSVC run:
one updated reference assertion, two forward-start comparisons, 24 maturity assertions (finiteness and zero for
each of 12 prices), 12 mixed day-counter comparisons, six zero/tiny-volatility comparisons, and the non-flat call.
With this patch, my runs of QuantLibTests/AsianOptionTests pass all 35 test cases on Windows with
MSVC 19.41.34123.0 (C++17) and on Linux with g++ 13.3.0 (Release, QL_COMPILE_WARNING_AS_ERROR=ON).

With a CMake build in build/ (Linux), run the suite with:

./build/test-suite/quantlib-test-suite --run_test=QuantLibTests/AsianOptionTests --log_level=message

AI disclosure: this was prepared with AI assistance (Claude Opus 5.5).

🤖 Generated with Claude Code

The analytic discrete geometric average-strike engine measured fixing
and residual times from the first fixing, which omitted the dividend
discount to a delayed first fixing for flat curves on a common clock.
It also mixed day counters and could return NaN at zero effective
variance. Take forwards from discount factors at the fixing and
exercise dates and variances on the volatility clock from its
reference date, accumulate the variance of log(S_T/G) from
nonnegative time increments, and return the discounted intrinsic value
when it is zero. Update the test reference that encoded the old value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JfvcPZyKcKQUG9fQGYfRMw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant