Skip to content

Commit e9bb44d

Browse files
authored
Merge pull request #4 from aleCombi/TestSuiteRedesign
Test suite redesign
2 parents dde57c8 + e422e21 commit e9bb44d

39 files changed

+1179
-1053
lines changed

Project.toml

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,13 @@ version = "1.0.0-DEV"
55

66
[deps]
77
Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697"
8-
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
9-
Bessels = "0e736298-9ec6-45e8-9647-e4fc86a2fe38"
108
DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0"
119
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
12-
DiffEqGPU = "071ae1c0-96b5-11e9-1965-c90190d839ea"
1310
DiffEqNoiseProcess = "77a26b50-5914-5dd7-bc55-306e6241c503"
1411
DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
1512
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
16-
FiniteDiff = "6a86dc24-6348-571c-b903-95158fe2bd41"
1713
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
1814
Integrals = "de52edbc-65ea-441a-8357-d3a637375a31"
19-
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
2015
NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
2116
Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"
2217
Polynomials = "f27b6e38-b328-58d1-80ce-0feddd5e7a45"
@@ -26,23 +21,16 @@ SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
2621
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
2722
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
2823
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
29-
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
30-
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
3124

3225
[compat]
3326
Accessors = "0.1.42"
34-
BenchmarkTools = "1.6.0"
35-
Bessels = "0.2.8"
3627
DataInterpolations = "7.2.0"
3728
Dates = "1.11.0"
38-
DiffEqGPU = "3.4.1"
3929
DiffEqNoiseProcess = "5.24.1"
4030
DifferentialEquations = "7.16.0"
4131
Distributions = "0.25"
42-
FiniteDiff = "2.27.0"
4332
ForwardDiff = "0.10.38"
4433
Integrals = "4.5.0"
45-
JuliaFormatter = "1.0.62"
4634
NonlinearSolve = "4.5.0"
4735
Optimization = "4.1.2"
4836
Polynomials = "4.0.19"
@@ -53,7 +41,6 @@ SpecialFunctions = "2.5.0"
5341
StaticArrays = "1.9.13"
5442
Statistics = "1.11.1"
5543
Test = "1.11.0"
56-
Zygote = "0.6.76"
5744
julia = "1.11"
5845

5946
[extras]

examples/calib.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Revise, Dates, Hedgehog2
2+
3+
reference_date = Date(2020, 1, 1)
4+
r, S0, sigma = 0.05, 100.0, 0.25
5+
market_inputs = BlackScholesInputs(reference_date, r, S0, sigma)
6+
7+
strikes = collect(60.0:5.0:140.0)
8+
expiry = reference_date + Day(365)
9+
payoffs = [VanillaOption(K, expiry, European(), Call(), Spot()) for K in strikes]
10+
11+
quotes = [
12+
solve(PricingProblem(p, market_inputs), BlackScholesAnalytic()).price for
13+
p in payoffs
14+
]
15+
16+
accessors = [VolLens(1,1)]
17+
initial_guess = [0.15]
18+
19+
basket = BasketPricingProblem(payoffs, market_inputs)
20+
calib = CalibrationProblem(basket, BlackScholesAnalytic(), accessors, quotes, 0.7*ones(length(payoffs)))
21+
result = solve(calib, OptimizerAlgo())
22+
@show result.objective

examples/calibration_heston.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ initial_guess = [0.02, 3.0, 0.03, 0.4, -0.3] # [v0, κ, θ, σ, ρ]
4848

4949
# Accessors for each parameter in HestonInputs
5050
accessors = [
51-
@optic(_.market.V0),
52-
@optic(_.market.κ),
53-
@optic(_.market.θ),
54-
@optic(_.market.σ),
55-
@optic(_.market.ρ)
51+
@optic(_.market_inputs.V0),
52+
@optic(_.market_inputs.κ),
53+
@optic(_.market_inputs.θ),
54+
@optic(_.market_inputs.σ),
55+
@optic(_.market_inputs.ρ)
5656
]
5757

5858

