Skip to content

Commit 4022754

Browse files
author
Alexander Robbins
committed
changed somethings to actually be able to run them
1 parent 39d3637 commit 4022754

17 files changed

Lines changed: 2612 additions & 126 deletions

.claude/settings.local.json

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(cmake --preset debug)",
5+
"Bash(cmake --build build-debug)",
6+
"Bash(./tests/tests.exe)",
7+
"Bash(cmd /c tests.exe --help)",
8+
"Bash(cmake --preset msvc-debug)",
9+
"Bash(\"build-debug/tests/tests.exe\" --help)",
10+
"Bash(echo \"Exit code: $?\")",
11+
"Bash(objdump -p \"/c/Users/xande/lfmc/lfmc/build-debug/tests/tests.exe\")",
12+
"Bash(for dll:*)",
13+
"Bash(do if:*)",
14+
"Bash(then echo:*)",
15+
"Bash(powershell -Command \"try { & ''build-debug\\\\tests\\\\tests.exe'' --help } catch { Write-Host ''Error:'' $_Exception.Message; Write-Host ''Exit code:'' $LASTEXITCODE }\")",
16+
"Bash(cmake --build build-debug --target tests)",
17+
"Bash(echo $PATH)",
18+
"Bash(./tests.exe --help)",
19+
"Bash(ldd ./tests.exe)",
20+
"Bash(objdump -x ./tests.exe)",
21+
"Bash(objdump -h ./tests.exe)",
22+
"Bash(gdb -batch -ex run -ex \"thread apply all backtrace\" --args ./tests.exe --help)",
23+
"Bash(objdump -t ./tests.exe)",
24+
"Bash(objdump -p ./tests.exe)",
25+
"Bash(cmake --build build-debug --verbose)",
26+
"Bash(cmake --build build-debug --clean-first)",
27+
"Bash(nm /c/Users/xande/lfmc/lfmc/build-debug/_deps/catch2-build/src/libCatch2Maind.a)",
28+
"Bash(c++ --version)",
29+
"Bash(nm /c/Users/xande/lfmc/lfmc/build-debug/_deps/catch2-build/src/libCatch2d.a)",
30+
"Bash(ctest --preset debug)",
31+
"Bash(nm /c/Users/xande/lfmc/lfmc/build-debug/tests/tests.exe)",
32+
"Bash(objdump -p ./build-debug/tests/tests.exe)",
33+
"Bash(objdump -p /c/Users/xande/lfmc/lfmc/build-debug/tests/tests.exe)",
34+
"Bash(ldd:*)",
35+
"Bash(strace -o /tmp/strace.txt ./tests.exe)",
36+
"Read(//tmp/**)",
37+
"Bash(cp /c/msys64/ucrt64/bin/libgcc_s_seh-1.dll /c/msys64/ucrt64/bin/libstdc++-6.dll /c/msys64/ucrt64/bin/libwinpthread-1.dll /c/Users/xande/lfmc/lfmc/build-debug/tests/)",
38+
"Bash(ls -la /c/Users/xande/lfmc/lfmc/build-debug/tests/*.dll)",
39+
"Bash(./build-debug/tests/tests.exe --help)",
40+
"Bash(git reset:*)",
41+
"Bash(ctest:*)",
42+
"Bash(cmd /c bench.exe)",
43+
"Bash(PATH=\"/c/msys64/ucrt64/bin:$PATH\" ./bench.exe 2>&1)",
44+
"Bash(PATH=\"/c/msys64/ucrt64/bin:$PATH\" ./bench.exe)",
45+
"Bash(ls build-debug/tests/*.dll)",
46+
"Bash(cmake --build build-debug --target bench)",
47+
"Bash(ls /c/Users/xande/lfmc/lfmc/build-debug/tests/*.dll)",
48+
"Bash(./bench.exe)",
49+
"Bash(cmake --preset debug -q)",
50+
"Bash(cmake --build build-debug --target quick_compare)",
51+
"Bash(./quick_compare.exe)",
52+
"Bash(echo \"EXIT: $?\")",
53+
"Bash(cp /c/msys64/ucrt64/bin/libgcc_s_seh-1.dll /c/Users/xande/lfmc/lfmc/build-debug/tests/)",
54+
"Bash(cp /c/msys64/ucrt64/bin/libstdc++-6.dll /c/Users/xande/lfmc/lfmc/build-debug/tests/)",
55+
"Bash(cp /c/msys64/ucrt64/bin/libwinpthread-1.dll /c/Users/xande/lfmc/lfmc/build-debug/tests/)",
56+
"Bash(chmod +x /c/Users/xande/lfmc/lfmc/build-debug/tests/quick_compare.exe)",
57+
"Bash(bash -c \"./quick_compare.exe\")",
58+
"Bash(echo exit=$?)",
59+
"Bash(winpty /c/Users/xande/lfmc/lfmc/build-debug/tests/quick_compare.exe)"
60+
]
61+
}
62+
}
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
#pragma once
2+
3+
#include "lfmc/numerical_scheme.hpp"
4+
#include "lfmc/payoff.hpp"
5+
#include "lfmc/stochastic_process.hpp"
6+
#include "lfmc/types.hpp"
7+
8+
#include <cassert>
9+
#include <cstdint>
10+
#include <functional>
11+
#include <memory>
12+
#include <random>
13+
#include <string>
14+
#include <thread>
15+
#include <utility>
16+
#include <vector>
17+
18+
namespace lfmc {
19+
20+
// A strategy sampler: given n samples and a seed, returns n iid samples of the
21+
// (possibly variance-reduced) payoff estimator. The seed guarantees RNG independence
22+
// between strategies and between exploration/exploitation phases.
23+
using SamplerFn = std::function<std::vector<double>(size_t n_samples, uint64_t seed)>;
24+
25+
// Per-strategy statistics collected during the exploration phase
26+
struct StrategyStats {
27+
std::string name;
28+
double mean;
29+
double sample_variance; // unbiased (n-1 denominator)
30+
double wall_time_ms;
31+
size_t n_samples;
32+
};
33+
34+
// Full result of one ASVR run
35+
struct ASVRResult {
36+
double estimate;
37+
double estimated_variance; // Var(mu_ASVR), estimated from exploration data
38+
double estimated_stderr; // sqrt(estimated_variance)
39+
40+
std::vector<StrategyStats> exploration_stats; // one entry per strategy
41+
std::vector<double> precision_weights; // w_k* used to allocate exploitation
42+
std::vector<size_t> exploitation_counts; // actual n_k samples per strategy
43+
44+
// Diagnostics — useful for paper tables
45+
double plain_mc_variance_estimate; // sigma_0^2 / N (first strategy treated as plain MC)
46+
double variance_reduction_ratio; // plain_mc_variance / estimated_variance
47+
size_t n_exploration;
48+
size_t n_exploitation;
49+
size_t total_samples;
50+
};
51+
52+
struct ASVRConfig {
53+
// Fraction of total_samples used for exploration (split equally across K strategies)
54+
double exploration_fraction = 0.1;
55+
// Total thread budget; exploration uses up to K threads, exploitation uses the rest
56+
size_t n_threads = std::thread::hardware_concurrency();
57+
// Minimum exploration samples per strategy regardless of exploration_fraction.
58+
// Needs to be large enough for a stable variance estimate (~100+ samples).
59+
size_t min_exploration_per_strategy = 100;
60+
};
61+
62+
// ─── ASVR Algorithm ──────────────────────────────────────────────────────────
63+
//
64+
// Phase 1 (Exploration): K strategies run in parallel, each generating
65+
// n_explore_each = max(min_per_strategy, alpha*N/K) samples.
66+
// Each strategy uses seed make_seed(k, 0) for full RNG independence.
67+
//
68+
// Weight computation: w_k* = (1/sigma_k^2) / sum_j(1/sigma_j^2)
69+
// (inverse-variance / "precision" weighting)
70+
//
71+
// Phase 2 (Exploitation): strategy k receives n_k = floor(w_k* * n_exploit) samples,
72+
// each run using seed make_seed(k, 1) — independent of the exploration phase.
73+
// Strategies with n_k > 0 run in parallel.
74+
//
75+
// Combination (unbiased by independence):
76+
// mu_explore = (1/K) * sum_k mu_k^explore (equal-weight pooled)
77+
// mu_exploit = sum_k w_k* * mu_k^exploit (precision-weighted)
78+
// mu_ASVR = alpha * mu_explore + (1-alpha) * mu_exploit
79+
//
80+
// The key unbiasedness guarantee: w_k* depends only on exploration samples;
81+
// exploitation samples are generated with fresh RNG seeds and are therefore
82+
// independent of the weight selection event.
83+
84+
class AdaptiveVarianceReduction {
85+
public:
86+
static ASVRResult run(std::vector<std::pair<std::string, SamplerFn>> strategies,
87+
size_t total_samples, ASVRConfig config = {});
88+
89+
static StrategyStats run_strategy_batch(const std::string& name, const SamplerFn& sampler,
90+
size_t n_samples, uint64_t seed);
91+
92+
private:
93+
static std::vector<double> compute_precision_weights(const std::vector<StrategyStats>& stats);
94+
};
95+
96+
// ─── Internal utilities ───────────────────────────────────────────────────────
97+
98+
namespace detail {
99+
100+
// Splitmix64-based seed derivation: strategy k, phase p → unique uint64_t seed.
101+
// Two calls with the same (k, p) give the same seed; different (k, p) pairs are
102+
// statistically independent because they hit different streams of splitmix64.
103+
inline uint64_t make_seed(size_t k, size_t phase) noexcept {
104+
uint64_t x = (static_cast<uint64_t>(k) * 0x9e3779b97f4a7c15ULL)
105+
^ (static_cast<uint64_t>(phase) * 0x6c62272e07bb0142ULL)
106+
^ 0xdeadbeefcafe0000ULL;
107+
x ^= x >> 30;
108+
x *= 0xbf58476d1ce4e5b9ULL;
109+
x ^= x >> 27;
110+
x *= 0x94d049bb133111ebULL;
111+
x ^= x >> 31;
112+
return x;
113+
}
114+
115+
// Generate `n` draws from N(0,1)
116+
inline std::vector<double> gen_normals(size_t n, std::mt19937_64& rng) {
117+
std::normal_distribution<double> dist{0.0, 1.0};
118+
std::vector<double> v(n);
119+
for (auto& x : v)
120+
x = dist(rng);
121+
return v;
122+
}
123+
124+
// Correct single-path generator (avoids the pre-allocate + push_back double-length bug
125+
// present in the existing PathGenerator)
126+
template <StochasticProcess SP, NumericalScheme<SP> NS>
127+
Path generate_path(const SP& process, const NS& scheme, const Normals& normals, size_t steps,
128+
double T) {
129+
const double dt = T / static_cast<double>(steps);
130+
Path path;
131+
path.reserve(steps + 1);
132+
double x = process.initial();
133+
path.push_back(x);
134+
double t = 0.0;
135+
for (size_t i = 0; i < steps; ++i) {
136+
x = scheme.step(process, t, x, dt, normals[i]);
137+
path.push_back(x);
138+
t += dt;
139+
}
140+
return path;
141+
}
142+
143+
// Unbiased sample mean and variance (Welford would be more numerically stable for large n,
144+
// but two-pass is fine for the batch sizes used here)
145+
inline std::pair<double, double> mean_variance(const std::vector<double>& samples) {
146+
assert(!samples.empty());
147+
const double n = static_cast<double>(samples.size());
148+
double sum = 0.0;
149+
for (double x : samples)
150+
sum += x;
151+
const double mean = sum / n;
152+
double sq = 0.0;
153+
for (double x : samples) {
154+
double d = x - mean;
155+
sq += d * d;
156+
}
157+
// n-1 denominator for unbiased variance; guard against n=1
158+
const double variance = (samples.size() > 1) ? sq / (n - 1.0) : 0.0;
159+
return {mean, variance};
160+
}
161+
162+
} // namespace detail
163+
164+
// ─── Strategy factory functions ──────────────────────────────────────────────
165+
//
166+
// Each factory captures the pricing parameters and returns a SamplerFn.
167+
// The SamplerFn is called with (n_samples, seed) and returns exactly n_samples
168+
// iid draws from the (possibly variance-reduced) estimator of E[payoff].
169+
170+
// Plain pseudo-random Monte Carlo — baseline strategy
171+
template <StochasticProcess SP, NumericalScheme<SP> NS>
172+
SamplerFn make_plain_mc_sampler(SP process, NS scheme, std::shared_ptr<Payoff> payoff,
173+
size_t steps, double T) {
174+
return [process, scheme, payoff, steps, T](size_t n, uint64_t seed) -> std::vector<double> {
175+
std::mt19937_64 rng{seed};
176+
std::vector<double> samples;
177+
samples.reserve(n);
178+
for (size_t i = 0; i < n; ++i) {
179+
auto normals = detail::gen_normals(steps, rng);
180+
auto path = detail::generate_path(process, scheme, normals, steps, T);
181+
auto result = payoff->generate_payoffs({path});
182+
if (result && !result->empty() && !(*result)[0].empty())
183+
samples.push_back((*result)[0][0]);
184+
}
185+
return samples;
186+
};
187+
}
188+
189+
// Antithetic variates — each sample is (payoff(Z) + payoff(-Z)) / 2
190+
// Returns n samples, each consuming one pair of paths. Variance is reduced
191+
// because the two paths are negatively correlated for monotone payoffs.
192+
template <StochasticProcess SP, NumericalScheme<SP> NS>
193+
SamplerFn make_antithetic_sampler(SP process, NS scheme, std::shared_ptr<Payoff> payoff,
194+
size_t steps, double T) {
195+
return [process, scheme, payoff, steps, T](size_t n, uint64_t seed) -> std::vector<double> {
196+
std::mt19937_64 rng{seed};
197+
std::vector<double> samples;
198+
samples.reserve(n);
199+
for (size_t i = 0; i < n; ++i) {
200+
auto z = detail::gen_normals(steps, rng);
201+
Normals neg_z(steps);
202+
for (size_t j = 0; j < steps; ++j)
203+
neg_z[j] = -z[j];
204+
205+
auto path_pos = detail::generate_path(process, scheme, z, steps, T);
206+
auto path_neg = detail::generate_path(process, scheme, neg_z, steps, T);
207+
208+
auto r_pos = payoff->generate_payoffs({path_pos});
209+
auto r_neg = payoff->generate_payoffs({path_neg});
210+
211+
if (r_pos && r_neg && !r_pos->empty() && !r_neg->empty() && !(*r_pos)[0].empty() &&
212+
!(*r_neg)[0].empty())
213+
samples.push_back(0.5 * ((*r_pos)[0][0] + (*r_neg)[0][0]));
214+
}
215+
return samples;
216+
};
217+
}
218+
219+
// Control variates with in-batch OLS beta estimation.
220+
// target_payoff: the option price we're estimating
221+
// control_payoff: a payoff with known expectation `control_mean`
222+
// (e.g. EuropeanCall(0.0) gives S_T; E[S_T] = S0 * exp(mu*T))
223+
// Returns: X_i - beta_hat * (Y_i - E[Y]) for each i
224+
template <StochasticProcess SP, NumericalScheme<SP> NS>
225+
SamplerFn make_control_variate_sampler(SP process, NS scheme, std::shared_ptr<Payoff> target,
226+
std::shared_ptr<Payoff> control, double control_mean,
227+
size_t steps, double T) {
228+
return [process, scheme, target, control, control_mean, steps,
229+
T](size_t n, uint64_t seed) -> std::vector<double> {
230+
std::mt19937_64 rng{seed};
231+
std::vector<double> xs, ys;
232+
xs.reserve(n);
233+
ys.reserve(n);
234+
235+
for (size_t i = 0; i < n; ++i) {
236+
auto z = detail::gen_normals(steps, rng);
237+
auto path = detail::generate_path(process, scheme, z, steps, T);
238+
auto rx = target->generate_payoffs({path});
239+
auto ry = control->generate_payoffs({path});
240+
if (rx && ry && !rx->empty() && !ry->empty() && !(*rx)[0].empty() &&
241+
!(*ry)[0].empty()) {
242+
xs.push_back((*rx)[0][0]);
243+
ys.push_back((*ry)[0][0]);
244+
}
245+
}
246+
247+
// OLS beta: minimises Var(X - beta*(Y - E[Y]))
248+
const size_t m = xs.size();
249+
double mean_x = 0.0, mean_y = 0.0;
250+
for (size_t i = 0; i < m; ++i) {
251+
mean_x += xs[i];
252+
mean_y += ys[i];
253+
}
254+
mean_x /= static_cast<double>(m);
255+
mean_y /= static_cast<double>(m);
256+
257+
double cov_xy = 0.0, var_y = 0.0;
258+
for (size_t i = 0; i < m; ++i) {
259+
cov_xy += (xs[i] - mean_x) * (ys[i] - mean_y);
260+
var_y += (ys[i] - mean_y) * (ys[i] - mean_y);
261+
}
262+
const double beta = (var_y > 0.0) ? cov_xy / var_y : 0.0;
263+
264+
std::vector<double> samples(m);
265+
for (size_t i = 0; i < m; ++i)
266+
samples[i] = xs[i] - beta * (ys[i] - control_mean);
267+
return samples;
268+
};
269+
}
270+
271+
// Antithetic + control variates combined.
272+
// Each raw sample is the antithetic average; OLS beta is estimated from those averages.
273+
template <StochasticProcess SP, NumericalScheme<SP> NS>
274+
SamplerFn make_antithetic_cv_sampler(SP process, NS scheme, std::shared_ptr<Payoff> target,
275+
std::shared_ptr<Payoff> control, double control_mean,
276+
size_t steps, double T) {
277+
return [process, scheme, target, control, control_mean, steps,
278+
T](size_t n, uint64_t seed) -> std::vector<double> {
279+
std::mt19937_64 rng{seed};
280+
std::vector<double> xs, ys;
281+
xs.reserve(n);
282+
ys.reserve(n);
283+
284+
for (size_t i = 0; i < n; ++i) {
285+
auto z = detail::gen_normals(steps, rng);
286+
Normals neg_z(steps);
287+
for (size_t j = 0; j < steps; ++j)
288+
neg_z[j] = -z[j];
289+
290+
auto path_p = detail::generate_path(process, scheme, z, steps, T);
291+
auto path_n = detail::generate_path(process, scheme, neg_z, steps, T);
292+
293+
auto xp = target->generate_payoffs({path_p});
294+
auto xn = target->generate_payoffs({path_n});
295+
auto yp = control->generate_payoffs({path_p});
296+
auto yn = control->generate_payoffs({path_n});
297+
298+
if (xp && xn && yp && yn && !xp->empty() && !xn->empty() && !yp->empty() &&
299+
!yn->empty() && !(*xp)[0].empty() && !(*xn)[0].empty() && !(*yp)[0].empty() &&
300+
!(*yn)[0].empty()) {
301+
xs.push_back(0.5 * ((*xp)[0][0] + (*xn)[0][0]));
302+
ys.push_back(0.5 * ((*yp)[0][0] + (*yn)[0][0]));
303+
}
304+
}
305+
306+
const size_t m = xs.size();
307+
double mean_x = 0.0, mean_y = 0.0;
308+
for (size_t i = 0; i < m; ++i) {
309+
mean_x += xs[i];
310+
mean_y += ys[i];
311+
}
312+
mean_x /= static_cast<double>(m);
313+
mean_y /= static_cast<double>(m);
314+
315+
double cov_xy = 0.0, var_y = 0.0;
316+
for (size_t i = 0; i < m; ++i) {
317+
cov_xy += (xs[i] - mean_x) * (ys[i] - mean_y);
318+
var_y += (ys[i] - mean_y) * (ys[i] - mean_y);
319+
}
320+
const double beta = (var_y > 0.0) ? cov_xy / var_y : 0.0;
321+
322+
std::vector<double> samples(m);
323+
for (size_t i = 0; i < m; ++i)
324+
samples[i] = xs[i] - beta * (ys[i] - control_mean);
325+
return samples;
326+
};
327+
}
328+
329+
} // namespace lfmc

0 commit comments

Comments
 (0)