Skip to content

Commit 2ed2f1b

Browse files
committed
Added test for antithetic variates
1 parent 36cf636 commit 2ed2f1b

File tree

3 files changed

+364
-1
lines changed

3 files changed

+364
-1
lines changed

test/antithetic_variates.jl

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
using Hedgehog2
2+
using Dates
3+
using Random
4+
using Statistics
5+
using Test
6+
using Printf
7+
8+
function analyze_path_correlation(paths, n_original)
9+
# Calculate correlations between original and antithetic paths
10+
correlations = Float64[]
11+
12+
for i in 1:n_original
13+
original_path = paths[i]
14+
antithetic_path = paths[i + n_original]
15+
16+
# Get price values at each time step
17+
if eltype(original_path.u) <: AbstractVector
18+
original_prices = [u[1] for u in original_path.u]
19+
antithetic_prices = [u[1] for u in antithetic_path.u]
20+
else
21+
original_prices = original_path.u
22+
antithetic_prices = antithetic_path.u
23+
end
24+
25+
# Calculate correlation
26+
path_correlation = cor(original_prices, antithetic_prices)
27+
push!(correlations, path_correlation)
28+
end
29+
30+
return correlations
31+
end
32+
33+
function calculate_return_correlation(paths, n_original)
34+
# Calculate correlations between log returns of original and antithetic paths
35+
return_correlations = Float64[]
36+
37+
for i in 1:n_original
38+
original_path = paths[i]
39+
antithetic_path = paths[i + n_original]
40+
41+
# Get price values at each time step
42+
if eltype(original_path.u) <: AbstractVector
43+
original_prices = [u[1] for u in original_path.u]
44+
antithetic_prices = [u[1] for u in antithetic_path.u]
45+
else
46+
original_prices = original_path.u
47+
antithetic_prices = antithetic_path.u
48+
end
49+
50+
# Calculate log returns
51+
original_returns = diff(log.(original_prices))
52+
antithetic_returns = diff(log.(antithetic_prices))
53+
54+
# Skip if either array has issues
55+
if any(isnan.(original_returns)) || any(isnan.(antithetic_returns)) ||
56+
any(isinf.(original_returns)) || any(isinf.(antithetic_returns))
57+
continue
58+
end
59+
60+
# Calculate correlation of returns
61+
return_corr = cor(original_returns, antithetic_returns)
62+
push!(return_correlations, return_corr)
63+
end
64+
65+
return return_correlations
66+
end
67+
68+
@testset "Antithetic Path Correlation Tests" begin
69+
# Common parameters
70+
Random.seed!(42)
71+
trajectories = 100
72+
steps = 100
73+
reference_date = Date(2020, 1, 1)
74+
expiry = reference_date + Year(1)
75+
spot = 100.0
76+
strike = 100.0
77+
78+
# Create European call option payoff
79+
payoff = VanillaOption(strike, expiry, European(), Call(), Spot())
80+
81+
@testset "Black-Scholes Model" begin
82+
# BS parameters
83+
rate_bs = 0.05
84+
sigma_bs = 0.20
85+
86+
# Create BS market inputs
87+
bs_market = BlackScholesInputs(reference_date, rate_bs, spot, sigma_bs)
88+
bs_prob = PricingProblem(payoff, bs_market)
89+
90+
# Create Monte Carlo with antithetic sampling for BS
91+
bs_seeds = rand(1:10^9, trajectories)
92+
bs_strategy = BlackScholesExact(trajectories ÷ 2, steps, seeds=bs_seeds[1:trajectories÷2], antithetic=true)
93+
bs_method = MonteCarlo(LognormalDynamics(), bs_strategy)
94+
95+
# Solve and get paths
96+
bs_solution = solve(bs_prob, bs_method)
97+
bs_paths = bs_solution.ensemble.solutions
98+
99+
# Analyze correlations
100+
bs_correlations = analyze_path_correlation(bs_paths, trajectories ÷ 2)
101+
bs_return_correlations = calculate_return_correlation(bs_paths, trajectories ÷ 2)
102+
103+
# Report statistics for diagnostic purposes
104+
println("Black-Scholes path correlations:")
105+
println(" Mean: ", mean(bs_correlations))
106+
println(" Min: ", minimum(bs_correlations))
107+
println(" Max: ", maximum(bs_correlations))
108+
109+
println("\nBlack-Scholes return correlations:")
110+
println(" Mean: ", mean(bs_return_correlations))
111+
println(" Min: ", minimum(bs_return_correlations))
112+
println(" Max: ", maximum(bs_return_correlations))
113+
114+
# Test that price paths show strong negative correlation
115+
@test mean(bs_correlations) < -0.7
116+
117+
# Test that log returns show near-perfect negative correlation
118+
@test mean(bs_return_correlations) < -0.95
119+
120+
# Test product of terminal values is consistent with theoretical value
121+
bs_original = bs_paths[1]
122+
bs_antithetic = bs_paths[trajectories ÷ 2 + 1]
123+
124+
if eltype(bs_original.u) <: AbstractVector
125+
bs_orig_terminal = bs_original.u[end][1]
126+
bs_anti_terminal = bs_antithetic.u[end][1]
127+
else
128+
bs_orig_terminal = bs_original.u[end]
129+
bs_anti_terminal = bs_antithetic.u[end]
130+
end
131+
132+
terminal_product = bs_orig_terminal * bs_anti_terminal
133+
theoretical_product = spot^2 * exp(2 * rate_bs * yearfrac(reference_date, expiry))
134+
135+
# The product should be roughly close to the theoretical value
136+
# Using a large tolerance due to randomness
137+
@test isapprox(terminal_product, theoretical_product, rtol=0.2)
138+
end
139+
140+
@testset "Heston Model" begin
141+
# Heston parameters
142+
rate_heston = 0.03
143+
V0 = 0.04
144+
κ = 2.0
145+
θ = 0.04
146+
σ = 0.3
147+
ρ = -0.7
148+
149+
# Create Heston market inputs
150+
heston_market = HestonInputs(reference_date, rate_heston, spot, V0, κ, θ, σ, ρ)
151+
heston_prob = PricingProblem(payoff, heston_market)
152+
153+
# Create Monte Carlo with antithetic sampling for Heston
154+
heston_seeds = rand(1:10^9, trajectories)
155+
heston_strategy = EulerMaruyama(trajectories ÷ 2, steps, seeds=heston_seeds[1:trajectories÷2], antithetic=true)
156+
heston_method = MonteCarlo(HestonDynamics(), heston_strategy)
157+
158+
# Solve and get paths
159+
heston_solution = solve(heston_prob, heston_method)
160+
heston_paths = heston_solution.ensemble.solutions
161+
162+
# Analyze correlations
163+
heston_correlations = analyze_path_correlation(heston_paths, trajectories ÷ 2)
164+
heston_return_correlations = calculate_return_correlation(heston_paths, trajectories ÷ 2)
165+
166+
# Report statistics for diagnostic purposes
167+
println("\nHeston path correlations:")
168+
println(" Mean: ", mean(heston_correlations))
169+
println(" Min: ", minimum(heston_correlations))
170+
println(" Max: ", maximum(heston_correlations))
171+
172+
println("\nHeston return correlations:")
173+
println(" Mean: ", mean(heston_return_correlations))
174+
println(" Min: ", minimum(heston_return_correlations))
175+
println(" Max: ", maximum(heston_return_correlations))
176+
177+
# For Heston, the correlation might not be as strong due to stochastic volatility
178+
@test mean(heston_correlations) < -0.5
179+
180+
# For return correlations, we still expect strong negative correlation
181+
@test mean(heston_return_correlations) < -0.7
182+
183+
# Test percentage of negative correlations
184+
# In Heston, we expect most but not necessarily all paths to show negative correlation
185+
percent_negative = sum(heston_correlations .< 0) / length(heston_correlations) * 100
186+
println(" Percentage of negative correlations: $(percent_negative)%")
187+
@test percent_negative > 90 # At least 90% should be negative
188+
end
189+
190+
@testset "Correlation Comparison" begin
191+
# Compare correlations between BS and Heston to verify both are working
192+
# but may have different characteristics
193+
194+
# Repeat setup briefly
195+
rate_bs = 0.05
196+
sigma_bs = 0.20
197+
bs_market = BlackScholesInputs(reference_date, rate_bs, spot, sigma_bs)
198+
bs_prob = PricingProblem(payoff, bs_market)
199+
bs_seeds = rand(1:10^9, trajectories)
200+
bs_strategy = BlackScholesExact(trajectories ÷ 2, steps, seeds=bs_seeds[1:trajectories÷2], antithetic=true)
201+
bs_method = MonteCarlo(LognormalDynamics(), bs_strategy)
202+
bs_solution = solve(bs_prob, bs_method)
203+
bs_paths = bs_solution.ensemble.solutions
204+
205+
rate_heston = 0.03
206+
V0 = 0.04
207+
κ = 2.0
208+
θ = 0.04
209+
σ = 0.3
210+
ρ = -0.7
211+
heston_market = HestonInputs(reference_date, rate_heston, spot, V0, κ, θ, σ, ρ)
212+
heston_prob = PricingProblem(payoff, heston_market)
213+
heston_seeds = rand(1:10^9, trajectories)
214+
heston_strategy = EulerMaruyama(trajectories ÷ 2, steps, seeds=heston_seeds[1:trajectories÷2], antithetic=true)
215+
heston_method = MonteCarlo(HestonDynamics(), heston_strategy)
216+
heston_solution = solve(heston_prob, heston_method)
217+
heston_paths = heston_solution.ensemble.solutions
218+
219+
# Get path correlations
220+
bs_correlations = analyze_path_correlation(bs_paths, trajectories ÷ 2)
221+
heston_correlations = analyze_path_correlation(heston_paths, trajectories ÷ 2)
222+
223+
# Black-Scholes should typically have stronger negative correlation
224+
# due to simpler model dynamics
225+
@test mean(bs_correlations) < mean(heston_correlations)
226+
end
227+
end