examples/montecarlo_blackscholes.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Revise
12
using Hedgehog2
23
using Dates
34
using Printf
@@ -27,14 +28,15 @@ bs_solution = solve(prob, bs_method)
2728
# Using 100,000 paths for more accurate results
2829
trajectories = 5_000
2930
mc_exact_method =
30-
MonteCarlo(LognormalDynamics(), BlackScholesExact(trajectories, antithetic = true))
31+
MonteCarlo(LognormalDynamics(), BlackScholesExact(), SimulationConfig(trajectories))
3132
mc_exact_solution = solve(prob, mc_exact_method)
3233

3334
# Method 3: Monte Carlo with Euler-Maruyama discretization
3435
# Using 100,000 paths and 100 time steps
36+
trajectories = 10_000
3537
steps = 100
3638
mc_euler_method =
37-
MonteCarlo(LognormalDynamics(), EulerMaruyama(trajectories, steps, antithetic = true))
39+
MonteCarlo(LognormalDynamics(), EulerMaruyama(), SimulationConfig(trajectories, steps=steps, variance_reduction=Hedgehog2.Antithetic()))
3840
mc_euler_solution = solve(prob, mc_euler_method)
3941

4042
# Print the results

src/Hedgehog2.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ end
1919

2020
if false
2121
include("../test/runtests.jl")
22+
include("../test/calibration.jl")
2223
end
2324

2425
# utilities
2526
include("date_functions.jl")
26-
include("solutions/pricing_solutions.jl")
2727

2828
# payoffs
2929
include("payoffs/payoffs.jl")
@@ -35,6 +35,7 @@ include("market_inputs/market_inputs.jl")
3535

3636
# pricing methods
3737
include("pricing_methods/pricing_methods.jl")
38+
include("solutions/pricing_solutions.jl")
3839
include("pricing_methods/black_scholes.jl")
3940
include("pricing_methods/cox_ross_rubinstein.jl")
4041
include("pricing_methods/montecarlo.jl")

src/date_functions.jl

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
const SECONDS_IN_YEAR_365 = 365 * 86400
22
const MILLISECONDS_IN_YEAR_365 = SECONDS_IN_YEAR_365 * 1000
3+
const MILLISECONDS_IN_DAY = 86400000
34

45
# --- Time Conversions ---
56

67
"""
78
to_ticks(x::Date)
89
9-
Convert a `Date` to milliseconds since the Unix epoch.
10+
Convert a `Date` to milliseconds since the Julia `Dates` module epoch (0000-01-01).
11+
12+
Note: This is calculated by converting the `Date` to days since epoch
13+
(`Dates.date2epochdays`) and multiplying by `MILLISECONDS_IN_DAY`.
1014
"""
1115
function to_ticks(x::Date)
12-
return Dates.datetime2epochms(DateTime(x))
16+
return Dates.date2epochdays(x) * MILLISECONDS_IN_DAY
1317
end
1418

