Skip to content

Commit 1dd382d

Browse files
committed
Various changes for readability and support of short fx positions
1 parent 46ed733 commit 1dd382d

File tree

4 files changed

+182
-101
lines changed

4 files changed

+182
-101
lines changed

derivatives/target_redemption_forward.cpp

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,11 @@ double TargetRedemptionForward::path(double spot,
4747
double dt,
4848
const RatesCurve& foreign_rates,
4949
const RatesCurve& domestic_rates) const {
50-
// Each path should return not just the NPV, but also
51-
// - the distribution of payments (right?)
52-
// - whether it got knocked out, and when
53-
5450
const double direction_multiplier =
5551
(specs_.direction == FxTradeDirection::kLong) ? 1.0 : -1.0;
5652

57-
double cumulative_profit = 0.;
58-
double npv = 0.;
53+
PathState state;
54+
state.current_fx = spot;
5955

6056
if (dt > specs_.settlement_date_frequency) {
6157
// This ensures that dt is (at most) the maximum sensible value. It
@@ -72,56 +68,58 @@ double TargetRedemptionForward::path(double spot,
7268
std::round(specs_.settlement_date_frequency / dt);
7369
dt = specs_.settlement_date_frequency / num_timesteps_in_period;
7470

75-
double fx = spot;
76-
double t = 0;
77-
double timesteps_taken = 0;
78-
bool trigger_reached = false;
79-
80-
double r_d =
81-
domestic_rates.forwardRate(t, t + specs_.settlement_date_frequency);
82-
double r_f =
83-
foreign_rates.forwardRate(t, t + specs_.settlement_date_frequency);
71+
double r_d = domestic_rates.forwardRate(
72+
state.current_time,
73+
state.current_time + specs_.settlement_date_frequency);
74+
double r_f = foreign_rates.forwardRate(
75+
state.current_time,
76+
state.current_time + specs_.settlement_date_frequency);
8477

85-
while (t < specs_.end_date_years && !trigger_reached) {
78+
while (state.current_time < specs_.end_date_years && !state.trigger_reached) {
8679
const double z = absl::Gaussian<double>(bitgen_, 0, 1);
8780
const double stoch_term = sigma * std::sqrt(dt) * z;
8881
const double drift_term = (r_d - r_f - 0.5 * sigma * sigma) * dt;
8982

90-
t += dt;
91-
++timesteps_taken;
92-
fx *= std::exp(drift_term + stoch_term);
83+
state.current_time += dt;
84+
++state.timesteps_since_last_settlement;
85+
state.current_fx *= std::exp(drift_term + stoch_term);
9386

94-
if (timesteps_taken == num_timesteps_in_period) {
95-
double payment_amount =
96-
direction_multiplier * specs_.notional * (fx - specs_.strike);
87+
if (state.timesteps_since_last_settlement == num_timesteps_in_period) {
88+
double payment_amount = direction_multiplier * specs_.notional *
89+
(state.current_fx - specs_.strike);
9790

9891
// If we reach the target on this payment date, then the current
9992
// payment is truncated to deliver the exact amount remaining
10093
// to hit the target.
101-
if (cumulative_profit + payment_amount > specs_.target) {
102-
trigger_reached = true;
103-
payment_amount = specs_.target - cumulative_profit;
94+
if (state.cumulative_profit + payment_amount > specs_.target) {
95+
state.trigger_reached = true;
96+
payment_amount = specs_.target - state.cumulative_profit;
10497
}
10598

10699
if (payment_amount > 0) {
107-
cumulative_profit += payment_amount;
100+
state.cumulative_profit += payment_amount;
108101
}
109102

110103
// Discount the payment amount on the domestic curve.
111-
const double discounted_pmt = payment_amount * domestic_rates.df(t);
112-
npv += discounted_pmt;
104+
const double discounted_pmt =
105+
payment_amount * domestic_rates.df(state.current_time);
106+
state.npv += discounted_pmt;
113107

114108
// Reset to the next period.
115-
timesteps_taken = 0;
109+
state.timesteps_since_last_settlement = 0;
116110

117111
// And look up the forward interest rates for the next simulation
118112
// period (we don't do this at each time step to avoid computing these
119113
// excessively, in case dt is very small).
120-
r_d = domestic_rates.forwardRate(t, t + specs_.settlement_date_frequency);
121-
r_f = foreign_rates.forwardRate(t, t + specs_.settlement_date_frequency);
114+
r_d = domestic_rates.forwardRate(
115+
state.current_time,
116+
state.current_time + specs_.settlement_date_frequency);
117+
r_f = foreign_rates.forwardRate(
118+
state.current_time,
119+
state.current_time + specs_.settlement_date_frequency);
122120
}
123121
}
124-
return npv;
122+
return state.npv;
125123
}
126124

127125
TarfPricingResult TargetRedemptionForward::price(
@@ -146,4 +144,56 @@ TarfPricingResult TargetRedemptionForward::price(
146144
return result;
147145
}
148146

147+
double findZeroNPVStrike(const TarfContractSpecs& specs,
148+
double spot,
149+
double sigma,
150+
const RatesCurve& foreign_rates,
151+
const RatesCurve& domestic_rates,
152+
size_t num_paths) {
153+
// TODO: ALSO then verify that the value of one is positive and the other is
154+
// negative.
155+
double atm_fwd = weightedAvgForward(spot,
156+
specs.end_date_years,
157+
specs.settlement_date_frequency,
158+
foreign_rates,
159+
domestic_rates);
160+
double k_low = atm_fwd * 0.5;
161+
double k_high = atm_fwd * 1.5;
162+
163+
TarfContractSpecs k_mid_specs = specs;
164+
k_mid_specs.strike = 0.5 * (k_low + k_high);
165+
166+
double tolerance_pct =
167+
0.0001; // 0.01% difference for starters. Do not hard-code!
168+
169+
// Relatively coarse timesteps.
170+
double dt = specs.settlement_date_frequency * 0.2;
171+
172+
// Initial method: bisection.
173+
while (std::abs(k_high / k_low - 1) > tolerance_pct) {
174+
TargetRedemptionForward tarf_mid(k_mid_specs);
175+
176+
double npv_mid =
177+
tarf_mid
178+
.price(spot, sigma, dt, num_paths, foreign_rates, domestic_rates)
179+
.mean_npv;
180+
181+
if (specs.direction == FxTradeDirection::kLong) {
182+
if (npv_mid > 0) {
183+
k_low = k_mid_specs.strike;
184+
} else {
185+
k_high = k_mid_specs.strike;
186+
}
187+
} else { // FxTradeDirection::kShort
188+
if (npv_mid > 0) {
189+
k_high = k_mid_specs.strike;
190+
} else {
191+
k_low = k_mid_specs.strike;
192+
}
193+
}
194+
k_mid_specs.strike = 0.5 * (k_low + k_high);
195+
}
196+
return k_mid_specs.strike;
197+
}
198+
149199
} // namespace smileexplorer

derivatives/target_redemption_forward.h

Lines changed: 23 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ class TargetRedemptionForward {
7171
// generator (whose internal state is updated every time a random number is
7272
// invoked).
7373
mutable absl::BitGen bitgen_;
74+
75+
// Stores the internal state of a single MC simulation path.
76+
struct PathState {
77+
double cumulative_profit = 0.0;
78+
double npv = 0.0;
79+
double current_fx = 0.0;
80+
double current_time = 0.0;
81+
size_t timesteps_since_last_settlement = 0;
82+
bool trigger_reached = false;
83+
};
84+
void processSettlement(PathState& state,
85+
const RatesCurve& domestic_rates) const;
7486
};
7587

7688
// Returns the weighted average of the forward FX rate, weighted by the
@@ -81,49 +93,17 @@ double weightedAvgForward(double spot,
8193
const RatesCurve& foreign_rates,
8294
const RatesCurve& domestic_rates);
8395

84-
inline double findZeroNPVStrike(const TarfContractSpecs& specs,
85-
double spot,
86-
double sigma,
87-
const RatesCurve& foreign_rates,
88-
const RatesCurve& domestic_rates,
89-
size_t num_paths = 2000) {
90-
// TODO: ALSO then verify that the value of one is positive and the other is
91-
// negative.
92-
double atm_fwd = weightedAvgForward(spot,
93-
specs.end_date_years,
94-
specs.settlement_date_frequency,
95-
foreign_rates,
96-
domestic_rates);
97-
double k_low = atm_fwd * 0.5;
98-
double k_high = atm_fwd * 1.1;
99-
100-
TarfContractSpecs k_mid_specs = specs;
101-
k_mid_specs.strike = 0.5 * (k_low + k_high);
102-
103-
double tolerance_pct =
104-
0.0001; // 0.01% difference for starters. Do not hard-code!
105-
106-
// Relatively coarse timesteps.
107-
double dt = specs.settlement_date_frequency * 0.2;
108-
109-
// Initial method: bisection.
110-
while (std::abs(k_high / k_low - 1) > tolerance_pct) {
111-
TargetRedemptionForward tarf_mid(k_mid_specs);
112-
113-
double npv_mid =
114-
tarf_mid
115-
.price(spot, sigma, dt, num_paths, foreign_rates, domestic_rates)
116-
.mean_npv;
117-
118-
if (npv_mid > 0) {
119-
k_low = k_mid_specs.strike;
120-
} else if (npv_mid < 0) {
121-
k_high = k_mid_specs.strike;
122-
}
123-
k_mid_specs.strike = 0.5 * (k_low + k_high);
124-
}
125-
return k_mid_specs.strike;
126-
}
96+
// Returns the (estimated) strike at which the TARF contract would have
97+
// a zero NPV at the time of the trade.
98+
//
99+
// Note that `specs.strike` is ignored, since the goal is to discover the
100+
// appropriate strike.
101+
double findZeroNPVStrike(const TarfContractSpecs& specs,
102+
double spot,
103+
double sigma,
104+
const RatesCurve& foreign_rates,
105+
const RatesCurve& domestic_rates,
106+
size_t num_paths = 4000);
127107

128108
} // namespace smileexplorer
129109

derivatives/target_redemption_forward_test.cpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ TEST_F(TargetRedemptionForwardTest, FindZeroNPVStrike) {
181181
const double strike =
182182
findZeroNPVStrike(specs, 125, 0.0001, *foreign_curve_, *domestic_curve_);
183183

184-
EXPECT_NEAR(135.657, strike, 0.002);
184+
EXPECT_NEAR(135.657, strike, 0.005);
185185
}
186186

187187
TEST_F(TargetRedemptionForwardTest, LoweringTargetReducesLongStrike) {
@@ -201,5 +201,22 @@ TEST_F(TargetRedemptionForwardTest, LoweringTargetReducesLongStrike) {
201201
EXPECT_LT(strike_5mm, strike_6mm);
202202
}
203203

204+
TEST_F(TargetRedemptionForwardTest, ShortFxPositionHasHigherStrikeThanLong) {
205+
TarfContractSpecs specs{.notional = 1e6,
206+
.target = 6e6,
207+
.strike = 125,
208+
.end_date_years = 4.0,
209+
.settlement_date_frequency = 0.25,
210+
.direction = FxTradeDirection::kLong};
211+
const double strike_long =
212+
findZeroNPVStrike(specs, 125, 0.05, *foreign_curve_, *domestic_curve_);
213+
214+
specs.direction = FxTradeDirection::kShort;
215+
const double strike_short =
216+
findZeroNPVStrike(specs, 125, 0.05, *foreign_curve_, *domestic_curve_);
217+
218+
EXPECT_GT(strike_short, strike_long);
219+
}
220+
204221
} // namespace
205222
} // namespace smileexplorer

0 commit comments

Comments
 (0)