I'm trying to replicate how Bloomberg's OVML prices options. 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: {points10000}
notional (abs): {notional:.2f}
premium (abs): {premium:.2f}
implied_volatility: {iv100:.2f}%
dummy_domestic_rate: {r_domestic100}%
implied_foreign_rate: {r_foreign100:.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 )

