Reconciling FX Option Premiums When Premium and Delivery Dates Coincide
Summary
The post investigates a small discrepancy between an AUD/USD vanilla option premium and implied volatility calculated to match a commercial pricing engine. The example has the premium date equal to the delivery date, and the author suspects that discounting the premium incorrectly may explain why the solved volatility and calculated premium differ slightly. The included implementation uses a Garman-Kohlhagen option model, infers a foreign interest rate from forward points, and compares continuous with simple discounting.
The example reports close but nonidentical model outputs and a quoted market-engine premium and volatility. However, the document contains no accepted answer or resolution, so it does not establish which convention causes the discrepancy. Its code also embeds assumptions about spot settlement, calendars, day counts, rates, and the relationship between option expiry and delivery. These details matter when reproducing FX option prices, and the comparison alone is not evidence that the date coincidence or discounting choice is the source of the difference.
Key ideas
- The post compares an FX option model price with a pricing engine for a trade whose premium and delivery dates coincide.
- It uses Garman-Kohlhagen pricing and derives a foreign rate from spot, forward points, and a domestic rate.
- Continuous and simple discounting produce similar but not identical reported outputs.
- Settlement dates, calendars, day counts, and premium conventions are potential sources of pricing differences.
- No answer identifies the cause, so the example does not establish a definitive pricing fix.
Tags
Full text
# FX Vanilla AUDUSD option with same Prem Date Delivery Date doesn't match Bloomberg's OVML pricing engine
# FX Vanilla AUDUSD option with same Prem Date Delivery Date doesn't match Bloomberg's OVML pricing engine
I'm trying to replicate how Bloomberg's OVML prices options.
Note that this deal has a premium date that's dated at the same date as the delivery/settlement date, which means that the time to delivery is 0.
My guess is whenever I'm solving for IV, I'm passing a premium that's too cheap, that's why the IV I solve overcompensates. This is probably because I'm discounting the premium improperly.
Same goes with the Premium being cheaper than the actual deal.
Here's the sample deal I'm trying to validate my pricer with using OVML.
```
# Sample Deal
deal_date = "April 8, 2026"
exp_date = "April 20, 2026"
delivery_date = "April 21, 2026"
opt_type = "Call"
strike = 0.7123
spot_ref = 0.7055
points = -0.00010569
notional = 618000
iv = 0.093107697 # Not a guess, actual OVML iv
premium = 1294.55 # Not a guess, actual OVML prem
```
Here's how OVML prices the option
Here's how my script prices the option
```
---DEAL DETAILS---
deal_date: April 8th, 2026
spot_date: April 10th, 2026
maturity_date: April 20th, 2026
delivery_date: April 21st, 2026
opt_type: Call
strike: 0.7123
spot_ref: 0.7055
points: -1.0569
notional (abs): 618000.00
premium (abs): 1294.55
implied_volatility: 9.31%
dummy_domestic_rate: 6.0%
implied_foreign_rate: 6.5814%
Using continuous discounting...
Solved Implied Volatility: 9.32%
Solved Premium: 1293.04
Using discrete discounting...
Solved Implied Volatility: 9.32%
Solved Premium: 1293.03
```
Here's my source code:
```
from typing import Tuple
import QuantLib as ql
from utils.helper_functions import to_ql_date # just a robust date converter
# PREREQUISITE SETUP
CAL_AU_QL = ql.Australia()
CAL_US_QL = ql.UnitedStates(ql.UnitedStates.FederalReserve)
CALENDAR_QL = ql.JointCalendar(CAL_AU_QL, CAL_US_QL, ql.JoinHolidays)
def implied_foreign_rate(
actual_days: int,
domestic_days_in_year: int,
foreign_days_in_year: int,
spot: float,
points: float,
domestic_rate: float = 0.05,
) -> float:
"""
Calculate the implied foreign interest rate using Covered Interest Rate Parity.
Parameters
----------
actual_days : int
The number of days in the tenor.
domestic_days_in_year : int
Day-count basis for the domestic currency (e.g., 360).
foreign_days_in_year : int
Day-count basis for the foreign currency (e.g., 365).
spot : float
The current FX spot rate.
points : float
The forward points (added to spot to get forward rate).
domestic_rate : float, default 0.05
The domestic interest rate in decimal (0.05 = 5%).
Returns
-------
float
The implied foreign rate in decimal form (e.g., 0.035 for 3.5%).
Raises
------
ZeroDivisionError
If 'spot + points' is zero or 'actual_days' is zero.
"""
if (spot + points) == 0:
raise ZeroDivisionError("Forward rate (spot + points) cannot be zero.")
if actual_days == 0:
raise ZeroDivisionError("actual_days cannot be zero for rate calculation.")
return (
((1 + domestic_rate * actual_days / domestic_days_in_year) * spot)
/ (spot + points)
- 1
) * (foreign_days_in_year / actual_days)
def calc_iv_and_prem(
deal_date,
exp_date,
delivery_date,
opt_type: str,
strike: float,
spot_ref: float,
points: float,
notional: float,
iv: float,
premium: float,
r_domestic: float = 0.06
) -> Tuple[float, float, float, float]:
"""
Calculate implied volatility and premium for AUDUSD options using both
continuous and discrete discounting methods.
This function utilizes the QuantLib library to model a Garman-Kohlhagen
process, accounting for joint Australian and US calendars and implied
foreign rates based on forward points.
Parameters
----------
deal_date : str or ql.Date
The date the deal is struck/evaluated.
exp_date : str or ql.Date
The expiration date of the option.
delivery_date : str or ql.Date
The settlement/delivery date of the option.
opt_type : str
The type of option, either "Call" or "Put".
strike : float
The strike price of the option.
spot_ref : float
The current spot reference price of AUDUSD.
points : float
The forward points (expressed as a decimal, e.g., -0.000036).
notional : float
The absolute notional amount in the foreign currency.
iv : float
The initial implied volatility (decimal) used for the pricing engine.
premium : float
The absolute domestic premium value used to solve for implied volatility.
r_domestic : float = 0.06
The deposit rate of the domestic currency. (Default is a dummy variable of 0.06)
Returns
-------
iv_cont : float
The solved implied volatility using continuous discounting.
price_cont : float
The solved premium amount using continuous discounting.
iv_disc : float
The solved implied volatility using discrete discounting.
price_disc : float
The solved premium amount using discrete discounting.
"""
eval_spot_date = CAL_US_QL.advance(
to_ql_date(deal_date), ql.Period(2, ql.Days), ql.ModifiedFollowing) # Use for T+2 standard settlement
dom_day_count = ql.Actual360()
for_day_count = ql.Actual365Fixed()
dom_prem_per_foreign = premium / notional # absolute value of domestic premium per foreign notional
ql.Settings.instance().evaluationDate = to_ql_date(deal_date)
r_foreign = implied_foreign_rate(to_ql_date(delivery_date)-eval_spot_date,
360,
365,
spot_ref,
points,
r_domestic
)
print("---DEAL DETAILS---")
print(f"""
deal_date: {to_ql_date(deal_date)}
spot_date: {eval_spot_date}
maturity_date: {to_ql_date(exp_date)}
delivery_date: {to_ql_date(delivery_date)}
opt_type: {opt_type}
strike: {strike:.4f}
spot_ref: {spot_ref:.4f}
points: {points*10000}
notional (abs): {notional:.2f}
premium (abs): {premium:.2f}
implied_volatility: {iv*100:.2f}%
dummy_domestic_rate: {r_domestic*100}%
implied_foreign_rate: {r_foreign*100:.4f}%
""" #implied_foreign_rate check with FXFA template/OVML calc (use points and dummy domestic rate) if given tenor does the implied rate match
)
# r_foreign * 100 # check with FXFA template if given tenor does the implied rate match
spot_handle = ql.QuoteHandle(
ql.SimpleQuote(
spot_ref
)
)
foreign_handle = ql.YieldTermStructureHandle(
ql.FlatForward(
0,
CAL_AU_QL,
r_foreign,
for_day_count
)
)
domestic_handle = ql.YieldTermStructureHandle(
ql.FlatForward(
0,
CAL_US_QL,
r_domestic,
dom_day_count
)
)
vol_handle = ql.BlackVolTermStructureHandle(
ql.BlackConstantVol(
to_ql_date(deal_date),
CALENDAR_QL,
iv,
ql.Actual365Fixed()
)
)
if opt_type == "Call":
payoff = ql.PlainVanillaPayoff(ql.Option.Call, strike)
else:
payoff = ql.PlainVanillaPayoff(ql.Option.Put, strike)
exercise = ql.EuropeanExercise(to_ql_date(exp_date))
option = ql.EuropeanOption(payoff, exercise)
process = ql.GarmanKohlagenProcess(
spot_handle,
foreign_handle,
domestic_handle,
vol_handle
)
print("Using continuous discounting...")
discount_factor_cont = domestic_handle.discount(to_ql_date(delivery_date)) # Uses continuous discounting (preferred for BSM)
adjusted_premium_cont = dom_prem_per_foreign * discount_factor_cont
iv_cont = option.impliedVolatility(adjusted_premium_cont, process) # RETURN OUTPUT
print(f"Solved Implied Volatility: {iv_cont * 100:.2f}%")
engine = ql.AnalyticEuropeanEngine(process)
option.setPricingEngine(engine)
option.NPV()
price_cont = option.NPV() / discount_factor_cont * notional # RETURN OUTPUT
print(f"Solved Premium: {price_cont:.2f}")
print("\nUsing discrete discounting...")
t = dom_day_count.yearFraction(to_ql_date(deal_date), to_ql_date(delivery_date)) # Uses discrete discounting (for test calc only)
discount_factor_disc = 1 / (1 + r_domestic * t)
adjusted_premium_disc = dom_prem_per_foreign * discount_factor_disc
iv_disc = option.impliedVolatility(adjusted_premium_disc, process) # RETURN OUTPUT
print(f"Solved Implied Volatility: {iv_disc * 100:.2f}%")
engine = ql.AnalyticEuropeanEngine(process)
option.setPricingEngine(engine)
option.NPV()
price_disc = option.NPV() / discount_factor_disc * notional # RETURN OUTPUT
print(f"Solved Premium: {price_disc:.2f}")
return iv_cont, price_cont, iv_disc, price_disc
# Sample Deal
deal_date = "April 8, 2026"
exp_date = "April 20, 2026"
delivery_date = "April 21, 2026"
opt_type = "Call"
strike = 0.7123
spot_ref = 0.7055
points = -0.00010569
notional = 618000
iv = 0.093107697 # Not a guess, actual OVML iv
premium = 1294.55 # Not a guess, actual OVML prem
calc_iv_and_prem(
deal_date,
exp_date,
delivery_date,
opt_type,
strike,
spot_ref,
points,
notional,
iv,
premium
)
```Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.