FX Volatility#
Interbank standard conventions for quoting FX volatility products are quite varied. None-the-less, rateslib provides the most common definitions and products, all priced using the Black-76 model.
Currently, in v1.2.x, there is no ability to build a volatility Surface.
However, there is a FXDeltaVolSmile
for options with consistent expiries,
and the ability to input vol
as an explicit value, to pricing methods.
The following Instruments are currently available.
|
Create an FX Call option. |
|
Create an FX Put option. |
|
Create an FX Risk Reversal option strategy. |
|
Create an FX Straddle option strategy. |
|
Create an FX Strangle option strategy. |
FXForwards Market#
As multi-currency derivatives, FX Options rely on the existence of an
FXForwards
object, which is usually determined
from non-volatility markets. See FX forwards. This will be used to forecast
forward FX rates relevant to the pricing of an arbitrary FX Option.
For the purpose of this user guide page, we create such a market below.
# FXForwards for FXOptions
In [1]: eureur = Curve(
...: {dt(2023, 3, 16): 1.0, dt(2023, 9, 16): 0.9851909811629752}, calendar="tgt", id="eureur"
...: )
...:
In [2]: usdusd = Curve(
...: {dt(2023, 3, 16): 1.0, dt(2023, 9, 16): 0.976009366603271}, calendar="nyc", id="usdusd"
...: )
...:
In [3]: eurusd = Curve(
...: {dt(2023, 3, 16): 1.0, dt(2023, 9, 16): 0.987092591908283}, id="eurusd"
...: )
...:
In [4]: fxr = FXRates({"eurusd": 1.0615}, settlement=dt(2023, 3, 20))
In [5]: fxf = FXForwards(
...: fx_curves={"eureur": eureur, "eurusd": eurusd, "usdusd": usdusd},
...: fx_rates=fxr
...: )
...:
In [6]: fxf._set_ad_order(1)
In [7]: fxf.swap("eurusd", [dt(2023, 3, 20), dt(2023, 6, 20)]) # should be 60.1 points
Out[7]: <Dual: 60.100000, (fx_eurusd, eurusd1, eurusd0, ...), [56.6, 5642.4, 5105.5, ...]>
Building and Pricing an Option#
Typing EURUSD Curncy OVML into Bloomberg will bring up the Bloomberg currency options pricer for Calls and Puts.
This can be replicated with rateslib native functionality via FXCall
and
FXPut
.
In [8]: fxc = FXCall(
...: pair="eurusd",
...: expiry=dt(2023, 6, 16),
...: notional=20e6,
...: strike=1.101,
...: payment_lag=dt(2023, 3, 20),
...: delivery_lag=2,
...: calendar="tgt",
...: modifier="mf",
...: premium_ccy="usd",
...: eval_date=NoInput(0),
...: option_fixing=NoInput(0),
...: premium=NoInput(0),
...: delta_type="forward",
...: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd","usd")],
...: spec=NoInput(0),
...: )
...:
In [9]: fxc.rate(fx=fxf, vol=8.9)
Out[9]: <Dual: 69.378270, (fx_eurusd, eurusd1, eurusd0, ...), [2501.2, 1403.4, 1269.8, ...]>
In [10]: fxc.analytic_greeks(vol=8.9, fx=fxf)
Out[10]:
{'delta': <Dual: 0.251754, (fx_eurusd, eurusd1, eurusd0, ...), [6.7, 3.8, 3.4, ...]>,
'delta_eur': <Dual: 5035073.728707, (fx_eurusd, eurusd1, eurusd0, ...), [134493518.8, 75460012.4, 68278851.0, ...]>,
'gamma': <Dual: 6.686817, (fx_eurusd, eurusd1, eurusd0, ...), [88.0, 49.4, 44.7, ...]>,
'gamma_eur_1%': <Dual: 1427648.701688, (fx_eurusd, eurusd1, eurusd0, ...), [20136216.9, 11297787.4, 10222632.0, ...]>,
'vega': <Dual: 0.168790, (fx_eurusd, eurusd1, eurusd0, ...), [2.5, 1.4, 1.3, ...]>,
'vega_usd': <Dual: 33757.945511, (fx_eurusd, eurusd1, eurusd0, ...), [507939.8, 284988.8, 257867.8, ...]>,
'vomma': <Dual: 0.905448, (fx_eurusd, eurusd1, eurusd0, ...), [-41.7, -23.4, -21.2, ...]>,
'vanna': <Dual: 2.557600, (fx_eurusd, eurusd1, eurusd0, ...), [-39.5, -22.2, -20.0, ...]>,
'_kega': <Dual: -0.369785, (fx_eurusd, eurusd1, eurusd0, ...), [11.7, 6.5, 5.9, ...]>,
'_kappa': <Dual: -0.234725, (fx_eurusd, eurusd1, eurusd0, ...), [-6.4, -3.6, -3.3, ...]>,
'__delta_type': 'forward',
'__vol': 0.08900000000000001,
'__strike': 1.101,
'__bs76': <Dual: 0.006934, (fx_eurusd, eurusd1, eurusd0, ...), [0.2, 0.1, 0.1, ...]>,
'__class': 'FXCallPeriod'}
The Call option priced above is partly unpriced becuase the premium is not directly specified. This means that rateslib will always assert the premium to be mid-market, based on the prevailing Curves, FXForwards and vol parameters supplied.
Changing some of the pricing parameters provides different prices. Rateslib is compared to Bloomberg’s OVML.
Premium currency: |
usd |
usd |
usd |
usd |
eur |
eur |
eur |
eur |
---|---|---|---|---|---|---|---|---|
Premium date: |
20/3/23 |
20/3/23 |
20/6/23 |
20/6/23 |
20/3/23 |
20/3/23 |
20/6/23 |
20/6/23 |
Delta type: |
Spot |
Forward |
Spot |
Forward |
Spot (pa) |
Forward (pa) |
Spot (pa) |
Forward (pa) |
Option rate (rateslib): |
69.3783 |
69.3783 |
70.2258 |
70.2258 |
0.65359 |
0.65359 |
0.65785 |
0.65785 |
Option rate (BBG): |
69.378 |
69.378 |
70.226 |
70.226 |
0.6536 |
0.6536 |
0.6578 |
0.6578 |
Delta % (rateslib): |
0.25012 |
0.25175 |
0.25012 |
0.25175 |
0.24359 |
0.24518 |
0.24359 |
0.24518 |
Delta % (BBG): |
0.25012 |
0.25175 |
0.25013 |
0.25176 |
0.24359 |
0.24518 |
0.24355 |
0.24518 |
Restrictions#
Rateslib currently allows the currency of the premium to only be either the domestic (LHS) or the foreign (RHS) currency of the FX pair of the option (which is also the default if none is specified).
If the currency is specified as foreign, then the pricing metric will be stated in pips and the percent delta calculations are unadjusted.
If the currency is stated as domestic, then the pricing metric is stated as percentage of notional and the percent delta calculations are premium adjusted.
Strikes given in Delta terms#
Commonly interbank Instruments are quoted in terms of delta values and the strikes are not explicitly stated. Suppose building a FXCall with a specified 25% delta.
In [11]: fxc = FXCall(
....: pair="eurusd",
....: expiry=dt(2023, 6, 16),
....: notional=20e6,
....: strike="25d",
....: payment_lag=2,
....: delivery_lag=2,
....: calendar="tgt",
....: premium_ccy="usd",
....: delta_type="spot",
....: )
....:
In [12]: fxc.rate(
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd","usd")],
....: fx=fxf,
....: vol=8.9
....: )
....:
Out[12]: <Dual: 70.180131, (fx_eurusd, eurusd1, eurusd0, ...), [2530.5, 1419.8, 1284.7, ...]>
When pricing functions are called, the strike on the option is implied from the vol and the delta value. This may
require a root finding algorithm particularly if the vol
is given as a Smile or a Surface. Relevant pricing
parameters can be seen by viewing analytic_greeks()
. The strike is also
automatically assigned, temporarily, to the attached FXCallPeriod
In [13]: fxc.analytic_greeks(
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd", "usd")],
....: fx=fxf,
....: vol=8.9
....: )
....:
Out[13]:
{'delta': <Dual: 0.250000, (fx_eurusd, eurusd1, eurusd0, ...), [6.7, 3.9, 3.3, ...]>,
'delta_eur': <Dual: 5000000.000000, (fx_eurusd, eurusd1, eurusd0, ...), [133587823.4, 77484546.1, 65319053.0, ...]>,
'gamma': <Dual: 6.679391, (fx_eurusd, eurusd1, eurusd0, ...), [88.0, 56.1, 38.0, ...]>,
'gamma_eur_1%': <Dual: 1418034.745178, (fx_eurusd, eurusd1, eurusd0, ...), [20012286.1, 11915312.4, 8063490.3, ...]>,
'vega': <Dual: 0.168746, (fx_eurusd, eurusd1, eurusd0, ...), [2.5, 1.4, 1.3, ...]>,
'vega_usd': <Dual: 33749.129781, (fx_eurusd, eurusd1, eurusd0, ...), [508084.8, 285070.2, 257941.4, ...]>,
'vomma': <Dual: 0.906235, (fx_eurusd, eurusd1, eurusd0, ...), [-41.7, -23.4, -21.1, ...]>,
'vanna': <Dual: 2.541766, (fx_eurusd, eurusd1, eurusd0, ...), [-39.2, -20.7, -21.2, ...]>,
'_kega': <Dual: -0.370007, (fx_eurusd, eurusd1, eurusd0, ...), [11.7, 6.5, 5.9, ...]>,
'_kappa': <Dual: -0.234606, (fx_eurusd, eurusd1, eurusd0, ...), [-6.4, -3.6, -3.3, ...]>,
'__delta_type': 'spot',
'__vol': 0.08900000000000001,
'__strike': 1.1010192011340847,
'__bs76': <Dual: 0.006930, (fx_eurusd, eurusd1, eurusd0, ...), [0.2, 0.1, 0.1, ...]>,
'__class': 'FXCallPeriod'}
In [14]: fxc.periods[0].strike
Out[14]: 1.1010192011340847
With altered pricing parameters, the Option strike will adapt accordingly to maintain the 25% spot delta calculation.
In [15]: fxc.rate(
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd","usd")],
....: fx=fxf,
....: vol=10.0, # <- A different vol will imply a different strike to maintain the same delta
....: )
....:
Out[15]: <Dual: 78.639814, (fx_eurusd, eurusd1, eurusd0, ...), [2530.5, 1419.8, 1284.7, ...]>
In [16]: fxc.analytic_greeks(
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd", "usd")],
....: fx=fxf,
....: vol=10.0
....: )
....:
Out[16]:
{'delta': <Dual: 0.250000, (fx_eurusd, eurusd1, eurusd0, ...), [5.9, 3.5, 2.9, ...]>,
'delta_eur': <Dual: 5000000.000000, (fx_eurusd, eurusd1, eurusd0, ...), [118893162.8, 69239842.0, 57858957.2, ...]>,
'gamma': <Dual: 5.944658, (fx_eurusd, eurusd1, eurusd0, ...), [69.1, 44.8, 29.1, ...]>,
'gamma_eur_1%': <Dual: 1262050.923208, (fx_eurusd, eurusd1, eurusd0, ...), [15851731.8, 9505382.0, 6181870.2, ...]>,
'vega': <Dual: 0.168746, (fx_eurusd, eurusd1, eurusd0, ...), [2.3, 1.3, 1.2, ...]>,
'vega_usd': <Dual: 33749.129781, (fx_eurusd, eurusd1, eurusd0, ...), [455692.8, 255674.7, 231343.4, ...]>,
'vomma': <Dual: 0.812787, (fx_eurusd, eurusd1, eurusd0, ...), [-33.0, -18.5, -16.8, ...]>,
'vanna': <Dual: 2.279667, (fx_eurusd, eurusd1, eurusd0, ...), [-30.8, -16.1, -16.8, ...]>,
'_kega': <Dual: -0.371474, (fx_eurusd, eurusd1, eurusd0, ...), [10.4, 5.8, 5.3, ...]>,
'_kappa': <Dual: -0.232923, (fx_eurusd, eurusd1, eurusd0, ...), [-5.7, -3.2, -2.9, ...]>,
'__delta_type': 'spot',
'__vol': 0.1,
'__strike': 1.1053863932903523,
'__bs76': <Dual: 0.007765, (fx_eurusd, eurusd1, eurusd0, ...), [0.2, 0.1, 0.1, ...]>,
'__class': 'FXCallPeriod'}
In [17]: fxc.periods[0].strike
Out[17]: 1.1053863932903523
Straddles#
An FXStraddle
is the most frequently traded instrument for outright exposure to
volatility. Straddles are defined by a single strike, which can be a defined numeric value (for a ‘struck’ deal),
or an or associated value, e.g. “atm_delta”, “atm_forward” or “atm_spot”.
The default pricing metric
for an FX Straddle is vol points.
In [18]: fxstr = FXStraddle(
....: pair="eurusd",
....: expiry=dt(2023, 6, 16),
....: notional=20e6,
....: strike="atm_delta",
....: payment_lag=2,
....: delivery_lag=2,
....: calendar="tgt",
....: premium_ccy="usd",
....: delta_type="spot",
....: )
....:
In [19]: fxstr.rate(
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd", "usd")],
....: fx=fxf,
....: vol=8.9,
....: )
....:
Out[19]: 8.9
In [20]: fxstr.analytic_greeks(
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd", "usd")],
....: fx=fxf,
....: vol=8.9,
....: )
....:
Out[20]:
{'gamma': <Dual: 16.713274, (fx_eurusd, eurusd1, eurusd0, ...), [-15.7, 8.1, -24.7, ...]>,
'_kappa': <Dual: 0.035191, (fx_eurusd, eurusd1, eurusd0, ...), [-16.6, -9.3, -8.4, ...]>,
'vega_usd': <Dual: 84447.582646, (fx_eurusd, eurusd1, eurusd0, ...), [79555.0, 44635.7, 40388.0, ...]>,
'vomma': <Dual: -0.000000, (fx_eurusd, eurusd1, eurusd0, ...), [-4.5, -2.5, -2.3, ...]>,
'vega': <Dual: 0.422238, (fx_eurusd, eurusd1, eurusd0, ...), [0.4, 0.2, 0.2, ...]>,
'delta': <Dual: 0.000000, (fx_eurusd, eurusd1, eurusd0, ...), [16.7, 9.4, 8.5, ...]>,
'gamma_eur_1%': <Dual: 3548227.972514, (fx_eurusd, eurusd1, eurusd0, ...), [-0.0, 1719168.6, -5245206.6, ...]>,
'vanna': <Dual: 0.397985, (fx_eurusd, eurusd1, eurusd0, ...), [-187.8, -105.2, -95.5, ...]>,
'_kega': <Dual: 0.000000, (fx_eurusd, eurusd1, eurusd0, ...), [22.6, 12.7, 11.5, ...]>,
'delta_eur': <Dual: 0.000000, (fx_eurusd, eurusd1, eurusd0, ...), [334265470.8, 187545666.1, 169697859.6, ...]>,
'__class': 'FXOptionStrat',
'__options': [{'delta': <Dual: -0.496763, (fx_eurusd, eurusd1, eurusd0, ...), [8.4, 4.4, 4.5, ...]>,
'delta_eur': <Dual: -9935253.353127, (fx_eurusd, eurusd1, eurusd0, ...), [167132735.4, 88740248.8, 89816556.5, ...]>,
'gamma': <Dual: 8.356637, (fx_eurusd, eurusd1, eurusd0, ...), [-7.9, 4.0, -12.4, ...]>,
'gamma_eur_1%': <Dual: 1774113.986257, (fx_eurusd, eurusd1, eurusd0, ...), [-0.0, 859584.3, -2622603.3, ...]>,
'vega': <Dual: 0.211119, (fx_eurusd, eurusd1, eurusd0, ...), [0.2, 0.1, 0.1, ...]>,
'vega_usd': <Dual: 42223.791323, (fx_eurusd, eurusd1, eurusd0, ...), [39777.5, 22317.9, 20194.0, ...]>,
'vomma': <Dual: -0.000000, (fx_eurusd, eurusd1, eurusd0, ...), [-2.2, -1.3, -1.1, ...]>,
'vanna': <Dual: 0.198992, (fx_eurusd, eurusd1, eurusd0, ...), [-93.9, -52.6, -47.8, ...]>,
'_kega': <Dual: 0.000000, (fx_eurusd, eurusd1, eurusd0, ...), [11.3, 6.3, 5.7, ...]>,
'_kappa': <Dual: 0.511301, (fx_eurusd, eurusd1, eurusd0, ...), [-8.3, -4.7, -4.2, ...]>,
'__delta_type': 'spot',
'__vol': 0.08900000000000001,
'__strike': 1.0685761878290885,
'__bs76': <Dual: 0.019328, (fx_eurusd, eurusd1, eurusd0, ...), [-0.5, -0.3, -0.3, ...]>,
'__class': 'FXPutPeriod'},
{'delta': <Dual: 0.496763, (fx_eurusd, eurusd1, eurusd0, ...), [8.4, 4.9, 4.0, ...]>,
'delta_eur': <Dual: 9935253.353127, (fx_eurusd, eurusd1, eurusd0, ...), [167132735.4, 98805417.4, 79881303.1, ...]>,
'gamma': <Dual: 8.356637, (fx_eurusd, eurusd1, eurusd0, ...), [-7.9, 4.0, -12.4, ...]>,
'gamma_eur_1%': <Dual: 1774113.986257, (fx_eurusd, eurusd1, eurusd0, ...), [-0.0, 859584.3, -2622603.3, ...]>,
'vega': <Dual: 0.211119, (fx_eurusd, eurusd1, eurusd0, ...), [0.2, 0.1, 0.1, ...]>,
'vega_usd': <Dual: 42223.791323, (fx_eurusd, eurusd1, eurusd0, ...), [39777.5, 22317.9, 20194.0, ...]>,
'vomma': <Dual: -0.000000, (fx_eurusd, eurusd1, eurusd0, ...), [-2.2, -1.3, -1.1, ...]>,
'vanna': <Dual: 0.198992, (fx_eurusd, eurusd1, eurusd0, ...), [-93.9, -52.6, -47.8, ...]>,
'_kega': <Dual: 0.000000, (fx_eurusd, eurusd1, eurusd0, ...), [11.3, 6.3, 5.7, ...]>,
'_kappa': <Dual: -0.476110, (fx_eurusd, eurusd1, eurusd0, ...), [-8.3, -4.7, -4.2, ...]>,
'__delta_type': 'spot',
'__vol': 0.08900000000000001,
'__strike': 1.0685761878290885,
'__bs76': <Dual: 0.018276, (fx_eurusd, eurusd1, eurusd0, ...), [0.5, 0.3, 0.3, ...]>,
'__class': 'FXCallPeriod'}],
'__delta_type': 'spot'}
In [21]: fxstr.plot_payoff(
....: range=[1.025, 1.11],
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd", "usd")],
....: fx=fxf,
....: vol=8.9,
....: )
....:
Out[21]:
(<Figure size 640x480 with 1 Axes>,
<Axes: >,
[<matplotlib.lines.Line2D at 0x7f8670a2d310>])
(Source code
, png
, hires.png
, pdf
)
Risk Reversals#
FXRiskReversal
are frequently traded products and often used
in calibrating a volatility Surface or Smile.
RiskReversals need to be specified by two different strike
values; a
lower and a higher strike. These can be entered in delta terms. Pricing also allows
two different vol
inputs if a volatility Surface or Smile is not given.
The default pricing metric
for a RiskReversal is ‘vol’ which calculates the difference in volatility
quotations for each option.
In [22]: fxrr = FXRiskReversal(
....: pair="eurusd",
....: expiry=dt(2023, 6, 16),
....: notional=20e6,
....: strike=("-25d", "25d"),
....: payment_lag=2,
....: delivery_lag=2,
....: calendar="tgt",
....: premium_ccy="usd",
....: delta_type="spot",
....: )
....:
In [23]: fxrr.rate(
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd", "usd")],
....: fx=fxf,
....: vol=[10.15, 8.9]
....: )
....:
Out[23]: -1.25
In [24]: fxrr.plot_payoff(
....: range=[1.025, 1.11],
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd", "usd")],
....: fx=fxf,
....: vol=[10.15, 8.9]
....: )
....:
Out[24]:
(<Figure size 640x480 with 1 Axes>,
<Axes: >,
[<matplotlib.lines.Line2D at 0x7f8670ab0e90>])
(Source code
, png
, hires.png
, pdf
)
Strangles#
The other common Instrument combination for calibrating Surfaces and Smiles is an
FXStrangle
. Again, the strangle requires two strike inputs,
which can be input in delta terms.
The default pricing metric
for a strangle is ‘single_vol’, which quotes a single volatility
value used to price the strike and premium for each option. Rateslib uses an iteration to
calculate this (see rate()
) from a Surface or Smile.
In [25]: fxstg = FXStrangle(
....: pair="eurusd",
....: expiry=dt(2023, 6, 16),
....: notional=20e6,
....: strike=("-25d", "25d"),
....: payment_lag=2,
....: delivery_lag=2,
....: calendar="tgt",
....: premium_ccy="usd",
....: delta_type="spot",
....: )
....:
In [26]: fxstg.rate(
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd", "usd")],
....: fx=fxf,
....: vol=[10.15, 8.9]
....: )
....:
Out[26]: <Dual: 9.533895, (fx_eurusd, eurusd1, eurusd0, ...), [-8.1, -4.6, -4.1, ...]>
In [27]: fxstg.plot_payoff(
....: range=[1.025, 1.11],
....: curves=[None, fxf.curve("eur", "usd"), None, fxf.curve("usd", "usd")],
....: fx=fxf,
....: vol=9.533895,
....: )
....:
Out[27]:
(<Figure size 640x480 with 1 Axes>,
<Axes: >,
[<matplotlib.lines.Line2D at 0x7f867098d5d0>])
(Source code
, png
, hires.png
, pdf
)