Skip to content

Commit a0d82f3

Browse files
committed
Implemented antithetic variates for discretized processes
1 parent 2c0a396 commit a0d82f3

File tree

7 files changed

+196
-24
lines changed

7 files changed

+196
-24
lines changed

examples/antithetic_heston_check.jl

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using Revise
2+
using Hedgehog2
3+
using Dates
4+
using Statistics
5+
using Random
6+
using Printf
7+
using Plots
8+
println("=== Heston Model: Monte Carlo with Antithetic Variates ===\n")
9+
10+
# --- Market Inputs ---
11+
reference_date = Date(2020, 1, 1)
12+
13+
# Heston model parameters
14+
S0 = 100.0 # Initial spot price
15+
V0 = 0.04 # Initial variance
16+
κ = 2.0 # Mean reversion speed
17+
θ = 0.04 # Long-term variance
18+
σ = 0.3 # Volatility of variance (vol-of-vol)
19+
ρ = -0.7 # Correlation between asset and variance processes
20+
r = 0.05 # Risk-free rate
21+
22+
market_inputs = HestonInputs(reference_date, r, S0, V0, κ, θ, σ, ρ)
23+
24+
# --- Payoff ---
25+
expiry = reference_date + Year(1)
26+
strike = S0 # ATM European call
27+
payoff = VanillaOption(strike, expiry, European(), Call(), Spot())
28+
29+
# --- Pricing problem ---
30+
prob = PricingProblem(payoff, market_inputs)
31+
32+
# --- Reference price (using Carr-Madan Fourier method) ---
33+
carr_madan_method = CarrMadan(1.0, 32.0, HestonDynamics())
34+
carr_madan_solution = solve(prob, carr_madan_method)
35+
reference_price = carr_madan_solution.price
36+
37+
println("Reference price (Carr-Madan): $reference_price\n")
38+
39+
# --- Function to run Monte Carlo trials ---
40+
# Create Euler-Maruyama strategy
41+
trajectories = 5000
42+
steps = 100
43+
strategy = EulerMaruyama(trajectories, steps; antithetic=true)
44+
method = MonteCarlo(HestonDynamics(), strategy)
45+
sol = solve(prob, method)
46+
47+
first_path = sol.ensemble.solutions[1]
48+
first_path_u = [first_path.u[i][1] for i in range(1,length(first_path))]
49+
plot(first_path.t, first_path_u)
50+
51+
anti_path = sol.ensemble.solutions[5001]
52+
anti_path_u = [anti_path.u[i][1] for i in range(1,length(first_path))]
53+
plot!(first_path.t, anti_path_u)

examples/montecarlo_heston.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ carr_madan_problem = PricingProblem(payoff, market_inputs)
3636
carr_madan_solution = solve(carr_madan_problem, carr_madan_method)
3737

3838
# --- Monte Carlo Parameters ---
39-
trajectories = 100_000
39+
trajectories = 1_000
4040
steps = 500
4141

