Skip to content

Commit d6bb75d

Browse files
committed
Antithetic variates for Broadie Kaya Heston simulation
1 parent 8350114 commit d6bb75d

File tree

8 files changed

+255
-20
lines changed

8 files changed

+255
-20
lines changed

docs/derivatives_pricing_roadmap.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@
3737

3838
## PHASE 3.5 — Monte Carlo Enhancements (1 week)
3939

40-
- [ ] Add antithetic variates toggle to `MonteCarlo` method (Exact Black Scholes [x])
40+
- [x] Add antithetic variates toggle to `MonteCarlo` method (Discrete and Exact Black Scholes [x], Discrete and Exact Heston[x])
4141
- [ ] Implement control variates with BS analytical formulas
4242
- [ ] Add reproducibility features: seeded RNG + control panel
4343
- [ ] Refactor MC framework to support pluggable variance reduction via `MCStrategy`
4444
- [ ] Optional: add stratified / quasi-random sampling hooks
45-
- [ ] Unit tests: check variance reduction and correctness vs analytic prices
45+
- [x] Unit tests: check variance reduction and correctness vs analytic prices
4646

4747
## PHASE 4 — Structured Payoffs (2–3 weeks)
4848

examples/bk_antithetic_analysis.jl

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
using Revise
2+
using Hedgehog2
3+
using Distributions
4+
using Random
5+
using Plots
6+
using Dates
7+
using Statistics
8+
using Printf
9+
10+
println("=== Heston Model: Antithetic Variates Variance Reduction Analysis ===")
11+
12+
# --- Market Inputs ---
13+
reference_date = Date(2020, 1, 1)
14+
15+
# Heston parameters (chosen to satisfy Feller condition)
16+
S0 = 1.0
17+
V0 = 0.04 # Initial variance (20% vol)
18+
κ = 1.0 # Mean reversion
19+
θ = 0.04 # Long-term variance
20+
σ = 0.2 # Vol-of-vol
21+
ρ = -0.5 # Correlation
22+
r = 0.02 # Risk-free rate
23+
24+
market_inputs = HestonInputs(reference_date, r, S0, V0, κ, θ, σ, ρ)
25+
26+
# --- Payoff ---
27+
expiry = reference_date + Year(5)
28+
strike = S0 # ATM call
29+
payoff = VanillaOption(strike, expiry, European(), Call(), Spot())
30+
31+
# --- Dynamics ---
32+
dynamics = HestonDynamics()
33+
34+
# --- Get reference price using Carr-Madan (Fourier) method ---
35+
α = 1.0
36+
boundary = 32.0
37+
carr_madan_method = CarrMadan(α, boundary, dynamics)
38+
prob = PricingProblem(payoff, market_inputs)
39+
carr_madan_solution = solve(prob, carr_madan_method)
40+
reference_price = carr_madan_solution.price
41+
42+
println("Reference price (Carr-Madan): $reference_price")
43+
44+
# --- Variance Reduction Analysis ---
45+
# We'll compare standard MC vs antithetic variates
46+
n_trials = 30 # Number of independent trials
47+
base_paths = 500 # Base number of paths
48+
time_steps = 20 # Time steps for each path
49+
Random.seed!(42) # For reproducibility
50+
51+
# Arrays to store results
52+
std_prices = Float64[]
53+
anti_prices = Float64[]
54+
std_times = Float64[]
55+
anti_times = Float64[]
56+
57+
# Repeat trials
58+
for trial in 1:n_trials
59+
# Different seed for each trial
60+
trial_seed = 42 + trial
61+
62+
# Standard Monte Carlo (full number of paths)
63+
std_strategy = HestonBroadieKaya(base_paths, steps=time_steps, seeds=rand(MersenneTwister(trial_seed), 1:10^9, base_paths))
64+
std_method = MonteCarlo(dynamics, std_strategy)
65+
66+
std_time = @elapsed begin
67+
std_solution = solve(prob, std_method)
68+
push!(std_prices, std_solution.price)
69+
end
70+
push!(std_times, std_time)
71+
72+
# Antithetic Monte Carlo (half the number of paths × 2)
73+
anti_strategy = HestonBroadieKaya(base_paths ÷ 2, steps=time_steps, seeds=rand(MersenneTwister(trial_seed), 1:10^9, base_paths ÷ 2), antithetic=true)
74+
anti_method = MonteCarlo(dynamics, anti_strategy)
75+
76+
anti_time = @elapsed begin
77+
anti_solution = solve(prob, anti_method)
78+
push!(anti_prices, anti_solution.price)
79+
end
80+
push!(anti_times, anti_time)
81+
82+
# Print progress
83+
if trial % 5 == 0
84+
println("Completed $trial/$n_trials trials")
85+
end
86+
end
87+
88+
# --- Calculate variance reduction statistics ---
89+
std_mean = mean(std_prices)
90+
anti_mean = mean(anti_prices)
91+
92+
std_var = var(std_prices)
93+
anti_var = var(anti_prices)
94+
95+
std_bias = std_mean - reference_price
96+
anti_bias = anti_mean - reference_price
97+
98+
std_mse = mean((std_prices .- reference_price).^2)
99+
anti_mse = mean((anti_prices .- reference_price).^2)
100+
101+
var_reduction_ratio = std_var / anti_var
102+
mse_reduction_ratio = std_mse / anti_mse
103+
time_ratio = mean(std_times) / mean(anti_times)
104+
105+
# Efficiency improvement (variance reduction per unit of computation)
106+
efficiency_gain = var_reduction_ratio * time_ratio
107+
108+
# --- Visualization ---
109+
# Distribution of prices
110+
histogram_plot = histogram(
111+
std_prices,
112+
alpha=0.5,
113+
label="Standard MC",
114+
bins=15,
115+
title="Distribution of Price Estimates",
116+
xlabel="Price",
117+
ylabel="Frequency"
118+
)
119+
120+
histogram!(
121+
histogram_plot,
122+
anti_prices,
123+
alpha=0.5,
124+
label="Antithetic MC",
125+
bins=15
126+
)
127+
128+
# Add reference price line
129+
vline!(
130+
histogram_plot,
131+
[reference_price],
132+
label="Reference Price",
133+
color=:red,
134+
linewidth=2,
135+
linestyle=:dash
136+
)
137+
138+
# --- Print results ---
139+
println("\n=== Variance Reduction Analysis ===")
140+
println("Number of trials: $n_trials")
141+
println("Paths (standard MC): $base_paths")
142+
println("Paths (antithetic MC): $(base_paths ÷ 2) × 2")
143+
println()
144+
println("Reference price: $reference_price")
145+
println()
146+
@printf("Standard MC mean: %.6f (bias: %.6f)\n", std_mean, std_bias)
147+
@printf("Antithetic MC mean: %.6f (bias: %.6f)\n", anti_mean, anti_bias)
148+
println()
149+
@printf("Standard MC variance: %.6e\n", std_var)
150+
@printf("Antithetic variance: %.6e\n", anti_var)
151+
@printf("Variance reduction: %.2fx\n", var_reduction_ratio)
152+
println()
153+
@printf("Standard MC MSE: %.6e\n", std_mse)
154+
@printf("Antithetic MC MSE: %.6e\n", anti_mse)
155+
@printf("MSE reduction: %.2fx\n", mse_reduction_ratio)
156+
println()
157+
@printf("Avg. time (standard): %.3f seconds\n", mean(std_times))
158+
@printf("Avg. time (antithetic): %.3f seconds\n", mean(anti_times))
159+
@printf("Time ratio: %.2fx\n", time_ratio)
160+
println()
161+
@printf("Efficiency gain: %.2fx\n", efficiency_gain)
162+
163+
# --- Visualize a sample path pair ---
164+
if length(std_prices) > 0 && length(anti_prices) > 0
165+
Random.seed!(42) # Reset seed for consistent visualization
166+
167+
# Simulate a single path with antithetic variates for visualization
168+
vis_strategy = HestonBroadieKaya(1, steps=time_steps, antithetic=true)
169+
vis_method = MonteCarlo(dynamics, vis_strategy)
170+
vis_solution = solve(prob, vis_method)
171+
172+
# Extract paths
173+
original_path = vis_solution.ensemble.solutions[1]
174+
antithetic_path = vis_solution.ensemble.solutions[2] # Second path is antithetic
175+
176+
# Extract time points
177+
time_points = original_path.t
178+
179+
# Extract spot prices and variances
180+
original_prices = [exp(p[1]) for p in original_path.u]
181+
antithetic_prices = [exp(p[1]) for p in antithetic_path.u]
182+
original_variance = [p[2] for p in original_path.u]
183+
antithetic_variance = [p[2] for p in antithetic_path.u]
184+
185+
# Create path plots
186+
p1 = plot(
187+
time_points, original_prices,
188+
label="Original Path",
189+
linewidth=2,
190+
title="Heston Model: Stock Price Paths",
191+
xlabel="Time (years)",
192+
ylabel="Stock Price",
193+
legend=:topleft
194+
)
195+
196+
plot!(
197+
p1,
198+
time_points, antithetic_prices,
199+
label="Antithetic Path",
200+
linewidth=2,
201+
linestyle=:dash,
202+
color=:red
203+
)
204+
205+
p2 = plot(
206+
time_points, original_variance,
207+
label="Original Path",
208+
linewidth=2,
209+
title="Heston Model: Variance Paths",
210+
xlabel="Time (years)",
211+
ylabel="Variance",
212+
legend=:topleft
213+
)
214+
215+
plot!(
216+
p2,
217+
time_points, antithetic_variance,
218+
label="Antithetic Path",
219+
linewidth=2,
220+
linestyle=:dash,
221+
color=:red
222+
)
223+
224+
# Combine plots
225+
path_plot = plot(p1, p2, layout=(2,1), size=(800, 600))
226+
227+
# Calculate price-path correlation
228+
path_correlation = cor(original_prices, antithetic_prices)
229+
var_correlation = cor(original_variance, antithetic_variance)
230+
231+
println("\n=== Path Correlation Analysis ===")
232+
@printf("Stock price path correlation: %.4f\n", path_correlation)
233+
@printf("Variance path correlation: %.4f\n", var_correlation)
234+
235+
# Display both plots
236+
display(histogram_plot)
237+
display(path_plot)
238+
end