1519
"""
1620
to_ticks(x::DateTime)
1721
18-
Convert a `DateTime` to milliseconds since the Unix epoch.
22+
Convert a `DateTime` to milliseconds since the Julia `Dates` module epoch (0000-01-01T00:00:00).
23+
24+
Uses `Dates.datetime2epochms`.
1925
"""
2026
function to_ticks(x::DateTime)
2127
return Dates.datetime2epochms(x)
@@ -24,8 +30,11 @@ end
2430
"""
2531
to_ticks(x::Real)
2632
27-
Assume `x` is already a timestamp in milliseconds (e.g., `Float64` or `Int`).
28-
Used to normalize mixed inputs.
33+
Assume `x` is already a timestamp in milliseconds since the Julia `Dates` module epoch
34+
(0000-01-01T00:00:00) and return it unchanged.
35+
36+
Used to normalize mixed inputs (e.g., `Date`, `DateTime`, `Real`) to a common
37+
tick representation for calculations like `yearfrac` or `add_yearfrac`.
2938
"""
3039
function to_ticks(x::Real)
3140
return x
@@ -38,7 +47,9 @@ end
3847
3948
Compute the ACT/365 year fraction between two time points.
4049
41-
Supports `Date`, `DateTime`, or ticks (`Int` or `Float64`).
50+
Inputs `start` and `stop` can be `Date`, `DateTime`, or ticks (`Int` or `Float64`).
51+
If ticks are provided, they are assumed to be milliseconds since the Julia `Dates`
52+
module epoch (0000-01-01T00:00:00), consistent with the output of `to_ticks`.
4253
"""
4354
function yearfrac(start, stop)
4455
ms_start = to_ticks(start)
@@ -47,12 +58,17 @@ function yearfrac(start, stop)
4758
end
4859

4960
"""
50-
yearfrac(p::Period)
61+
yearfrac(p::AbstractTime)
5162
52-
Compute the ACT/365 year fraction from a `Period` object.
63+
Compute the ACT/365 year fraction from a `Period` object (e.g., `Year(1)`, `Day(180)`).
64+
It allows also for `CompoundPeriod`, and even Dates, interpreting them as time periods from year 0 of the Gregorian calendar.
65+
Calculates the duration represented by the period in milliseconds and divides by
66+
`MILLISECONDS_IN_YEAR_365`.
5367
"""
54-
function yearfrac(p::Period)
55-
ref = DateTime(1970, 1, 1)
68+
function yearfrac(p::Dates.AbstractTime)
69+
# The choice of reference date here is arbitrary and does not affect the result,
70+
# as only the difference (duration) matters.
71+
ref = DateTime(1970, 1, 1)
5672
return yearfrac(ref, ref + p)
5773
end
5874

@@ -61,8 +77,10 @@ end
6177
"""
6278
add_yearfrac(t::Real, yf::Real) -> Real
6379
64-
Add a fractional number of years (ACT/365) to a timestamp in milliseconds since epoch.
65-
Returns the updated timestamp in milliseconds as `Float64`.
80+
Add a fractional number of years (`yf`, computed as ACT/365) to a timestamp `t`.
81+
82+
The timestamp `t` is assumed to be in milliseconds since the Julia `Dates` module
83+
epoch (0000-01-01T00:00:00). Returns the updated timestamp in milliseconds as `Float64`.
6684
6785
This version is AD-compatible.
6886
"""
@@ -73,9 +91,14 @@ end
7391
"""
7492
add_yearfrac(t::TimeType, yf::Real) -> DateTime
7593
76-
Add a fractional number of years (ACT/365) to a `Date` or `DateTime`.
77-
Returns a `DateTime` object.
94+
Add a fractional number of years (`yf`, computed as ACT/365) to a `Date` or `DateTime` `t`.
95+
96+
Converts `t` to milliseconds since the Julia `Dates` module epoch, adds the
97+
duration corresponding to `yf`, and converts the result back to a `DateTime` object.
7898
"""
7999
function add_yearfrac(t::TimeType, yf::Real)
100+
# `to_ticks(t)` uses the Julia Dates epoch (Year 0000)
101+
# `add_yearfrac(Real,Real)` adds the correct millisecond duration
102+
# `epochms2datetime` converts back from the Julia Dates epoch (Year 0000)
80103
return Dates.epochms2datetime(add_yearfrac(to_ticks(t), yf))
81-
end
104+
end

src/greeks/greeks_problem.jl

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ end
6363

6464
function compute_fd_derivative(::FDCentral, prob, lens, ε, pricing_method)
6565
x₀ = lens(prob)
66-
6766
prob_up = set(prob, lens, x₀ * (1 + ε))
6867
prob_down = set(prob, lens, x₀ * (1 - ε))
6968
v_up = solve(prob_up, pricing_method).price
@@ -173,16 +172,14 @@ function solve(
173172
# Delta = ∂V/∂S = ∂V/∂F * ∂F/∂S = (cp * N(cp·d1)) * (1/D)
174173
cp * Φ(cp * d1)
175174

176-
elseif lens === @optic _.market_inputs.sigma
175+
elseif lens === VolLens(1,1) #TODO: add logic
177176
# Vega = ∂V/∂σ = D · F · φ(d1) · √T
178177
D * F * ϕ(d1) * T
179178

180179
elseif lens === @optic _.payoff.expiry
181-
@assert is_flat(prob.market_inputs.rate)
182-
183180
# Assume flat rate: z(T) = r ⇒ D(T) = exp(-rT), F(T) = S / D(T)
184181
r = zero_rate_yf(prob.market_inputs.rate, T)
185-
(r * prob.payoff.strike * D * Φ(d2) + F * D * prob.market_inputs.sigma * ϕ(d1) / (2T)) / (MILLISECONDS_IN_YEAR_365) #against ticks, to match AD and FD. Observe that the sign is counterintuitive as it is a derivative against expiry in tticks, not against time-to-maturity in yearfrac
182+
(r * prob.payoff.strike * D * Φ(d2) + F * D * σ * ϕ(d1) / (2T)) / (MILLISECONDS_IN_YEAR_365) #against ticks, to match AD and FD. Observe that the sign is counterintuitive as it is a derivative against expiry in tticks, not against time-to-maturity in yearfrac
186183

187184
else
188185
error("Unsupported lens for analytic Greek")
@@ -198,9 +195,9 @@ function solve(gprob::SecondOrderGreekProblem, ::AnalyticGreek, ::BlackScholesAn
198195
inputs = prob.market_inputs
199196

200197
S = inputs.spot
201-
σ = inputs.sigma
202198
T = yearfrac(inputs.referenceDate, prob.payoff.expiry)
203199
K = prob.payoff.strike
200+
σ = get_vol_yf(inputs.sigma, T, K)
204201

205202
D = df(inputs.rate, prob.payoff.expiry)
206203
F = S / D
@@ -214,7 +211,7 @@ function solve(gprob::SecondOrderGreekProblem, ::AnalyticGreek, ::BlackScholesAn
214211
# Gamma = ∂²V/∂S² = φ(d1) / (Sσ√T)
215212
ϕ(d1) / (S * σ * T)
216213

217-
elseif (lens1 === @optic _.market_inputs.sigma) && (lens2 === @optic _.market_inputs.sigma)
214+
elseif (lens1 === VolLens(1,1)) && (lens2 === VolLens(1,1)) #TODO: introduce logic for sigma
218215
# Volga = Vega * d1 * d2 / σ
219216
vega = D * F * ϕ(d1) * T
220217
vega * d1 * d2 / σ
@@ -223,7 +220,7 @@ function solve(gprob::SecondOrderGreekProblem, ::AnalyticGreek, ::BlackScholesAn
223220
error("Unsupported second-order analytic Greek")
224221
end
225222

226-
return GreekResult(deriv)
223+
return GreekResult(greek)
227224
end
228225

229226
struct BatchGreekProblem{P,L}

src/market_inputs/rate_curve.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ zero_rate(curve::RateCurve, ticks::T) where T <: Number =
6464
zero_rate(curve::FlatRateCurve, ticks::T) where T <: Number =
6565
curve.rate
6666

67-
zero_rate(curve::R, t::Date) where R <: AbstractRateCurve = zero_rate(curve, to_ticks(t))
67+
zero_rate(curve::R, t::D) where {R <: AbstractRateCurve, D<:TimeType} = zero_rate(curve, to_ticks(t))
6868

6969
zero_rate_yf(curve::RateCurve, yf::R) where R <: Number = curve.interpolator(yf)
7070
zero_rate_yf(curve::FlatRateCurve, yf::R) where R <: Number = curve.rate

src/market_inputs/vol_surface.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ function RectVolSurface(
211211
end
212212

213213
vols = Matrix{Float64}(undef, nrows, ncols)
214-
accessor = @optic _.market_inputs.sigma
214+
accessor = VolLens(1,1)
215215
for i = 1:nrows, j = 1:ncols
216216
expiry = reference_date + tenors[i]
217217
strike = strikes[j]

src/pricing_methods/black_scholes.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Computes the price of a European vanilla option under the Black-Scholes model.
3737
"""
3838
function solve(
3939
prob::PricingProblem{VanillaOption{TS,TE,European,B,C}, I},
40-
::BlackScholesAnalytic,
40+
method::BlackScholesAnalytic,
4141
) where {TS,TE,B,C, I <: BlackScholesInputs}
4242

4343
payoff = prob.payoff
@@ -60,5 +60,5 @@ function solve(
6060
D * cp * (F * cdf(N, cp * d1) - K * cdf(N, cp * d2))
6161
end
6262

63-
return AnalyticSolution(price)
63+
return AnalyticSolution(prob, method, price)
6464
end

0 commit comments

Comments
 (0)