EUR_USD = 1.18
spot_quote = ql.SimpleQuote(EUR_USD)
today = ql.Date(16, ql.September, 2025)
r_d = 0.045
r_f = 0.02
vol_quote = ql.SimpleQuote(0.13)
spot_handle = ql.QuoteHandle(spot_quote)
vol_handle = ql.QuoteHandle(vol_quote)
dc = ql.Actual365Fixed()
calendar = ql.JointCalendar(ql.Italy(), ql.UnitedStates(ql.UnitedStates.NYSE))
expiration_date = today + ql.Period(6, ql.Months)
expiration_time = dc.yearFraction(today, expiration_date)
domestic_rf_handle = ql.YieldTermStructureHandle(ql.FlatForward(today, r_d, dc))
foreign_rf_handle = ql.YieldTermStructureHandle(ql.FlatForward(today, r_f, dc))
black_vol = ql.BlackConstantVol(today, ql.NullCalendar(), vol_handle, dc)
vol_ts_handle = ql.BlackVolTermStructureHandle(black_vol)
# Setting the global evaluation date
ql.Settings.instance().evaluationDate = todayIn this notebook, we take on the role of a trader working on an investment bank’s exotic options desk, focusing on FX derivatives. We will explore how such traders hedge their books and how the portfolio’s value and Greek exposures change as the underlying market parameters evolve. To keep things simple, we assume the underlying follows a geometric Brownian motion, with flat term structures for both the volatility surface and the domestic and foreign interest rates. This places us in a Black-Scholes framework (specifically the BS-extended model for FX options, the Garman-Kohlhagen model), described by the following dynamics under the risk-neutral measure:
\[ \begin{align*} dS_t &= (r_d - r_f) S_t \, dt + \sigma S_t \, dW_t^d, \\ dB^d_t &= r_d B^d_t\, dt, \\ dB^f_t &= r_f B^f_t\, dt \end{align*} \]
The investments banks are said to be on the “sell-side”, which basically means that they facilitate transactions between institutions, and provide access to financial markets. In our case we are manifacturing exotics options for our clients, who are seeking market opportunities. This leaves the trader with a significant exposition, as the bank take the opposite side of client trades. To manage the risk and ensure the bank meet its obligations when options are exercised, the trader must hedge against market movements.
The hedging of exotics does not only involves hedging first order greeks (the \(\Delta\) and the \(\mathcal{V}\)), but its involves hedging high order greeks like \(\Gamma\), \(Vanna\) (DvolDspot) and \(Volga\) (DvegaDvol). Another completixity when hedging exotics option, especially when dealing with binary or barriers, pin risks needs to be taken into account when the barrier is hit.
The library we use for option pricing is QuantLib, the largest open-source quantitative finance library available. QuantLib offers a comprehensive suite of tools for modeling, pricing, and risk management of financial derivatives. Its active community and extensive documentation make it a popular choice among both practitioners and researchers in the field.
Market Data
Let’s assume that we are an USD based desk and we are dealing with EURUSD exotics. The initial market data at time \(t = 0\) is the following
\[ \begin{align*} S_0 &= 1.18 \\ Date(Today) &= 16/09/2025 \\ T &= 0.5 \text{ (time to expiration in years)} \\ r_d &= 0.045 \\ r_f &= 0.02 \\ \sigma &= 0.13 \\ \end{align*} \]
eur_dates = [ql.Date(27, ql.August, 2025), ql.Date(27, ql.August, 2026), ql.Date(27, ql.August, 2027)]
eur_dfs = [1.0, 0.98, 0.95]
eur_curve = ql.DiscountCurve(eur_dates, eur_dfs, dc)
eur_curve.enableExtrapolation()
usd_dates = [ql.Date(27, ql.August, 2025), ql.Date(27, ql.August, 2026), ql.Date(27, ql.August, 2027)]
usd_dfs = [1.0, 0.985, 0.96]
usd_curve = ql.DiscountCurve(usd_dates, usd_dfs, dc)
usd_curve.enableExtrapolation()The forex option market has a particular way of quoting options, the strike prices are quoted in terms of the Delta of the option: this means that before closing the deal, the strike level is not determined yet in absolute terms. Once the deal is closed, given the level of FX spot rate and the IV agreed upon, the strike will be set at a level yielding the BS Delta the two counterparties were dealing. This way of quoting is smart: it allows us not to worry about small movements of the underlying during the bargaining process, because the absolute strike will be defined only after the agreement on the price, so that the trader is sure to trade an option with given features in terms of exposures both to the underlying asset and to the implied volatility.
The most liquid FX optios are usually the 25-\(\Delta\) calls and put and the ATM calls and puts, thus those are going to be our basic hedging instruments and the strikes that we are going to refer to for the exotic in our book.
Here is an example on how FX volatility are quoted:
| \(10\Delta p\) | \(25\Delta p\) | \(35\Delta p\) | ATM | \(35\Delta c\) | \(25\Delta c\) | \(10\Delta c\) | |
|---|---|---|---|---|---|---|---|
| 1W | 11.96% | 11.69% | 11.67% | 11.75% | 11.94% | 12.19% | 12.93% |
| 2W | 11.81% | 11.54% | 11.52% | 11.60% | 11.79% | 12.04% | 12.78% |
| 1M | 11.60% | 11.39% | 11.39% | 11.50% | 11.72% | 11.99% | 12.77% |
| 2M | 11.43% | 11.16% | 11.15% | 11.25% | 11.48% | 11.76% | 12.60% |
| 3M | 11.22% | 10.92% | 10.90% | 11.00% | 11.23% | 11.52% | 12.39% |
| 6M | 11.12% | 10.78% | 10.76% | 10.87% | 11.12% | 11.43% | 12.39% |
| 9M | 11.04% | 10.72% | 10.71% | 10.83% | 11.09% | 11.41% | 12.39% |
| 1Y | 11.00% | 10.69% | 10.68% | 10.80% | 11.06% | 11.39% | 12.38% |
| 2Y | 11.02% | 10.63% | 10.60% | 10.70% | 10.94% | 11.28% | 12.34% |
To get the strike from the Delta-Vol quote QuantLib comes in handy with a BlackDeltaCalculator that from a given delta (and option type, discount factor, spot value, and delta-vol quote value) it returns the strike associated with it.
call_barrier_factory = FxBarrierOptionFactory(spot_handle, ql.Option.Call)
put_barrier_factory = FxBarrierOptionFactory(spot_handle, ql.Option.Put)
put_vanilla_factory = FxVanillaOptionFactory(spot_handle, expiration_date, ql.Option.Put)
call_vanilla_factory = FxVanillaOptionFactory(spot_handle, expiration_date, ql.Option.Call)
delta_call_calc = ql.BlackDeltaCalculator(
ql.Option.Call,
ql.DeltaVolQuote.Spot,
EUR_USD,
eur_curve.discount(expiration_time),
usd_curve.discount(expiration_time),
np.sqrt(black_vol.blackVariance(expiration_time, EUR_USD)))
delta_put_calc = ql.BlackDeltaCalculator(
ql.Option.Put,
ql.DeltaVolQuote.Spot,
EUR_USD,
eur_curve.discount(expiration_time),
usd_curve.discount(expiration_time),
np.sqrt(black_vol.blackVariance(expiration_time, EUR_USD)))
# Get the strikes equivalent for the 0.25 delta for call and put options
d25_call_strike = delta_call_calc.strikeFromDelta(0.25)
d25_put_strike = delta_put_calc.strikeFromDelta(-0.25)
atm_strike = delta_call_calc.atmStrike(ql.DeltaVolQuote.AtmDeltaNeutral)The model that extends the Black-Scholes model when dealing with FX options is the Garman-Kohlhagen model, whose dynamics have been defined above. Thus we need to create an instance of the Garman-Kohlhagen process in order to price FX options.
For the sake of simplicity we assume that all the options on the book have the same expiration, and again since we are under the Garman-Kohlhagen model the vol curve is flat, thus the vol across the different strikes is the same (unrealistic assumption). Here’s how the trader book is composed of:
1st Barrier
\[ \begin{align*} \text{Option type} &= \text{Call} \\ \text{Notional} &= \$1000000 \\ \text{Barrier} &= 1.30 \\ \text{Strike} &= 25\Delta c \\ \text{Barrier Type} &= \text{Up and Out} \end{align*} \]
# Barrier 1
notional = 1_000_000
barrier = 1.30
barrier_type = ql.Barrier.UpOut
exercise = ql.EuropeanExercise(expiration_date)
barrier_25d_130b = call_barrier_factory.get_option(d25_call_strike, exercise, barrier, barrier_type)
barrier_1 = BarrierPosition(barrier_25d_130b, notional, NullInstrument())2nd Barrier
\[ \begin{align*} \text{Option type} &= \text{Call} \\ \text{Notional} &= \$2000000 \\ \text{Barrier} &= 1.08 \\ \text{Strike} &= 25\Delta c \\ \text{Barrier Type} &= \text{Down and Out} \end{align*} \]
# Barrier 2
notional = 2_000_000
barrier = 1.08
barrier_type = ql.Barrier.DownOut
exercise = ql.EuropeanExercise(expiration_date)
barrier_25d_108b = call_barrier_factory.get_option(d25_call_strike, exercise, barrier, barrier_type)
barrier_2 = BarrierPosition(barrier_25d_108b, notional, NullInstrument())3rd Barrier
\[ \begin{align*} \text{Option type} &= \text{Call} \\ \text{Notional} &= \$1000000 \\ \text{Barrier} &= 1.20 \\ \text{Strike} &= 25\Delta c \\ \text{Barrier Type} &= \text{Up and In} \end{align*} \]
# Barrier 3
notional = 1_000_000
barrier = 1.20
barrier_type = ql.Barrier.UpIn
exercise = ql.EuropeanExercise(expiration_date)
barrier_25d_120b = call_barrier_factory.get_option(d25_call_strike, exercise, barrier, barrier_type)
backup_vanilla = call_vanilla_factory.get_option(d25_call_strike)
FxVanillaOptionFactory.assign_analyitcal_price_engine(backup_vanilla, process)
barrier_3 = BarrierPosition(barrier_25d_120b, notional, backup_vanilla)4th Barrier
\[ \begin{align*} \text{Option type} &= \text{Put} \\ \text{Notional} &= \$1000000 \\ \text{Barrier} &= 1.05 \\ \text{Strike} &= 25\Delta p \\ \text{Barrier Type} &= \text{Down and Out} \end{align*} \]
# Barrier 4
notional = 1_000_000
barrier = 1.05
barrier_type = ql.Barrier.DownOut
exercise = ql.EuropeanExercise(expiration_date)
barrier_25d_105b = put_barrier_factory.get_option(d25_put_strike, exercise, barrier, barrier_type)
barrier_4 = BarrierPosition(barrier_25d_105b, notional, NullInstrument())5th Barrier
\[ \begin{align*} \text{Option type} &= \text{Put} \\ \text{Notional} &= \$1000000 \\ \text{Barrier} &= 1.25 \\ \text{Strike} &= 25\Delta p \\ \text{Barrier Type} &= \text{Up and Out} \end{align*} \]
# Barrier 5
notional = 2_000_000
barrier = 1.25
barrier_type = ql.Barrier.UpOut
exercise = ql.EuropeanExercise(expiration_date)
barrier_25d_105b = put_barrier_factory.get_option(d25_put_strike, exercise, barrier, barrier_type)
barrier_5 = BarrierPosition(barrier_25d_105b, notional, NullInstrument())book: List[BarrierPosition] = [barrier_1, barrier_2, barrier_3, barrier_4, barrier_5]
for ins in book:
FxBarrierOptionFactory.assign_analyitcal_price_engine(ins.instrument, process)Thus the premium of the options will be the following
[309.09996820797926,
36387.852679406504,
18342.987983359482,
2064.118402600054,
25897.462236607375]
Since the AnalyticBarrierEngine in QuantLib doesn’t provide greek calculation functionalities, the simplest method that we can use to calculate the greeks numerically is by using the finite difference method. Let’s the define the basic central differences formulas below, and then specialize them using a partial function application to get the greeks of interest.
First order Finite Difference:
\[ f'(x) = \frac{f(x + h) - f(x - h)}{2 \cdot h} \]
Second order Finite Difference:
\[ f''(x) = \frac{f(x + h) - 2f(x) + f(x - h)}{h^2} \]
Cross partial derivative:
\[ f_{xy}(x, y) = \frac{f(x + h, y + k) - f(x +h, y - k) - f(x - h, y + k) + f(x - h, y - k)}{4hk} \]
def central_diff(instrument: ql.Instrument, h: float, quote: ql.SimpleQuote) -> float:
u0 = quote.value()
quote.setValue(u0 + h)
P_Plus = instrument.NPV()
quote.setValue(u0 - h)
P_Minus = instrument.NPV()
quote.setValue(u0)
return (P_Plus - P_Minus) / (2 * h)
def central_diff_2nd(instrument: ql.Instrument, h: float, quote: ql.SimpleQuote) -> float:
u0 = quote.value()
P = instrument.NPV()
quote.setValue(u0 + h)
P_Plus = instrument.NPV()
quote.setValue(u0 - h)
P_Minus = instrument.NPV()
quote.setValue(u0)
return (P_Plus - 2*P + P_Minus) / (h * h)
def cross_central_diff(instrument: ql.Instrument, h: float, k: float, quote_1: ql.SimpleQuote, quote_2: ql.SimpleQuote) -> float:
u1_0 = quote_1.value()
u2_0 = quote_2.value()
quote_1.setValue(u1_0 + h)
quote_2.setValue(u1_0 + k)
P_Plus_Plus = instrument.NPV()
quote_1.setValue(u1_0 + h)
quote_2.setValue(u1_0 - k)
P_Plus_Minus = instrument.NPV()
quote_1.setValue(u1_0 - h)
quote_2.setValue(u1_0 + k)
P_Minus_Plus = instrument.NPV()
quote_1.setValue(u1_0 - h)
quote_2.setValue(u1_0 - k)
P_Minus_Minus = instrument.NPV()
quote_1.setValue(u1_0)
quote_2.setValue(u2_0)
return (P_Plus_Plus - P_Plus_Minus - P_Minus_Plus + P_Minus_Minus) / (4 * h * k)From that we can easily define the greeks functions:
delta = partial(central_diff, quote=spot_quote)
vega = partial(central_diff, quote=vol_quote)
gamma = partial(central_diff_2nd, quote=spot_quote)
volga = partial(central_diff_2nd, quote=vol_quote)
vanna = partial(cross_central_diff, quote_1=spot_quote, quote_2=vol_quote)Hedging the book
Now that we have set up the trader portfolio, we need to find the optimal hedge for that portfolio that is going to minimize the sensitivities that we want not be exposed to. Usually a trader sitting in a exotic desk will not just keep low exposure to the classical greeks like \(\Delta\), \(\Gamma\) and \(\mathcal{V}\), but also in some higher order Greeks live DVegaDvol (a.k.a. \(Volga\)) and the DVegaDSpot (a.k.a. \(Vanna\)).
This need comes from the fact that in case of exotics (especially for barrier and binaries) the “barrier” risks needs to be hedged: as spot moves closer to the barrier, vega can spike, that’s vanna risk.
The most common way to hedge those greeks in the FX option market is by using simple structure that can be obtained by \(25 \Delta\) calls and puts and ATM calls and puts. Here’s the basic structures that will compose our hedging portfolio:
- Spot: to oviously to hedge the \(\Delta\)
- ATM straddles: involves buying an ATM vanilla call and ATM vanilla put, this structure provides positive gamma and positive vega
- RR (Risks reversals): involves buying an \(25 \Delta\) vanilla call and sell a \(25 \Delta\) vanilla put, provides a strong vanna exposure (becuase calls and puts respond differently as spot moves)
- Butterflies: involves buying an \(25 \Delta\) vanilla call and sell a \(25 \Delta\) vanilla put and selling twice a straddle, provides a strong volga exposure.
Hedge methodology
The way we are going to find the optimal weights of our hedging portfolio is by using the classical parameter hedging methodology. Which mathematically means to set up a linear system where:
The exposure vector is:
\[ \mathbf{E} = \begin{bmatrix} \Delta \\ \Gamma \\ \text{Vega} \\ \text{Vanna} \\ \text{Volga} \end{bmatrix}. \]
which represent the total exposure of our portfolio (the weighted sum of each instrument greek in our exotic portfolio). For each hedge instrument \(j\) (spot, ATM straddle, RR, BF), compute its greek vector \(\mathbf{h}_j\). Form the hedge matrix:
\[ H = \begin{bmatrix} \mathbf{h}_1 & \mathbf{h}_2 & \dots & \mathbf{h}_n \end{bmatrix}. \]
Then solve for weights \(\mathbf{x}\) (position sizes):
\[ H \mathbf{x} - E \approx 0. \]
In our case the exposure vector is bigger than the hedging vector \(x\), since we only 4 instruments and 5 greeks to hedge. We end up with a linear system with 4 variables and 5 equations, thus there is not an exact solution, but we can find a solution that minimized the sum of residual squares.
When we want to calculate the greeks of our portfolio, we want to do it in meaningful way, thus this requires to us to set the shift operator in a way that makes sense to measure the specific risk of our porfolio. The two main ways that industry professional do that is by using 1% of the spot change and 1% vol change.
spot_change = 0.01 * spot_quote.value() / 2
vol_change = 0.01 / 2
delta_book = sum([pos.sensitivity(delta, h=spot_change) for pos in book])
vega_book = sum([pos.sensitivity(vega, h=vol_change) for pos in book])
volga_book = sum([pos.sensitivity(volga, h=vol_change) for pos in book])
gamma_book = sum([pos.sensitivity(gamma, h=spot_change) for pos in book])
vanna_book = sum([pos.sensitivity(vanna, h=spot_change, k=vol_change) for pos in book])The raw Greek exposition for our book is then the following:
Book Delta exposure: 371644.134787602
Book Gamma exposure: 13330632.102691187
Book Vega exposure: 1183872.580309415
Book Volga exposure: 380633.56067277736
Book Vanna exposure: 190025.47439962075
In FX, Greeks can be confusing, because they depend on the quotation of the currency pair as well as the currency in which they are calculated. Furthermore, premium can be included or excluded, smile-effect can be included or not included, and numerical approximations may further add to the confusion.
What traders use in real life are a “modified version” of the plain greeks:
The plain \(\Delta\) of a forex option gives the amount of Foreing currency a trader would have to buy in the spot-market to delta-hedge a sold-option. To get the amount of dollars to buy those EURs (in our case) we need to multiply the delta by the current spot value.
Traders’ \(\Gamma\): where the spot in the finite diff method is varied by 0.5% and then multiplied by 1%, since traders consider the change of delta as spot changes relatively by 1%.
\[ \Gamma^{tr}_t = \Gamma_t \frac{S_t}{100} \]
- Traders’ \(\mathcal{V}\): A trader will typically consider vega as the change of the USD or EUR value of a derivative contract (or a book of derivatives) assuming a 1% absolute/constant change in volatility.
\[ \frac{V(\sigma + 0.5\%) - V(\sigma - 0.5\%)}{1\%} \cdot 1\% \]
Book Delta exposure: $438540.0790493703
Book Gamma exposure: $157301.458811756
Book Vega exposure: $11838.72580309415
Book Volga exposure: $3806.3356067277737
Book Vanna exposure: $2242.300597915525
As you might guess the exposition is negative since we are on the sell-side of the trade, thus we need to buy the hedging structures to hedge ourselves from those greeks.
Our exposure vector \(E = [ \Delta, \Gamma, \mathcal{V}, Volga, Vanna ]\) is:
# Greek exposure vector
E = np.array([delta_book, gamma_book, vega_book, volga_book, vanna_book])
Earray([ 371644.1347876 , 13330632.10269119, 1183872.58030942,
380633.56067278, 190025.47439962])
For each of the base instrument (Spot, ATM call, ATM put, \(25\Delta\) call and \(25\Delta\) put) let’s calculate the basic greeks.
# hedge_matrix
base_notional = np.float64(1_000_000)
spot_greeks = np.array([1, 0, 0, 0, 0]) * base_notional
atm_call_greeks = np.array([
delta(atm_call, h=spot_change),
gamma(atm_call, h=spot_change),
vega(atm_call, h=vol_change),
volga(atm_call, h=vol_change),
vanna(atm_call, h=spot_change, k=vol_change),
]) * base_notional
atm_put_greeks = np.array([
delta(atm_put, h=spot_change),
gamma(atm_put, h=spot_change),
vega(atm_put, h=vol_change),
volga(atm_put, h=vol_change),
vanna(atm_put, h=spot_change, k=vol_change),
]) * base_notional
delta25_call_greeks = np.array([
delta(delta25_call, h=spot_change),
gamma(delta25_call, h=spot_change),
vega(delta25_call, h=vol_change),
volga(delta25_call, h=vol_change),
vanna(delta25_call, h=spot_change, k=vol_change),
]) * base_notional
delta25_put_greeks = np.array([
delta(delta25_put, h=spot_change),
gamma(delta25_put, h=spot_change),
vega(delta25_put, h=vol_change),
volga(delta25_put, h=vol_change),
vanna(delta25_put, h=spot_change, k=vol_change),
]) * base_notionalFrom there let’s calculate the greek exposure for the strcutures that we are going to use in out hedging portfolio, and then create our Hedge Matrix:
\[ H = \left[ \begin{array}{c|cccc} & \text{Spot} & \text{Straddle} & \text{RR} & \text{Butterfly} \\ \hline \Delta & h_{11} & h_{12} & h_{13} & h_{14} \\ \Gamma & h_{21} & h_{22} & h_{23} & h_{24} \\ \text{Vega} & h_{31} & h_{32} & h_{33} & h_{34} \\ \text{Volga} & h_{41} & h_{42} & h_{43} & h_{44} \\ \text{Vanna} & h_{51} & h_{52} & h_{53} & h_{54} \\ \end{array} \right] \]
# Let's create the straddle, RR, Butterfly from the base vanilla options
atm_straddle = atm_call_greeks + atm_put_greeks
rr = delta25_call_greeks - delta25_put_greeks
butterfly = delta25_call_greeks + delta25_put_greeks - 2 * atm_straddle
# Hedge Matrix
H = np.stack([spot_greeks, atm_straddle, rr, butterfly]).TSince our system have more equations than variables, we must use a least square method (provided by np.linalg.lstsq) to solve it. Least square method minimizes the Euclidian 2-norm $ || E - Hx || $.
x, _, _, _ = np.linalg.lstsq(H, E)Now that we have a solution let’s see what’s our remaining exposure to each of the choosen hedged greeks. We do that by
\[ H x - E \]
residual_greeks = (H @ x - E).tolist()
print(f"Residual Delta exposure: ${residual_greeks[0] * spot_quote.value()}")
print(f"Residual Gamma exposure: ${residual_greeks[1] * 0.01 * spot_quote.value()}")
print(f"Residual Vega exposure: ${residual_greeks[2] * 0.01}")
print(f"Residual Volga exposure: ${residual_greeks[3] * 0.01}")
print(f"Residual Vanna exposure: ${residual_greeks[4] * spot_quote.value() * 0.01}")Residual Delta exposure: $-2.6100315153598783e-09
Residual Gamma exposure: $-13.422826034608855
Residual Vega exposure: $126.72764540341916
Residual Volga exposure: $0.016476123058237136
Residual Vanna exposure: $-0.09291220850032404
Another crucial thing when hedging a book is, of course, to measure the P&L from one rebalancing period to another. Let’s then calculate the value of the various components of our hedging portfolio. We have the the P&L from time \(t\) is calculated as:
\[ P\&L_{t \rightarrow t + 1} = (V_{t + 1}^{book} + V_{t + 1}^{hedge} + C_{t + 1}) - (V_{t}^{book} + V_{t}^{hedge} + C_{t}) \]
where:
- \(V_{t}^{book}\) is the value of our book at time \(t\), which is the sum of the premiums that we’ve been collected
- \(V_{t}^{hedge}\) is the value of the hedging portfolio, the sum of the premium of the structures that we have bought to hedge our from the various sensistivies
- \(C_t\) the cash/bank account of our portfolio which includes financing of cash and trade cashflows
A shorter way to express the P&L, is just by considering the variation of the value of the book + the variation of the value of the hedges + cost of borrowing + interest accrued:
\[ P\&L_{t \rightarrow t + 1} = \delta V_{t, t + 1}^{book} + \delta V_{t, t + 1}^{hedge} + \text{cost of borrowing} + \text{interest accrued} \]
since the rebalancing of the hedges of the bank account happend at the end of trading day.
spot_premium_eur = x[0] * base_notional
spot_premium_usd = x[0] * base_notional * spot_quote.value()
atm_straddle_premium = atm_call.NPV() + atm_put.NPV() * base_notional * x[1]
rr_premium = delta25_call.NPV() - delta25_put.NPV() * base_notional * x[2]
butterfly_premium = (delta25_call.NPV() + delta25_put.NPV() - 2 * (atm_call.NPV() + atm_put.NPV())) * x[3] * base_notionalThe usd borrowed (which is the sum of the premiums of or hedging instrument) is crucial for use to calculate the interest accrued for borrowing the usd dollars to buy the hedging structures.
hedging_str_premium = atm_straddle_premium + rr_premium + butterfly_premium + spot_premium_usd
hedging_str_premiumnp.float64(4182631.052146415)
Thus the value of the cash at time \(t = 0\) is the equal to all the cash that we’ve borrowed to build the hedging structures plus the premiums that we’ve been collected from selling the exotics options
bank_account_value = - hedging_str_premium + np.sum(premiums)
bank_account_valuenp.float64(-4099629.5308762337)
port_value = [hedging_str_premium - np.sum(premiums) + bank_account_value]
print(f"The portfolio value at time t = 0 is {port_value[0]}")The portfolio value at time t = 0 is 0.0
Changes of the hedging portfolio with the changes of the market conditions
Now that we have setup our hedging portfolio see verify how its will change as the spot and vol change. Let’s assume that in a week period the spot went go down by 300 pips and the vol went up by \(1\%\).
spot_quote.setValue(1.15)
vol_quote.setValue(0.14)
ql.Settings.instance().evaluationDate = today + ql.Period(7, ql.Days)new_premiums = [pos.NPV() for pos in book]With the new spot and vol let’s recalculate the book exposure
delta_book = sum([pos.sensitivity(delta, h=spot_change) for pos in book])
vega_book = sum([pos.sensitivity(vega, h=vol_change) for pos in book])
volga_book = sum([pos.sensitivity(volga, h=vol_change) for pos in book])
gamma_book = sum([pos.sensitivity(gamma, h=spot_change) for pos in book])
vanna_book = sum([pos.sensitivity(vanna, h=spot_change, k=vol_change) for pos in book])
# new Exposure vector
E = np.array([delta_book, gamma_book, vega_book, volga_book, vanna_book])Book Delta exposure: $16549.207988699432
Book Gamma exposure: $138778.80052508457
Book Vega exposure: $11096.72161278602
Book Volga exposure: $560.5276947207037
Book Vanna exposure: $2416.5207043533637
As before let’s recalculate the greeks for the basic trading instruments, and then combine to obtain the greeks for the structures used in the hedging portfolio.
# hedge_matrix
base_notional = np.float64(1_000_000)
spot_greeks = np.array([1, 0, 0, 0, 0]) * base_notional
atm_call_greeks = np.array([
delta(atm_call, h=spot_change),
gamma(atm_call, h=spot_change),
vega(atm_call, h=vol_change),
volga(atm_call, h=vol_change),
vanna(atm_call, h=spot_change, k=vol_change),
]) * base_notional
atm_put_greeks = np.array([
delta(atm_put, h=spot_change),
gamma(atm_put, h=spot_change),
vega(atm_put, h=vol_change),
volga(atm_put, h=vol_change),
vanna(atm_put, h=spot_change, k=vol_change),
]) * base_notional
delta25_call_greeks = np.array([
delta(delta25_call, h=spot_change),
gamma(delta25_call, h=spot_change),
vega(delta25_call, h=vol_change),
volga(delta25_call, h=vol_change),
vanna(delta25_call, h=spot_change, k=vol_change),
]) * base_notional
delta25_put_greeks = np.array([
delta(delta25_put, h=spot_change),
gamma(delta25_put, h=spot_change),
vega(delta25_put, h=vol_change),
volga(delta25_put, h=vol_change),
vanna(delta25_put, h=spot_change, k=vol_change),
]) * base_notional# Let's create the straddle, RR, Butterfly from the base vanilla options
atm_straddle = atm_call_greeks + atm_put_greeks
rr = delta25_call_greeks - delta25_put_greeks
butterfly = delta25_call_greeks + delta25_put_greeks - 2 * atm_straddle
# Hedge Matrix
H = np.stack([spot_greeks, atm_straddle, rr, butterfly]).TThe esposure of our hedging portfolio is now is:
# New exposure of the hedging portfolio
exposure = (H @ x - E).tolist()
print(f"Residual Delta exposure: ${exposure[0] * spot_quote.value()}")
print(f"Residual Gamma exposure: ${exposure[1] * 0.01 * spot_quote.value()}")
print(f"Residual Vega exposure: ${exposure[2] * 0.01}")
print(f"Residual Volga exposure: ${exposure[3] * 0.01}")
print(f"Residual Vanna exposure: ${exposure[4] * 0.01 * spot_quote.value()}")Residual Delta exposure: $-348789.7260608019
Residual Gamma exposure: $73758.42210270112
Residual Vega exposure: $5879.026689021433
Residual Volga exposure: $-54024.14332019365
Residual Vanna exposure: $117.87655605534354
To calculate the P&L from time t to t+1 we need to calculate the change of the value of the hedging portfolio as well
spot_premium_usd = x[0] * base_notional * spot_quote.value()
atm_straddle_premium = atm_call.NPV() + atm_put.NPV() * base_notional * x[1]
rr_premium = delta25_call.NPV() - delta25_put.NPV() * base_notional * x[2]
butterfly_premium = (delta25_call.NPV() + delta25_put.NPV() - 2 * (atm_call.NPV() + atm_put.NPV())) * x[3] * base_notional
new_hedging_str_premium = atm_straddle_premium + rr_premium + butterfly_premium + spot_premium_usddt = dc.yearFraction(today, today + ql.Period(7, ql.Days))
usd_interest_cost = hedging_str_premium * r_d * dt
eur_interest_accr = spot_premium_eur * r_f * dt
new_total_portfolio_value = new_hedging_str_premium - np.sum(new_premiums) + bank_account_value - usd_interest_cost + eur_interest_accr * spot_quote.value()
print(f"Portfolio value at time t = 1 is: ${new_total_portfolio_value}")Portfolio value at time t = 1 is: $-8014.640804257949
The P&L after a week is: $-8014.640804257949
If we want to reduce our exposure and bring it back to a lower one, we need to recalculate the optiomal hedging weight for our hedging portfolio as before:
x_new, _, _, _ = np.linalg.lstsq(H, E)This how our hedging portfolio has to change:
x_new - xarray([-0.33252725, 3.27195222, 0.99407244, 3.52132318])
The residual greek exposure after the rebalancing is:
Residual Delta exposure: $-4.2255123844370243e-10
Residual Gamma exposure: $1.7120357189066706
Residual Vega exposure: $-16.21605768425856
Residual Volga exposure: $-0.0021280367333383764
Residual Vanna exposure: $0.014062687186218682