Backtesting engine for the delta hedging of basket call options, in C# / .NET 10.
Sell a call on a weighted basket of shares, then replicate it day by day with a self-financing portfolio of the underlyings and cash. The engine replays a market path, maintains the hedge, and records how far the portfolio drifts from the option it is meant to replicate.
- Basket calls on 1 to n underlyings, payoff
max(Σ wᵢ·Sᵢ − K, 0) - Two rebalancing rules — every N business days, or whenever an underlying has moved more than a threshold since the last rebalancing
- Self-financing by construction — rebalancing only moves money between shares and cash; the portfolio value is unchanged at the instant of every update
- Source-agnostic — the engine consumes a stream of daily price snapshots and does not know whether they came from a CSV or a simulator, so backtests and forward tests share every line of hedging logic
- Verified — 20 unit tests, plus an independent re-simulation of the daily hedge that matches the engine's output to 7 × 10⁻¹⁴ over 124 days
The pricer takes today's date and spots and returns the option's theoretical price and its deltas — the sensitivity of the price to each underlying. The whole strategy is four rules:
V₀ = p₀ the initial capital is the option premium
qᵢ = δᵢ hold delta of each share
cash = V − Σ qᵢ·Sᵢ whatever is not in shares is cash (usually borrowed)
V(t) = Σ qᵢ·Sᵢ(t) + cash · e^(r·τ) shares at today's prices, cash accrued since last rebalancing
On every rebalancing the value is measured before the composition changes, the quantities are replaced by the new deltas, and the cash absorbs the difference. The premium is used exactly once, on day one; after that the portfolio's value is its own, and the gap to the option price is the hedging error — measured, never corrected.
The pricer itself (Black-Scholes basket approximation, constant 2% risk-free rate, 252 business
days per year, bank-holiday calendar) is a pre-built library shipped in lib/.
src/CoreBacktester/
Backtester.cs the day-by-day loop
Hedging/HedgeComposer the only class that talks to the pricer
PortfolioHandler/Portfolio a cash amount and a composition — knows nothing about deltas
Oracles/ RegularOracle · SpotOracle · factory from the scenario file
MarketData/ CSV provider · simulated provider · shared date grouping
Parameters/ · Output/ JSON in, JSON out
src/BacktestConsole/ three arguments, no logic
tests/CoreBacktester.Tests/ 20 NUnit tests
The boundary that matters is between the mathematical model and the financial one. The
pricer works in ordered arrays aligned on its own share ordering; the portfolio works in
compositions keyed by share id. HedgeComposer is the single place where the two meet — it builds
the pricer's spot array from a price snapshot, and turns the pricer's deltas back into a
composition. Nothing else in the codebase sees an ordered array, so the ordering contract cannot be
broken anywhere else.
Backtester depends only on interfaces: IMarketDataProvider, IRebalancingOracle, IPortfolio.
Switching from a historical CSV to a simulated path, or from a periodic to a threshold oracle, is
one line in Program.cs.
Twelve scenarios in scenarios/: three single-underlying calls and three five-underlying baskets,
each with both rebalancing rules on the same price path. Outputs are in results/, and the analysis
notebook is in notebooks/.
| scenario | underlyings | strike | oracle | days | rebalancings | V₀ | final price | final value | tracking error |
|---|---|---|---|---|---|---|---|---|---|
| Test_1_1 | 1 | 10 | daily | 124 | 124 | 0.193 | 0.216 | 0.247 | +16.0 % |
| Test_1_1_Spot | 1 | 10 | 2 % move | 124 | 19 | 0.193 | 0.216 | 0.239 | +12.1 % |
| Test_1_2 | 1 | 9 | daily | 124 | 124 | 1.239 | 1.347 | 1.364 | +1.4 % |
| Test_1_2_Spot | 1 | 9 | 2 % move | 124 | 20 | 1.239 | 1.347 | 1.363 | +1.3 % |
| Test_1_3 | 1 | 12 | daily | 124 | 124 | 0.725 | 0.044 | 0.224 | +24.9 % |
| Test_1_3_Spot | 1 | 12 | 2 % move | 124 | 26 | 0.725 | 0.044 | 0.205 | +22.3 % |
| Test_5_1 | 5 | 12 | every 3 days | 199 | 67 | 1.487 | 0.000 | 0.074 | +5.0 % |
| Test_5_1_Spot | 5 | 12 | 2 % move | 199 | 105 | 1.487 | 0.000 | 0.074 | +5.0 % |
| Test_5_2 | 5 | 12 | daily | 244 | 244 | 1.200 | 0.000 | 0.037 | +3.1 % |
| Test_5_2_Spot | 5 | 12 | 2 % move | 244 | 241 | 1.200 | 0.000 | 0.037 | +3.1 % |
| Test_5_3 | 5 | 10 | daily | 244 | 244 | 0.936 | 2.776 | 2.882 | +11.4 % |
| Test_5_3_Spot | 5 | 10 | 2 % move | 244 | 220 | 0.936 | 2.776 | 2.872 | +10.3 % |
Tracking error is the final value − price, relative to the initial option price. It reads large
where that price is small: Test_1_1 starts out of the money at 0.19, so an absolute error of 0.03
is 16 %; the same option struck at 9 starts at 1.24 and shows 1.4 % for a comparable absolute error.
Deep in the money the payoff is Σ wᵢ·Sᵢ − K, whose derivative with respect to each share is its
weight. The hedge therefore ends up holding the basket itself — without that being coded anywhere.
On a single calm underlying the spot rule rebalances 19 times instead of 124 and lands slightly closer to the option. Discrete hedging error is path-dependent noise, so that is not a rule — but most daily rebalancings on a calm path are churn, and with transaction costs modelled the spot rule would win on both counts.
| Check | Result |
|---|---|
| Portfolio value at inception equals the option premium | identical to all 17 digits |
| Rebalancing leaves the portfolio value unchanged | within 10⁻¹⁰, over sequences of rebalancings |
| Cash accrual restarts from each rebalancing date | unit-tested |
| Hedge is invariant to the insertion order of the price snapshot | unit-tested, at portfolio and engine level |
Final option price equals the contractual payoff max(basket − K, 0) |
exact, all 12 scenarios |
| Engine output vs an independent Python re-simulation of the daily hedge | max gap 7.1 × 10⁻¹⁴ over 124 days |
| Final deltas of a deep-in-the-money basket vs its weights | equal to 4 decimals |
The Python re-simulation also recovers the library's risk-free rate: fitting the accrual that reproduces the engine's value path gives 2.0000 %.
Requires the .NET 10 SDK.
dotnet build DeltaHedging.slnx -c Release
dotnet test tests/CoreBacktester.Tests/CoreBacktester.Tests.csproj -c Release
dotnet src/BacktestConsole/bin/Release/net10.0/BacktestConsole.dll \
scenarios/Test_1_1/params_1_1.json \
scenarios/Test_1_1/data_1_1.csv \
results/Test_1_1.jsonAll twelve scenarios:
for d in scenarios/*/; do n=$(basename "$d")
dotnet src/BacktestConsole/bin/Release/net10.0/BacktestConsole.dll "$d"/*.json "$d"/*.csv "results/$n.json"
doneThe notebook (pandas, matplotlib) reads results/ and regenerates every figure in docs/plots/.
params_*.json describes the option and the rebalancing rule:
{
"basketOption": { "strike": 10, "maturity": "2023-12-12T00:00:00",
"underlyingShareIds": ["share_1"], "weights": [1] },
"pricingParams": { "volatilities": [0.2], "correlations": [[1]] },
"rebalancingOracleDescription": { "type": "Regular", "period": 1 }
}data_*.csv is one row per share per business day (Id,DateOfPrice,Value). Weekends and bank
holidays are absent from the grid, and the engine never generates dates of its own — it iterates
what the file contains.
- Transaction costs — the output carries a
transactionCostsfield, held at zero - Stochastic rates or volatility — both are constant in the pricer
- Bid–ask — every fill is at the snapshot price
All twelve final gaps are positive. Over six independent paths that is suggestive, not conclusive, and consistent with simulated paths drifting above the risk-free rate — a delta-hedged short call is long the underlying and benefits from that drift.
Research and educational use. All figures are computed from the scenario files in this repository under the assumptions stated above.