examples/broadie_kaya_antithetic.jl

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,9 @@ println("Reference price (Carr-Madan): $reference_price")
4040

4141
# --- Monte Carlo with Broadie-Kaya ---
4242
# Just a few paths for visualization
43-
trajectories = 10
43+
trajectories = 2000
4444
steps = 20
4545

46-
# Set a seed for reproducibility
47-
Random.seed!(42)
48-
4946
# Create Broadie-Kaya strategy with antithetic variates
5047
strategy = HestonBroadieKaya(trajectories ÷ 2, steps=steps, antithetic=true)
5148
bk_method = MonteCarlo(dynamics, strategy)

examples/montecarlo_heston.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ trajectories = 1_000
4040
steps = 500
4141

4242
# --- Euler–Maruyama ---
43-
euler_strategy = EulerMaruyama(trajectories, steps)
43+
euler_strategy = EulerMaruyama(trajectories, steps=steps)
4444
euler_method = MonteCarlo(dynamics, euler_strategy)
4545
euler_problem = PricingProblem(payoff, market_inputs)
4646
euler_solution = solve(euler_problem, euler_method)
4747

4848
# --- Broadie–Kaya ---
49-
bk_strategy = HestonBroadieKaya(trajectories, steps=2)
49+
bk_strategy = HestonBroadieKaya(trajectories, steps=1)
5050
bk_method = MonteCarlo(dynamics, bk_strategy)
5151
bk_problem = PricingProblem(payoff, market_inputs)
5252
bk_solution = solve(bk_problem, bk_method)