4242
# --- Euler–Maruyama ---
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using Revise
2+
using Hedgehog2
3+
using Dates
4+
using Statistics
5+
using Random
6+
using Printf
7+
8+
println("=== Heston Model: Monte Carlo with Antithetic Variates ===\n")
9+
10+
# --- Market Inputs ---
11+
reference_date = Date(2020, 1, 1)
12+
13+
# Heston model parameters
14+
S0 = 100.0 # Initial spot price
15+
V0 = 0.04 # Initial variance
16+
κ = 2.0 # Mean reversion speed
17+
θ = 0.04 # Long-term variance
18+
σ = 0.3 # Volatility of variance (vol-of-vol)
19+
ρ = -0.7 # Correlation between asset and variance processes
20+
r = 0.05 # Risk-free rate
21+
22+
market_inputs = HestonInputs(reference_date, r, S0, V0, κ, θ, σ, ρ)
23+
24+
# --- Payoff ---
25+
expiry = reference_date + Year(1)
26+
strike = S0 # ATM European call
27+
payoff = VanillaOption(strike, expiry, European(), Call(), Spot())
28+
29+
# --- Pricing problem ---
30+
prob = PricingProblem(payoff, market_inputs)
31+
32+
# --- Reference price (using Carr-Madan Fourier method) ---
33+
carr_madan_method = CarrMadan(1.0, 32.0, HestonDynamics())
34+
carr_madan_solution = solve(prob, carr_madan_method)
35+
reference_price = carr_madan_solution.price
36+
37+
println("Reference price (Carr-Madan): $reference_price\n")
38+
39+
# --- Function to run Monte Carlo trials ---
40+
function run_mc_trials(n_trials, trajectories, steps; use_antithetic=false)
41+
prices = Float64[]
42+
times = Float64[]
43+
for trial in 1:n_trials
44+
# Create Euler-Maruyama strategy
45+
strategy = EulerMaruyama(trajectories, steps; antithetic=use_antithetic)
46+
method = MonteCarlo(HestonDynamics(), strategy)
47+
48+
# Measure time and solve
49+
time = @elapsed begin
50+
sol = solve(prob, method)
51+
push!(prices, sol.price)
52+
end
53+
54+
push!(times, time)
55+
end
56+
57+
return prices, times
58+
end
59+
60+
# --- Run experiments ---
61+
n_trials = 20
62+
trajectories = 5_000
63+
steps = 100
64+
65+
println("Running experiments with $n_trials trials, $trajectories paths, $steps steps...")
66+
67+
# Standard Monte Carlo
68+
std_prices, std_times = run_mc_trials(n_trials, trajectories, steps, use_antithetic=false)
69+
70+
# Antithetic Monte Carlo (same number of total paths)
71+
anti_prices, anti_times = run_mc_trials(n_trials, trajectories ÷ 2, steps, use_antithetic=true)
72+
73+
# --- Compute metrics ---
74+
std_mean = mean(std_prices)
75+
std_error = std_mean - reference_price
76+
std_stdev = std(std_prices)
77+
std_rmse = sqrt(mean((std_prices .- reference_price).^2))
78+
std_time = mean(std_times)
79+
80+
anti_mean = mean(anti_prices)
81+
anti_error = anti_mean - reference_price
82+
anti_stdev = std(anti_prices)
83+
anti_rmse = sqrt(mean((anti_prices .- reference_price).^2))
84+
anti_time = mean(anti_times)
85+
86+
# --- Print results ---
87+
println("\n=== Results ===")
88+
println("Metric | Standard MC | Antithetic MC | Improvement")
89+
println("--------------------+-------------------+--------------------+-------------")
90+
@printf("Mean price | %.6f | %.6f | -\n", std_mean, anti_mean)
91+
@printf("Bias | %.6f | %.6f | %.2fx\n",
92+
std_error, anti_error, abs(std_error)/max(abs(anti_error), 1e-10))
93+
@printf("Standard deviation | %.6f | %.6f | %.2fx\n",
94+
std_stdev, anti_stdev, std_stdev/anti_stdev)
95+
@printf("RMSE | %.6f | %.6f | %.2fx\n",
96+
std_rmse, anti_rmse, std_rmse/anti_rmse)
97+
@printf("Avg. execution time | %.3f s | %.3f s | %.2fx\n",
98+
std_time, anti_time, std_time/anti_time)
99+
100+
# --- Path distribution visualization (code) ---
101+
println("\nNote: To visualize the distribution of prices across trials, you can plot a histogram")
102+
println("of the price vectors 'std_prices' and 'anti_prices' to see the variance reduction effect.")