test/montecarlo_heston.jl

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using Test
2+
using Hedgehog2
3+
using Dates
4+
using Random
5+
using Statistics
6+
using Printf
7+
8+
@testset "Heston Model Monte Carlo Test" begin
9+
# --------------------------
10+
# Setup common test parameters
11+
# --------------------------
12+
# Heston model parameters
13+
spot = 100.0
14+
strike = 100.0
15+
reference_date = Date(2020, 1, 1)
16+
expiry = reference_date + Year(1)
17+
r = 0.03 # Risk-free rate
18+
V0 = 0.04 # Initial variance
19+
κ = 2.0 # Mean reversion speed
20+
θ = 0.04 # Long-term variance
21+
σ = 0.3 # Vol-of-vol
22+
ρ = -0.7 # Correlation
23+
24+
# Print parameter configuration for debugging
25+
println("Test Parameters:")
26+
println(" Spot: $spot")
27+
println(" Strike: $strike")
28+
println(" Rate: $r")
29+
println(" Initial variance (V0): $V0")
30+
println(" Mean reversion (κ): ")
31+
println(" Long-term variance (θ): ")
32+
println(" Vol-of-vol (σ): ")
33+
println(" Correlation (ρ): ")
34+
println(" Time to maturity: 1 year")
35+
println(" Option type: European Call")
36+
37+
# Define payoff and market inputs
38+
payoff = VanillaOption(strike, expiry, European(), Call(), Spot())
39+
market_inputs = HestonInputs(reference_date, r, spot, V0, κ, θ, σ, ρ)
40+
prob = PricingProblem(payoff, market_inputs)
41+
42+
# Define test parameters - increase trajectories for more reliable results
43+
trajectories = 5_000
44+
steps = 100
45+
46+
# Reference price using Carr-Madan (Fourier) method
47+
carr_madan_method = CarrMadan(1.0, 32.0, HestonDynamics())
48+
carr_madan_sol = solve(prob, carr_madan_method)
49+
reference_price = carr_madan_sol.price
50+
51+
# Print reference price
52+
println("Reference price (Carr-Madan): $reference_price")
53+
54+
# Test scenarios for Euler-Maruyama
55+
scenarios = [
56+
("Euler-Maruyama without antithetic",
57+
EulerMaruyama(trajectories, steps, seeds=nothing),
58+
HestonDynamics()),
59+
60+
("Euler-Maruyama with antithetic",
61+
EulerMaruyama(trajectories ÷ 2, steps, antithetic=true, seeds=nothing),
62+
HestonDynamics())
63+
]
64+
65+
results = Dict()
66+
67+
# Run all scenarios
68+
for (scenario_name, strategy, dynamics) in scenarios
69+
# Print current scenario
70+
println("Running scenario: ", scenario_name)
71+
72+
# Execute multiple trials to measure variance
73+
prices = Float64[]
74+
for trial in 1:5
75+
# Create a new RNG with a trial-specific seed
76+
trial_rng = MersenneTwister(42 + trial)
77+
78+
# Generate random seeds for each path
79+
trial_seeds = rand(trial_rng, 1:10^9, trajectories)
80+
81+
# Create modified strategy with trial seeds
82+
modified_strategy = @set strategy.seeds = trial_seeds
83+
84+
# Create Monte Carlo method
85+
mc_method = MonteCarlo(dynamics, modified_strategy)
86+
87+
# Solve with current seed
88+
sol = solve(prob, mc_method)
89+
push!(prices, sol.price)
90+
91+
# Print diagnostic info
92+
println(" Trial $trial: Price = $(sol.price)")
93+
end
94+
95+
# Calculate statistics
96+
mean_price = mean(prices)
97+
price_variance = var(prices)
98+
error = abs(mean_price - reference_price)
99+
rel_error = error / reference_price
100+
101+
# Store results
102+
results[scenario_name] = (
103+
mean_price = mean_price,
104+
reference_price = reference_price,
105+
error = error,
106+
rel_error = rel_error,
107+
variance = price_variance
108+
)
109+
110+
# Test the result with a generous tolerance (Heston MC typically needs more paths)
111+
@test isapprox(mean_price, reference_price, rtol=0.05)
112+
end
113+
114+
# Compare variance reduction
115+
if length(scenarios) >= 2
116+
std_var = results["Euler-Maruyama without antithetic"].variance
117+
anti_var = results["Euler-Maruyama with antithetic"].variance
118+
var_reduction = std_var / anti_var
119+
120+
@info "Variance reduction (Euler-Maruyama): $(var_reduction)×"
121+
@test var_reduction > 1.0 # Antithetic should reduce variance
122+
end
123+
124+
# Print summary
125+
println("\n=== Heston Model Monte Carlo Test Results ===")
126+
println("Reference price (Carr-Madan): $reference_price")
127+
println("Scenario | Price | Rel Error | Variance")
128+
println("----------------------------|------------|-----------|----------")
129+
130+
for (name, res) in results
131+
@printf("%-28s | %.6f | %.6f | %.2e\n",
132+
name, res.mean_price, res.rel_error, res.variance)
133+
end
134+
end

test/runtests.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ include("least_squares_montecarlo.jl")
77
include("implied_vol.jl")
88
include("rate_curve.jl")
99
include("greeks.jl")
10-
include("montecarlo_black_scholes.jl")
10+
include("montecarlo_black_scholes.jl")
11+
include("montecarlo_heston.jl")
12+
include("antithetic_variates.jl")

0 commit comments

Comments
 (0)