examples/plot_bk_path.jl

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ reference_date = Date(2020, 1, 1)
1313
# Heston parameters
1414
S0 = 1.0
1515
V0 = 0.010201
16-
κ = 6.21
16+
κ = 4
1717
θ = 0.019
18-
σ = 0.61
18+
σ = 0.15
1919
ρ = -0.7
2020
r = 0.0319
2121

@@ -30,9 +30,8 @@ payoff = VanillaOption(strike, expiry, European(), Call(), Spot())
3030
prob = PricingProblem(payoff, market_inputs)
3131

3232
# --- Monte Carlo with Broadie-Kaya ---
33-
Random.seed!(42) # for reproducibility
3433
trajectories = 1
35-
steps = 20 # Increase steps for better visualization
34+
steps = 20 # Increase steps for better visualization
3635

3736
# Create Broadie-Kaya strategy
3837
strategy = HestonBroadieKaya(trajectories, steps=steps)

src/calibration/calibration.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
struct CalibrationProblem{P, L}
2+
pricing_problem::P
3+
wrt::L # accessor (from Accessors.jl)
4+
end

src/distributions/heston.jl

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,11 @@ Constructs a custom `NoiseProcess` using the exact Heston distribution for the n
4545
Returns a `NoiseProcess` sampling from the Heston distribution at each timestep.
4646
"""
4747
function HestonNoise(μ, κ, θ, σ, ρ, t0, W0, Z0 = nothing; kwargs...)
48-
println(W0)
4948
@inline function Heston_dist(DW, W, dt, u, p, t, rng) #dist
5049
S, V = exp(W[end][1]), W[end][2]
51-
println("price ", S)
52-
println(V)
5350
heston_dist_at_t = HestonDistribution(S, V, κ, θ, σ, ρ, μ, dt)
54-
return @fastmath rand(rng, heston_dist_at_t; kwargs...) # Calls exact Heston sampler
51+
S1, V1 = @fastmath rand(rng, heston_dist_at_t; kwargs...) # Calls exact Heston sampler
52+
return [S1 - W[end][1], V1 - W[end][2]]
5553
end
5654

5755
return NoiseProcess{false}(t0, W0, Z0, Heston_dist, nothing)
@@ -88,7 +86,6 @@ function sample_V_T(rng::AbstractRNG, d::HestonDistribution)
8886
d = 4*κ*θ / σ^2 # Degrees of freedom
8987
λ = 4*κ * exp(-κ * T) * V0 /^2 * (- expm1(-κ * T))) # Noncentrality parameter
9088
c = σ^2 * (- expm1(-κ * T)) / (4*κ) # Scaling factor
91-
9289
V_T = c * Distributions.rand(rng, NoncentralChisq(d, λ))
9390
return V_T
9491
end
@@ -206,7 +203,7 @@ Useful for testing and diagnostics.
206203
"""
207204
function rand(rng::AbstractRNG, d::HestonDistribution; antithetic=false, kwargs...)
208205
d1 = HestonDistribution(d.S0, d.V0, d.κ, d.θ, d.σ, d.ρ, d.r, d.T)
209-
206+
# println(d)
210207
# Step 1: Sample V_T
211208
V_T = sample_V_T(rng, d1)
212209

src/distributions/sample_from_cf.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function sample_from_cf(rng, ϕ; n=5, kwargs...)
1818
initial_guess = normal_sample > 0 ? normal_sample : mean * 0.01
1919
max_guess = mean + 11*sqrt(σ²)
2020
# Fourier inversion to find the cdf, following Broadie Kaya
21-
h = π / (mean + n * variance)
21+
h = π / (mean + n * σ²)
2222
cdf = x -> cdf_from_cf(ϕ, x, h; kwargs...)
2323
sample = inverse_cdf(cdf, u, initial_guess, max_guess; kwargs...)
2424
return sample
@@ -31,7 +31,7 @@ end
3131
3232
Estimates the mean and variance from the characteristic function `ϕ_iter` using central differences.
3333
"""
34-
function moments_from_cf(ϕ_iter::HestonCFIterator; h=1e-4)
34+
function moments_from_cf(ϕ_iter::HestonCFIterator; h=1e-2)
3535
θ_prev = nothing
3636

3737
ϕp, θ_prev = evaluate_chf(ϕ_iter, h, θ_prev)

0 commit comments

Comments
 (0)