src/market_inputs/market_inputs.jl

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ end
6565
Market data inputs for the Heston stochastic volatility model.
6666
6767
# Fields
68-
- `referenceDate`: The base date for maturity calculation.
68+
- `referenceDate`: The base date for maturity calculation (in ticks).
6969
- `rate`: The risk-free interest rate (annualized).
70-
- `spot`: The current spot price of the underlying asset.
70+
- `spot`: The current spot price of the underlying.
7171
- `V0`: The initial variance of the underlying.
7272
- `κ`: The rate at which variance reverts to its long-term mean.
7373
- `θ`: The long-term mean of the variance.
@@ -77,7 +77,7 @@ Market data inputs for the Heston stochastic volatility model.
7777
Used for pricing under the Heston model and simulation of stochastic volatility paths.
7878
"""
7979
struct HestonInputs <: AbstractMarketInputs
80-
referenceDate
80+
referenceDate::Real
8181
rate::RateCurve
8282
spot
8383
V0
@@ -88,12 +88,23 @@ struct HestonInputs <: AbstractMarketInputs
8888
end
8989

9090
HestonInputs(
91-
reference_date,
91+
reference_date::TimeType,
92+
rate::RateCurve,
93+
spot,
94+
V0,
95+
κ,
96+
θ,
97+
σ,
98+
ρ
99+
) = HestonInputs(to_ticks(reference_date), rate, spot, V0, κ, θ, σ, ρ)
100+
101+
HestonInputs(
102+
reference_date::TimeType,
92103
rate::Real,
93104
spot,
94105
V0,
95106
κ,
96107
θ,
97108
σ,
98109
ρ
99-
) = HestonInputs(reference_date, FlatRateCurve(rate), spot, V0, κ, θ, σ, ρ)
110+
) = HestonInputs(reference_date, FlatRateCurve(rate; reference_date=reference_date), spot, V0, κ, θ, σ, ρ)

src/pricing_methods/carr_madan.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ function solve(
5252
method::CarrMadan
5353
) where {C, I <: AbstractMarketInputs}
5454

55+
println("started carr madan")
5556
if !is_flat(prob.market.rate)
5657
throw(ArgumentError("Carr–Madan pricing only supports flat rate curves."))
5758
end

src/pricing_methods/montecarlo.jl

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,30 +59,32 @@ function sde_problem(::LognormalDynamics, s::BlackScholesExact, market::BlackSch
5959
return NoiseProblem(noise, tspan; s.kwargs...)
6060
end
6161

62-
function antithetic_sde_problem(d::LognormalDynamics, s::SimulationStrategy, m::BlackScholesInputs, tspan, normal_sol)
63-
s_flipped = @set s.seeds = normal_sol.seeds
64-
m_flipped = @set m.sigma = -m.sigma
65-
return sde_problem(d, s_flipped, m_flipped, tspan)
66-
end
67-
6862
function sde_problem(::HestonDynamics, ::EulerMaruyama, m::HestonInputs, tspan)
6963
@assert is_flat(m.rate) "Heston simulation requires flat rate curve"
7064
rate = zero_rate(m.rate, 0.0)
7165
return HestonProblem(rate, m.κ, m.θ, m.σ, m.ρ, [m.spot, m.V0], tspan)
7266
end
7367

74-
function antithetic_sde_problem(d::PriceDynamics, s::EulerMaruyama, m::AbstractMarketInputs, tspan, normal_sol::CustomEnsembleSolution)
75-
base_prob = original_sol.solutions[1].prob
68+
function get_antithetic_ensemble_problem(::PriceDynamics, ::EulerMaruyama, normal_sol::CustomEnsembleSolution)
69+
base_prob = normal_sol.solutions[1].prob
7670

7771
antithetic_modify = function (_base_prob, _seed, i)
78-
sol = original_sol.solutions[i]
72+
sol = normal_sol.solutions[i]
7973
flipped_noise = NoiseGrid(sol.W.t, -sol.W.W)
8074
return remake(_base_prob; noise=flipped_noise)
8175
end
8276

8377
return CustomEnsembleProblem(base_prob, normal_sol.seeds, antithetic_modify)
8478
end
8579

80+
function get_antithetic_ensemble_problem(d::LognormalDynamics , s::BlackScholesExact, normal_sol::CustomEnsembleSolution)
81+
tspan = normal_sol.solutions[1].prob.tspan
82+
s_flipped = @set s.seeds = normal_sol.seeds
83+
m_flipped = @set m.sigma = -m.sigma
84+
flipped_problem = sde_problem(d, s_flipped, m_flipped, tspan)
85+
return CustomEnsembleProblem(flipped_problem, normal_sol.seeds, antithetic_modify)
86+
end
87+
8688
function sde_problem(::HestonDynamics, strategy::HestonBroadieKaya, m::HestonInputs, tspan)
8789
@assert is_flat(m.rate) "Heston simulation requires flat rate curve"
8890
rate = zero_rate(m.rate, 0.0)
@@ -100,21 +102,21 @@ end
100102

101103
function marginal_law(::HestonDynamics, m::HestonInputs, t)
102104
α = yearfrac(m.rate.reference_date, t)
103-
return HestonDistribution(m.spot, m.V0, m.κ, m.θ, m.σ, m.ρ, m.rate, α)
105+
rate = zero_rate(m.rate, t)
106+
return HestonDistribution(m.spot, m.V0, m.κ, m.θ, m.σ, m.ρ, rate, α)
104107
end
105108

106109
# ------------------ Ensemble Simulation Wrapper ------------------
107110

108-
function montecarlo_solution(problem::Union{NoiseProblem, SDEProblem}, strategy::S) where {S <: SimulationStrategy}
109-
dt = problem.tspan[end] / strategy.steps
111+
function get_ensemble_problem(problem::Union{NoiseProblem, SDEProblem}, strategy::S) where {S <: SimulationStrategy}
110112
N = strategy.trajectories
111113

112114
seeds = strategy isa BlackScholesExact && strategy.seeds !== nothing ? strategy.seeds :
113115
Base.rand(1_000_000_000:2_000_000_000, N)
114116

115117
modify = (p, seed, _) -> remake(p; seed=seed)
116118
custom_prob = CustomEnsembleProblem(problem, collect(seeds), modify)
117-
return solve_custom_ensemble(custom_prob; dt=dt)
119+
return custom_prob
118120
end
119121

120122
# ------------------ Terminal Value Extractors ------------------
@@ -133,21 +135,23 @@ function simulate_paths(method::MonteCarlo, market_inputs::I, T) where {I <: Abs
133135
strategy = method.strategy
134136
dynamics = method.dynamics
135137
tspan = (0.0, T)
138+
dt = T / strategy.steps
136139

137140
antithetic = get(strategy.kwargs, :antithetic, false)
138141

139142
# Step 1: simulate original paths
140143
normal_prob = sde_problem(dynamics, strategy, market_inputs, tspan)
141-
normal_sol = montecarlo_solution(normal_prob, strategy)
144+
ensemble_prob = get_ensemble_problem(normal_prob, strategy)
145+
normal_sol = solve_custom_ensemble(ensemble_prob; dt=dt)
142146

143147
if !antithetic
144148
return normal_sol
145149
end
146150

147151
# Step 2: simulate antithetic paths using same seeds, flipped sigma
148-
antithetic_prob = antithetic_sde_problem(dynamics, strategy, market_inputs, tspan, normal_sol)
149-
antithetic_sol = montecarlo_solution(antithetic_prob, strategy)
150-
152+
antithetic_ensemble_prob = get_antithetic_ensemble_problem(dynamics, strategy, normal_sol)
153+
antithetic_sol = solve_custom_ensemble(antithetic_ensemble_prob; dt=dt)
154+
151155
# Step 3: combine both solutions
152156
combined_paths = vcat(normal_sol.solutions, antithetic_sol.solutions)
153157

src/solutions/pricing_solutions.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ function solve_custom_ensemble(prob::CustomEnsembleProblem; dt, solver=EM(), tra
2222

2323
Threads.@threads for i in 1:N
2424
seed = seeds[i]
25+
pmod = remake(prob.base_problem; seed=seed)
2526
pmod = prob.modify(prob.base_problem, seed, i)
26-
sols[i] = DifferentialEquations.solve(pmod, solver; dt=dt)
27+
sols[i] = DifferentialEquations.solve(pmod, solver; dt=dt, save_noise=true)
2728
end
2829

2930
return CustomEnsembleSolution(sols, seeds)

0 commit comments

Comments
 (0)