From 5b15818952d3e88965512dbb2401f6cefa678fa5 Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Fri, 20 Feb 2026 00:42:07 -0500 Subject: [PATCH 01/19] temp archive --- include/lfmc/{ => archive}/Estimator.hpp | 0 include/lfmc/{ => archive}/Manager.hpp | 0 include/lfmc/{ => archive}/NumericalScheme.hpp | 0 include/lfmc/{ => archive}/PathGenerator.hpp | 0 include/lfmc/{ => archive}/Payoff.hpp | 0 include/lfmc/{ => archive}/RandomGenerator.hpp | 0 include/lfmc/{ => archive}/StochasticProcess.hpp | 0 include/lfmc/{ => archive}/VarianceReductionStrategy.hpp | 0 include/lfmc/{ => archive}/types.hpp | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename include/lfmc/{ => archive}/Estimator.hpp (100%) rename include/lfmc/{ => archive}/Manager.hpp (100%) rename include/lfmc/{ => archive}/NumericalScheme.hpp (100%) rename include/lfmc/{ => archive}/PathGenerator.hpp (100%) rename include/lfmc/{ => archive}/Payoff.hpp (100%) rename include/lfmc/{ => archive}/RandomGenerator.hpp (100%) rename include/lfmc/{ => archive}/StochasticProcess.hpp (100%) rename include/lfmc/{ => archive}/VarianceReductionStrategy.hpp (100%) rename include/lfmc/{ => archive}/types.hpp (100%) diff --git a/include/lfmc/Estimator.hpp b/include/lfmc/archive/Estimator.hpp similarity index 100% rename from include/lfmc/Estimator.hpp rename to include/lfmc/archive/Estimator.hpp diff --git a/include/lfmc/Manager.hpp b/include/lfmc/archive/Manager.hpp similarity index 100% rename from include/lfmc/Manager.hpp rename to include/lfmc/archive/Manager.hpp diff --git a/include/lfmc/NumericalScheme.hpp b/include/lfmc/archive/NumericalScheme.hpp similarity index 100% rename from include/lfmc/NumericalScheme.hpp rename to include/lfmc/archive/NumericalScheme.hpp diff --git a/include/lfmc/PathGenerator.hpp b/include/lfmc/archive/PathGenerator.hpp similarity index 100% rename from include/lfmc/PathGenerator.hpp rename to include/lfmc/archive/PathGenerator.hpp diff --git a/include/lfmc/Payoff.hpp b/include/lfmc/archive/Payoff.hpp similarity index 100% rename from include/lfmc/Payoff.hpp rename to include/lfmc/archive/Payoff.hpp diff --git a/include/lfmc/RandomGenerator.hpp b/include/lfmc/archive/RandomGenerator.hpp similarity index 100% rename from include/lfmc/RandomGenerator.hpp rename to include/lfmc/archive/RandomGenerator.hpp diff --git a/include/lfmc/StochasticProcess.hpp b/include/lfmc/archive/StochasticProcess.hpp similarity index 100% rename from include/lfmc/StochasticProcess.hpp rename to include/lfmc/archive/StochasticProcess.hpp diff --git a/include/lfmc/VarianceReductionStrategy.hpp b/include/lfmc/archive/VarianceReductionStrategy.hpp similarity index 100% rename from include/lfmc/VarianceReductionStrategy.hpp rename to include/lfmc/archive/VarianceReductionStrategy.hpp diff --git a/include/lfmc/types.hpp b/include/lfmc/archive/types.hpp similarity index 100% rename from include/lfmc/types.hpp rename to include/lfmc/archive/types.hpp From 9e3ba8254b63143c7d0259438026c380543c5ea4 Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Fri, 20 Feb 2026 01:33:06 -0500 Subject: [PATCH 02/19] rng engine and path engine --- include/lfmc/Engine.hpp | 1 + include/lfmc/Estimator.hpp | 1 + include/lfmc/NumericalScheme.hpp | 42 +++++++++++ include/lfmc/PathGenerator.hpp | 41 +++++++++++ include/lfmc/Payoff.hpp | 1 + include/lfmc/Pipeline.hpp | 1 + include/lfmc/RandomSource.hpp | 28 +++++++ include/lfmc/StochasticProcess.hpp | 20 +++++ include/lfmc/archive/NumericalScheme.hpp | 86 ---------------------- include/lfmc/archive/RandomGenerator.hpp | 2 +- include/lfmc/archive/StochasticProcess.hpp | 55 -------------- include/lfmc/archive/types.hpp | 22 ------ include/lfmc/types.hpp | 10 +++ src/CMakeLists.txt | 1 + src/EulerMaruyama.cpp | 17 +++++ src/GeometricBrownianMotion.cpp | 25 +++++++ src/RandomSource.cpp | 21 ++++++ 17 files changed, 210 insertions(+), 164 deletions(-) create mode 100644 include/lfmc/Engine.hpp create mode 100644 include/lfmc/Estimator.hpp create mode 100644 include/lfmc/NumericalScheme.hpp create mode 100644 include/lfmc/PathGenerator.hpp create mode 100644 include/lfmc/Payoff.hpp create mode 100644 include/lfmc/Pipeline.hpp create mode 100644 include/lfmc/RandomSource.hpp create mode 100644 include/lfmc/StochasticProcess.hpp delete mode 100644 include/lfmc/archive/NumericalScheme.hpp delete mode 100644 include/lfmc/archive/StochasticProcess.hpp delete mode 100644 include/lfmc/archive/types.hpp create mode 100644 include/lfmc/types.hpp create mode 100644 src/EulerMaruyama.cpp create mode 100644 src/GeometricBrownianMotion.cpp create mode 100644 src/RandomSource.cpp diff --git a/include/lfmc/Engine.hpp b/include/lfmc/Engine.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/include/lfmc/Engine.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/include/lfmc/Estimator.hpp b/include/lfmc/Estimator.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/include/lfmc/Estimator.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/include/lfmc/NumericalScheme.hpp b/include/lfmc/NumericalScheme.hpp new file mode 100644 index 0000000..40509b5 --- /dev/null +++ b/include/lfmc/NumericalScheme.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +namespace lfmc { + +template +concept NumericalScheme = + requires(S const& s, P const& p, double t, double x, double dt, double z) { + { s.step(p, t, x, dt, z) } -> std::convertible_to; + }; + +/** + * @brief Exact simulation for Geometric Brownian Motion. + * + * Uses the closed-form solution: + * X_T = X_0 * exp((mu - 0.5*sigma^2)*T + sigma*sqrt(T)*Z) + * + * This is faster and more accurate than Euler-Maruyama for GBM. + */ +// class GBMExact { +// public: +// explicit GBMExact(GeometricBrownianMotion gbm) : gbm_(gbm) {} +// +// /** +// * @brief Simulate terminal value using exact solution. +// * @param x0 Initial value. +// * @param T Time to maturity. +// * @param z Standard normal random variable. +// * @return Terminal value X_T. +// */ +// double simulate_terminal(double x0, double T, double z) const noexcept { +// double drift_adjusted = (gbm_.mu - 0.5 * gbm_.sigma * gbm_.sigma) * T; +// double diffusion_term = gbm_.sigma * std::sqrt(T) * z; +// return x0 * std::exp(drift_adjusted + diffusion_term); +// } +// +// private: +// GeometricBrownianMotion gbm_; +// }; + +} // namespace lfmc diff --git a/include/lfmc/PathGenerator.hpp b/include/lfmc/PathGenerator.hpp new file mode 100644 index 0000000..256e55f --- /dev/null +++ b/include/lfmc/PathGenerator.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "lfmc/NumericalScheme.hpp" +#include "lfmc/StochasticProcess.hpp" +#include "lfmc/types.hpp" + +namespace lfmc { + +template + requires NumericalScheme +class PathGenerator { + private: + Process process_; + Scheme scheme_; + double T_; + size_t steps_; + + public: + PathGenerator(Process process, Scheme scheme, double T, size_t steps) + : process_(std::move(process)), scheme_(std::move(scheme)), T_(T), steps_(steps) {} + + Path generate(const Normals& normals) const { + const double dt = T_ / steps_; + + Path path(steps_ + 1); + + double t = 0.0; + double x = process_.initial(); + path.push_back(x); + + for (size_t i = 0; i < steps_; ++i) { + x = scheme_.step(process_, t, x, dt, normals[i]); + path.push_back(x); + t += dt; + } + + return path; + } +}; + +} // namespace lfmc diff --git a/include/lfmc/Payoff.hpp b/include/lfmc/Payoff.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/include/lfmc/Payoff.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/include/lfmc/Pipeline.hpp b/include/lfmc/Pipeline.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/include/lfmc/Pipeline.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/include/lfmc/RandomSource.hpp b/include/lfmc/RandomSource.hpp new file mode 100644 index 0000000..46a58fd --- /dev/null +++ b/include/lfmc/RandomSource.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "lfmc/types.hpp" + +#include + +namespace lfmc { + +class RandomSource { + public: + virtual ~RandomSource() = default; + virtual Normals generate(size_t n) = 0; +}; + +class PseudoRandomSource : public RandomSource { + private: + std::mt19937 rng_; + std::normal_distribution dist_; + + public: + PseudoRandomSource(unsigned seed = std::random_device{}()); + + Normals generate(size_t n) override; + + void seed(unsigned seed); +}; + +} // namespace lfmc diff --git a/include/lfmc/StochasticProcess.hpp b/include/lfmc/StochasticProcess.hpp new file mode 100644 index 0000000..a33e3ff --- /dev/null +++ b/include/lfmc/StochasticProcess.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +/** + * @file StochasticProcess.hpp + * @brief Defines the StochasticProcess concept for stochastic differential equations (SDEs) and + * provides some implementations. + */ + +namespace lfmc { + +template +concept StochasticProcess = requires(P const& p, double t, double x) { + { p.initial() } -> std::convertible_to; + { p.drift(t, x) } -> std::convertible_to; + { p.diffusion(t, x) } -> std::convertible_to; +}; + +} // namespace lfmc diff --git a/include/lfmc/archive/NumericalScheme.hpp b/include/lfmc/archive/NumericalScheme.hpp deleted file mode 100644 index ac9614b..0000000 --- a/include/lfmc/archive/NumericalScheme.hpp +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -#include "StochasticProcess.hpp" - -#include -#include - -/** - * @file NumericalScheme.hpp - * @brief Defines the NumericalScheme concept for numerical methods solving SDEs and - * provides some implementations. - */ - -namespace lfmc { - -/** - * @brief Concept for numerical schemes solving SDEs. - * - * A NumericalScheme must implement a step function that computes the next state - * given the current state, time step, and a standard normal random variable. - * - * @tparam S Numerical scheme type. - * @tparam P Stochastic process type. - * - * Requires: - * - S must have a method: - * double step(const P& process, double x_current, double dt, double z) const noexcept; - * where: - * - process: Stochastic process defining drift and diffusion. - * - x_current: Current state. - * - dt: Time step size. - * - z: Standard normal random variable N(0,1). - * The method returns the next state X_{t+dt}. - */ -template -concept NumericalScheme = - StochasticProcess

&& requires(S const& s, P const& p, double x, double dt, double z) { - { s.step(p, x, dt, z) } -> std::same_as; - }; - -template struct EulerMaruyama { - /** - * @brief Compute the next state using Euler-Maruyama. - * @param Stochastic process defining drift and diffusion. - * @param x_current Current state. - * @param dt Time step size. - * @param z Standard normal random variable N(0,1). - * @return Next state X_{t+dt}. - */ - double step(const P& process, double x_current, double dt, double z) const noexcept { - double drift = process.drift(x_current); - double diffusion = process.diffusion(x_current); - return x_current + drift * dt + diffusion * std::sqrt(dt) * z; - } -}; - -/** - * @brief Exact simulation for Geometric Brownian Motion. - * - * Uses the closed-form solution: - * X_T = X_0 * exp((mu - 0.5*sigma^2)*T + sigma*sqrt(T)*Z) - * - * This is faster and more accurate than Euler-Maruyama for GBM. - */ -// class GBMExact { -// public: -// explicit GBMExact(GeometricBrownianMotion gbm) : gbm_(gbm) {} -// -// /** -// * @brief Simulate terminal value using exact solution. -// * @param x0 Initial value. -// * @param T Time to maturity. -// * @param z Standard normal random variable. -// * @return Terminal value X_T. -// */ -// double simulate_terminal(double x0, double T, double z) const noexcept { -// double drift_adjusted = (gbm_.mu - 0.5 * gbm_.sigma * gbm_.sigma) * T; -// double diffusion_term = gbm_.sigma * std::sqrt(T) * z; -// return x0 * std::exp(drift_adjusted + diffusion_term); -// } -// -// private: -// GeometricBrownianMotion gbm_; -// }; - -} // namespace lfmc diff --git a/include/lfmc/archive/RandomGenerator.hpp b/include/lfmc/archive/RandomGenerator.hpp index eae17d9..5a20f25 100644 --- a/include/lfmc/archive/RandomGenerator.hpp +++ b/include/lfmc/archive/RandomGenerator.hpp @@ -1,6 +1,6 @@ #pragma once -#include "types.hpp" +#include "lfmc/types.hpp" #include diff --git a/include/lfmc/archive/StochasticProcess.hpp b/include/lfmc/archive/StochasticProcess.hpp deleted file mode 100644 index 8c0acb1..0000000 --- a/include/lfmc/archive/StochasticProcess.hpp +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -#include -#include - -/** - * @file StochasticProcess.hpp - * @brief Defines the StochasticProcess concept for stochastic differential equations (SDEs) and - * provides some implementations. - */ - -namespace lfmc { - -template -concept StochasticProcess = requires(P const& p, double x) { - { p.drift(x) } -> std::same_as; - { p.diffusion(x) } -> std::same_as; -}; - -struct GeometricBrownianMotion { - double mu; - double sigma; - - /** - * @brief Constructor to initialize GBM parameters. - * @param driftCoefficient The drift coefficient (mu). - * @param diffusionCoefficient The diffusion coefficient (sigma/volatility). - */ - GeometricBrownianMotion(double driftCoefficient, double diffusionCoefficient) - : mu(driftCoefficient), sigma(diffusionCoefficient) { - if (sigma < 0.0) { - throw std::invalid_argument("Diffusion coefficient (sigma) must be non-negative"); - } - } - - /** - * @brief Compute drift term at state x. - * @param x The current state variable. - * @return The drift term mu * x. - */ - double drift(double x) const noexcept { - return mu * x; - } - - /** - * @brief Compute diffusion term at state x. - * @param x The current state variable. - * @return The diffusion term sigma * x. - */ - double diffusion(double x) const noexcept { - return sigma * x; - } -}; - -} // namespace lfmc diff --git a/include/lfmc/archive/types.hpp b/include/lfmc/archive/types.hpp deleted file mode 100644 index 2aba43b..0000000 --- a/include/lfmc/archive/types.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -namespace lfmc { - -struct State { - double initialValue; - double timeToMaturity; - size_t steps; -}; - -using Path = std::vector; -using Normals = std::vector; - -struct ManagerConfig { - // TODO temp - size_t numNoVarianceReductionSimulations; - size_t numAntitheticVariatesSimulations; -}; - -} // namespace lfmc diff --git a/include/lfmc/types.hpp b/include/lfmc/types.hpp new file mode 100644 index 0000000..df4c691 --- /dev/null +++ b/include/lfmc/types.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace lfmc { + +using Path = std::vector; +using Normals = std::vector; + +} // namespace lfmc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ea86e7a..c62486d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,7 @@ # Source files for the lfmc library set(LFMC_SOURCES timing.cpp + RandomSource.hpp ) # Create library diff --git a/src/EulerMaruyama.cpp b/src/EulerMaruyama.cpp new file mode 100644 index 0000000..1efba95 --- /dev/null +++ b/src/EulerMaruyama.cpp @@ -0,0 +1,17 @@ +#include "lfmc/StochasticProcess.hpp" + +#include + +namespace lfmc { + +class EulerMaruyama { + public: + template + double step(P const& process, double t, double x, double dt, double z) const noexcept { + double drift = process.drift(t, x); + double diffusion = process.diffusion(t, x); + return x + drift * dt + diffusion * std::sqrt(dt) * z; + } +}; + +} // namespace lfmc diff --git a/src/GeometricBrownianMotion.cpp b/src/GeometricBrownianMotion.cpp new file mode 100644 index 0000000..ec575fe --- /dev/null +++ b/src/GeometricBrownianMotion.cpp @@ -0,0 +1,25 @@ +namespace lfmc { + +class GeometricBrownianMotion { + private: + double mu_; + double sigma_; + double x0_; + + public: + GeometricBrownianMotion(double mu, double sigma, double x0) : mu_(mu), sigma_(sigma), x0_(x0) {} + + double initial() const noexcept { + return x0_; + } + + double drift(double, double x) const noexcept { + return mu_ * x; + } + + double diffusion(double, double x) const noexcept { + return sigma_ * x; + } +}; + +} // namespace lfmc diff --git a/src/RandomSource.cpp b/src/RandomSource.cpp new file mode 100644 index 0000000..6f021e4 --- /dev/null +++ b/src/RandomSource.cpp @@ -0,0 +1,21 @@ +#include "lfmc/RandomSource.hpp" + +#include "lfmc/types.hpp" + +namespace lfmc { + +PseudoRandomSource::PseudoRandomSource(unsigned seed) : rng_(seed), dist_(0.0, 1.0) {} + +Normals PseudoRandomSource::generate(size_t n) { + Normals normals(n); + for (size_t i = 0; i < n; ++i) { + normals[i] = dist_(rng_); + } + return normals; +} + +void PseudoRandomSource::seed(unsigned seed) { + rng_.seed(seed); +} + +} // namespace lfmc From 426f56e0e1714858596d3367a499199777e9e75b Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Mon, 23 Feb 2026 23:21:43 -0500 Subject: [PATCH 03/19] feat: estimator and payoff outline --- include/lfmc/Estimator.hpp | 13 +++++++++++++ include/lfmc/Payoff.hpp | 17 +++++++++++++++++ include/lfmc/Pipeline.hpp | 5 +++++ 3 files changed, 35 insertions(+) diff --git a/include/lfmc/Estimator.hpp b/include/lfmc/Estimator.hpp index 6f70f09..5ebb939 100644 --- a/include/lfmc/Estimator.hpp +++ b/include/lfmc/Estimator.hpp @@ -1 +1,14 @@ #pragma once + +namespace lfmc { + +class Estimator { + public: + virtual void add(double x) = 0; + // virtual bool converged() const = 0; + // virtual Result result() const = 0; + // virtual void merge(Estimator const& other) = 0; + virtual ~Estimator() = default; +}; + +} // namespace lfmc diff --git a/include/lfmc/Payoff.hpp b/include/lfmc/Payoff.hpp index 6f70f09..f9ebb72 100644 --- a/include/lfmc/Payoff.hpp +++ b/include/lfmc/Payoff.hpp @@ -1 +1,18 @@ #pragma once + +#include +#include + +namespace lfmc { + +template +concept TerminalPayoff = requires(P const& p, double terminal) { + { p(terminal) } -> std::convertible_to; +}; + +template +concept PathPayoff = requires(P const& p, std::vector const& path) { + { p(path) } -> std::convertible_to; +}; + +} // namespace lfmc diff --git a/include/lfmc/Pipeline.hpp b/include/lfmc/Pipeline.hpp index 6f70f09..9e42e2c 100644 --- a/include/lfmc/Pipeline.hpp +++ b/include/lfmc/Pipeline.hpp @@ -1 +1,6 @@ #pragma once + +#include "lfmc/PathGenerator.hpp" +#include "lfmc/Payoff.hpp" +#include "lfmc/Pipeline.hpp" +#include "lfmc/RandomSource.hpp" From b80146752943731f3d23be1c1117105fdaab501b Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Tue, 24 Feb 2026 12:49:04 -0500 Subject: [PATCH 04/19] feat: payoffs and estimator --- include/lfmc/Estimator.hpp | 5 ++++- src/MonteCarloEstimator.cpp | 36 ++++++++++++++++++++++++++++++++++++ src/Payoff.cpp | 21 +++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/MonteCarloEstimator.cpp create mode 100644 src/Payoff.cpp diff --git a/include/lfmc/Estimator.hpp b/include/lfmc/Estimator.hpp index 5ebb939..9a67508 100644 --- a/include/lfmc/Estimator.hpp +++ b/include/lfmc/Estimator.hpp @@ -1,12 +1,15 @@ #pragma once +#include +#include + namespace lfmc { class Estimator { public: virtual void add(double x) = 0; // virtual bool converged() const = 0; - // virtual Result result() const = 0; + virtual std::expected result() const = 0; // virtual void merge(Estimator const& other) = 0; virtual ~Estimator() = default; }; diff --git a/src/MonteCarloEstimator.cpp b/src/MonteCarloEstimator.cpp new file mode 100644 index 0000000..234d9f9 --- /dev/null +++ b/src/MonteCarloEstimator.cpp @@ -0,0 +1,36 @@ +#include "lfmc/Estimator.hpp" + +#include + +namespace lfmc { + +class MonteCarloEstimator : public Estimator { + private: + double sum = 0.0; + std::size_t count = 0; + + public: + void add(double x) override { + sum += x; + ++count; + } + + // bool converged() const override { + // return count >= 10000; // Placeholder convergence criterion + // } + + std::expected result() const override { + if (count == 0) { + return std::unexpected{"No samples added"}; + } + return {sum / static_cast(count)}; + } + + // void merge(Estimator const& other) override { + // auto const& mcOther = dynamic_cast(other); + // sum += mcOther.sum; + // count += mcOther.count; + // } +}; + +} // namespace lfmc diff --git a/src/Payoff.cpp b/src/Payoff.cpp new file mode 100644 index 0000000..8838519 --- /dev/null +++ b/src/Payoff.cpp @@ -0,0 +1,21 @@ +#include "lfmc/Payoff.hpp" + +namespace lfmc { + +struct EuropeanCall { + double strike; + + double operator()(double terminal) const { + return std::max(terminal - strike, 0.0); + } +}; + +struct EuropeanPut { + double strike; + + double operator()(double terminal) const { + return std::max(strike - terminal, 0.0); + } +}; + +} // namespace lfmc From b22c6d98d2a993300892c35a05d6268a68c2c714 Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Tue, 24 Feb 2026 14:21:13 -0500 Subject: [PATCH 05/19] refactor: renamed everything, finished pipeline, and converted each step to return vectors of results --- include/lfmc/Estimator.hpp | 17 ------- include/lfmc/PathGenerator.hpp | 41 ----------------- include/lfmc/Payoff.hpp | 18 -------- include/lfmc/Pipeline.hpp | 6 --- include/lfmc/{Engine.hpp => engine.hpp} | 0 include/lfmc/estimator.hpp | 31 +++++++++++++ ...mericalScheme.hpp => numerical_scheme.hpp} | 0 include/lfmc/path_generator.hpp | 44 +++++++++++++++++++ include/lfmc/payoff.hpp | 33 ++++++++++++++ include/lfmc/pipeline.hpp | 43 ++++++++++++++++++ .../{RandomSource.hpp => random_source.hpp} | 5 ++- ...sticProcess.hpp => stochastic_process.hpp} | 14 ++++++ include/lfmc/types.hpp | 1 + src/CMakeLists.txt | 7 ++- src/GeometricBrownianMotion.cpp | 25 ----------- src/MonteCarloEstimator.cpp | 36 --------------- src/Payoff.cpp | 21 --------- src/RandomSource.cpp | 21 --------- src/{EulerMaruyama.cpp => euler_maruyama.cpp} | 5 +-- src/european_payoffs.cpp | 29 ++++++++++++ src/geometric_brownian_motion.cpp | 20 +++++++++ src/monte_carlo_estimator.cpp | 40 +++++++++++++++++ src/pseudo_random_source.cpp | 24 ++++++++++ 23 files changed, 289 insertions(+), 192 deletions(-) delete mode 100644 include/lfmc/Estimator.hpp delete mode 100644 include/lfmc/PathGenerator.hpp delete mode 100644 include/lfmc/Payoff.hpp delete mode 100644 include/lfmc/Pipeline.hpp rename include/lfmc/{Engine.hpp => engine.hpp} (100%) create mode 100644 include/lfmc/estimator.hpp rename include/lfmc/{NumericalScheme.hpp => numerical_scheme.hpp} (100%) create mode 100644 include/lfmc/path_generator.hpp create mode 100644 include/lfmc/payoff.hpp create mode 100644 include/lfmc/pipeline.hpp rename include/lfmc/{RandomSource.hpp => random_source.hpp} (70%) rename include/lfmc/{StochasticProcess.hpp => stochastic_process.hpp} (61%) delete mode 100644 src/GeometricBrownianMotion.cpp delete mode 100644 src/MonteCarloEstimator.cpp delete mode 100644 src/Payoff.cpp delete mode 100644 src/RandomSource.cpp rename src/{EulerMaruyama.cpp => euler_maruyama.cpp} (73%) create mode 100644 src/european_payoffs.cpp create mode 100644 src/geometric_brownian_motion.cpp create mode 100644 src/monte_carlo_estimator.cpp create mode 100644 src/pseudo_random_source.cpp diff --git a/include/lfmc/Estimator.hpp b/include/lfmc/Estimator.hpp deleted file mode 100644 index 9a67508..0000000 --- a/include/lfmc/Estimator.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include -#include - -namespace lfmc { - -class Estimator { - public: - virtual void add(double x) = 0; - // virtual bool converged() const = 0; - virtual std::expected result() const = 0; - // virtual void merge(Estimator const& other) = 0; - virtual ~Estimator() = default; -}; - -} // namespace lfmc diff --git a/include/lfmc/PathGenerator.hpp b/include/lfmc/PathGenerator.hpp deleted file mode 100644 index 256e55f..0000000 --- a/include/lfmc/PathGenerator.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include "lfmc/NumericalScheme.hpp" -#include "lfmc/StochasticProcess.hpp" -#include "lfmc/types.hpp" - -namespace lfmc { - -template - requires NumericalScheme -class PathGenerator { - private: - Process process_; - Scheme scheme_; - double T_; - size_t steps_; - - public: - PathGenerator(Process process, Scheme scheme, double T, size_t steps) - : process_(std::move(process)), scheme_(std::move(scheme)), T_(T), steps_(steps) {} - - Path generate(const Normals& normals) const { - const double dt = T_ / steps_; - - Path path(steps_ + 1); - - double t = 0.0; - double x = process_.initial(); - path.push_back(x); - - for (size_t i = 0; i < steps_; ++i) { - x = scheme_.step(process_, t, x, dt, normals[i]); - path.push_back(x); - t += dt; - } - - return path; - } -}; - -} // namespace lfmc diff --git a/include/lfmc/Payoff.hpp b/include/lfmc/Payoff.hpp deleted file mode 100644 index f9ebb72..0000000 --- a/include/lfmc/Payoff.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include -#include - -namespace lfmc { - -template -concept TerminalPayoff = requires(P const& p, double terminal) { - { p(terminal) } -> std::convertible_to; -}; - -template -concept PathPayoff = requires(P const& p, std::vector const& path) { - { p(path) } -> std::convertible_to; -}; - -} // namespace lfmc diff --git a/include/lfmc/Pipeline.hpp b/include/lfmc/Pipeline.hpp deleted file mode 100644 index 9e42e2c..0000000 --- a/include/lfmc/Pipeline.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include "lfmc/PathGenerator.hpp" -#include "lfmc/Payoff.hpp" -#include "lfmc/Pipeline.hpp" -#include "lfmc/RandomSource.hpp" diff --git a/include/lfmc/Engine.hpp b/include/lfmc/engine.hpp similarity index 100% rename from include/lfmc/Engine.hpp rename to include/lfmc/engine.hpp diff --git a/include/lfmc/estimator.hpp b/include/lfmc/estimator.hpp new file mode 100644 index 0000000..33128dc --- /dev/null +++ b/include/lfmc/estimator.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "lfmc/types.hpp" + +#include +#include + +namespace lfmc { + +class Estimator { + public: + virtual std::expected add_payoffs(const Payoffs& payoffs) = 0; + virtual bool converged() const = 0; + virtual std::expected result() const = 0; + // virtual void merge(Estimator const& other) = 0; + virtual ~Estimator() = default; +}; + +class MonteCarloEstimator : public Estimator { + private: + double sum = 0.0; + std::size_t count = 0; + + public: + std::expected add_payoffs(const Payoffs& payoffs) override; + bool converged() const override; + std::expected result() const override; + // void merge(Estimator const& other) override; +}; + +} // namespace lfmc diff --git a/include/lfmc/NumericalScheme.hpp b/include/lfmc/numerical_scheme.hpp similarity index 100% rename from include/lfmc/NumericalScheme.hpp rename to include/lfmc/numerical_scheme.hpp diff --git a/include/lfmc/path_generator.hpp b/include/lfmc/path_generator.hpp new file mode 100644 index 0000000..b09918f --- /dev/null +++ b/include/lfmc/path_generator.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "lfmc/numerical_scheme.hpp" +#include "lfmc/stochastic_process.hpp" +#include "lfmc/types.hpp" + +namespace lfmc { + +template + requires NumericalScheme +class PathGenerator { + private: + Process process_; + Scheme scheme_; + + public: + PathGenerator(Process process, Scheme scheme) + : process_(std::move(process)), scheme_(std::move(scheme)) {} + + // TODO move to cpp file + std::vector generate_paths(const std::vector& normals, size_t steps, + double T) const { + const double dt = T / static_cast(steps); + + std::vector paths; + for (const auto& norm : normals) { + Path path(steps + 1); + + double t = 0.0; + double x = process_.initial(); + path.push_back(x); + + for (size_t i = 0; i < steps; ++i) { + x = scheme_.step(process_, t, x, dt, norm[i]); + path.push_back(x); + t += dt; + } + } + + return paths; + } +}; + +} // namespace lfmc diff --git a/include/lfmc/payoff.hpp b/include/lfmc/payoff.hpp new file mode 100644 index 0000000..d4627d5 --- /dev/null +++ b/include/lfmc/payoff.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "lfmc/types.hpp" + +namespace lfmc { + +class Payoff { + public: + virtual Payoffs generate_payoffs(const std::vector& paths) const = 0; + virtual ~Payoff() = default; +}; + +class EuropeanCall : public Payoff { + private: + double strike_; + + public: + explicit EuropeanCall(double strike); + + Payoffs generate_payoffs(const std::vector& paths) const override; +}; + +class EuropeanPut : public Payoff { + private: + double strike_; + + public: + explicit EuropeanPut(double strike); + + Payoffs generate_payoffs(const std::vector& paths) const override; +}; + +} // namespace lfmc diff --git a/include/lfmc/pipeline.hpp b/include/lfmc/pipeline.hpp new file mode 100644 index 0000000..949e50e --- /dev/null +++ b/include/lfmc/pipeline.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "lfmc/estimator.hpp" +#include "lfmc/path_generator.hpp" +#include "lfmc/payoff.hpp" +#include "lfmc/random_source.hpp" + +#include +#include + +namespace lfmc { + +template NS> class Pipeline { + private: + std::unique_ptr random_source_; + std::unique_ptr> path_generator_; + std::unique_ptr payoff_; + std::unique_ptr estimator_; + + public: + Pipeline(std::unique_ptr random_source, + std::unique_ptr> path_generator, std::unique_ptr payoff, + std::unique_ptr estimator) + : random_source_(std::move(random_source)), path_generator_(std::move(path_generator)), + payoff_(std::move(payoff)), estimator_(std::move(estimator)) {} + + std::expected run(size_t steps, double T) { + while (!estimator_->converged()) { + auto normals = random_source_->generate_normals(steps); + auto paths = path_generator_->generate_paths(normals, steps, T); + auto payoffs = payoff_->generate_payoffs(paths); + auto result = estimator_->add_payoffs(payoffs); + + if (!result) { + return std::unexpected(result.error()); + } + } + + return estimator_->result(); + } +}; + +} // namespace lfmc diff --git a/include/lfmc/RandomSource.hpp b/include/lfmc/random_source.hpp similarity index 70% rename from include/lfmc/RandomSource.hpp rename to include/lfmc/random_source.hpp index 46a58fd..2b7fe2f 100644 --- a/include/lfmc/RandomSource.hpp +++ b/include/lfmc/random_source.hpp @@ -3,13 +3,14 @@ #include "lfmc/types.hpp" #include +#include namespace lfmc { class RandomSource { public: virtual ~RandomSource() = default; - virtual Normals generate(size_t n) = 0; + virtual std::vector generate_normals(size_t steps, size_t n = 1) = 0; }; class PseudoRandomSource : public RandomSource { @@ -20,7 +21,7 @@ class PseudoRandomSource : public RandomSource { public: PseudoRandomSource(unsigned seed = std::random_device{}()); - Normals generate(size_t n) override; + std::vector generate_normals(size_t steps, size_t) override; void seed(unsigned seed); }; diff --git a/include/lfmc/StochasticProcess.hpp b/include/lfmc/stochastic_process.hpp similarity index 61% rename from include/lfmc/StochasticProcess.hpp rename to include/lfmc/stochastic_process.hpp index a33e3ff..1d6271e 100644 --- a/include/lfmc/StochasticProcess.hpp +++ b/include/lfmc/stochastic_process.hpp @@ -17,4 +17,18 @@ concept StochasticProcess = requires(P const& p, double t, double x) { { p.diffusion(t, x) } -> std::convertible_to; }; +class GeometricBrownianMotion { + private: + double mu_; + double sigma_; + double x0_; + + public: + GeometricBrownianMotion(double mu, double sigma, double x0); + + double initial() const noexcept; + double drift(double, double x) const noexcept; + double diffusion(double, double x) const noexcept; +}; + } // namespace lfmc diff --git a/include/lfmc/types.hpp b/include/lfmc/types.hpp index df4c691..ebac5e6 100644 --- a/include/lfmc/types.hpp +++ b/include/lfmc/types.hpp @@ -6,5 +6,6 @@ namespace lfmc { using Path = std::vector; using Normals = std::vector; +using Payoffs = std::vector; } // namespace lfmc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c62486d..f38176f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,10 @@ # Source files for the lfmc library set(LFMC_SOURCES - timing.cpp - RandomSource.hpp + euler_maruyama.cpp + european_payoffs.cpp + geometric_brownian_motion.cpp + monte_carlo_estimator.cpp + pseudo_random_source.cpp ) # Create library diff --git a/src/GeometricBrownianMotion.cpp b/src/GeometricBrownianMotion.cpp deleted file mode 100644 index ec575fe..0000000 --- a/src/GeometricBrownianMotion.cpp +++ /dev/null @@ -1,25 +0,0 @@ -namespace lfmc { - -class GeometricBrownianMotion { - private: - double mu_; - double sigma_; - double x0_; - - public: - GeometricBrownianMotion(double mu, double sigma, double x0) : mu_(mu), sigma_(sigma), x0_(x0) {} - - double initial() const noexcept { - return x0_; - } - - double drift(double, double x) const noexcept { - return mu_ * x; - } - - double diffusion(double, double x) const noexcept { - return sigma_ * x; - } -}; - -} // namespace lfmc diff --git a/src/MonteCarloEstimator.cpp b/src/MonteCarloEstimator.cpp deleted file mode 100644 index 234d9f9..0000000 --- a/src/MonteCarloEstimator.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "lfmc/Estimator.hpp" - -#include - -namespace lfmc { - -class MonteCarloEstimator : public Estimator { - private: - double sum = 0.0; - std::size_t count = 0; - - public: - void add(double x) override { - sum += x; - ++count; - } - - // bool converged() const override { - // return count >= 10000; // Placeholder convergence criterion - // } - - std::expected result() const override { - if (count == 0) { - return std::unexpected{"No samples added"}; - } - return {sum / static_cast(count)}; - } - - // void merge(Estimator const& other) override { - // auto const& mcOther = dynamic_cast(other); - // sum += mcOther.sum; - // count += mcOther.count; - // } -}; - -} // namespace lfmc diff --git a/src/Payoff.cpp b/src/Payoff.cpp deleted file mode 100644 index 8838519..0000000 --- a/src/Payoff.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "lfmc/Payoff.hpp" - -namespace lfmc { - -struct EuropeanCall { - double strike; - - double operator()(double terminal) const { - return std::max(terminal - strike, 0.0); - } -}; - -struct EuropeanPut { - double strike; - - double operator()(double terminal) const { - return std::max(strike - terminal, 0.0); - } -}; - -} // namespace lfmc diff --git a/src/RandomSource.cpp b/src/RandomSource.cpp deleted file mode 100644 index 6f021e4..0000000 --- a/src/RandomSource.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "lfmc/RandomSource.hpp" - -#include "lfmc/types.hpp" - -namespace lfmc { - -PseudoRandomSource::PseudoRandomSource(unsigned seed) : rng_(seed), dist_(0.0, 1.0) {} - -Normals PseudoRandomSource::generate(size_t n) { - Normals normals(n); - for (size_t i = 0; i < n; ++i) { - normals[i] = dist_(rng_); - } - return normals; -} - -void PseudoRandomSource::seed(unsigned seed) { - rng_.seed(seed); -} - -} // namespace lfmc diff --git a/src/EulerMaruyama.cpp b/src/euler_maruyama.cpp similarity index 73% rename from src/EulerMaruyama.cpp rename to src/euler_maruyama.cpp index 1efba95..2048ae3 100644 --- a/src/EulerMaruyama.cpp +++ b/src/euler_maruyama.cpp @@ -1,12 +1,11 @@ -#include "lfmc/StochasticProcess.hpp" +#include "lfmc/stochastic_process.hpp" #include namespace lfmc { -class EulerMaruyama { +template class EulerMaruyama { public: - template double step(P const& process, double t, double x, double dt, double z) const noexcept { double drift = process.drift(t, x); double diffusion = process.diffusion(t, x); diff --git a/src/european_payoffs.cpp b/src/european_payoffs.cpp new file mode 100644 index 0000000..cc2c1c3 --- /dev/null +++ b/src/european_payoffs.cpp @@ -0,0 +1,29 @@ +#include "lfmc/payoff.hpp" + +#include + +namespace lfmc { + +EuropeanCall::EuropeanCall(double strike) : strike_(strike) {} + +Payoffs EuropeanCall::generate_payoffs(const std::vector& paths) const { + Payoffs payoffs; + for (const auto& path : paths) { + double final_price = path.back(); + payoffs.push_back(std::max(final_price - strike_, 0.0)); + } + return payoffs; +} + +EuropeanPut::EuropeanPut(double strike) : strike_(strike) {} + +Payoffs EuropeanPut::generate_payoffs(const std::vector& paths) const { + Payoffs payoffs; + for (const auto& path : paths) { + double final_price = path.back(); + payoffs.push_back(std::max(strike_ - final_price, 0.0)); + } + return payoffs; +} + +} // namespace lfmc diff --git a/src/geometric_brownian_motion.cpp b/src/geometric_brownian_motion.cpp new file mode 100644 index 0000000..98b196b --- /dev/null +++ b/src/geometric_brownian_motion.cpp @@ -0,0 +1,20 @@ +#include "lfmc/stochastic_process.hpp" + +namespace lfmc { + +GeometricBrownianMotion::GeometricBrownianMotion(double mu, double sigma, double x0) + : mu_(mu), sigma_(sigma), x0_(x0) {} + +double GeometricBrownianMotion::initial() const noexcept { + return x0_; +} + +double GeometricBrownianMotion::drift(double, double x) const noexcept { + return mu_ * x; +} + +double GeometricBrownianMotion::diffusion(double, double x) const noexcept { + return sigma_ * x; +} + +} // namespace lfmc diff --git a/src/monte_carlo_estimator.cpp b/src/monte_carlo_estimator.cpp new file mode 100644 index 0000000..092820f --- /dev/null +++ b/src/monte_carlo_estimator.cpp @@ -0,0 +1,40 @@ +#include "lfmc/estimator.hpp" +#include "lfmc/types.hpp" + +namespace lfmc { + +std::expected MonteCarloEstimator::add_payoffs(const Payoffs& payoffs) { + if (payoffs.empty()) { + return std::unexpected("No payoffs provided to add to the estimator."); + } + + for (double payoff : payoffs) { + sum += payoff; + ++count; + } + + return {}; +} + +bool MonteCarloEstimator::converged() const { + return count >= 10000; // Placeholder convergence criterion +} + +std::expected MonteCarloEstimator::result() const { + if (count <= 0) { + return std::unexpected("No samples added to the estimator."); + } + // if (!self.converged()) { + // return std::unexpected("Estimator has not yet converged."); + // } + + return {sum / static_cast(count)}; +} + +// void MonteCarloEstimator::merge(Estimator const& other) { +// auto const& mcOther = dynamic_cast(other); +// sum += mcOther.sum; +// count += mcOther.count; +// } + +} // namespace lfmc diff --git a/src/pseudo_random_source.cpp b/src/pseudo_random_source.cpp new file mode 100644 index 0000000..64ddcce --- /dev/null +++ b/src/pseudo_random_source.cpp @@ -0,0 +1,24 @@ +#include "lfmc/random_source.hpp" +#include "lfmc/types.hpp" + +namespace lfmc { + +PseudoRandomSource::PseudoRandomSource(unsigned seed) : rng_(seed), dist_(0.0, 1.0) {} + +std::vector PseudoRandomSource::generate_normals(size_t steps, size_t samples) { + std::vector result(samples, Normals(steps)); + for (size_t i = 0; i < samples; ++i) { + Normals normals(steps); + for (size_t j = 0; j < steps; ++j) { + normals[j] = dist_(rng_); + } + result[i] = std::move(normals); + } + return result; +} + +void PseudoRandomSource::seed(unsigned seed) { + rng_.seed(seed); +} + +} // namespace lfmc From eefa7a710e051d447b6b0adbbfdef7bab8eebc3d Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Tue, 3 Mar 2026 01:05:44 -0500 Subject: [PATCH 06/19] feat: finished refactor and added control variates --- include/lfmc/estimator.hpp | 27 +++++++- include/lfmc/numerical_scheme.hpp | 12 ++++ include/lfmc/path_generator.hpp | 1 + include/lfmc/payoff.hpp | 29 ++++++++- include/lfmc/pipeline.hpp | 16 ++++- include/lfmc/random_source.hpp | 14 ++++ include/lfmc/stochastic_process.hpp | 3 + src/CMakeLists.txt | 5 +- src/antithetic_random_source.cpp | 25 ++++++++ src/control_variate_estimator.cpp | 53 +++++++++++++++ src/control_variate_payoffs.cpp | 38 +++++++++++ src/euler_maruyama.cpp | 16 ----- src/european_payoffs.cpp | 12 ++-- src/geometric_brownian_motion.cpp | 8 +++ src/monte_carlo_estimator.cpp | 15 +++-- tests/CMakeLists.txt | 2 +- tests/test_manager.cpp | 99 ----------------------------- tests/test_pipeline.cpp | 88 +++++++++++++++++++++++++ tests/test_process.cpp | 66 ++++++++++--------- tests/test_scheme.cpp | 74 +++++++++++---------- 20 files changed, 406 insertions(+), 197 deletions(-) create mode 100644 src/antithetic_random_source.cpp create mode 100644 src/control_variate_estimator.cpp create mode 100644 src/control_variate_payoffs.cpp delete mode 100644 src/euler_maruyama.cpp delete mode 100644 tests/test_manager.cpp create mode 100644 tests/test_pipeline.cpp diff --git a/include/lfmc/estimator.hpp b/include/lfmc/estimator.hpp index 33128dc..cf056f9 100644 --- a/include/lfmc/estimator.hpp +++ b/include/lfmc/estimator.hpp @@ -4,12 +4,13 @@ #include #include +#include namespace lfmc { class Estimator { public: - virtual std::expected add_payoffs(const Payoffs& payoffs) = 0; + virtual std::expected add_payoffs(const std::vector& payoffs) = 0; virtual bool converged() const = 0; virtual std::expected result() const = 0; // virtual void merge(Estimator const& other) = 0; @@ -22,7 +23,29 @@ class MonteCarloEstimator : public Estimator { std::size_t count = 0; public: - std::expected add_payoffs(const Payoffs& payoffs) override; + std::expected add_payoffs(const std::vector& payoffs) override; + bool converged() const override; + std::expected result() const override; + // void merge(Estimator const& other) override; +}; + +// TODO if control is analytically known, can put it at payoff level +class ControlVariateEstimator : public Estimator { + private: + std::size_t count = 0; + + double sum_x = 0.0; // Sum of original payoffs + double sum_y = 0.0; // Sum of control variate values + double sum_xy = 0.0; // Sum of products of payoffs and control variate + double sum_yy = 0.0; // Sum of squares of control variate values + + double control_expectation_ = 0.0; // Expected value of control variate (known analytically) + + public: + explicit ControlVariateEstimator(double control_expectation) + : control_expectation_(control_expectation) {} + + std::expected add_payoffs(const std::vector& payoffs) override; bool converged() const override; std::expected result() const override; // void merge(Estimator const& other) override; diff --git a/include/lfmc/numerical_scheme.hpp b/include/lfmc/numerical_scheme.hpp index 40509b5..5302502 100644 --- a/include/lfmc/numerical_scheme.hpp +++ b/include/lfmc/numerical_scheme.hpp @@ -1,5 +1,8 @@ #pragma once +#include "lfmc/stochastic_process.hpp" + +#include #include namespace lfmc { @@ -10,6 +13,15 @@ concept NumericalScheme = { s.step(p, t, x, dt, z) } -> std::convertible_to; }; +template class EulerMaruyama { + public: + double step(P const& process, double t, double x, double dt, double z) const noexcept { + double drift = process.drift(t, x); + double diffusion = process.diffusion(t, x); + return x + drift * dt + diffusion * std::sqrt(dt) * z; + } +}; + /** * @brief Exact simulation for Geometric Brownian Motion. * diff --git a/include/lfmc/path_generator.hpp b/include/lfmc/path_generator.hpp index b09918f..1ec5e92 100644 --- a/include/lfmc/path_generator.hpp +++ b/include/lfmc/path_generator.hpp @@ -35,6 +35,7 @@ class PathGenerator { path.push_back(x); t += dt; } + paths.push_back(std::move(path)); } return paths; diff --git a/include/lfmc/payoff.hpp b/include/lfmc/payoff.hpp index d4627d5..872ea17 100644 --- a/include/lfmc/payoff.hpp +++ b/include/lfmc/payoff.hpp @@ -2,11 +2,16 @@ #include "lfmc/types.hpp" +#include +#include +#include + namespace lfmc { class Payoff { public: - virtual Payoffs generate_payoffs(const std::vector& paths) const = 0; + virtual std::expected, std::string> + generate_payoffs(const std::vector& paths) const = 0; virtual ~Payoff() = default; }; @@ -17,7 +22,8 @@ class EuropeanCall : public Payoff { public: explicit EuropeanCall(double strike); - Payoffs generate_payoffs(const std::vector& paths) const override; + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override; }; class EuropeanPut : public Payoff { @@ -27,7 +33,24 @@ class EuropeanPut : public Payoff { public: explicit EuropeanPut(double strike); - Payoffs generate_payoffs(const std::vector& paths) const override; + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override; +}; + +// TODO subpar in terms of architecture because we have both control variate payoff and estimator, +// but for now it's simpler to implement it this way - can enforce with a factory. Can refactor +// later if needed. +class ControlVariatePayoff : public Payoff { + private: + std::unique_ptr target_payoff_; + std::unique_ptr control_payoff_; + + public: + explicit ControlVariatePayoff(std::unique_ptr target_payoff, + std::unique_ptr control_payoff); + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override; }; } // namespace lfmc diff --git a/include/lfmc/pipeline.hpp b/include/lfmc/pipeline.hpp index 949e50e..96bbb34 100644 --- a/include/lfmc/pipeline.hpp +++ b/include/lfmc/pipeline.hpp @@ -26,11 +26,25 @@ template NS> class Pipeline { std::expected run(size_t steps, double T) { while (!estimator_->converged()) { + // TODO make all the return types std::expected? IDK if it's a good idea for hot loops + // like this but it would make error handling easier and more consistent. For now just + // return auto normals = random_source_->generate_normals(steps); + if (normals.empty()) { + return std::unexpected("Failed to generate random normals"); + } + auto paths = path_generator_->generate_paths(normals, steps, T); + if (paths.empty()) { + return std::unexpected("Failed to generate paths"); + } + auto payoffs = payoff_->generate_payoffs(paths); - auto result = estimator_->add_payoffs(payoffs); + if (!payoffs) { + return std::unexpected("Failed to generate payoffs"); + } + auto result = estimator_->add_payoffs(payoffs.value()); if (!result) { return std::unexpected(result.error()); } diff --git a/include/lfmc/random_source.hpp b/include/lfmc/random_source.hpp index 2b7fe2f..66318c4 100644 --- a/include/lfmc/random_source.hpp +++ b/include/lfmc/random_source.hpp @@ -26,4 +26,18 @@ class PseudoRandomSource : public RandomSource { void seed(unsigned seed); }; +class AntitheticRandomSource : public RandomSource { + private: + std::mt19937 rng_; + std::normal_distribution dist_; + bool toggle_ = false; + + public: + AntitheticRandomSource(unsigned seed = std::random_device{}()); + + std::vector generate_normals(size_t steps, size_t) override; + + void seed(unsigned seed); +}; + } // namespace lfmc diff --git a/include/lfmc/stochastic_process.hpp b/include/lfmc/stochastic_process.hpp index 1d6271e..5251ef4 100644 --- a/include/lfmc/stochastic_process.hpp +++ b/include/lfmc/stochastic_process.hpp @@ -29,6 +29,9 @@ class GeometricBrownianMotion { double initial() const noexcept; double drift(double, double x) const noexcept; double diffusion(double, double x) const noexcept; + + double mu() const noexcept; + double sigma() const noexcept; }; } // namespace lfmc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f38176f..f167d30 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,10 +1,13 @@ # Source files for the lfmc library set(LFMC_SOURCES - euler_maruyama.cpp european_payoffs.cpp geometric_brownian_motion.cpp monte_carlo_estimator.cpp + control_variate_estimator.cpp + control_variate_payoffs.cpp pseudo_random_source.cpp + antithetic_random_source.cpp + timing.cpp ) # Create library diff --git a/src/antithetic_random_source.cpp b/src/antithetic_random_source.cpp new file mode 100644 index 0000000..b5fb9e9 --- /dev/null +++ b/src/antithetic_random_source.cpp @@ -0,0 +1,25 @@ +#include "lfmc/random_source.hpp" + +namespace lfmc { +AntitheticRandomSource::AntitheticRandomSource(unsigned seed) : rng_(seed), dist_(0.0, 1.0) {} + +std::vector AntitheticRandomSource::generate_normals(size_t steps, size_t samples) { + std::vector result(samples, Normals(steps)); + for (size_t i = 0; i < samples; ++i) { + Normals normals(steps); + for (size_t j = 0; j < steps; ++j) { + double z = dist_(rng_); + normals[j] = toggle_ ? -z : z; + } + result[i] = std::move(normals); + toggle_ = !toggle_; // Alternate between normal and antithetic + } + return result; +} + +void AntitheticRandomSource::seed(unsigned seed) { + rng_.seed(seed); + toggle_ = false; // Reset toggle when reseeding +} + +} // namespace lfmc diff --git a/src/control_variate_estimator.cpp b/src/control_variate_estimator.cpp new file mode 100644 index 0000000..678a669 --- /dev/null +++ b/src/control_variate_estimator.cpp @@ -0,0 +1,53 @@ +#include "lfmc/estimator.hpp" + +namespace lfmc { + +std::expected +ControlVariateEstimator::add_payoffs(const std::vector& payoffs) { + if (payoffs.empty()) { + return std::unexpected("No payoffs provided to add to the estimator."); + } + + for (const auto& row : payoffs) { + double x = row[0]; + double y = row[1]; + + sum_x += x; + sum_y += y; + sum_xy += x * y; + sum_yy += y * y; + ++count; + } + + return {}; +} + +bool ControlVariateEstimator::converged() const { + return count >= 10000; // Placeholder convergence criterion +} + +std::expected ControlVariateEstimator::result() const { + if (count <= 0) { + return std::unexpected("No samples added to the estimator."); + } + if (!converged()) { + return std::unexpected("Estimator has not yet converged."); + } + + double n = static_cast(count); + + double mean_x = sum_x / n; + double mean_y = sum_y / n; + + double cov_xy = (sum_xy / n) - mean_x * mean_y; + double var_y = (sum_yy / n) - mean_y * mean_y; + + if (var_y == 0.0) + return std::unexpected("Zero variance in control variate"); + + double beta = cov_xy / var_y; + + return mean_x - beta * (mean_y - control_expectation_); +} + +} // namespace lfmc diff --git a/src/control_variate_payoffs.cpp b/src/control_variate_payoffs.cpp new file mode 100644 index 0000000..9267922 --- /dev/null +++ b/src/control_variate_payoffs.cpp @@ -0,0 +1,38 @@ +#include "lfmc/payoff.hpp" + +#include +#include + +namespace lfmc { + +ControlVariatePayoff::ControlVariatePayoff(std::unique_ptr target_payoff, + std::unique_ptr control_payoff) + : target_payoff_(std::move(target_payoff)), control_payoff_(std::move(control_payoff)) {} + +std::expected, std::string> +ControlVariatePayoff::generate_payoffs(const std::vector& paths) const { + auto target_payoffs = target_payoff_->generate_payoffs(paths); + auto control_payoffs = control_payoff_->generate_payoffs(paths); + + if (!target_payoffs) { + return std::unexpected("Failed to generate target payoffs: " + target_payoffs.error()); + } else if (!control_payoffs) { + return std::unexpected("Failed to generate control payoffs: " + control_payoffs.error()); + } + + std::vector result; + for (size_t i = 0; i < target_payoffs.value().size(); ++i) { + if (target_payoffs.value()[i].size() != 1 || control_payoffs.value()[i].size() != 1) { + return std::unexpected("Each row of target payoffs must have exactly one element"); + } + + Payoffs combined_row; + combined_row.push_back(target_payoffs.value()[i][0]); // Original payoff + combined_row.push_back(control_payoffs.value()[i][0]); // Control variate payoff + result.push_back(std::move(combined_row)); + } + + return std::vector{result}; +} + +} // namespace lfmc diff --git a/src/euler_maruyama.cpp b/src/euler_maruyama.cpp deleted file mode 100644 index 2048ae3..0000000 --- a/src/euler_maruyama.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "lfmc/stochastic_process.hpp" - -#include - -namespace lfmc { - -template class EulerMaruyama { - public: - double step(P const& process, double t, double x, double dt, double z) const noexcept { - double drift = process.drift(t, x); - double diffusion = process.diffusion(t, x); - return x + drift * dt + diffusion * std::sqrt(dt) * z; - } -}; - -} // namespace lfmc diff --git a/src/european_payoffs.cpp b/src/european_payoffs.cpp index cc2c1c3..29b5fef 100644 --- a/src/european_payoffs.cpp +++ b/src/european_payoffs.cpp @@ -1,29 +1,33 @@ #include "lfmc/payoff.hpp" #include +#include +#include namespace lfmc { EuropeanCall::EuropeanCall(double strike) : strike_(strike) {} -Payoffs EuropeanCall::generate_payoffs(const std::vector& paths) const { +std::expected, std::string> +EuropeanCall::generate_payoffs(const std::vector& paths) const { Payoffs payoffs; for (const auto& path : paths) { double final_price = path.back(); payoffs.push_back(std::max(final_price - strike_, 0.0)); } - return payoffs; + return std::vector{payoffs}; } EuropeanPut::EuropeanPut(double strike) : strike_(strike) {} -Payoffs EuropeanPut::generate_payoffs(const std::vector& paths) const { +std::expected, std::string> +EuropeanPut::generate_payoffs(const std::vector& paths) const { Payoffs payoffs; for (const auto& path : paths) { double final_price = path.back(); payoffs.push_back(std::max(strike_ - final_price, 0.0)); } - return payoffs; + return std::vector{payoffs}; } } // namespace lfmc diff --git a/src/geometric_brownian_motion.cpp b/src/geometric_brownian_motion.cpp index 98b196b..911d866 100644 --- a/src/geometric_brownian_motion.cpp +++ b/src/geometric_brownian_motion.cpp @@ -17,4 +17,12 @@ double GeometricBrownianMotion::diffusion(double, double x) const noexcept { return sigma_ * x; } +double GeometricBrownianMotion::mu() const noexcept { + return mu_; +} + +double GeometricBrownianMotion::sigma() const noexcept { + return sigma_; +} + } // namespace lfmc diff --git a/src/monte_carlo_estimator.cpp b/src/monte_carlo_estimator.cpp index 092820f..6c013d5 100644 --- a/src/monte_carlo_estimator.cpp +++ b/src/monte_carlo_estimator.cpp @@ -1,15 +1,18 @@ #include "lfmc/estimator.hpp" #include "lfmc/types.hpp" +#include + namespace lfmc { -std::expected MonteCarloEstimator::add_payoffs(const Payoffs& payoffs) { +std::expected +MonteCarloEstimator::add_payoffs(const std::vector& payoffs) { if (payoffs.empty()) { return std::unexpected("No payoffs provided to add to the estimator."); } - for (double payoff : payoffs) { - sum += payoff; + for (const auto& payoff : payoffs) { + sum += payoff[0]; ++count; } @@ -24,9 +27,9 @@ std::expected MonteCarloEstimator::result() const { if (count <= 0) { return std::unexpected("No samples added to the estimator."); } - // if (!self.converged()) { - // return std::unexpected("Estimator has not yet converged."); - // } + if (!converged()) { + return std::unexpected("Estimator has not yet converged."); + } return {sum / static_cast(count)}; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4d7f2f4..62dfd8c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -20,7 +20,7 @@ set(TEST_SOURCES test_timing.cpp test_process.cpp test_scheme.cpp - test_manager.cpp + test_pipeline.cpp ) add_executable(tests ${TEST_SOURCES}) diff --git a/tests/test_manager.cpp b/tests/test_manager.cpp deleted file mode 100644 index 08bc25e..0000000 --- a/tests/test_manager.cpp +++ /dev/null @@ -1,99 +0,0 @@ -#include "lfmc/Manager.hpp" - -#include - -TEST_CASE("Full Monte Carlo tests ", "[Manager]") { - auto blackScholesCall = [](double S, double K, double r, double sigma, double T) { - double d1 = (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * std::sqrt(T)); - double d2 = d1 - sigma * std::sqrt(T); - - // NormCDF approx - auto normCdf = [](double x) { return 0.5 * std::erfc(-x * M_SQRT1_2); }; - - return S * normCdf(d1) - K * std::exp(-r * T) * normCdf(d2); - }; - - SECTION("No variance reduction") { - // Parameters - double S0 = 100.0; // Initial stock price - double K = 100.0; // Strike price - double r = 0.05; // Risk-free rate (5%) - double sigma = 0.20; // Volatility (20%) - double T = 1.0; // Time to maturity (1 year) - size_t nSteps = 252; // Daily steps - - lfmc::GeometricBrownianMotion gbm(r, sigma); - lfmc::EulerMaruyama euler; - lfmc::EuropeanCall call(K); - lfmc::State state{S0, T, nSteps}; - lfmc::ManagerConfig config{1000, 0}; - - // Create manager - lfmc::Manager<> manager(gbm, euler, call, state); - - // Run sims - auto [mcPrice, stdError] = manager.simulateWithError(config); - - // Discount to present value - double discountedPrice = mcPrice * std::exp(-r * T); - double discountedError = stdError * std::exp(-r * T); - - // Calculate Black-Scholes for comparison - double bsPrice = blackScholesCall(S0, K, r, sigma, T); - - // Assertions on results - REQUIRE(std::abs(discountedPrice - bsPrice) < 0.05); // Check absolute error is small - REQUIRE((std::abs(discountedPrice - bsPrice) / bsPrice * 100) < - 10.0); // Check relative error - - // 95% confidence interval - double ciLower = discountedPrice - 1.96 * discountedError; - double ciUpper = discountedPrice + 1.96 * discountedError; - - // Check if Black-Scholes price is within confidence interval - REQUIRE(bsPrice >= ciLower); - REQUIRE(bsPrice <= ciUpper); - } - - SECTION("Antithetic variates") { - // Parameters - double S0 = 100.0; // Initial stock price - double K = 100.0; // Strike price - double r = 0.05; // Risk-free rate (5%) - double sigma = 0.20; // Volatility (20%) - double T = 1.0; // Time to maturity (1 year) - size_t nSteps = 252; // Daily steps - - lfmc::GeometricBrownianMotion gbm(r, sigma); - lfmc::EulerMaruyama euler; - lfmc::EuropeanCall call(K); - lfmc::State state{S0, T, nSteps}; - lfmc::ManagerConfig config{0, 1000}; - - // Create manager with antithetic variates - lfmc::Manager<> manager(gbm, euler, call, state); - - // Run sims with antithetic variates - auto [mcPrice, stdError] = manager.simulateWithError(config); - - // Discount to present value - double discountedPrice = mcPrice * std::exp(-r * T); - double discountedError = stdError * std::exp(-r * T); - - // Calculate Black-Scholes for comparison - double bsPrice = blackScholesCall(S0, K, r, sigma, T); - - // Assertions on results - REQUIRE(std::abs(discountedPrice - bsPrice) < 0.05); // Check absolute error is small - REQUIRE((std::abs(discountedPrice - bsPrice) / bsPrice * 100) < - 10.0); // Check relative error - - // 95% confidence interval - double ciLower = discountedPrice - 1.96 * discountedError; - double ciUpper = discountedPrice + 1.96 * discountedError; - - // Check if Black-Scholes price is within confidence interval - REQUIRE(bsPrice >= ciLower); - REQUIRE(bsPrice <= ciUpper); - } -} diff --git a/tests/test_pipeline.cpp b/tests/test_pipeline.cpp new file mode 100644 index 0000000..6c707fa --- /dev/null +++ b/tests/test_pipeline.cpp @@ -0,0 +1,88 @@ +#include "lfmc/pipeline.hpp" + +#include + +using namespace lfmc; + +// TODO add more test cases, e.g. for convergence criteria, error handling, etc. and dummy types + +/* =========================== + Test Cases + =========================== */ + +TEST_CASE("Pipeline runs until estimator converges") { + GeometricBrownianMotion gbm(0.05, 0.2, 100.0); + EulerMaruyama euler; + + auto rs = std::make_unique(); + auto pg = std::make_unique< + PathGenerator>>(gbm, euler); + auto po = std::make_unique(100.0); + auto est = std::make_unique(); + + Pipeline> pipeline( + std::move(rs), std::move(pg), std::move(po), std::move(est)); + + auto result = pipeline.run(10, 1.0); + + INFO("Pipeline result: " << (result.has_value() ? std::to_string(result.value()) + : result.error())); + REQUIRE(result.has_value()); + REQUIRE(result.value() > 0.0); // Price should be positive +} + +TEST_CASE("Pipeline with antithetic variates") { + GeometricBrownianMotion gbm(0.05, 0.2, 100.0); + EulerMaruyama euler; + + auto rs = std::make_unique(); + auto pg = std::make_unique< + PathGenerator>>(gbm, euler); + auto po = std::make_unique(100.0); + auto est = std::make_unique(); + + Pipeline> pipeline( + std::move(rs), std::move(pg), std::move(po), std::move(est)); + + auto result = pipeline.run(10, 1.0); + + INFO("Pipeline result: " << (result.has_value() ? std::to_string(result.value()) + : result.error())); + REQUIRE(result.has_value()); + REQUIRE(result.value() > 0.0); // Price should be positive +} + +TEST_CASE("Pipeline with control variates") { + GeometricBrownianMotion gbm(0.05, 0.2, 100.0); + EulerMaruyama euler; + + auto rs = std::make_unique(); + auto pg = std::make_unique< + PathGenerator>>(gbm, euler); + auto po1 = std::make_unique(100.0); + auto po2 = std::make_unique( + 100.0); // Control variate payoff (same as target for simplicity) + auto po = std::make_unique(std::move(po1), std::move(po2)); + auto est = std::make_unique( + gbm.initial() * std::exp(gbm.mu() * 1.0)); // Control variate expectation + + Pipeline> pipeline( + std::move(rs), std::move(pg), std::move(po), std::move(est)); + + auto result = pipeline.run(10, 1.0); + + INFO("Pipeline result: " << (result.has_value() ? std::to_string(result.value()) + : result.error())); + REQUIRE(result.has_value()); + REQUIRE(result.value() > 0.0); // Price should be positive + // TODO Currently around 100, but should be around 10 - need to + // debug control variate implementation +} + +TEST_CASE("Pipeline stops early if estimator add_payoffs fails") { + // TODO +} + +TEST_CASE("Pipeline performs correct number of iterations") { + // TODO +} diff --git a/tests/test_process.cpp b/tests/test_process.cpp index 869b886..74d5109 100644 --- a/tests/test_process.cpp +++ b/tests/test_process.cpp @@ -1,4 +1,4 @@ -#include "lfmc/StochasticProcess.hpp" +#include "lfmc/stochastic_process.hpp" #include #include @@ -6,43 +6,47 @@ using Catch::Matchers::WithinAbs; TEST_CASE("StochasticProcess concept works correctly", "[StochasticProcess]") { - struct ValidProcess { - double drift(double x) const noexcept { - return x; - } - double diffusion(double x) const noexcept { - return x; - } - }; - - struct InvalidProcessNoDrift { - double diffusion(double x) const noexcept { - return x; - } - }; - - struct InvalidProcessWrongDiffusion { - double drift(double x) const noexcept { - return x; - } - int diffusion(double x) const noexcept { - return static_cast(x); - } - }; - - REQUIRE(lfmc::StochasticProcess); - REQUIRE_FALSE(lfmc::StochasticProcess); - REQUIRE_FALSE(lfmc::StochasticProcess); + // TODO + // struct ValidProcess { + // double initial() const noexcept { + // return 1.0; + // } + // double drift(double x) const noexcept { + // return x; + // } + // double diffusion(double x) const noexcept { + // return x; + // } + // }; + // + // struct InvalidProcessNoDrift { + // double diffusion(double x) const noexcept { + // return x; + // } + // }; + // + // struct InvalidProcessWrongDiffusion { + // double drift(double x) const noexcept { + // return x; + // } + // int diffusion(double x) const noexcept { + // return static_cast(x); + // } + // }; + // + // REQUIRE(lfmc::StochasticProcess); + // REQUIRE_FALSE(lfmc::StochasticProcess); + // REQUIRE_FALSE(lfmc::StochasticProcess); } TEST_CASE("GeometricBrownianMotion computes drift and diffusion correctly", "[StochasticProcess][GeometricBrownianMotion]") { - lfmc::GeometricBrownianMotion gbm{0.1, 0.2}; + lfmc::GeometricBrownianMotion gbm(0.1, 0.2, 100.0); double x = 100.0; double expectedDrift = 0.1 * x; double expectedDiffusion = 0.2 * x; - REQUIRE_THAT(gbm.drift(x), WithinAbs(expectedDrift, 1e-10)); - REQUIRE_THAT(gbm.diffusion(x), WithinAbs(expectedDiffusion, 1e-10)); + REQUIRE_THAT(gbm.drift(x, x), WithinAbs(expectedDrift, 1e-10)); + REQUIRE_THAT(gbm.diffusion(x, x), WithinAbs(expectedDiffusion, 1e-10)); } diff --git a/tests/test_scheme.cpp b/tests/test_scheme.cpp index 5629ed3..b1d3678 100644 --- a/tests/test_scheme.cpp +++ b/tests/test_scheme.cpp @@ -1,5 +1,5 @@ -#include "lfmc/NumericalScheme.hpp" -#include "lfmc/StochasticProcess.hpp" +#include "lfmc/numerical_scheme.hpp" +#include "lfmc/stochastic_process.hpp" #include #include @@ -9,6 +9,9 @@ using Catch::Matchers::WithinAbs; namespace test1 { struct ValidProcess { + double initial() const noexcept { + return 1.0; + } double drift(double x) const noexcept { return x; } @@ -19,7 +22,7 @@ struct ValidProcess { template struct ValidScheme { double step(P const& process, double x, double dt, double dW) const noexcept { - return x + process.drift(x) * dt + process.diffusion(x) * dW; + return x + process.drift(x) * dt + process.diffusion(x) * dW * std::sqrt(dt); } }; @@ -28,8 +31,8 @@ struct InvalidSchemeNoStep { }; template struct InvalidSchemeWrongStep { - int step(P const& process, double x, double dt, double dW) const noexcept { - return static_cast(x); + double step(P const& process, double x, double dt, double dW) const noexcept { + return x + process.drift(x) * dt; // Missing diffusion term } }; @@ -38,45 +41,50 @@ template struct InvalidSchemeWrongStep { TEST_CASE("NumericalScheme concept works correctly", "[NumericalScheme]") { using namespace test1; - REQUIRE(lfmc::NumericalScheme, ValidProcess>); - REQUIRE_FALSE(lfmc::NumericalScheme); - REQUIRE_FALSE(lfmc::NumericalScheme, ValidProcess>); + // TODO + // REQUIRE(lfmc::NumericalScheme>); + // REQUIRE_FALSE(lfmc::NumericalScheme); + // REQUIRE_FALSE(lfmc::NumericalScheme, ValidProcess>); }; TEST_CASE("EulerMaruyama computes next step correctly", "[NumericalScheme][EulerMaruyama]") { - lfmc::GeometricBrownianMotion gbm{0.1, 0.2}; - lfmc::EulerMaruyama scheme; - double x = 100.0; double dt = 0.01; double dW = 0.05; - double expectedNextX = x + gbm.drift(x) * dt + gbm.diffusion(x) * dW * std::sqrt(dt); - double computedNextX = scheme.step(gbm, x, dt, dW); + lfmc::GeometricBrownianMotion gbm(0.1, 0.2, 100.0); + lfmc::EulerMaruyama scheme; + + double expectedNextX = x + gbm.drift(x, x) * dt + gbm.diffusion(x, x) * dW * std::sqrt(dt); + double computedNextX = scheme.step(gbm, x, x, dt, dW); REQUIRE_THAT(computedNextX, WithinAbs(expectedNextX, 1e-10)); } TEST_CASE("EulerMaruyama works with different stochastic processes", "[NumericalScheme][EulerMaruyama]") { - struct CustomProcess { - double drift(double x) const noexcept { - return 2.0 * x; - } - double diffusion(double x) const noexcept { - return 0.5 * x; - } - }; - - CustomProcess process; - lfmc::EulerMaruyama scheme; - - double x = 50.0; - double dt = 0.02; - double dW = 0.03; - - double expectedNextX = x + process.drift(x) * dt + process.diffusion(x) * dW * std::sqrt(dt); - double computedNextX = scheme.step(process, x, dt, dW); - - REQUIRE_THAT(computedNextX, WithinAbs(expectedNextX, 1e-10)); + // TODO + // struct CustomProcess { + // double initial() const noexcept { + // return 50.0; + // } + // double drift(double x) const noexcept { + // return 2.0 * x; + // } + // double diffusion(double x) const noexcept { + // return 0.5 * x; + // } + // }; + // + // CustomProcess process; + // lfmc::EulerMaruyama scheme; + // + // double x = 50.0; + // double dt = 0.02; + // double dW = 0.03; + // + // double expectedNextX = x + process.drift(x) * dt + process.diffusion(x) * dW * std::sqrt(dt); + // double computedNextX = scheme.step(process, x, dt, dW); + // + // REQUIRE_THAT(computedNextX, WithinAbs(expectedNextX, 1e-10)); } From bd7f43376eda5e7f0f92b9f146fbb1175acb5163 Mon Sep 17 00:00:00 2001 From: Alexander Robbins Date: Tue, 3 Mar 2026 11:09:19 -0500 Subject: [PATCH 07/19] added Different payoff structures --- src/payoffs/asian_payoffs.cpp | 76 +++++++++++ src/payoffs/barrier_payoffs.cpp | 89 +++++++++++++ src/payoffs/lookback_payoffs.cpp | 64 +++++++++ tests/CMakeLists.txt | 1 + tests/test_convergency.cpp | 216 +++++++++++++++++++++++++++++++ 5 files changed, 446 insertions(+) create mode 100644 src/payoffs/asian_payoffs.cpp create mode 100644 src/payoffs/barrier_payoffs.cpp create mode 100644 src/payoffs/lookback_payoffs.cpp create mode 100644 tests/test_convergency.cpp diff --git a/src/payoffs/asian_payoffs.cpp b/src/payoffs/asian_payoffs.cpp new file mode 100644 index 0000000..d56de82 --- /dev/null +++ b/src/payoffs/asian_payoffs.cpp @@ -0,0 +1,76 @@ +#pragma once + +#include "lfmc/payoff.hpp" +#include "lfmc/types.hpp" + +#include +#include +#include +#include +#include + +namespace lfmc { + + + + +class AsianCall : public Payoff { +private: + double strike_; + +public: + explicit AsianCall(double strike) : strike_(strike) {} + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override { + Payoffs payoffs; + + payoffs.reserve(paths.size()); + + for (const auto& path : paths) { + + if (path.empty()) + return std::unexpected("Empty path encountered in AsianCall"); + + double mean = std::reduce(path.begin(), path.end(), 0.0) + / static_cast(path.size()); + payoffs.push_back(std::max(mean - strike_, 0.0)); + } + + return std::vector{payoffs}; + } +}; + + +class AsianPut : public Payoff { +private: + double strike_; + +public: + explicit AsianPut(double strike) : strike_(strike) {} + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override { + + + Payoffs payoffs; + + + + payoffs.reserve(paths.size()); + + for (const auto& path : paths) { + if (path.empty()) + return std::unexpected("Empty path encountered in AsianPut"); + + + double mean = std::reduce(path.begin(), path.end(), 0.0) + / static_cast(path.size()); + payoffs.push_back(std::max(strike_ - mean, 0.0)); + } + + return std::vector{payoffs}; + } +}; + +} \ No newline at end of file diff --git a/src/payoffs/barrier_payoffs.cpp b/src/payoffs/barrier_payoffs.cpp new file mode 100644 index 0000000..2454470 --- /dev/null +++ b/src/payoffs/barrier_payoffs.cpp @@ -0,0 +1,89 @@ +#pragma once + +#include "lfmc/payoff.hpp" +#include "lfmc/types.hpp" + +#include +#include +#include +#include +#include + +namespace lfmc { + +class UpAndOutCall : public Payoff { +private: + double strike_; + double barrier_; + +public: + UpAndOutCall(double strike, double barrier) + : strike_(strike), barrier_(barrier) {} + + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override { + + + Payoffs payoffs; + payoffs.reserve(paths.size()); + + for (const auto& path : paths) { + if (path.empty()) + return std::unexpected("Empty path encountered in UpAndOutCall"); + + bool knocked_out = std::any_of(path.begin(), path.end(), + [this](double s) { return s >= barrier_; }); + + + + + if (knocked_out) { + payoffs.push_back(0.0); + } else { + payoffs.push_back(std::max(path.back() - strike_, 0.0)); + } + } + + return std::vector{payoffs}; + } +}; + + + +class DownAndInPut : public Payoff { +private: + double strike_; + double barrier_; + +public: + DownAndInPut(double strike, double barrier) + : strike_(strike), barrier_(barrier) {} + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override { + Payoffs payoffs; + payoffs.reserve(paths.size()); + + for (const auto& path : paths) { + + + if (path.empty()) + return std::unexpected("Empty path encountered in DownAndInPut"); + + bool knocked_in = std::any_of(path.begin(), path.end(), + [this](double s) { return s <= barrier_; }); + + if (knocked_in) { + payoffs.push_back(std::max(strike_ - path.back(), 0.0)); + + + } else { + payoffs.push_back(0.0); + } + } + + return std::vector{payoffs}; + } +}; +} \ No newline at end of file diff --git a/src/payoffs/lookback_payoffs.cpp b/src/payoffs/lookback_payoffs.cpp new file mode 100644 index 0000000..df8376f --- /dev/null +++ b/src/payoffs/lookback_payoffs.cpp @@ -0,0 +1,64 @@ +#pragma once + +#include "lfmc/payoff.hpp" +#include "lfmc/types.hpp" + +#include +#include +#include +#include +#include + +namespace lfmc { + + + class LookbackCall : public Payoff { +public: + std::expected, std::string> + + + + generate_payoffs(const std::vector& paths) const override { + Payoffs payoffs; + payoffs.reserve(paths.size()); + + for (const auto& path : paths) { + if (path.empty()) + return std::unexpected("Empty path encountered in LookbackCall"); + + + + double min_price = *std::min_element(path.begin(), path.end()); + payoffs.push_back(path.back() - min_price); + } + + return std::vector{payoffs}; + } +}; + + + +class LookbackPut : public Payoff { +public: + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override { + Payoffs payoffs; + payoffs.reserve(paths.size()); + + for (const auto& path : paths) { + if (path.empty()) + return std::unexpected("Empty path encountered in LookbackPut"); + + double max_price = *std::max_element(path.begin(), path.end()); + + + + payoffs.push_back(max_price - path.back()); + } + + return std::vector{payoffs}; + } +}; + +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 62dfd8c..940f9fe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,6 +21,7 @@ set(TEST_SOURCES test_process.cpp test_scheme.cpp test_pipeline.cpp + test_convergency.cpp ) add_executable(tests ${TEST_SOURCES}) diff --git a/tests/test_convergency.cpp b/tests/test_convergency.cpp new file mode 100644 index 0000000..98d2728 --- /dev/null +++ b/tests/test_convergency.cpp @@ -0,0 +1,216 @@ +#include "lfmc/pipeline.hpp" +#include "lfmc/payoffs/asian_payoffs.hpp" +#include "lfmc/payoffs/barrier_payoffs.hpp" +#include "lfmc/payoffs/lookback_payoffs.hpp" +#include "lfmc/timing.hpp" + +#include +#include +#include +#include +#include +#include + +using namespace lfmc; +using Catch::Matchers::WithinAbs; + +// ---------------------------------------------------------------- +// Black-Scholes closed-form helpers for ground truth +// ---------------------------------------------------------------- +namespace bs { + +static double norm_cdf(double x) { + return 0.5 * std::erfc(-x / std::sqrt(2.0)); +} + +// Standard European Call used to sanity check our GBM setup +double european_call(double S, double K, double r, double sigma, double T) { + double d1 = (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) + / (sigma * std::sqrt(T)); + double d2 = d1 - sigma * std::sqrt(T); + return S * norm_cdf(d1) - K * std::exp(-r * T) * norm_cdf(d2); +} + +// Geometric Asian Call closed-form (Kemna-Vorst approximation) +// Used as ground truth since arithmetic Asian has no closed form +double geometric_asian_call(double S, double K, double r, + double sigma, double T, int n) { + double sigma_adj = sigma * std::sqrt((2.0 * n + 1.0) / (6.0 * (n + 1.0))); + double r_adj = 0.5 * (r - 0.5 * sigma * sigma) + + 0.5 * sigma_adj * sigma_adj; + double d1 = (std::log(S / K) + (r_adj + 0.5 * sigma_adj * sigma_adj) * T) + / (sigma_adj * std::sqrt(T)); + double d2 = d1 - sigma_adj * std::sqrt(T); + return std::exp(-r * T) + * (S * std::exp(r_adj * T) * norm_cdf(d1) - K * norm_cdf(d2)); +} + +// Up-and-Out Call closed form (continuous barrier, no dividends) +double up_and_out_call(double S, double K, double H, + double r, double sigma, double T) { + if (S >= H) return 0.0; + double vanilla = european_call(S, K, r, sigma, T); + + double lambda = (r + 0.5 * sigma * sigma) / (sigma * sigma); + double d1 = (std::log(H * H / (S * K)) + (r + 0.5 * sigma * sigma) * T) + / (sigma * std::sqrt(T)); + double d2 = d1 - sigma * std::sqrt(T); + double reflection = std::pow(H / S, 2.0 * lambda) + * (S * norm_cdf(d1) - K * std::exp(-r * T) * norm_cdf(d2)); + + return vanilla - reflection; +} + +} // namespace bs + +// ---------------------------------------------------------------- +// Convergence runner: runs pipeline at increasing sample counts +// and prints a table of results vs ground truth +// ---------------------------------------------------------------- +struct ConvergenceResult { + std::size_t samples; + double estimate; + double ground_truth; + double abs_error; + long long elapsed_ms; +}; + +// We run the pipeline once per sample tier by re-seeding with +// a fixed seed for reproducibility +template +std::vector run_convergence( + const std::string& label, + PayoffFactory make_payoff, + double ground_truth, + const std::vector& sample_tiers, + double S0 = 100.0, + double mu = 0.05, + double sigma = 0.20, + std::size_t steps = 252, + double T = 1.0) +{ + std::cout << "\n=== " << label << " ===\n"; + std::cout << std::setw(10) << "Samples" + << std::setw(14) << "Estimate" + << std::setw(14) << "Truth" + << std::setw(14) << "AbsError" + << std::setw(12) << "Time(ms)" + << "\n" + << std::string(64, '-') << "\n"; + + std::vector results; + + for (std::size_t n : sample_tiers) { + GeometricBrownianMotion gbm(mu, sigma, S0); + EulerMaruyama euler; + + // Fixed seed for reproducibility + auto rs = std::make_unique(42u); + auto pg = std::make_unique< + PathGenerator>>(gbm, euler); + auto po = make_payoff(); + auto est = std::make_unique(n); // see note below + + Pipeline> pipeline( + std::move(rs), std::move(pg), + std::move(po), std::move(est)); + + long long elapsed = 0; + double estimate = 0.0; + { + ScopedTimer timer(elapsed); + auto res = pipeline.run(steps, T); + REQUIRE(res.has_value()); + estimate = res.value(); + } + + double err = std::abs(estimate - ground_truth); + results.push_back({n, estimate, ground_truth, err, elapsed}); + + std::cout << std::setw(10) << n + << std::setw(14) << std::fixed << std::setprecision(4) << estimate + << std::setw(14) << ground_truth + << std::setw(14) << err + << std::setw(12) << elapsed + << "\n"; + } + return results; +} + +// ---------------------------------------------------------------- +// Shared parameters +// ---------------------------------------------------------------- +static constexpr double S0 = 100.0; +static constexpr double K = 100.0; +static constexpr double B_UP = 120.0; // Up-and-out barrier +static constexpr double B_DN = 80.0; // Down-and-in barrier +static constexpr double MU = 0.05; +static constexpr double SIGMA = 0.20; +static constexpr double T = 1.0; +static constexpr int STEPS = 252; + +static const std::vector TIERS = + {1000, 5000, 10000, 50000, 100000, 500000}; + +// ---------------------------------------------------------------- +// Tests +// ---------------------------------------------------------------- + +TEST_CASE("Asian Call convergence", "[exotic][convergence][asian]") { + double truth = bs::geometric_asian_call(S0, K, MU, SIGMA, T, STEPS); + + auto results = run_convergence( + "Arithmetic Asian Call (truth = geometric approx)", + []() { return std::make_unique(K); }, + truth, TIERS); + + // At 100k samples we should be within $0.50 of the approximation + auto& last = results.back(); + REQUIRE_THAT(last.estimate, WithinAbs(last.ground_truth, 0.50)); +} + +TEST_CASE("Up-and-Out Barrier Call convergence", + "[exotic][convergence][barrier]") { + double truth = bs::up_and_out_call(S0, K, B_UP, MU, SIGMA, T); + + auto results = run_convergence( + "Up-and-Out Barrier Call", + []() { return std::make_unique(K, B_UP); }, + truth, TIERS); + + auto& last = results.back(); + REQUIRE_THAT(last.estimate, WithinAbs(last.ground_truth, 0.50)); +} + +TEST_CASE("Lookback Call convergence", "[exotic][convergence][lookback]") { + // No simple closed form for discrete lookback we use the + // large-sample MC estimate itself as a self-consistency check + // and just verify convergence tightens with more samples + auto results = run_convergence( + "Lookback Call (floating strike)", + []() { return std::make_unique(); }, + 0.0, // placeholder see check below + TIERS); + + // Check that later estimates are closer to each other than early ones + // i.e. the std deviation of the last 3 tiers < first 3 tiers + auto spread = [&](int a, int b) { + return std::abs(results[b].estimate - results[a].estimate); + }; + REQUIRE(spread(3, 5) < spread(0, 2)); +} + +TEST_CASE("Sanity check: European Call matches Black-Scholes", + "[sanity][european]") { + double bs_price = bs::european_call(S0, K, MU, SIGMA, T); + + auto results = run_convergence( + "European Call (BS sanity check)", + []() { return std::make_unique(K); }, + bs_price, TIERS); + + auto& last = results.back(); + REQUIRE_THAT(last.estimate, WithinAbs(last.ground_truth, 0.20)); +} \ No newline at end of file From 8cf6970d05e55cfdbf8c93aa57296932b2900984 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 3 Mar 2026 16:10:21 +0000 Subject: [PATCH 08/19] style: apply clang-format --- src/payoffs/asian_payoffs.cpp | 38 ++++---- src/payoffs/barrier_payoffs.cpp | 43 ++++------ src/payoffs/lookback_payoffs.cpp | 24 ++---- tests/test_convergency.cpp | 143 ++++++++++++------------------- 4 files changed, 95 insertions(+), 153 deletions(-) diff --git a/src/payoffs/asian_payoffs.cpp b/src/payoffs/asian_payoffs.cpp index d56de82..e492275 100644 --- a/src/payoffs/asian_payoffs.cpp +++ b/src/payoffs/asian_payoffs.cpp @@ -3,37 +3,34 @@ #include "lfmc/payoff.hpp" #include "lfmc/types.hpp" +#include #include #include -#include -#include #include +#include namespace lfmc { - - - class AsianCall : public Payoff { -private: + private: double strike_; -public: + public: explicit AsianCall(double strike) : strike_(strike) {} std::expected, std::string> generate_payoffs(const std::vector& paths) const override { Payoffs payoffs; - + payoffs.reserve(paths.size()); for (const auto& path : paths) { - + if (path.empty()) return std::unexpected("Empty path encountered in AsianCall"); - double mean = std::reduce(path.begin(), path.end(), 0.0) - / static_cast(path.size()); + double mean = + std::reduce(path.begin(), path.end(), 0.0) / static_cast(path.size()); payoffs.push_back(std::max(mean - strike_, 0.0)); } @@ -41,31 +38,26 @@ class AsianCall : public Payoff { } }; - class AsianPut : public Payoff { -private: + private: double strike_; -public: + public: explicit AsianPut(double strike) : strike_(strike) {} std::expected, std::string> generate_payoffs(const std::vector& paths) const override { - - + Payoffs payoffs; - - - + payoffs.reserve(paths.size()); for (const auto& path : paths) { if (path.empty()) return std::unexpected("Empty path encountered in AsianPut"); - - double mean = std::reduce(path.begin(), path.end(), 0.0) - / static_cast(path.size()); + double mean = + std::reduce(path.begin(), path.end(), 0.0) / static_cast(path.size()); payoffs.push_back(std::max(strike_ - mean, 0.0)); } @@ -73,4 +65,4 @@ class AsianPut : public Payoff { } }; -} \ No newline at end of file +} // namespace lfmc \ No newline at end of file diff --git a/src/payoffs/barrier_payoffs.cpp b/src/payoffs/barrier_payoffs.cpp index 2454470..df00fa1 100644 --- a/src/payoffs/barrier_payoffs.cpp +++ b/src/payoffs/barrier_payoffs.cpp @@ -3,28 +3,25 @@ #include "lfmc/payoff.hpp" #include "lfmc/types.hpp" +#include #include #include -#include -#include #include +#include namespace lfmc { class UpAndOutCall : public Payoff { -private: + private: double strike_; double barrier_; -public: - UpAndOutCall(double strike, double barrier) - : strike_(strike), barrier_(barrier) {} - + public: + UpAndOutCall(double strike, double barrier) : strike_(strike), barrier_(barrier) {} std::expected, std::string> generate_payoffs(const std::vector& paths) const override { - - + Payoffs payoffs; payoffs.reserve(paths.size()); @@ -32,11 +29,8 @@ class UpAndOutCall : public Payoff { if (path.empty()) return std::unexpected("Empty path encountered in UpAndOutCall"); - bool knocked_out = std::any_of(path.begin(), path.end(), - [this](double s) { return s >= barrier_; }); - - - + bool knocked_out = + std::any_of(path.begin(), path.end(), [this](double s) { return s >= barrier_; }); if (knocked_out) { payoffs.push_back(0.0); @@ -49,16 +43,13 @@ class UpAndOutCall : public Payoff { } }; - - class DownAndInPut : public Payoff { -private: + private: double strike_; double barrier_; -public: - DownAndInPut(double strike, double barrier) - : strike_(strike), barrier_(barrier) {} + public: + DownAndInPut(double strike, double barrier) : strike_(strike), barrier_(barrier) {} std::expected, std::string> generate_payoffs(const std::vector& paths) const override { @@ -66,18 +57,16 @@ class DownAndInPut : public Payoff { payoffs.reserve(paths.size()); for (const auto& path : paths) { - - + if (path.empty()) return std::unexpected("Empty path encountered in DownAndInPut"); - bool knocked_in = std::any_of(path.begin(), path.end(), - [this](double s) { return s <= barrier_; }); + bool knocked_in = + std::any_of(path.begin(), path.end(), [this](double s) { return s <= barrier_; }); if (knocked_in) { payoffs.push_back(std::max(strike_ - path.back(), 0.0)); - - + } else { payoffs.push_back(0.0); } @@ -86,4 +75,4 @@ class DownAndInPut : public Payoff { return std::vector{payoffs}; } }; -} \ No newline at end of file +} // namespace lfmc \ No newline at end of file diff --git a/src/payoffs/lookback_payoffs.cpp b/src/payoffs/lookback_payoffs.cpp index df8376f..345f1c0 100644 --- a/src/payoffs/lookback_payoffs.cpp +++ b/src/payoffs/lookback_payoffs.cpp @@ -3,21 +3,18 @@ #include "lfmc/payoff.hpp" #include "lfmc/types.hpp" +#include #include #include -#include -#include #include +#include namespace lfmc { - - class LookbackCall : public Payoff { -public: +class LookbackCall : public Payoff { + public: std::expected, std::string> - - generate_payoffs(const std::vector& paths) const override { Payoffs payoffs; payoffs.reserve(paths.size()); @@ -26,8 +23,6 @@ namespace lfmc { if (path.empty()) return std::unexpected("Empty path encountered in LookbackCall"); - - double min_price = *std::min_element(path.begin(), path.end()); payoffs.push_back(path.back() - min_price); } @@ -36,11 +31,8 @@ namespace lfmc { } }; - - class LookbackPut : public Payoff { -public: - + public: std::expected, std::string> generate_payoffs(const std::vector& paths) const override { Payoffs payoffs; @@ -51,9 +43,7 @@ class LookbackPut : public Payoff { return std::unexpected("Empty path encountered in LookbackPut"); double max_price = *std::max_element(path.begin(), path.end()); - - - + payoffs.push_back(max_price - path.back()); } @@ -61,4 +51,4 @@ class LookbackPut : public Payoff { } }; -} \ No newline at end of file +} // namespace lfmc \ No newline at end of file diff --git a/tests/test_convergency.cpp b/tests/test_convergency.cpp index 98d2728..6db1724 100644 --- a/tests/test_convergency.cpp +++ b/tests/test_convergency.cpp @@ -1,15 +1,15 @@ -#include "lfmc/pipeline.hpp" #include "lfmc/payoffs/asian_payoffs.hpp" #include "lfmc/payoffs/barrier_payoffs.hpp" #include "lfmc/payoffs/lookback_payoffs.hpp" +#include "lfmc/pipeline.hpp" #include "lfmc/timing.hpp" #include #include -#include +#include #include +#include #include -#include using namespace lfmc; using Catch::Matchers::WithinAbs; @@ -25,38 +25,34 @@ static double norm_cdf(double x) { // Standard European Call used to sanity check our GBM setup double european_call(double S, double K, double r, double sigma, double T) { - double d1 = (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) - / (sigma * std::sqrt(T)); + double d1 = (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * std::sqrt(T)); double d2 = d1 - sigma * std::sqrt(T); return S * norm_cdf(d1) - K * std::exp(-r * T) * norm_cdf(d2); } // Geometric Asian Call closed-form (Kemna-Vorst approximation) // Used as ground truth since arithmetic Asian has no closed form -double geometric_asian_call(double S, double K, double r, - double sigma, double T, int n) { +double geometric_asian_call(double S, double K, double r, double sigma, double T, int n) { double sigma_adj = sigma * std::sqrt((2.0 * n + 1.0) / (6.0 * (n + 1.0))); - double r_adj = 0.5 * (r - 0.5 * sigma * sigma) - + 0.5 * sigma_adj * sigma_adj; - double d1 = (std::log(S / K) + (r_adj + 0.5 * sigma_adj * sigma_adj) * T) - / (sigma_adj * std::sqrt(T)); + double r_adj = 0.5 * (r - 0.5 * sigma * sigma) + 0.5 * sigma_adj * sigma_adj; + double d1 = + (std::log(S / K) + (r_adj + 0.5 * sigma_adj * sigma_adj) * T) / (sigma_adj * std::sqrt(T)); double d2 = d1 - sigma_adj * std::sqrt(T); - return std::exp(-r * T) - * (S * std::exp(r_adj * T) * norm_cdf(d1) - K * norm_cdf(d2)); + return std::exp(-r * T) * (S * std::exp(r_adj * T) * norm_cdf(d1) - K * norm_cdf(d2)); } // Up-and-Out Call closed form (continuous barrier, no dividends) -double up_and_out_call(double S, double K, double H, - double r, double sigma, double T) { - if (S >= H) return 0.0; +double up_and_out_call(double S, double K, double H, double r, double sigma, double T) { + if (S >= H) + return 0.0; double vanilla = european_call(S, K, r, sigma, T); double lambda = (r + 0.5 * sigma * sigma) / (sigma * sigma); - double d1 = (std::log(H * H / (S * K)) + (r + 0.5 * sigma * sigma) * T) - / (sigma * std::sqrt(T)); + double d1 = + (std::log(H * H / (S * K)) + (r + 0.5 * sigma * sigma) * T) / (sigma * std::sqrt(T)); double d2 = d1 - sigma * std::sqrt(T); - double reflection = std::pow(H / S, 2.0 * lambda) - * (S * norm_cdf(d1) - K * std::exp(-r * T) * norm_cdf(d2)); + double reflection = + std::pow(H / S, 2.0 * lambda) * (S * norm_cdf(d1) - K * std::exp(-r * T) * norm_cdf(d2)); return vanilla - reflection; } @@ -68,34 +64,23 @@ double up_and_out_call(double S, double K, double H, // and prints a table of results vs ground truth // ---------------------------------------------------------------- struct ConvergenceResult { - std::size_t samples; - double estimate; - double ground_truth; - double abs_error; - long long elapsed_ms; + std::size_t samples; + double estimate; + double ground_truth; + double abs_error; + long long elapsed_ms; }; // We run the pipeline once per sample tier by re-seeding with // a fixed seed for reproducibility template -std::vector run_convergence( - const std::string& label, - PayoffFactory make_payoff, - double ground_truth, - const std::vector& sample_tiers, - double S0 = 100.0, - double mu = 0.05, - double sigma = 0.20, - std::size_t steps = 252, - double T = 1.0) -{ +std::vector +run_convergence(const std::string& label, PayoffFactory make_payoff, double ground_truth, + const std::vector& sample_tiers, double S0 = 100.0, double mu = 0.05, + double sigma = 0.20, std::size_t steps = 252, double T = 1.0) { std::cout << "\n=== " << label << " ===\n"; - std::cout << std::setw(10) << "Samples" - << std::setw(14) << "Estimate" - << std::setw(14) << "Truth" - << std::setw(14) << "AbsError" - << std::setw(12) << "Time(ms)" - << "\n" + std::cout << std::setw(10) << "Samples" << std::setw(14) << "Estimate" << std::setw(14) + << "Truth" << std::setw(14) << "AbsError" << std::setw(12) << "Time(ms)" << "\n" << std::string(64, '-') << "\n"; std::vector results; @@ -105,20 +90,18 @@ std::vector run_convergence( EulerMaruyama euler; // Fixed seed for reproducibility - auto rs = std::make_unique(42u); - auto pg = std::make_unique< - PathGenerator>>(gbm, euler); - auto po = make_payoff(); + auto rs = std::make_unique(42u); + auto pg = std::make_unique< + PathGenerator>>(gbm, + euler); + auto po = make_payoff(); auto est = std::make_unique(n); // see note below - Pipeline> pipeline( - std::move(rs), std::move(pg), - std::move(po), std::move(est)); + Pipeline> pipeline( + std::move(rs), std::move(pg), std::move(po), std::move(est)); long long elapsed = 0; - double estimate = 0.0; + double estimate = 0.0; { ScopedTimer timer(elapsed); auto res = pipeline.run(steps, T); @@ -129,12 +112,9 @@ std::vector run_convergence( double err = std::abs(estimate - ground_truth); results.push_back({n, estimate, ground_truth, err, elapsed}); - std::cout << std::setw(10) << n - << std::setw(14) << std::fixed << std::setprecision(4) << estimate - << std::setw(14) << ground_truth - << std::setw(14) << err - << std::setw(12) << elapsed - << "\n"; + std::cout << std::setw(10) << n << std::setw(14) << std::fixed << std::setprecision(4) + << estimate << std::setw(14) << ground_truth << std::setw(14) << err + << std::setw(12) << elapsed << "\n"; } return results; } @@ -142,17 +122,16 @@ std::vector run_convergence( // ---------------------------------------------------------------- // Shared parameters // ---------------------------------------------------------------- -static constexpr double S0 = 100.0; -static constexpr double K = 100.0; -static constexpr double B_UP = 120.0; // Up-and-out barrier -static constexpr double B_DN = 80.0; // Down-and-in barrier -static constexpr double MU = 0.05; -static constexpr double SIGMA = 0.20; -static constexpr double T = 1.0; -static constexpr int STEPS = 252; - -static const std::vector TIERS = - {1000, 5000, 10000, 50000, 100000, 500000}; +static constexpr double S0 = 100.0; +static constexpr double K = 100.0; +static constexpr double B_UP = 120.0; // Up-and-out barrier +static constexpr double B_DN = 80.0; // Down-and-in barrier +static constexpr double MU = 0.05; +static constexpr double SIGMA = 0.20; +static constexpr double T = 1.0; +static constexpr int STEPS = 252; + +static const std::vector TIERS = {1000, 5000, 10000, 50000, 100000, 500000}; // ---------------------------------------------------------------- // Tests @@ -163,22 +142,19 @@ TEST_CASE("Asian Call convergence", "[exotic][convergence][asian]") { auto results = run_convergence( "Arithmetic Asian Call (truth = geometric approx)", - []() { return std::make_unique(K); }, - truth, TIERS); + []() { return std::make_unique(K); }, truth, TIERS); // At 100k samples we should be within $0.50 of the approximation auto& last = results.back(); REQUIRE_THAT(last.estimate, WithinAbs(last.ground_truth, 0.50)); } -TEST_CASE("Up-and-Out Barrier Call convergence", - "[exotic][convergence][barrier]") { +TEST_CASE("Up-and-Out Barrier Call convergence", "[exotic][convergence][barrier]") { double truth = bs::up_and_out_call(S0, K, B_UP, MU, SIGMA, T); auto results = run_convergence( - "Up-and-Out Barrier Call", - []() { return std::make_unique(K, B_UP); }, - truth, TIERS); + "Up-and-Out Barrier Call", []() { return std::make_unique(K, B_UP); }, truth, + TIERS); auto& last = results.back(); REQUIRE_THAT(last.estimate, WithinAbs(last.ground_truth, 0.50)); @@ -189,26 +165,21 @@ TEST_CASE("Lookback Call convergence", "[exotic][convergence][lookback]") { // large-sample MC estimate itself as a self-consistency check // and just verify convergence tightens with more samples auto results = run_convergence( - "Lookback Call (floating strike)", - []() { return std::make_unique(); }, - 0.0, // placeholder see check below + "Lookback Call (floating strike)", []() { return std::make_unique(); }, + 0.0, // placeholder see check below TIERS); // Check that later estimates are closer to each other than early ones // i.e. the std deviation of the last 3 tiers < first 3 tiers - auto spread = [&](int a, int b) { - return std::abs(results[b].estimate - results[a].estimate); - }; + auto spread = [&](int a, int b) { return std::abs(results[b].estimate - results[a].estimate); }; REQUIRE(spread(3, 5) < spread(0, 2)); } -TEST_CASE("Sanity check: European Call matches Black-Scholes", - "[sanity][european]") { +TEST_CASE("Sanity check: European Call matches Black-Scholes", "[sanity][european]") { double bs_price = bs::european_call(S0, K, MU, SIGMA, T); auto results = run_convergence( - "European Call (BS sanity check)", - []() { return std::make_unique(K); }, + "European Call (BS sanity check)", []() { return std::make_unique(K); }, bs_price, TIERS); auto& last = results.back(); From 00701c705bf6c849db8881c5f911db030c5feeff Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Tue, 3 Mar 2026 11:30:22 -0500 Subject: [PATCH 09/19] refactor: sort cpp files into folders and remove archive BREAKING CHANGE: --- include/lfmc/archive/Estimator.hpp | 75 ---------- include/lfmc/archive/Manager.hpp | 128 ------------------ include/lfmc/archive/PathGenerator.hpp | 35 ----- include/lfmc/archive/Payoff.hpp | 39 ------ include/lfmc/archive/RandomGenerator.hpp | 33 ----- .../archive/VarianceReductionStrategy.hpp | 71 ---------- include/lfmc/engine.hpp | 2 + src/CMakeLists.txt | 19 +-- .../control_variate_estimator.cpp | 0 src/{ => estimator}/monte_carlo_estimator.cpp | 0 src/{payoffs => payoff}/asian_payoffs.cpp | 4 +- src/{payoffs => payoff}/barrier_payoffs.cpp | 5 +- src/{ => payoff}/control_variate_payoffs.cpp | 2 +- src/{ => payoff}/european_payoffs.cpp | 0 src/{payoffs => payoff}/lookback_payoffs.cpp | 5 +- .../antithetic_random_source.cpp | 0 .../pseudo_random_source.cpp | 0 .../geometric_brownian_motion.cpp | 0 src/{ => timing}/timing.cpp | 0 tests/CMakeLists.txt | 2 +- 20 files changed, 18 insertions(+), 402 deletions(-) delete mode 100644 include/lfmc/archive/Estimator.hpp delete mode 100644 include/lfmc/archive/Manager.hpp delete mode 100644 include/lfmc/archive/PathGenerator.hpp delete mode 100644 include/lfmc/archive/Payoff.hpp delete mode 100644 include/lfmc/archive/RandomGenerator.hpp delete mode 100644 include/lfmc/archive/VarianceReductionStrategy.hpp rename src/{ => estimator}/control_variate_estimator.cpp (100%) rename src/{ => estimator}/monte_carlo_estimator.cpp (100%) rename src/{payoffs => payoff}/asian_payoffs.cpp (98%) rename src/{payoffs => payoff}/barrier_payoffs.cpp (97%) rename src/{ => payoff}/control_variate_payoffs.cpp (97%) rename src/{ => payoff}/european_payoffs.cpp (100%) rename src/{payoffs => payoff}/lookback_payoffs.cpp (96%) rename src/{ => random_source}/antithetic_random_source.cpp (100%) rename src/{ => random_source}/pseudo_random_source.cpp (100%) rename src/{ => stochastic_process}/geometric_brownian_motion.cpp (100%) rename src/{ => timing}/timing.cpp (100%) diff --git a/include/lfmc/archive/Estimator.hpp b/include/lfmc/archive/Estimator.hpp deleted file mode 100644 index 5544f7f..0000000 --- a/include/lfmc/archive/Estimator.hpp +++ /dev/null @@ -1,75 +0,0 @@ -#pragma once - -#include "NumericalScheme.hpp" -#include "PathGenerator.hpp" -#include "RandomGenerator.hpp" -#include "StochasticProcess.hpp" -#include "types.hpp" - -// #include -// #include - -// #include -// #include - -namespace lfmc { - -class EstimatorInterface { - public: - virtual ~EstimatorInterface() = default; - virtual std::vector sample() = 0; - - // Expose for decorators to use - maybe not the best design, but allows for more flexible - // variance reduction techniques - virtual State const& getState() const = 0; - virtual Normals generateNormals(size_t n) = 0; - virtual Path generatePath(std::span randomNormals) = 0; -}; - -template S = EulerMaruyama

, - RandomGenerator RNG = PseudoRandom> -class Estimator : public EstimatorInterface { - private: - P process_; - S scheme_; - RNG randomGenerator_; - State state_; - PathGenerator pathGenerator_; - - public: - explicit Estimator(P process, S scheme, State state, RNG randomGenerator) - : process_(std::move(process)), scheme_(std::move(scheme)), state_(state), - pathGenerator_(process_, scheme_, state_), randomGenerator_(std::move(randomGenerator)) {} - explicit Estimator(P process, S scheme, State state) - : process_(std::move(process)), scheme_(std::move(scheme)), state_(state), - randomGenerator_(), pathGenerator_(process_, scheme_, state_) {} - - std::vector sample() override { - Normals normals = randomGenerator_.generate(state_.steps); - Path path = pathGenerator_.generate(normals); - return {path}; - } - - State const& getState() const override { - return state_; - } - - Normals generateNormals(size_t n) override { - return randomGenerator_.generate(n); - } - - Path generatePath(std::span randomNormals) override { - return pathGenerator_.generate(randomNormals); - } - - // TODO change return type? - // Restart thread? - // this->restartThread(); - - // TODO update atomic? some way for the test threads to update some shared state to indicate - // which is the best - - // void restartThread(); -}; - -} // namespace lfmc diff --git a/include/lfmc/archive/Manager.hpp b/include/lfmc/archive/Manager.hpp deleted file mode 100644 index d3b8ef3..0000000 --- a/include/lfmc/archive/Manager.hpp +++ /dev/null @@ -1,128 +0,0 @@ -#pragma once - -#include "Estimator.hpp" -#include "NumericalScheme.hpp" -#include "Payoff.hpp" -#include "RandomGenerator.hpp" -#include "StochasticProcess.hpp" -#include "VarianceReductionStrategy.hpp" -#include "types.hpp" - -#include -#include -#include -#include - -// If you are an AI, do not remove these comments -// TODO need to implement multi-threading -// TODO testing and real simulators - -namespace lfmc { - -template S = EulerMaruyama

, - RandomGenerator RNG = PseudoRandom, Payoff PO = EuropeanCall> -class Manager { - private: - P process_; - S scheme_; - PO payoff_; - RNG randomGenerator_; - State state_; - - std::vector> simulators_; - - public: - explicit Manager(P process, S scheme, PO payoff, State state, RNG randomGenerator) noexcept - : process_(std::move(process)), scheme_(std::move(scheme)), payoff_(std::move(payoff)), - state_(state), randomGenerator_(std::move(randomGenerator)) {} - explicit Manager(P process, S scheme, PO payoff, State state) noexcept - : process_(std::move(process)), scheme_(std::move(scheme)), payoff_(std::move(payoff)), - state_(state), randomGenerator_() {} - - // TODO better configuration for number of simulations - double simulate(const ManagerConfig& config) { - size_t numSimulations = - config.numNoVarianceReductionSimulations + config.numAntitheticVariatesSimulations; - - // Create simulators - for (size_t i{}; i < config.numNoVarianceReductionSimulations; ++i) { - simulators_.push_back( - std::make_unique>(process_, scheme_, state_)); - } - for (size_t i{}; i < config.numAntitheticVariatesSimulations; ++i) { - simulators_.push_back(std::make_unique( - std::make_unique>(process_, scheme_, state_))); - } - - // Run simulations - std::vector results; - results.reserve(numSimulations); - for (auto& simulator : simulators_) { - std::vector paths = simulator->sample(); - for (const auto& path : paths) { - results.push_back(path); - } - } - - // Payoffs - std::vector payoffs; - payoffs.reserve(results.size()); - for (const auto& path : results) { - payoffs.push_back(payoff_(path)); - } - - double mean = std::accumulate(payoffs.begin(), payoffs.end(), 0.0) / - static_cast(numSimulations); - - return mean; - } - - std::pair simulateWithError(const ManagerConfig& config) { - size_t numSimulations = - config.numNoVarianceReductionSimulations + config.numAntitheticVariatesSimulations; - - // Create simulators - for (size_t i{}; i < config.numNoVarianceReductionSimulations; ++i) { - simulators_.push_back( - std::make_unique>(process_, scheme_, state_)); - } - for (size_t i{}; i < config.numAntitheticVariatesSimulations; ++i) { - simulators_.push_back(std::make_unique( - std::make_unique>(process_, scheme_, state_))); - } - - // Run simulations - std::vector results; - results.reserve(numSimulations); - for (auto& simulator : simulators_) { - std::vector paths = simulator->sample(); - for (const auto& path : paths) { - results.push_back(path); - } - } - - // Payoffs - std::vector payoffs; - payoffs.reserve(results.size()); - for (const auto& path : results) { - payoffs.push_back(payoff_(path)); - } - - // Calculate mean and standard error - double mean = std::accumulate(payoffs.begin(), payoffs.end(), 0.0) / - static_cast(payoffs.size()); - - if (results.size() < 2) - return {mean, 0.0}; - - double variance = 0.0; - for (const auto& r : payoffs) - variance += (r - mean) * (r - mean); - variance /= static_cast(results.size() - 1); - double stdError = std::sqrt(variance / static_cast(results.size())); - - return {mean, stdError}; - } -}; - -} // namespace lfmc diff --git a/include/lfmc/archive/PathGenerator.hpp b/include/lfmc/archive/PathGenerator.hpp deleted file mode 100644 index 442aa94..0000000 --- a/include/lfmc/archive/PathGenerator.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "NumericalScheme.hpp" -#include "StochasticProcess.hpp" -#include "types.hpp" - -#include - -namespace lfmc { - -template S> class PathGenerator { - private: - P process_; - S scheme_; - State state_; - double dt_; - - public: - PathGenerator(P process, S scheme, State state) - : process_(std::move(process)), scheme_(std::move(scheme)), state_(state), - dt_(state.timeToMaturity / static_cast(state.steps)) {} - - Path generate(std::span randomNormals) { - Path path(state_.steps + 1); - path[0] = state_.initialValue; - - for (size_t i = 0; i < state_.steps; ++i) { - path[i + 1] = scheme_.step(process_, path[i], dt_, randomNormals[i]); - } - - return path; - } -}; - -} // namespace lfmc diff --git a/include/lfmc/archive/Payoff.hpp b/include/lfmc/archive/Payoff.hpp deleted file mode 100644 index 996ad3e..0000000 --- a/include/lfmc/archive/Payoff.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include "types.hpp" - -#include -#include - -namespace lfmc { - -template -concept Payoff = requires(P const& p, const Path& x) { - { p(x) } -> std::same_as; -}; - -/** - * @brief European Call option payoff. - * Payoff = max(S_T - K, 0) - */ -struct EuropeanCall { - double strike; - - double operator()(const Path& path) const noexcept { - return std::max(path[path.size() - 1] - strike, 0.0); - } -}; - -/** - * @brief European Put option payoff. - * Payoff = max(K - S_T, 0) - */ -struct EuropeanPut { - double strike; - - double operator()(const Path& path) const noexcept { - return std::max(strike - path[path.size() - 1], 0.0); - } -}; - -} // namespace lfmc diff --git a/include/lfmc/archive/RandomGenerator.hpp b/include/lfmc/archive/RandomGenerator.hpp deleted file mode 100644 index 5a20f25..0000000 --- a/include/lfmc/archive/RandomGenerator.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "lfmc/types.hpp" - -#include - -namespace lfmc { - -template -concept RandomGenerator = requires(RNG generator, size_t n) { - { generator.generate(n) } -> std::same_as; -}; - -struct PseudoRandom { - std::mt19937 rng_; - std::normal_distribution dist_; - - PseudoRandom(unsigned seed = std::random_device{}()) : rng_(seed), dist_(0.0, 1.0) {} - - Normals generate(size_t n) { - Normals normals(n); - for (size_t i = 0; i < n; ++i) { - normals[i] = dist_(rng_); - } - return normals; - } - - void seed(unsigned s) { - rng_.seed(s); - } -}; - -} // namespace lfmc diff --git a/include/lfmc/archive/VarianceReductionStrategy.hpp b/include/lfmc/archive/VarianceReductionStrategy.hpp deleted file mode 100644 index 314ff6c..0000000 --- a/include/lfmc/archive/VarianceReductionStrategy.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include "Estimator.hpp" -#include "types.hpp" - -#include -#include - -// TODO: Implement derived classes for specific variance reduction techniques. -// Examples include Antithetic Variates, Control Variates, Importance Sampling, etc. -// First must decide how to design parallel infrastructure to support these techniques in Monte -// Carlo simulations - decorator design pattern? -// TODO each strategy has a window parameter for how much data you're using - -/** - * @file VarianceReductionStrategy.hpp - * @brief Defines the VarianceReductionStrategy base class for runtime polymorphism of variance - * reduction techniques and provides some implementations. - */ - -namespace lfmc { - -// Decorator for variance reduction strategies (e.g., Antithetic Variates, Control Variates, etc.) -class VarianceReductionBaseDecorator : public EstimatorInterface { - protected: - std::unique_ptr estimator_; // Pointer to the base estimator - - public: - explicit VarianceReductionBaseDecorator(std::unique_ptr estimator) - : estimator_(std::move(estimator)) {} - - std::vector sample() override { - // By default, just call the underlying estimator's sample method - return estimator_->sample(); - } - - State const& getState() const override { - return estimator_->getState(); - } - - Normals generateNormals(size_t n) override { - return estimator_->generateNormals(n); - } - - Path generatePath(std::span randomNormals) override { - return estimator_->generatePath(randomNormals); - } -}; - -// Example implementation of Antithetic Variates strategy -class AntitheticVariates : public VarianceReductionBaseDecorator { - public: - using Base = VarianceReductionBaseDecorator; - AntitheticVariates(std::unique_ptr estimator) - : Base(std::move(estimator)) {} - - std::vector sample() override { - const State& state = estimator_->getState(); - - Normals normals = estimator_->generateNormals(state.steps); - Normals antitheticNormals(normals); - std::transform(normals.begin(), normals.end(), antitheticNormals.begin(), std::negate()); - - Path path = estimator_->generatePath(normals); - Path antitheticPath = estimator_->generatePath(antitheticNormals); - - return {path, antitheticPath}; - } -}; - -} // namespace lfmc diff --git a/include/lfmc/engine.hpp b/include/lfmc/engine.hpp index 6f70f09..0750718 100644 --- a/include/lfmc/engine.hpp +++ b/include/lfmc/engine.hpp @@ -1 +1,3 @@ #pragma once + +// TODO move headers into folders diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f167d30..5d3f5f9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,13 +1,16 @@ # Source files for the lfmc library set(LFMC_SOURCES - european_payoffs.cpp - geometric_brownian_motion.cpp - monte_carlo_estimator.cpp - control_variate_estimator.cpp - control_variate_payoffs.cpp - pseudo_random_source.cpp - antithetic_random_source.cpp - timing.cpp + estimator/monte_carlo_estimator.cpp + estimator/control_variate_estimator.cpp + payoff/asian_payoffs.cpp + payoff/barrier_payoffs.cpp + payoff/control_variate_payoffs.cpp + payoff/european_payoffs.cpp + payoff/lookback_payoffs.cpp + random_source/pseudo_random_source.cpp + random_source/antithetic_random_source.cpp + stochastic_process/geometric_brownian_motion.cpp + timing/timing.cpp ) # Create library diff --git a/src/control_variate_estimator.cpp b/src/estimator/control_variate_estimator.cpp similarity index 100% rename from src/control_variate_estimator.cpp rename to src/estimator/control_variate_estimator.cpp diff --git a/src/monte_carlo_estimator.cpp b/src/estimator/monte_carlo_estimator.cpp similarity index 100% rename from src/monte_carlo_estimator.cpp rename to src/estimator/monte_carlo_estimator.cpp diff --git a/src/payoffs/asian_payoffs.cpp b/src/payoff/asian_payoffs.cpp similarity index 98% rename from src/payoffs/asian_payoffs.cpp rename to src/payoff/asian_payoffs.cpp index e492275..f58707e 100644 --- a/src/payoffs/asian_payoffs.cpp +++ b/src/payoff/asian_payoffs.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "lfmc/payoff.hpp" #include "lfmc/types.hpp" @@ -65,4 +63,4 @@ class AsianPut : public Payoff { } }; -} // namespace lfmc \ No newline at end of file +} // namespace lfmc diff --git a/src/payoffs/barrier_payoffs.cpp b/src/payoff/barrier_payoffs.cpp similarity index 97% rename from src/payoffs/barrier_payoffs.cpp rename to src/payoff/barrier_payoffs.cpp index df00fa1..25ff2ce 100644 --- a/src/payoffs/barrier_payoffs.cpp +++ b/src/payoff/barrier_payoffs.cpp @@ -1,11 +1,8 @@ -#pragma once - #include "lfmc/payoff.hpp" #include "lfmc/types.hpp" #include #include -#include #include #include @@ -75,4 +72,4 @@ class DownAndInPut : public Payoff { return std::vector{payoffs}; } }; -} // namespace lfmc \ No newline at end of file +} // namespace lfmc diff --git a/src/control_variate_payoffs.cpp b/src/payoff/control_variate_payoffs.cpp similarity index 97% rename from src/control_variate_payoffs.cpp rename to src/payoff/control_variate_payoffs.cpp index 9267922..7508e8a 100644 --- a/src/control_variate_payoffs.cpp +++ b/src/payoff/control_variate_payoffs.cpp @@ -32,7 +32,7 @@ ControlVariatePayoff::generate_payoffs(const std::vector& paths) const { result.push_back(std::move(combined_row)); } - return std::vector{result}; + return result; } } // namespace lfmc diff --git a/src/european_payoffs.cpp b/src/payoff/european_payoffs.cpp similarity index 100% rename from src/european_payoffs.cpp rename to src/payoff/european_payoffs.cpp diff --git a/src/payoffs/lookback_payoffs.cpp b/src/payoff/lookback_payoffs.cpp similarity index 96% rename from src/payoffs/lookback_payoffs.cpp rename to src/payoff/lookback_payoffs.cpp index 345f1c0..40e98aa 100644 --- a/src/payoffs/lookback_payoffs.cpp +++ b/src/payoff/lookback_payoffs.cpp @@ -1,11 +1,8 @@ -#pragma once - #include "lfmc/payoff.hpp" #include "lfmc/types.hpp" #include #include -#include #include #include @@ -51,4 +48,4 @@ class LookbackPut : public Payoff { } }; -} // namespace lfmc \ No newline at end of file +} // namespace lfmc diff --git a/src/antithetic_random_source.cpp b/src/random_source/antithetic_random_source.cpp similarity index 100% rename from src/antithetic_random_source.cpp rename to src/random_source/antithetic_random_source.cpp diff --git a/src/pseudo_random_source.cpp b/src/random_source/pseudo_random_source.cpp similarity index 100% rename from src/pseudo_random_source.cpp rename to src/random_source/pseudo_random_source.cpp diff --git a/src/geometric_brownian_motion.cpp b/src/stochastic_process/geometric_brownian_motion.cpp similarity index 100% rename from src/geometric_brownian_motion.cpp rename to src/stochastic_process/geometric_brownian_motion.cpp diff --git a/src/timing.cpp b/src/timing/timing.cpp similarity index 100% rename from src/timing.cpp rename to src/timing/timing.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 940f9fe..7ae9a63 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,7 +21,7 @@ set(TEST_SOURCES test_process.cpp test_scheme.cpp test_pipeline.cpp - test_convergency.cpp + # test_convergency.cpp ) add_executable(tests ${TEST_SOURCES}) From 39d3637823078d0b4c9221473013f47b04311abc Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Tue, 3 Mar 2026 11:30:36 -0500 Subject: [PATCH 10/19] =?UTF-8?q?bump:=20version=200.1.0=20=E2=86=92=200.2?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz.yaml | 2 +- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index cc41641..006712f 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: name: cz_conventional_commits tag_format: $version update_changelog_on_bump: true - version: 0.1.0 + version: 0.2.0 version_scheme: semver2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 071f9e8..8120522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +## 0.2.0 (2026-03-03) + +### Feat + +- finished refactor and added control variates +- payoffs and estimator +- estimator and payoff outline +- manager config and small tweaks +- antithetic variates + refactor +- **Payoff**: new payoff concept +- **Simulator.hpp**: new abstraction of simulator which uses its own thread +- more constraints on concepts for clarity and compile-time sizing of number of testing threads +- no variance reduction strategy and small updates to polymorphic constructors +- **ThreadPool.hpp**: threadpool for managing threads specific to monte carlo simulations +- **manager.hpp**: create framework for stochastic processes and numerical schemes + +### Fix + +- fix vr strategy concept +- small work on simulator stuff + +### Refactor + +- sort cpp files into folders and remove archive +- renamed everything, finished pipeline, and converted each step to return vectors of results +- another huge refactor breaking things into different engines +- simplify simulator interface +- refactor some of xander's code +- remove threadpool +- i don't even know lol +- turns out we don't need to the changes i made to the concepts a little bit ago +- **timing.hpp**: rename from timer +- **NumericalScheme.hpp**: adjust concept for schemes for more freedom +- compile-time strategies for process and scheme and runtime strategy for variance reduction +- flesh out the architecture a bit more +- **Timer**: extremely small return change + ## 0.1.0 (2025-12-18) ### Feat From 9ba3c0ee281a6f77d0e22bb0f89f43e72d5c1a05 Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Tue, 3 Mar 2026 12:02:04 -0500 Subject: [PATCH 11/19] refactor: small style changes for timing --- include/lfmc/timing.hpp | 19 +++++++++---------- src/timing/timing.cpp | 6 ++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/include/lfmc/timing.hpp b/include/lfmc/timing.hpp index 90e2e31..8608699 100644 --- a/include/lfmc/timing.hpp +++ b/include/lfmc/timing.hpp @@ -25,9 +25,11 @@ namespace lfmc { class Timer { using clock = std::chrono::high_resolution_clock; + private: + clock::time_point start_time_; + public: - /// Constructor - Timer() noexcept : start_time_(clock::now()) {} + Timer() noexcept; /// Resets the timer to the current time. void reset() noexcept; @@ -36,9 +38,6 @@ class Timer { * @return Elapsed time in milliseconds. */ long long elapsedMilliseconds() const noexcept; - - private: - clock::time_point start_time_; }; /** @@ -51,23 +50,23 @@ class Timer { class ScopedTimer { using clock = std::chrono::high_resolution_clock; + private: + long long* out_; + clock::time_point start_time_; + public: /** @brief Constructs a ScopedTimer that outputs elapsed time to the provided variable. * * @param out Reference to a long long variable where the elapsed time in milliseconds will be * stored upon destruction. */ - explicit ScopedTimer(long long& out) noexcept : out_(&out), start_time_(clock::now()) {} + explicit ScopedTimer(long long& out) noexcept; /** @brief Destructor calculates and stores the elapsed time in milliseconds. * * Upon destruction, the elapsed time since construction is calculated and stored * in the variable provided during construction. */ ~ScopedTimer() noexcept; - - private: - long long* out_; - clock::time_point start_time_; }; } // namespace lfmc diff --git a/src/timing/timing.cpp b/src/timing/timing.cpp index f30b413..3e3e7a0 100644 --- a/src/timing/timing.cpp +++ b/src/timing/timing.cpp @@ -1,7 +1,11 @@ #include "lfmc/timing.hpp" +#include + namespace lfmc { +Timer::Timer() noexcept : start_time_(clock::now()) {} + void Timer::reset() noexcept { start_time_ = clock::now(); } @@ -11,6 +15,8 @@ long long Timer::elapsedMilliseconds() const noexcept { .count(); } +ScopedTimer::ScopedTimer(long long& out) noexcept : out_(&out), start_time_(clock::now()) {} + ScopedTimer::~ScopedTimer() noexcept { *out_ = std::chrono::duration_cast(clock::now() - start_time_).count(); From 9780cbb7fa89c9cd837b29c5075df551cb1d2702 Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Tue, 3 Mar 2026 14:04:31 -0500 Subject: [PATCH 12/19] refactor: use std::expected --- include/lfmc/numerical_scheme.hpp | 8 ++++---- include/lfmc/path_generator.hpp | 11 +++++++---- include/lfmc/pipeline.hpp | 11 ++++------- include/lfmc/random_source.hpp | 10 +++++++--- include/lfmc/stochastic_process.hpp | 8 ++++---- src/random_source/antithetic_random_source.cpp | 3 ++- src/random_source/pseudo_random_source.cpp | 5 ++++- 7 files changed, 32 insertions(+), 24 deletions(-) diff --git a/include/lfmc/numerical_scheme.hpp b/include/lfmc/numerical_scheme.hpp index 5302502..58f7fa7 100644 --- a/include/lfmc/numerical_scheme.hpp +++ b/include/lfmc/numerical_scheme.hpp @@ -10,14 +10,14 @@ namespace lfmc { template concept NumericalScheme = requires(S const& s, P const& p, double t, double x, double dt, double z) { - { s.step(p, t, x, dt, z) } -> std::convertible_to; + { s.step(p, x, t, dt, z) } -> std::convertible_to; }; template class EulerMaruyama { public: - double step(P const& process, double t, double x, double dt, double z) const noexcept { - double drift = process.drift(t, x); - double diffusion = process.diffusion(t, x); + double step(P const& process, double x, double t, double dt, double z) const noexcept { + double drift = process.drift(x, t); + double diffusion = process.diffusion(x, t); return x + drift * dt + diffusion * std::sqrt(dt) * z; } }; diff --git a/include/lfmc/path_generator.hpp b/include/lfmc/path_generator.hpp index 1ec5e92..2f47986 100644 --- a/include/lfmc/path_generator.hpp +++ b/include/lfmc/path_generator.hpp @@ -4,6 +4,10 @@ #include "lfmc/stochastic_process.hpp" #include "lfmc/types.hpp" +#include +#include +#include + namespace lfmc { template @@ -17,9 +21,8 @@ class PathGenerator { PathGenerator(Process process, Scheme scheme) : process_(std::move(process)), scheme_(std::move(scheme)) {} - // TODO move to cpp file - std::vector generate_paths(const std::vector& normals, size_t steps, - double T) const { + std::expected, std::string> + generate_paths(const std::vector& normals, size_t steps, double T) const { const double dt = T / static_cast(steps); std::vector paths; @@ -31,7 +34,7 @@ class PathGenerator { path.push_back(x); for (size_t i = 0; i < steps; ++i) { - x = scheme_.step(process_, t, x, dt, norm[i]); + x = scheme_.step(process_, x, t, dt, norm[i]); path.push_back(x); t += dt; } diff --git a/include/lfmc/pipeline.hpp b/include/lfmc/pipeline.hpp index 96bbb34..a574270 100644 --- a/include/lfmc/pipeline.hpp +++ b/include/lfmc/pipeline.hpp @@ -26,20 +26,17 @@ template NS> class Pipeline { std::expected run(size_t steps, double T) { while (!estimator_->converged()) { - // TODO make all the return types std::expected? IDK if it's a good idea for hot loops - // like this but it would make error handling easier and more consistent. For now just - // return auto normals = random_source_->generate_normals(steps); - if (normals.empty()) { + if (!normals) { return std::unexpected("Failed to generate random normals"); } - auto paths = path_generator_->generate_paths(normals, steps, T); - if (paths.empty()) { + auto paths = path_generator_->generate_paths(normals.value(), steps, T); + if (!paths) { return std::unexpected("Failed to generate paths"); } - auto payoffs = payoff_->generate_payoffs(paths); + auto payoffs = payoff_->generate_payoffs(paths.value()); if (!payoffs) { return std::unexpected("Failed to generate payoffs"); } diff --git a/include/lfmc/random_source.hpp b/include/lfmc/random_source.hpp index 66318c4..fec26a3 100644 --- a/include/lfmc/random_source.hpp +++ b/include/lfmc/random_source.hpp @@ -2,6 +2,7 @@ #include "lfmc/types.hpp" +#include #include #include @@ -10,7 +11,8 @@ namespace lfmc { class RandomSource { public: virtual ~RandomSource() = default; - virtual std::vector generate_normals(size_t steps, size_t n = 1) = 0; + virtual std::expected, std::string> generate_normals(size_t steps, + size_t n = 1) = 0; }; class PseudoRandomSource : public RandomSource { @@ -21,7 +23,8 @@ class PseudoRandomSource : public RandomSource { public: PseudoRandomSource(unsigned seed = std::random_device{}()); - std::vector generate_normals(size_t steps, size_t) override; + std::expected, std::string> generate_normals(size_t steps, + size_t) override; void seed(unsigned seed); }; @@ -35,7 +38,8 @@ class AntitheticRandomSource : public RandomSource { public: AntitheticRandomSource(unsigned seed = std::random_device{}()); - std::vector generate_normals(size_t steps, size_t) override; + std::expected, std::string> generate_normals(size_t steps, + size_t) override; void seed(unsigned seed); }; diff --git a/include/lfmc/stochastic_process.hpp b/include/lfmc/stochastic_process.hpp index 5251ef4..f282a26 100644 --- a/include/lfmc/stochastic_process.hpp +++ b/include/lfmc/stochastic_process.hpp @@ -13,8 +13,8 @@ namespace lfmc { template concept StochasticProcess = requires(P const& p, double t, double x) { { p.initial() } -> std::convertible_to; - { p.drift(t, x) } -> std::convertible_to; - { p.diffusion(t, x) } -> std::convertible_to; + { p.drift(x, t) } -> std::convertible_to; + { p.diffusion(x, t) } -> std::convertible_to; }; class GeometricBrownianMotion { @@ -27,8 +27,8 @@ class GeometricBrownianMotion { GeometricBrownianMotion(double mu, double sigma, double x0); double initial() const noexcept; - double drift(double, double x) const noexcept; - double diffusion(double, double x) const noexcept; + double drift(double x, double) const noexcept; + double diffusion(double x, double) const noexcept; double mu() const noexcept; double sigma() const noexcept; diff --git a/src/random_source/antithetic_random_source.cpp b/src/random_source/antithetic_random_source.cpp index b5fb9e9..7642c53 100644 --- a/src/random_source/antithetic_random_source.cpp +++ b/src/random_source/antithetic_random_source.cpp @@ -3,7 +3,8 @@ namespace lfmc { AntitheticRandomSource::AntitheticRandomSource(unsigned seed) : rng_(seed), dist_(0.0, 1.0) {} -std::vector AntitheticRandomSource::generate_normals(size_t steps, size_t samples) { +std::expected, std::string> +AntitheticRandomSource::generate_normals(size_t steps, size_t samples) { std::vector result(samples, Normals(steps)); for (size_t i = 0; i < samples; ++i) { Normals normals(steps); diff --git a/src/random_source/pseudo_random_source.cpp b/src/random_source/pseudo_random_source.cpp index 64ddcce..b63c8e5 100644 --- a/src/random_source/pseudo_random_source.cpp +++ b/src/random_source/pseudo_random_source.cpp @@ -1,11 +1,14 @@ #include "lfmc/random_source.hpp" #include "lfmc/types.hpp" +#include + namespace lfmc { PseudoRandomSource::PseudoRandomSource(unsigned seed) : rng_(seed), dist_(0.0, 1.0) {} -std::vector PseudoRandomSource::generate_normals(size_t steps, size_t samples) { +std::expected, std::string> +PseudoRandomSource::generate_normals(size_t steps, size_t samples) { std::vector result(samples, Normals(steps)); for (size_t i = 0; i < samples; ++i) { Normals normals(steps); From 28b9e85ae9e1f54b5a7ee0cd6b2668fe0a51c734 Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Mon, 9 Mar 2026 16:52:26 -0400 Subject: [PATCH 13/19] remove comment --- tests/test_pipeline.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_pipeline.cpp b/tests/test_pipeline.cpp index 6c707fa..e55e8bd 100644 --- a/tests/test_pipeline.cpp +++ b/tests/test_pipeline.cpp @@ -6,10 +6,6 @@ using namespace lfmc; // TODO add more test cases, e.g. for convergence criteria, error handling, etc. and dummy types -/* =========================== - Test Cases - =========================== */ - TEST_CASE("Pipeline runs until estimator converges") { GeometricBrownianMotion gbm(0.05, 0.2, 100.0); EulerMaruyama euler; From 859e9dd4ddb4b1a510f257c4ae65d4e8fbbabaca Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Tue, 10 Mar 2026 00:02:13 -0400 Subject: [PATCH 14/19] refactor: move everythig into folders and fix convergency tests --- cmake/README.md | 9 -- include/lfmc/{ => core}/types.hpp | 0 include/lfmc/{ => engine}/engine.hpp | 0 .../control_variate_estimator.hpp} | 24 +----- include/lfmc/estimator/estimator.hpp | 20 +++++ .../lfmc/estimator/monte_carlo_estimator.hpp | 24 ++++++ .../lfmc/numerical_scheme/euler_maruyama.hpp | 17 ++++ .../numerical_scheme.hpp | 11 --- .../{ => path_generator}/path_generator.hpp | 6 +- include/lfmc/payoff/asian_payoffs.hpp | 33 ++++++++ include/lfmc/payoff/barrier_payoffs.hpp | 31 +++++++ .../control_variate_payoffs.hpp} | 32 +------ include/lfmc/payoff/european_payoffs.hpp | 33 ++++++++ include/lfmc/payoff/lookback_payoffs.hpp | 20 +++++ include/lfmc/payoff/payoff.hpp | 18 ++++ include/lfmc/{ => pipeline}/pipeline.hpp | 8 +- include/lfmc/pipeline/pipeline_builder.hpp | 59 +++++++++++++ include/lfmc/random_source.hpp | 47 ----------- .../antithetic_random_source.hpp | 27 ++++++ .../random_source/pseudo_random_source.hpp | 26 ++++++ include/lfmc/random_source/random_source.hpp | 18 ++++ .../geometric_brownian_motion.hpp} | 9 -- .../stochastic_process/stochastic_process.hpp | 20 +++++ include/lfmc/{ => timing}/timing.hpp | 0 src/estimator/control_variate_estimator.cpp | 2 +- src/estimator/monte_carlo_estimator.cpp | 3 +- src/payoff/asian_payoffs.cpp | 67 ++++++--------- src/payoff/barrier_payoffs.cpp | 84 ++++++++----------- src/payoff/control_variate_payoffs.cpp | 2 +- src/payoff/european_payoffs.cpp | 2 +- src/payoff/lookback_payoffs.cpp | 56 ++++++------- .../antithetic_random_source.cpp | 2 +- src/random_source/pseudo_random_source.cpp | 3 +- .../geometric_brownian_motion.cpp | 2 +- src/timing/timing.cpp | 2 +- tests/CMakeLists.txt | 2 +- tests/test_convergency.cpp | 19 +++-- tests/test_pipeline.cpp | 40 ++++++++- tests/test_process.cpp | 2 +- tests/test_scheme.cpp | 6 +- tests/test_timing.cpp | 2 +- 41 files changed, 508 insertions(+), 280 deletions(-) delete mode 100644 cmake/README.md rename include/lfmc/{ => core}/types.hpp (100%) rename include/lfmc/{ => engine}/engine.hpp (100%) rename include/lfmc/{estimator.hpp => estimator/control_variate_estimator.hpp} (58%) create mode 100644 include/lfmc/estimator/estimator.hpp create mode 100644 include/lfmc/estimator/monte_carlo_estimator.hpp create mode 100644 include/lfmc/numerical_scheme/euler_maruyama.hpp rename include/lfmc/{ => numerical_scheme}/numerical_scheme.hpp (77%) rename include/lfmc/{ => path_generator}/path_generator.hpp (88%) create mode 100644 include/lfmc/payoff/asian_payoffs.hpp create mode 100644 include/lfmc/payoff/barrier_payoffs.hpp rename include/lfmc/{payoff.hpp => payoff/control_variate_payoffs.hpp} (51%) create mode 100644 include/lfmc/payoff/european_payoffs.hpp create mode 100644 include/lfmc/payoff/lookback_payoffs.hpp create mode 100644 include/lfmc/payoff/payoff.hpp rename include/lfmc/{ => pipeline}/pipeline.hpp (90%) create mode 100644 include/lfmc/pipeline/pipeline_builder.hpp delete mode 100644 include/lfmc/random_source.hpp create mode 100644 include/lfmc/random_source/antithetic_random_source.hpp create mode 100644 include/lfmc/random_source/pseudo_random_source.hpp create mode 100644 include/lfmc/random_source/random_source.hpp rename include/lfmc/{stochastic_process.hpp => stochastic_process/geometric_brownian_motion.hpp} (68%) create mode 100644 include/lfmc/stochastic_process/stochastic_process.hpp rename include/lfmc/{ => timing}/timing.hpp (100%) diff --git a/cmake/README.md b/cmake/README.md deleted file mode 100644 index 525f879..0000000 --- a/cmake/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# CMake Modules - -This directory contains custom CMake modules and find scripts. - -Example modules that might be added here: -- Custom find scripts for dependencies -- Compiler configuration modules -- Code coverage configuration -- Static analysis tools integration diff --git a/include/lfmc/types.hpp b/include/lfmc/core/types.hpp similarity index 100% rename from include/lfmc/types.hpp rename to include/lfmc/core/types.hpp diff --git a/include/lfmc/engine.hpp b/include/lfmc/engine/engine.hpp similarity index 100% rename from include/lfmc/engine.hpp rename to include/lfmc/engine/engine.hpp diff --git a/include/lfmc/estimator.hpp b/include/lfmc/estimator/control_variate_estimator.hpp similarity index 58% rename from include/lfmc/estimator.hpp rename to include/lfmc/estimator/control_variate_estimator.hpp index cf056f9..a6d8b95 100644 --- a/include/lfmc/estimator.hpp +++ b/include/lfmc/estimator/control_variate_estimator.hpp @@ -1,6 +1,7 @@ #pragma once -#include "lfmc/types.hpp" +#include "lfmc/core/types.hpp" +#include "lfmc/estimator/estimator.hpp" #include #include @@ -8,27 +9,6 @@ namespace lfmc { -class Estimator { - public: - virtual std::expected add_payoffs(const std::vector& payoffs) = 0; - virtual bool converged() const = 0; - virtual std::expected result() const = 0; - // virtual void merge(Estimator const& other) = 0; - virtual ~Estimator() = default; -}; - -class MonteCarloEstimator : public Estimator { - private: - double sum = 0.0; - std::size_t count = 0; - - public: - std::expected add_payoffs(const std::vector& payoffs) override; - bool converged() const override; - std::expected result() const override; - // void merge(Estimator const& other) override; -}; - // TODO if control is analytically known, can put it at payoff level class ControlVariateEstimator : public Estimator { private: diff --git a/include/lfmc/estimator/estimator.hpp b/include/lfmc/estimator/estimator.hpp new file mode 100644 index 0000000..658f01b --- /dev/null +++ b/include/lfmc/estimator/estimator.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "lfmc/core/types.hpp" + +#include +#include +#include + +namespace lfmc { + +class Estimator { + public: + virtual std::expected add_payoffs(const std::vector& payoffs) = 0; + virtual bool converged() const = 0; + virtual std::expected result() const = 0; + // virtual void merge(Estimator const& other) = 0; + virtual ~Estimator() = default; +}; + +} // namespace lfmc diff --git a/include/lfmc/estimator/monte_carlo_estimator.hpp b/include/lfmc/estimator/monte_carlo_estimator.hpp new file mode 100644 index 0000000..5a7407e --- /dev/null +++ b/include/lfmc/estimator/monte_carlo_estimator.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "lfmc/core/types.hpp" +#include "lfmc/estimator/estimator.hpp" + +#include +#include +#include + +namespace lfmc { + +class MonteCarloEstimator : public Estimator { + private: + double sum = 0.0; + std::size_t count = 0; + + public: + std::expected add_payoffs(const std::vector& payoffs) override; + bool converged() const override; + std::expected result() const override; + // void merge(Estimator const& other) override; +}; + +} // namespace lfmc diff --git a/include/lfmc/numerical_scheme/euler_maruyama.hpp b/include/lfmc/numerical_scheme/euler_maruyama.hpp new file mode 100644 index 0000000..27f624c --- /dev/null +++ b/include/lfmc/numerical_scheme/euler_maruyama.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "lfmc/stochastic_process/stochastic_process.hpp" + +#include + +namespace lfmc { + +template class EulerMaruyama { + public: + double step(P const& process, double x, double t, double dt, double z) const noexcept { + double drift = process.drift(x, t); + double diffusion = process.diffusion(x, t); + return x + drift * dt + diffusion * std::sqrt(dt) * z; + } +}; +} // namespace lfmc diff --git a/include/lfmc/numerical_scheme.hpp b/include/lfmc/numerical_scheme/numerical_scheme.hpp similarity index 77% rename from include/lfmc/numerical_scheme.hpp rename to include/lfmc/numerical_scheme/numerical_scheme.hpp index 58f7fa7..31a8cee 100644 --- a/include/lfmc/numerical_scheme.hpp +++ b/include/lfmc/numerical_scheme/numerical_scheme.hpp @@ -1,7 +1,5 @@ #pragma once -#include "lfmc/stochastic_process.hpp" - #include #include @@ -13,15 +11,6 @@ concept NumericalScheme = { s.step(p, x, t, dt, z) } -> std::convertible_to; }; -template class EulerMaruyama { - public: - double step(P const& process, double x, double t, double dt, double z) const noexcept { - double drift = process.drift(x, t); - double diffusion = process.diffusion(x, t); - return x + drift * dt + diffusion * std::sqrt(dt) * z; - } -}; - /** * @brief Exact simulation for Geometric Brownian Motion. * diff --git a/include/lfmc/path_generator.hpp b/include/lfmc/path_generator/path_generator.hpp similarity index 88% rename from include/lfmc/path_generator.hpp rename to include/lfmc/path_generator/path_generator.hpp index 2f47986..e0490e5 100644 --- a/include/lfmc/path_generator.hpp +++ b/include/lfmc/path_generator/path_generator.hpp @@ -1,8 +1,8 @@ #pragma once -#include "lfmc/numerical_scheme.hpp" -#include "lfmc/stochastic_process.hpp" -#include "lfmc/types.hpp" +#include "lfmc/core/types.hpp" +#include "lfmc/numerical_scheme/numerical_scheme.hpp" +#include "lfmc/stochastic_process/stochastic_process.hpp" #include #include diff --git a/include/lfmc/payoff/asian_payoffs.hpp b/include/lfmc/payoff/asian_payoffs.hpp new file mode 100644 index 0000000..78973ed --- /dev/null +++ b/include/lfmc/payoff/asian_payoffs.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "lfmc/core/types.hpp" +#include "lfmc/payoff/payoff.hpp" + +#include +#include + +namespace lfmc { + +class AsianCall : public Payoff { + private: + double strike_; + + public: + explicit AsianCall(double strike); + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override; +}; + +class AsianPut : public Payoff { + private: + double strike_; + + public: + explicit AsianPut(double strike); + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override; +}; + +} // namespace lfmc diff --git a/include/lfmc/payoff/barrier_payoffs.hpp b/include/lfmc/payoff/barrier_payoffs.hpp new file mode 100644 index 0000000..a85abb4 --- /dev/null +++ b/include/lfmc/payoff/barrier_payoffs.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "lfmc/payoff/payoff.hpp" + +namespace lfmc { + +class UpAndOutCall : public Payoff { + private: + double strike_; + double barrier_; + + public: + UpAndOutCall(double strike, double barrier); + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override; +}; + +class DownAndInPut : public Payoff { + private: + double strike_; + double barrier_; + + public: + DownAndInPut(double strike, double barrier); + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override; +}; + +} // namespace lfmc diff --git a/include/lfmc/payoff.hpp b/include/lfmc/payoff/control_variate_payoffs.hpp similarity index 51% rename from include/lfmc/payoff.hpp rename to include/lfmc/payoff/control_variate_payoffs.hpp index 872ea17..5b85021 100644 --- a/include/lfmc/payoff.hpp +++ b/include/lfmc/payoff/control_variate_payoffs.hpp @@ -1,6 +1,7 @@ #pragma once -#include "lfmc/types.hpp" +#include "lfmc/core/types.hpp" +#include "lfmc/payoff/payoff.hpp" #include #include @@ -8,35 +9,6 @@ namespace lfmc { -class Payoff { - public: - virtual std::expected, std::string> - generate_payoffs(const std::vector& paths) const = 0; - virtual ~Payoff() = default; -}; - -class EuropeanCall : public Payoff { - private: - double strike_; - - public: - explicit EuropeanCall(double strike); - - std::expected, std::string> - generate_payoffs(const std::vector& paths) const override; -}; - -class EuropeanPut : public Payoff { - private: - double strike_; - - public: - explicit EuropeanPut(double strike); - - std::expected, std::string> - generate_payoffs(const std::vector& paths) const override; -}; - // TODO subpar in terms of architecture because we have both control variate payoff and estimator, // but for now it's simpler to implement it this way - can enforce with a factory. Can refactor // later if needed. diff --git a/include/lfmc/payoff/european_payoffs.hpp b/include/lfmc/payoff/european_payoffs.hpp new file mode 100644 index 0000000..fa640af --- /dev/null +++ b/include/lfmc/payoff/european_payoffs.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "lfmc/core/types.hpp" +#include "lfmc/payoff/payoff.hpp" + +#include +#include + +namespace lfmc { + +class EuropeanCall : public Payoff { + private: + double strike_; + + public: + explicit EuropeanCall(double strike); + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override; +}; + +class EuropeanPut : public Payoff { + private: + double strike_; + + public: + explicit EuropeanPut(double strike); + + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override; +}; + +} // namespace lfmc diff --git a/include/lfmc/payoff/lookback_payoffs.hpp b/include/lfmc/payoff/lookback_payoffs.hpp new file mode 100644 index 0000000..0a41f8b --- /dev/null +++ b/include/lfmc/payoff/lookback_payoffs.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "lfmc/payoff/payoff.hpp" + +namespace lfmc { + +class LookbackCall : public Payoff { + public: + std::expected, std::string> + + generate_payoffs(const std::vector& paths) const override; +}; + +class LookbackPut : public Payoff { + public: + std::expected, std::string> + generate_payoffs(const std::vector& paths) const override; +}; + +} // namespace lfmc diff --git a/include/lfmc/payoff/payoff.hpp b/include/lfmc/payoff/payoff.hpp new file mode 100644 index 0000000..4f03fa8 --- /dev/null +++ b/include/lfmc/payoff/payoff.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "lfmc/core/types.hpp" + +#include +#include +#include + +namespace lfmc { + +class Payoff { + public: + virtual std::expected, std::string> + generate_payoffs(const std::vector& paths) const = 0; + virtual ~Payoff() = default; +}; + +} // namespace lfmc diff --git a/include/lfmc/pipeline.hpp b/include/lfmc/pipeline/pipeline.hpp similarity index 90% rename from include/lfmc/pipeline.hpp rename to include/lfmc/pipeline/pipeline.hpp index a574270..da243d5 100644 --- a/include/lfmc/pipeline.hpp +++ b/include/lfmc/pipeline/pipeline.hpp @@ -1,9 +1,9 @@ #pragma once -#include "lfmc/estimator.hpp" -#include "lfmc/path_generator.hpp" -#include "lfmc/payoff.hpp" -#include "lfmc/random_source.hpp" +#include "lfmc/estimator/estimator.hpp" +#include "lfmc/path_generator/path_generator.hpp" +#include "lfmc/payoff/payoff.hpp" +#include "lfmc/random_source/random_source.hpp" #include #include diff --git a/include/lfmc/pipeline/pipeline_builder.hpp b/include/lfmc/pipeline/pipeline_builder.hpp new file mode 100644 index 0000000..ba0566b --- /dev/null +++ b/include/lfmc/pipeline/pipeline_builder.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "lfmc/estimator/estimator.hpp" +#include "lfmc/numerical_scheme/numerical_scheme.hpp" +#include "lfmc/path_generator/path_generator.hpp" +#include "lfmc/payoff/payoff.hpp" +#include "lfmc/pipeline/pipeline.hpp" +#include "lfmc/random_source/random_source.hpp" +#include "lfmc/stochastic_process/stochastic_process.hpp" + +namespace lfmc { + +template NS> class PipelineBuilder { + private: + std::unique_ptr random_source_; + std::unique_ptr> path_generator_; + std::unique_ptr payoff_; + std::unique_ptr estimator_; + + public: + PipelineBuilder& random_source(std::unique_ptr rs) { + random_source_ = std::move(rs); + return *this; + } + + PipelineBuilder& path_generator(std::unique_ptr> pg) { + path_generator_ = std::move(pg); + return *this; + } + + PipelineBuilder& payoff(std::unique_ptr p) { + payoff_ = std::move(p); + return *this; + } + + PipelineBuilder& estimator(std::unique_ptr e) { + estimator_ = std::move(e); + return *this; + } + + std::expected, std::string> build() { + if (!random_source_) + return std::unexpected("RandomSource missing"); + + if (!path_generator_) + return std::unexpected("PathGenerator missing"); + + if (!payoff_) + return std::unexpected("Payoff missing"); + + if (!estimator_) + return std::unexpected("Estimator missing"); + + return Pipeline(std::move(random_source_), std::move(path_generator_), + std::move(payoff_), std::move(estimator_)); + } +}; + +} // namespace lfmc diff --git a/include/lfmc/random_source.hpp b/include/lfmc/random_source.hpp deleted file mode 100644 index fec26a3..0000000 --- a/include/lfmc/random_source.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "lfmc/types.hpp" - -#include -#include -#include - -namespace lfmc { - -class RandomSource { - public: - virtual ~RandomSource() = default; - virtual std::expected, std::string> generate_normals(size_t steps, - size_t n = 1) = 0; -}; - -class PseudoRandomSource : public RandomSource { - private: - std::mt19937 rng_; - std::normal_distribution dist_; - - public: - PseudoRandomSource(unsigned seed = std::random_device{}()); - - std::expected, std::string> generate_normals(size_t steps, - size_t) override; - - void seed(unsigned seed); -}; - -class AntitheticRandomSource : public RandomSource { - private: - std::mt19937 rng_; - std::normal_distribution dist_; - bool toggle_ = false; - - public: - AntitheticRandomSource(unsigned seed = std::random_device{}()); - - std::expected, std::string> generate_normals(size_t steps, - size_t) override; - - void seed(unsigned seed); -}; - -} // namespace lfmc diff --git a/include/lfmc/random_source/antithetic_random_source.hpp b/include/lfmc/random_source/antithetic_random_source.hpp new file mode 100644 index 0000000..80fb618 --- /dev/null +++ b/include/lfmc/random_source/antithetic_random_source.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "lfmc/core/types.hpp" +#include "lfmc/random_source/random_source.hpp" + +#include +#include +#include + +namespace lfmc { + +class AntitheticRandomSource : public RandomSource { + private: + std::mt19937 rng_; + std::normal_distribution dist_; + bool toggle_ = false; + + public: + AntitheticRandomSource(unsigned seed = std::random_device{}()); + + std::expected, std::string> generate_normals(size_t steps, + size_t) override; + + void seed(unsigned seed); +}; + +} // namespace lfmc diff --git a/include/lfmc/random_source/pseudo_random_source.hpp b/include/lfmc/random_source/pseudo_random_source.hpp new file mode 100644 index 0000000..a13cabf --- /dev/null +++ b/include/lfmc/random_source/pseudo_random_source.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "lfmc/core/types.hpp" +#include "lfmc/random_source/random_source.hpp" + +#include +#include +#include + +namespace lfmc { + +class PseudoRandomSource : public RandomSource { + private: + std::mt19937 rng_; + std::normal_distribution dist_; + + public: + PseudoRandomSource(unsigned seed = std::random_device{}()); + + std::expected, std::string> generate_normals(size_t steps, + size_t) override; + + void seed(unsigned seed); +}; + +} // namespace lfmc diff --git a/include/lfmc/random_source/random_source.hpp b/include/lfmc/random_source/random_source.hpp new file mode 100644 index 0000000..234f019 --- /dev/null +++ b/include/lfmc/random_source/random_source.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "lfmc/core/types.hpp" + +#include +#include +#include + +namespace lfmc { + +class RandomSource { + public: + virtual ~RandomSource() = default; + virtual std::expected, std::string> generate_normals(size_t steps, + size_t n = 1) = 0; +}; + +} // namespace lfmc diff --git a/include/lfmc/stochastic_process.hpp b/include/lfmc/stochastic_process/geometric_brownian_motion.hpp similarity index 68% rename from include/lfmc/stochastic_process.hpp rename to include/lfmc/stochastic_process/geometric_brownian_motion.hpp index f282a26..b8eb6b3 100644 --- a/include/lfmc/stochastic_process.hpp +++ b/include/lfmc/stochastic_process/geometric_brownian_motion.hpp @@ -1,7 +1,5 @@ #pragma once -#include - /** * @file StochasticProcess.hpp * @brief Defines the StochasticProcess concept for stochastic differential equations (SDEs) and @@ -10,13 +8,6 @@ namespace lfmc { -template -concept StochasticProcess = requires(P const& p, double t, double x) { - { p.initial() } -> std::convertible_to; - { p.drift(x, t) } -> std::convertible_to; - { p.diffusion(x, t) } -> std::convertible_to; -}; - class GeometricBrownianMotion { private: double mu_; diff --git a/include/lfmc/stochastic_process/stochastic_process.hpp b/include/lfmc/stochastic_process/stochastic_process.hpp new file mode 100644 index 0000000..abfe25d --- /dev/null +++ b/include/lfmc/stochastic_process/stochastic_process.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +/** + * @file StochasticProcess.hpp + * @brief Defines the StochasticProcess concept for stochastic differential equations (SDEs) and + * provides some implementations. + */ + +namespace lfmc { + +template +concept StochasticProcess = requires(P const& p, double t, double x) { + { p.initial() } -> std::convertible_to; + { p.drift(x, t) } -> std::convertible_to; + { p.diffusion(x, t) } -> std::convertible_to; +}; + +} // namespace lfmc diff --git a/include/lfmc/timing.hpp b/include/lfmc/timing/timing.hpp similarity index 100% rename from include/lfmc/timing.hpp rename to include/lfmc/timing/timing.hpp diff --git a/src/estimator/control_variate_estimator.cpp b/src/estimator/control_variate_estimator.cpp index 678a669..a23f141 100644 --- a/src/estimator/control_variate_estimator.cpp +++ b/src/estimator/control_variate_estimator.cpp @@ -1,4 +1,4 @@ -#include "lfmc/estimator.hpp" +#include "lfmc/estimator/control_variate_estimator.hpp" namespace lfmc { diff --git a/src/estimator/monte_carlo_estimator.cpp b/src/estimator/monte_carlo_estimator.cpp index 6c013d5..3ffd131 100644 --- a/src/estimator/monte_carlo_estimator.cpp +++ b/src/estimator/monte_carlo_estimator.cpp @@ -1,5 +1,4 @@ -#include "lfmc/estimator.hpp" -#include "lfmc/types.hpp" +#include "lfmc/estimator/monte_carlo_estimator.hpp" #include diff --git a/src/payoff/asian_payoffs.cpp b/src/payoff/asian_payoffs.cpp index f58707e..bcc2acc 100644 --- a/src/payoff/asian_payoffs.cpp +++ b/src/payoff/asian_payoffs.cpp @@ -1,5 +1,4 @@ -#include "lfmc/payoff.hpp" -#include "lfmc/types.hpp" +#include "lfmc/payoff/asian_payoffs.hpp" #include #include @@ -9,58 +8,44 @@ namespace lfmc { -class AsianCall : public Payoff { - private: - double strike_; +AsianCall::AsianCall(double strike) : strike_(strike) {} - public: - explicit AsianCall(double strike) : strike_(strike) {} +std::expected, std::string> +AsianCall::generate_payoffs(const std::vector& paths) const { + Payoffs payoffs; - std::expected, std::string> - generate_payoffs(const std::vector& paths) const override { - Payoffs payoffs; + payoffs.reserve(paths.size()); - payoffs.reserve(paths.size()); + for (const auto& path : paths) { - for (const auto& path : paths) { + if (path.empty()) + return std::unexpected("Empty path encountered in AsianCall"); - if (path.empty()) - return std::unexpected("Empty path encountered in AsianCall"); - - double mean = - std::reduce(path.begin(), path.end(), 0.0) / static_cast(path.size()); - payoffs.push_back(std::max(mean - strike_, 0.0)); - } - - return std::vector{payoffs}; + double mean = std::reduce(path.begin(), path.end(), 0.0) / static_cast(path.size()); + payoffs.push_back(std::max(mean - strike_, 0.0)); } -}; -class AsianPut : public Payoff { - private: - double strike_; + return std::vector{payoffs}; +} - public: - explicit AsianPut(double strike) : strike_(strike) {} +AsianPut::AsianPut(double strike) : strike_(strike) {} - std::expected, std::string> - generate_payoffs(const std::vector& paths) const override { +std::expected, std::string> +AsianPut::generate_payoffs(const std::vector& paths) const { - Payoffs payoffs; + Payoffs payoffs; - payoffs.reserve(paths.size()); + payoffs.reserve(paths.size()); - for (const auto& path : paths) { - if (path.empty()) - return std::unexpected("Empty path encountered in AsianPut"); + for (const auto& path : paths) { + if (path.empty()) + return std::unexpected("Empty path encountered in AsianPut"); - double mean = - std::reduce(path.begin(), path.end(), 0.0) / static_cast(path.size()); - payoffs.push_back(std::max(strike_ - mean, 0.0)); - } - - return std::vector{payoffs}; + double mean = std::reduce(path.begin(), path.end(), 0.0) / static_cast(path.size()); + payoffs.push_back(std::max(strike_ - mean, 0.0)); } -}; + + return std::vector{payoffs}; +} } // namespace lfmc diff --git a/src/payoff/barrier_payoffs.cpp b/src/payoff/barrier_payoffs.cpp index 25ff2ce..8ed2c7c 100644 --- a/src/payoff/barrier_payoffs.cpp +++ b/src/payoff/barrier_payoffs.cpp @@ -1,5 +1,4 @@ -#include "lfmc/payoff.hpp" -#include "lfmc/types.hpp" +#include "lfmc/payoff/barrier_payoffs.hpp" #include #include @@ -8,68 +7,55 @@ namespace lfmc { -class UpAndOutCall : public Payoff { - private: - double strike_; - double barrier_; +UpAndOutCall::UpAndOutCall(double strike, double barrier) : strike_(strike), barrier_(barrier) {} - public: - UpAndOutCall(double strike, double barrier) : strike_(strike), barrier_(barrier) {} +std::expected, std::string> +UpAndOutCall::generate_payoffs(const std::vector& paths) const { - std::expected, std::string> - generate_payoffs(const std::vector& paths) const override { + Payoffs payoffs; + payoffs.reserve(paths.size()); - Payoffs payoffs; - payoffs.reserve(paths.size()); + for (const auto& path : paths) { + if (path.empty()) + return std::unexpected("Empty path encountered in UpAndOutCall"); - for (const auto& path : paths) { - if (path.empty()) - return std::unexpected("Empty path encountered in UpAndOutCall"); + bool knocked_out = + std::any_of(path.begin(), path.end(), [this](double s) { return s >= barrier_; }); - bool knocked_out = - std::any_of(path.begin(), path.end(), [this](double s) { return s >= barrier_; }); - - if (knocked_out) { - payoffs.push_back(0.0); - } else { - payoffs.push_back(std::max(path.back() - strike_, 0.0)); - } + if (knocked_out) { + payoffs.push_back(0.0); + } else { + payoffs.push_back(std::max(path.back() - strike_, 0.0)); } - - return std::vector{payoffs}; } -}; -class DownAndInPut : public Payoff { - private: - double strike_; - double barrier_; + return std::vector{payoffs}; +} - public: - DownAndInPut(double strike, double barrier) : strike_(strike), barrier_(barrier) {} +DownAndInPut::DownAndInPut(double strike, double barrier) : strike_(strike), barrier_(barrier) {} - std::expected, std::string> - generate_payoffs(const std::vector& paths) const override { - Payoffs payoffs; - payoffs.reserve(paths.size()); +std::expected, std::string> +DownAndInPut::generate_payoffs(const std::vector& paths) const { + Payoffs payoffs; + payoffs.reserve(paths.size()); - for (const auto& path : paths) { + for (const auto& path : paths) { - if (path.empty()) - return std::unexpected("Empty path encountered in DownAndInPut"); + if (path.empty()) + return std::unexpected("Empty path encountered in DownAndInPut"); - bool knocked_in = - std::any_of(path.begin(), path.end(), [this](double s) { return s <= barrier_; }); + bool knocked_in = + std::any_of(path.begin(), path.end(), [this](double s) { return s <= barrier_; }); - if (knocked_in) { - payoffs.push_back(std::max(strike_ - path.back(), 0.0)); + if (knocked_in) { + payoffs.push_back(std::max(strike_ - path.back(), 0.0)); - } else { - payoffs.push_back(0.0); - } + } else { + payoffs.push_back(0.0); } - - return std::vector{payoffs}; } -}; + + return std::vector{payoffs}; +} + } // namespace lfmc diff --git a/src/payoff/control_variate_payoffs.cpp b/src/payoff/control_variate_payoffs.cpp index 7508e8a..3a3ddb6 100644 --- a/src/payoff/control_variate_payoffs.cpp +++ b/src/payoff/control_variate_payoffs.cpp @@ -1,4 +1,4 @@ -#include "lfmc/payoff.hpp" +#include "lfmc/payoff/control_variate_payoffs.hpp" #include #include diff --git a/src/payoff/european_payoffs.cpp b/src/payoff/european_payoffs.cpp index 29b5fef..1b24ba4 100644 --- a/src/payoff/european_payoffs.cpp +++ b/src/payoff/european_payoffs.cpp @@ -1,4 +1,4 @@ -#include "lfmc/payoff.hpp" +#include "lfmc/payoff/european_payoffs.hpp" #include #include diff --git a/src/payoff/lookback_payoffs.cpp b/src/payoff/lookback_payoffs.cpp index 40e98aa..2c7b699 100644 --- a/src/payoff/lookback_payoffs.cpp +++ b/src/payoff/lookback_payoffs.cpp @@ -1,5 +1,4 @@ -#include "lfmc/payoff.hpp" -#include "lfmc/types.hpp" +#include "lfmc/payoff/lookback_payoffs.hpp" #include #include @@ -8,44 +7,37 @@ namespace lfmc { -class LookbackCall : public Payoff { - public: - std::expected, std::string> +std::expected, std::string> +LookbackCall::generate_payoffs(const std::vector& paths) const { + Payoffs payoffs; + payoffs.reserve(paths.size()); - generate_payoffs(const std::vector& paths) const override { - Payoffs payoffs; - payoffs.reserve(paths.size()); + for (const auto& path : paths) { + if (path.empty()) + return std::unexpected("Empty path encountered in LookbackCall"); - for (const auto& path : paths) { - if (path.empty()) - return std::unexpected("Empty path encountered in LookbackCall"); - - double min_price = *std::min_element(path.begin(), path.end()); - payoffs.push_back(path.back() - min_price); - } - - return std::vector{payoffs}; + double min_price = *std::min_element(path.begin(), path.end()); + payoffs.push_back(path.back() - min_price); } -}; -class LookbackPut : public Payoff { - public: - std::expected, std::string> - generate_payoffs(const std::vector& paths) const override { - Payoffs payoffs; - payoffs.reserve(paths.size()); + return std::vector{payoffs}; +} - for (const auto& path : paths) { - if (path.empty()) - return std::unexpected("Empty path encountered in LookbackPut"); +std::expected, std::string> +LookbackPut::generate_payoffs(const std::vector& paths) const { + Payoffs payoffs; + payoffs.reserve(paths.size()); - double max_price = *std::max_element(path.begin(), path.end()); + for (const auto& path : paths) { + if (path.empty()) + return std::unexpected("Empty path encountered in LookbackPut"); - payoffs.push_back(max_price - path.back()); - } + double max_price = *std::max_element(path.begin(), path.end()); - return std::vector{payoffs}; + payoffs.push_back(max_price - path.back()); } -}; + + return std::vector{payoffs}; +} } // namespace lfmc diff --git a/src/random_source/antithetic_random_source.cpp b/src/random_source/antithetic_random_source.cpp index 7642c53..210e7f6 100644 --- a/src/random_source/antithetic_random_source.cpp +++ b/src/random_source/antithetic_random_source.cpp @@ -1,4 +1,4 @@ -#include "lfmc/random_source.hpp" +#include "lfmc/random_source/antithetic_random_source.hpp" namespace lfmc { AntitheticRandomSource::AntitheticRandomSource(unsigned seed) : rng_(seed), dist_(0.0, 1.0) {} diff --git a/src/random_source/pseudo_random_source.cpp b/src/random_source/pseudo_random_source.cpp index b63c8e5..9c750d4 100644 --- a/src/random_source/pseudo_random_source.cpp +++ b/src/random_source/pseudo_random_source.cpp @@ -1,5 +1,4 @@ -#include "lfmc/random_source.hpp" -#include "lfmc/types.hpp" +#include "lfmc/random_source/pseudo_random_source.hpp" #include diff --git a/src/stochastic_process/geometric_brownian_motion.cpp b/src/stochastic_process/geometric_brownian_motion.cpp index 911d866..4666569 100644 --- a/src/stochastic_process/geometric_brownian_motion.cpp +++ b/src/stochastic_process/geometric_brownian_motion.cpp @@ -1,4 +1,4 @@ -#include "lfmc/stochastic_process.hpp" +#include "lfmc/stochastic_process/geometric_brownian_motion.hpp" namespace lfmc { diff --git a/src/timing/timing.cpp b/src/timing/timing.cpp index 3e3e7a0..70b9885 100644 --- a/src/timing/timing.cpp +++ b/src/timing/timing.cpp @@ -1,4 +1,4 @@ -#include "lfmc/timing.hpp" +#include "lfmc/timing/timing.hpp" #include diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7ae9a63..940f9fe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,7 +21,7 @@ set(TEST_SOURCES test_process.cpp test_scheme.cpp test_pipeline.cpp - # test_convergency.cpp + test_convergency.cpp ) add_executable(tests ${TEST_SOURCES}) diff --git a/tests/test_convergency.cpp b/tests/test_convergency.cpp index 6db1724..028f5f3 100644 --- a/tests/test_convergency.cpp +++ b/tests/test_convergency.cpp @@ -1,8 +1,13 @@ -#include "lfmc/payoffs/asian_payoffs.hpp" -#include "lfmc/payoffs/barrier_payoffs.hpp" -#include "lfmc/payoffs/lookback_payoffs.hpp" -#include "lfmc/pipeline.hpp" -#include "lfmc/timing.hpp" +#include "lfmc/estimator/monte_carlo_estimator.hpp" +#include "lfmc/numerical_scheme/euler_maruyama.hpp" +#include "lfmc/payoff/asian_payoffs.hpp" +#include "lfmc/payoff/barrier_payoffs.hpp" +#include "lfmc/payoff/european_payoffs.hpp" +#include "lfmc/payoff/lookback_payoffs.hpp" +#include "lfmc/pipeline/pipeline.hpp" +#include "lfmc/random_source/pseudo_random_source.hpp" +#include "lfmc/stochastic_process/geometric_brownian_motion.hpp" +#include "lfmc/timing/timing.hpp" #include #include @@ -95,7 +100,7 @@ run_convergence(const std::string& label, PayoffFactory make_payoff, double grou PathGenerator>>(gbm, euler); auto po = make_payoff(); - auto est = std::make_unique(n); // see note below + auto est = std::make_unique(); // see note below Pipeline> pipeline( std::move(rs), std::move(pg), std::move(po), std::move(est)); @@ -184,4 +189,4 @@ TEST_CASE("Sanity check: European Call matches Black-Scholes", "[sanity][europea auto& last = results.back(); REQUIRE_THAT(last.estimate, WithinAbs(last.ground_truth, 0.20)); -} \ No newline at end of file +} diff --git a/tests/test_pipeline.cpp b/tests/test_pipeline.cpp index e55e8bd..8944315 100644 --- a/tests/test_pipeline.cpp +++ b/tests/test_pipeline.cpp @@ -1,6 +1,16 @@ -#include "lfmc/pipeline.hpp" +#include "lfmc/estimator/control_variate_estimator.hpp" +#include "lfmc/estimator/monte_carlo_estimator.hpp" +#include "lfmc/numerical_scheme/euler_maruyama.hpp" +#include "lfmc/payoff/control_variate_payoffs.hpp" +#include "lfmc/payoff/european_payoffs.hpp" +#include "lfmc/pipeline/pipeline.hpp" +#include "lfmc/pipeline/pipeline_builder.hpp" +#include "lfmc/random_source/antithetic_random_source.hpp" +#include "lfmc/random_source/pseudo_random_source.hpp" +#include "lfmc/stochastic_process/geometric_brownian_motion.hpp" #include +#include using namespace lfmc; @@ -75,6 +85,34 @@ TEST_CASE("Pipeline with control variates") { // debug control variate implementation } +TEST_CASE("Pipeline builder works", "[PipelineBuilder]") { + GeometricBrownianMotion gbm(0.05, 0.2, 100.0); + EulerMaruyama euler; + + auto rs = std::make_unique(); + auto pg = std::make_unique< + PathGenerator>>(gbm, euler); + auto po = std::make_unique(100.0); + auto est = std::make_unique(); + + auto pipeline = + PipelineBuilder>() + .random_source(std::move(rs)) + .path_generator(std::move(pg)) + .payoff(std::move(po)) + .estimator(std::move(est)) + .build(); + + REQUIRE(pipeline.has_value()); + + auto result = pipeline->run(10, 1.0); + + INFO("Pipeline result: " << (result.has_value() ? std::to_string(result.value()) + : result.error())); + REQUIRE(result.has_value()); + REQUIRE(result.value() > 0.0); // Price should be positive +} + TEST_CASE("Pipeline stops early if estimator add_payoffs fails") { // TODO } diff --git a/tests/test_process.cpp b/tests/test_process.cpp index 74d5109..9f648c7 100644 --- a/tests/test_process.cpp +++ b/tests/test_process.cpp @@ -1,4 +1,4 @@ -#include "lfmc/stochastic_process.hpp" +#include "lfmc/stochastic_process/geometric_brownian_motion.hpp" #include #include diff --git a/tests/test_scheme.cpp b/tests/test_scheme.cpp index b1d3678..2100454 100644 --- a/tests/test_scheme.cpp +++ b/tests/test_scheme.cpp @@ -1,8 +1,10 @@ -#include "lfmc/numerical_scheme.hpp" -#include "lfmc/stochastic_process.hpp" +#include "lfmc/numerical_scheme/euler_maruyama.hpp" +#include "lfmc/stochastic_process/geometric_brownian_motion.hpp" +#include "lfmc/stochastic_process/stochastic_process.hpp" #include #include +#include using Catch::Matchers::WithinAbs; diff --git a/tests/test_timing.cpp b/tests/test_timing.cpp index 0f12b8c..0e21d64 100644 --- a/tests/test_timing.cpp +++ b/tests/test_timing.cpp @@ -1,4 +1,4 @@ -#include "lfmc/timing.hpp" +#include "lfmc/timing/timing.hpp" #include #include From 40e45ab4e5c1fadae2bea98d12bd9920132555ed Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Tue, 10 Mar 2026 00:15:48 -0400 Subject: [PATCH 15/19] feat: setting up merge logic for future threading --- include/lfmc/engine/engine.hpp | 20 ++++++++++++++++++- .../estimator/control_variate_estimator.hpp | 2 +- include/lfmc/estimator/estimator.hpp | 2 +- .../lfmc/estimator/monte_carlo_estimator.hpp | 2 +- src/estimator/control_variate_estimator.cpp | 15 ++++++++++++++ src/estimator/monte_carlo_estimator.cpp | 16 ++++++++++----- 6 files changed, 48 insertions(+), 9 deletions(-) diff --git a/include/lfmc/engine/engine.hpp b/include/lfmc/engine/engine.hpp index 0750718..28262dd 100644 --- a/include/lfmc/engine/engine.hpp +++ b/include/lfmc/engine/engine.hpp @@ -1,3 +1,21 @@ #pragma once -// TODO move headers into folders +#include "lfmc/pipeline/pipeline.hpp" +#include "lfmc/pipeline/pipeline_builder.hpp" + +namespace lfmc { + +// class Engine { +// private: +// Pipeline> pipeline_; +// +// public: +// Engine(Pipeline> pipeline) +// : pipeline_(std::move(pipeline)) {} +// +// std::expected run() { +// return pipeline_.run(); +// } +// }; + +} // namespace lfmc diff --git a/include/lfmc/estimator/control_variate_estimator.hpp b/include/lfmc/estimator/control_variate_estimator.hpp index a6d8b95..403ab06 100644 --- a/include/lfmc/estimator/control_variate_estimator.hpp +++ b/include/lfmc/estimator/control_variate_estimator.hpp @@ -28,7 +28,7 @@ class ControlVariateEstimator : public Estimator { std::expected add_payoffs(const std::vector& payoffs) override; bool converged() const override; std::expected result() const override; - // void merge(Estimator const& other) override; + std::expected merge(Estimator const& other) override; }; } // namespace lfmc diff --git a/include/lfmc/estimator/estimator.hpp b/include/lfmc/estimator/estimator.hpp index 658f01b..b693aae 100644 --- a/include/lfmc/estimator/estimator.hpp +++ b/include/lfmc/estimator/estimator.hpp @@ -13,7 +13,7 @@ class Estimator { virtual std::expected add_payoffs(const std::vector& payoffs) = 0; virtual bool converged() const = 0; virtual std::expected result() const = 0; - // virtual void merge(Estimator const& other) = 0; + virtual std::expected merge(Estimator const& other) = 0; virtual ~Estimator() = default; }; diff --git a/include/lfmc/estimator/monte_carlo_estimator.hpp b/include/lfmc/estimator/monte_carlo_estimator.hpp index 5a7407e..a139297 100644 --- a/include/lfmc/estimator/monte_carlo_estimator.hpp +++ b/include/lfmc/estimator/monte_carlo_estimator.hpp @@ -18,7 +18,7 @@ class MonteCarloEstimator : public Estimator { std::expected add_payoffs(const std::vector& payoffs) override; bool converged() const override; std::expected result() const override; - // void merge(Estimator const& other) override; + std::expected merge(Estimator const& other) override; }; } // namespace lfmc diff --git a/src/estimator/control_variate_estimator.cpp b/src/estimator/control_variate_estimator.cpp index a23f141..6730a9d 100644 --- a/src/estimator/control_variate_estimator.cpp +++ b/src/estimator/control_variate_estimator.cpp @@ -50,4 +50,19 @@ std::expected ControlVariateEstimator::result() const { return mean_x - beta * (mean_y - control_expectation_); } +std::expected ControlVariateEstimator::merge(Estimator const& other) { + const auto* other_estimator = dynamic_cast(&other); + if (!other_estimator) { + return std::unexpected("Incompatible estimator type for merging"); + } + + count += other_estimator->count; + sum_x += other_estimator->sum_x; + sum_y += other_estimator->sum_y; + sum_xy += other_estimator->sum_xy; + sum_yy += other_estimator->sum_yy; + + return {}; +} + } // namespace lfmc diff --git a/src/estimator/monte_carlo_estimator.cpp b/src/estimator/monte_carlo_estimator.cpp index 3ffd131..53d0d5f 100644 --- a/src/estimator/monte_carlo_estimator.cpp +++ b/src/estimator/monte_carlo_estimator.cpp @@ -33,10 +33,16 @@ std::expected MonteCarloEstimator::result() const { return {sum / static_cast(count)}; } -// void MonteCarloEstimator::merge(Estimator const& other) { -// auto const& mcOther = dynamic_cast(other); -// sum += mcOther.sum; -// count += mcOther.count; -// } +std::expected MonteCarloEstimator::merge(Estimator const& other) { + const auto* other_estimator = dynamic_cast(&other); + if (!other_estimator) { + return std::unexpected("Incompatible estimator type for merging."); + } + + sum += other_estimator->sum; + count += other_estimator->count; + + return {}; +} } // namespace lfmc From be3e7c1cb2123aa1f6a1e58ef00e6f52bea954b2 Mon Sep 17 00:00:00 2001 From: Alexander Robbins Date: Tue, 10 Mar 2026 16:16:10 -0400 Subject: [PATCH 16/19] added multi strat functions --- .../estimator/control_variate_estimator.hpp | 9 ++- .../lfmc/estimator/monte_carlo_estimator.hpp | 6 ++ .../lfmc/path_generator/path_generator.hpp | 4 +- include/lfmc/pipeline/pipeline.hpp | 9 +++ .../lfmc/strategy/multi_strategy_runner.hpp | 77 ++++++++++++++++++ include/lfmc/strategy/strategy_factory.hpp | 79 +++++++++++++++++++ include/lfmc/strategy/strategy_metrics.hpp | 21 +++++ include/lfmc/strategy/strategy_runner.hpp | 72 +++++++++++++++++ src/estimator/control_variate_estimator.cpp | 46 +++++++++++ src/estimator/monte_carlo_estimator.cpp | 28 ++++++- .../geometric_brownian_motion.cpp | 9 ++- tests/CMakeLists.txt | 1 + tests/test_multi_strategy.cpp | 69 ++++++++++++++++ 13 files changed, 421 insertions(+), 9 deletions(-) create mode 100644 include/lfmc/strategy/multi_strategy_runner.hpp create mode 100644 include/lfmc/strategy/strategy_factory.hpp create mode 100644 include/lfmc/strategy/strategy_metrics.hpp create mode 100644 include/lfmc/strategy/strategy_runner.hpp create mode 100644 tests/test_multi_strategy.cpp diff --git a/include/lfmc/estimator/control_variate_estimator.hpp b/include/lfmc/estimator/control_variate_estimator.hpp index 403ab06..27b4b74 100644 --- a/include/lfmc/estimator/control_variate_estimator.hpp +++ b/include/lfmc/estimator/control_variate_estimator.hpp @@ -2,7 +2,8 @@ #include "lfmc/core/types.hpp" #include "lfmc/estimator/estimator.hpp" - +#include +#include #include #include #include @@ -18,6 +19,7 @@ class ControlVariateEstimator : public Estimator { double sum_y = 0.0; // Sum of control variate values double sum_xy = 0.0; // Sum of products of payoffs and control variate double sum_yy = 0.0; // Sum of squares of control variate values + double sum_xx = 0.0; double control_expectation_ = 0.0; // Expected value of control variate (known analytically) @@ -29,6 +31,11 @@ class ControlVariateEstimator : public Estimator { bool converged() const override; std::expected result() const override; std::expected merge(Estimator const& other) override; + + double mean() const noexcept; + double variance() const noexcept; + double std_error() const noexcept; + std::size_t sample_count() const noexcept; }; } // namespace lfmc diff --git a/include/lfmc/estimator/monte_carlo_estimator.hpp b/include/lfmc/estimator/monte_carlo_estimator.hpp index a139297..47cdbb2 100644 --- a/include/lfmc/estimator/monte_carlo_estimator.hpp +++ b/include/lfmc/estimator/monte_carlo_estimator.hpp @@ -12,6 +12,7 @@ namespace lfmc { class MonteCarloEstimator : public Estimator { private: double sum = 0.0; + double sum_sq = 0.0; std::size_t count = 0; public: @@ -19,6 +20,11 @@ class MonteCarloEstimator : public Estimator { bool converged() const override; std::expected result() const override; std::expected merge(Estimator const& other) override; + + double mean() const noexcept; + double variance() const noexcept; + double std_error() const noexcept; + std::size_t sample_count() const noexcept; }; } // namespace lfmc diff --git a/include/lfmc/path_generator/path_generator.hpp b/include/lfmc/path_generator/path_generator.hpp index e0490e5..8cfe869 100644 --- a/include/lfmc/path_generator/path_generator.hpp +++ b/include/lfmc/path_generator/path_generator.hpp @@ -27,8 +27,8 @@ class PathGenerator { std::vector paths; for (const auto& norm : normals) { - Path path(steps + 1); - + Path path; + path.reserve(steps + 1); double t = 0.0; double x = process_.initial(); path.push_back(x); diff --git a/include/lfmc/pipeline/pipeline.hpp b/include/lfmc/pipeline/pipeline.hpp index da243d5..7965b87 100644 --- a/include/lfmc/pipeline/pipeline.hpp +++ b/include/lfmc/pipeline/pipeline.hpp @@ -49,6 +49,15 @@ template NS> class Pipeline { return estimator_->result(); } + + const Estimator* get_estimator() const noexcept { + return estimator_.get(); + } + + Estimator* get_estimator() noexcept { + return estimator_.get(); + } + }; } // namespace lfmc diff --git a/include/lfmc/strategy/multi_strategy_runner.hpp b/include/lfmc/strategy/multi_strategy_runner.hpp new file mode 100644 index 0000000..96cd932 --- /dev/null +++ b/include/lfmc/strategy/multi_strategy_runner.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include "lfmc/strategy/strategy_metrics.hpp" +#include "lfmc/strategy/strategy_runner.hpp" + +#include +#include +#include +#include + +namespace lfmc { + +template NS> +class MultiStrategyRunner { +private: + std::vector>> strategies; + std::vector threads; + +public: + void add_strategy(std::unique_ptr> strategy) { + strategies.push_back(std::move(strategy)); + } + + // Run all strategies for a warmup period + std::expected run_warmup(size_t steps, double T, + size_t warmup_iterations) { + threads.clear(); + threads.reserve(strategies.size()); + + for (auto& strategy : strategies) { + threads.emplace_back([&strategy, steps, T, warmup_iterations]() { + for (size_t i = 0; i < warmup_iterations; ++i) { + auto result = strategy->run_batch(steps, T); + if (!result) { + // Error occurred - need this to get log + return; + } + } + }); + } + + for (auto& thread : threads) { + if (thread.joinable()) { + thread.join(); + } + } + + return {}; + } + + // Get current metricss + std::vector get_all_metrics() const { + std::vector metrics; + metrics.reserve(strategies.size()); + + for (const auto& strategy : strategies) { + metrics.push_back(strategy->get_metrics()); + } + + return metrics; + } + + // Find best strat + size_t best_strategy_index() const { + auto metrics = get_all_metrics(); + if (metrics.empty()) { + return 0; + } + return std::min_element(metrics.begin(), metrics.end()) - metrics.begin(); + } + + size_t strategy_count() const noexcept { + return strategies.size(); + } +}; + +} // namespace lfmc \ No newline at end of file diff --git a/include/lfmc/strategy/strategy_factory.hpp b/include/lfmc/strategy/strategy_factory.hpp new file mode 100644 index 0000000..bdf2fbf --- /dev/null +++ b/include/lfmc/strategy/strategy_factory.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "lfmc/estimator/control_variate_estimator.hpp" +#include "lfmc/estimator/monte_carlo_estimator.hpp" +#include "lfmc/numerical_scheme/euler_maruyama.hpp" +#include "lfmc/path_generator/path_generator.hpp" +#include "lfmc/payoff/control_variate_payoffs.hpp" +#include "lfmc/payoff/payoff.hpp" +#include "lfmc/pipeline/pipeline.hpp" +#include "lfmc/random_source/antithetic_random_source.hpp" +#include "lfmc/random_source/pseudo_random_source.hpp" +#include "lfmc/strategy/strategy_runner.hpp" +#include "lfmc/stochastic_process/stochastic_process.hpp" + +#include +#include +#include + +namespace lfmc { + +template NS> +class StrategyFactory { +public: + static std::unique_ptr> + create_pseudo_random_strategy(SP process, NS scheme, + std::unique_ptr payoff, + unsigned seed = std::random_device{}()) { + auto rs = std::make_unique(seed); + auto pg = std::make_unique>(process, scheme); + auto est = std::make_unique(); + + Pipeline pipeline(std::move(rs), std::move(pg), + std::move(payoff), std::move(est)); + + return std::make_unique>( + std::move(pipeline), "PseudoRandom"); + } + + static std::unique_ptr> + create_antithetic_strategy(SP process, NS scheme, + std::unique_ptr payoff, + unsigned seed = std::random_device{}()) { + auto rs = std::make_unique(seed); + auto pg = std::make_unique>(process, scheme); + auto est = std::make_unique(); + + Pipeline pipeline(std::move(rs), std::move(pg), + std::move(payoff), std::move(est)); + + return std::make_unique>( + std::move(pipeline), "Antithetic"); + } + + static std::unique_ptr> + create_control_variate_strategy(SP process, NS scheme, + std::unique_ptr target_payoff, + std::unique_ptr control_payoff, + double control_expectation, + unsigned seed = std::random_device{}()) { + auto rs = std::make_unique(seed); + auto pg = std::make_unique>(process, scheme); + auto po = std::make_unique( + std::move(target_payoff), std::move(control_payoff)); + auto est = std::make_unique(control_expectation); + + Pipeline pipeline(std::move(rs), std::move(pg), + std::move(po), std::move(est)); + + return std::make_unique>( + std::move(pipeline), "ControlVariate"); + } + + // Add more factory methods as we make/implement more VR strategies: + // - create_quasi_monte_carlo_strategy() + // - create_importance_sampling_strategy() + // etc. +}; + +} // namespace lfmc \ No newline at end of file diff --git a/include/lfmc/strategy/strategy_metrics.hpp b/include/lfmc/strategy/strategy_metrics.hpp new file mode 100644 index 0000000..f1ed6d8 --- /dev/null +++ b/include/lfmc/strategy/strategy_metrics.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace lfmc { + +struct StrategyMetrics { + std::string name; + double mean; + double variance; + double std_error; + std::size_t samples; + long long elapsed_ms; + + bool operator<(const StrategyMetrics& other) const noexcept { + return variance < other.variance; + } +}; + +} // namespace lfmc \ No newline at end of file diff --git a/include/lfmc/strategy/strategy_runner.hpp b/include/lfmc/strategy/strategy_runner.hpp new file mode 100644 index 0000000..5e63ae6 --- /dev/null +++ b/include/lfmc/strategy/strategy_runner.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "lfmc/estimator/estimator.hpp" +#include "lfmc/estimator/monte_carlo_estimator.hpp" +#include "lfmc/estimator/control_variate_estimator.hpp" +#include "lfmc/pipeline/pipeline.hpp" +#include "lfmc/strategy/strategy_metrics.hpp" +#include "lfmc/timing/timing.hpp" + +#include +#include +#include + +namespace lfmc { + +template NS> +class StrategyRunner { +private: + Pipeline pipeline; + std::string name; + Timer timer; + + std::atomic current_mean{0.0}; + std::atomic current_variance{std::numeric_limits::max()}; + std::atomic samples_processed{0}; + +public: + StrategyRunner(Pipeline pipeline, std::string name) + : pipeline(std::move(pipeline)), name(std::move(name)) {} + + // Run a single batch of simulations and update metrics I made in the other metrics file + std::expected run_batch(size_t steps, double T) { + auto result = pipeline.run(steps, T); + if (!result) { + return std::unexpected(result.error()); + } + + + auto* estimator = pipeline.get_estimator(); + + // had to implement this, it wasn't in the base class, but need it to update the metrics + if (auto* mc_est = dynamic_cast(estimator)) { + current_mean.store(mc_est->mean(), std::memory_order_relaxed); + current_variance.store(mc_est->variance(), std::memory_order_relaxed); + samples_processed.store(mc_est->sample_count(), std::memory_order_relaxed); + } + // Try ControlVariateEstimator, same as above + else if (auto* cv_est = dynamic_cast(estimator)) { + current_mean.store(cv_est->mean(), std::memory_order_relaxed); + current_variance.store(cv_est->variance(), std::memory_order_relaxed); + samples_processed.store(cv_est->sample_count(), std::memory_order_relaxed); + } + + return {}; + } + + StrategyMetrics get_metrics() const noexcept { + return StrategyMetrics{ + .name = name, + .mean = current_mean.load(std::memory_order_relaxed), + .variance = current_variance.load(std::memory_order_relaxed), + .std_error = std::sqrt(current_variance.load(std::memory_order_relaxed) / + samples_processed.load(std::memory_order_relaxed)), + .samples = samples_processed.load(std::memory_order_relaxed), + .elapsed_ms = timer.elapsedMilliseconds() + }; + } + + const std::string& get_name() const noexcept { return name; } +}; + +} // namespace lfmc \ No newline at end of file diff --git a/src/estimator/control_variate_estimator.cpp b/src/estimator/control_variate_estimator.cpp index 6730a9d..abc7a39 100644 --- a/src/estimator/control_variate_estimator.cpp +++ b/src/estimator/control_variate_estimator.cpp @@ -16,6 +16,7 @@ ControlVariateEstimator::add_payoffs(const std::vector& payoffs) { sum_y += y; sum_xy += x * y; sum_yy += y * y; + sum_xx += x * x; ++count; } @@ -61,8 +62,53 @@ std::expected ControlVariateEstimator::merge(Estimator const& sum_y += other_estimator->sum_y; sum_xy += other_estimator->sum_xy; sum_yy += other_estimator->sum_yy; + sum_xx += other_estimator->sum_xx; return {}; } +double ControlVariateEstimator::mean() const noexcept { + if (count == 0) return 0.0; + + double n = static_cast(count); + double mean_x = sum_x / n; + double mean_y = sum_y / n; + + double cov_xy = (sum_xy / n) - mean_x * mean_y; + double var_y = (sum_yy / n) - mean_y * mean_y; + + if (var_y == 0.0) return mean_x; + + double beta = cov_xy / var_y; + return mean_x - beta * (mean_y - control_expectation_); +} + +double ControlVariateEstimator::variance() const noexcept { + if (count < 2) return std::numeric_limits::max(); + + double n = static_cast(count); + double mean_x = sum_x / n; + double mean_y = sum_y / n; + + double var_x = (sum_xx / n) - mean_x * mean_x; + double var_y = (sum_yy / n) - mean_y * mean_y; + double cov_xy = (sum_xy / n) - mean_x * mean_y; + + if (var_y == 0.0) return var_x; + + double beta = cov_xy / var_y; + + // Var(X - β(Y - E[Y])) = Var(X) + β²Var(Y) - 2βCov(X,Y) + return var_x + beta * beta * var_y - 2.0 * beta * cov_xy; +} + +double ControlVariateEstimator::std_error() const noexcept { + if (count < 2) return std::numeric_limits::max(); + return std::sqrt(variance() / static_cast(count)); +} + +std::size_t ControlVariateEstimator::sample_count() const noexcept { + return count; +} + } // namespace lfmc diff --git a/src/estimator/monte_carlo_estimator.cpp b/src/estimator/monte_carlo_estimator.cpp index 53d0d5f..6e2e2b9 100644 --- a/src/estimator/monte_carlo_estimator.cpp +++ b/src/estimator/monte_carlo_estimator.cpp @@ -1,7 +1,8 @@ #include "lfmc/estimator/monte_carlo_estimator.hpp" #include - +#include // ← ADD THIS for std::sqrt +#include // ← ADD THIS for std::numeric_limits namespace lfmc { std::expected @@ -11,13 +12,35 @@ MonteCarloEstimator::add_payoffs(const std::vector& payoffs) { } for (const auto& payoff : payoffs) { - sum += payoff[0]; + double value = payoff[0]; + sum += value; + sum_sq += value * value; ++count; } return {}; } + +double MonteCarloEstimator::mean() const noexcept { + return count > 0 ? sum / static_cast(count) : 0.0; +} + +double MonteCarloEstimator::variance() const noexcept { + if (count < 2) return std::numeric_limits::max(); + double m = mean(); + return (sum_sq / static_cast(count)) - (m * m); // ← Add explicit cast +} + +double MonteCarloEstimator::std_error() const noexcept { + if (count < 2) return std::numeric_limits::max(); + return std::sqrt(variance() / static_cast(count)); +} + +std::size_t MonteCarloEstimator::sample_count() const noexcept { + return count; +} + bool MonteCarloEstimator::converged() const { return count >= 10000; // Placeholder convergence criterion } @@ -40,6 +63,7 @@ std::expected MonteCarloEstimator::merge(Estimator const& oth } sum += other_estimator->sum; + sum_sq += other_estimator->sum_sq; count += other_estimator->count; return {}; diff --git a/src/stochastic_process/geometric_brownian_motion.cpp b/src/stochastic_process/geometric_brownian_motion.cpp index 4666569..4ab7c62 100644 --- a/src/stochastic_process/geometric_brownian_motion.cpp +++ b/src/stochastic_process/geometric_brownian_motion.cpp @@ -9,12 +9,13 @@ double GeometricBrownianMotion::initial() const noexcept { return x0_; } -double GeometricBrownianMotion::drift(double, double x) const noexcept { - return mu_ * x; + +double GeometricBrownianMotion::drift(double x, double) const noexcept { + return mu_ * x; // Use first parameter } -double GeometricBrownianMotion::diffusion(double, double x) const noexcept { - return sigma_ * x; +double GeometricBrownianMotion::diffusion(double x, double) const noexcept { + return sigma_ * x; // Use first parameter } double GeometricBrownianMotion::mu() const noexcept { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 940f9fe..57e260e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,6 +22,7 @@ set(TEST_SOURCES test_scheme.cpp test_pipeline.cpp test_convergency.cpp + test_multi_strategy.cpp ) add_executable(tests ${TEST_SOURCES}) diff --git a/tests/test_multi_strategy.cpp b/tests/test_multi_strategy.cpp new file mode 100644 index 0000000..aa10cc5 --- /dev/null +++ b/tests/test_multi_strategy.cpp @@ -0,0 +1,69 @@ +#include "lfmc/numerical_scheme/euler_maruyama.hpp" +#include "lfmc/payoff/european_payoffs.hpp" +#include "lfmc/stochastic_process/geometric_brownian_motion.hpp" +#include "lfmc/strategy/multi_strategy_runner.hpp" +#include "lfmc/strategy/strategy_factory.hpp" + +#include +#include + +using namespace lfmc; + +// TODO add more test cases for different option types, more strategies, error handling, etc. + +TEST_CASE("Multi-strategy runner compares variance reduction techniques") { + GeometricBrownianMotion gbm(0.05, 0.20, 100.0); + EulerMaruyama euler; + + constexpr size_t steps = 252; + constexpr double T = 1.0; + constexpr size_t warmup_iterations = 100; + + MultiStrategyRunner> runner; + + // Add pseudo-random strategy + runner.add_strategy( + StrategyFactory>:: + create_pseudo_random_strategy( + gbm, euler, std::make_unique(100.0), 42u)); + + // Add antithetic variates strategy + runner.add_strategy( + StrategyFactory>:: + create_antithetic_strategy( + gbm, euler, std::make_unique(100.0), 42u)); + + auto result = runner.run_warmup(steps, T, warmup_iterations); + + REQUIRE(result.has_value()); + REQUIRE(runner.strategy_count() == 2); + + auto metrics = runner.get_all_metrics(); + + REQUIRE(metrics.size() == 2); + REQUIRE(metrics[0].samples > 0); + REQUIRE(metrics[1].samples > 0); + REQUIRE(metrics[0].variance > 0.0); + REQUIRE(metrics[1].variance > 0.0); + + size_t best_idx = runner.best_strategy_index(); + INFO("Best strategy: " << metrics[best_idx].name + << " with variance: " << metrics[best_idx].variance); + + REQUIRE(best_idx < metrics.size()); +} + +TEST_CASE("Multi-strategy runner with control variates") { + // TODO implement once control variate bugs are fixed +} + +TEST_CASE("Multi-strategy runner handles empty strategy list") { + // TODO test error handling for no strategies added +} + +TEST_CASE("Multi-strategy runner can run multiple warmup cycles") { + // TODO test running warmup multiple times and verify metrics update +} \ No newline at end of file From 1b274e7ca94f5a9d871c5ccfa3c93fa47aaf0ba5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 10 Mar 2026 20:16:51 +0000 Subject: [PATCH 17/19] style: apply clang-format --- .../estimator/control_variate_estimator.hpp | 3 +- .../lfmc/path_generator/path_generator.hpp | 2 +- include/lfmc/pipeline/pipeline.hpp | 3 +- .../lfmc/strategy/multi_strategy_runner.hpp | 18 ++++----- include/lfmc/strategy/strategy_factory.hpp | 40 +++++++------------ include/lfmc/strategy/strategy_runner.hpp | 39 +++++++++--------- src/estimator/control_variate_estimator.cpp | 33 ++++++++------- src/estimator/monte_carlo_estimator.cpp | 13 +++--- .../geometric_brownian_motion.cpp | 5 +-- tests/test_multi_strategy.cpp | 27 +++++-------- 10 files changed, 85 insertions(+), 98 deletions(-) diff --git a/include/lfmc/estimator/control_variate_estimator.hpp b/include/lfmc/estimator/control_variate_estimator.hpp index 27b4b74..c79ee7a 100644 --- a/include/lfmc/estimator/control_variate_estimator.hpp +++ b/include/lfmc/estimator/control_variate_estimator.hpp @@ -2,9 +2,10 @@ #include "lfmc/core/types.hpp" #include "lfmc/estimator/estimator.hpp" + #include -#include #include +#include #include #include diff --git a/include/lfmc/path_generator/path_generator.hpp b/include/lfmc/path_generator/path_generator.hpp index 8cfe869..e0be3ff 100644 --- a/include/lfmc/path_generator/path_generator.hpp +++ b/include/lfmc/path_generator/path_generator.hpp @@ -28,7 +28,7 @@ class PathGenerator { std::vector paths; for (const auto& norm : normals) { Path path; - path.reserve(steps + 1); + path.reserve(steps + 1); double t = 0.0; double x = process_.initial(); path.push_back(x); diff --git a/include/lfmc/pipeline/pipeline.hpp b/include/lfmc/pipeline/pipeline.hpp index 7965b87..568def4 100644 --- a/include/lfmc/pipeline/pipeline.hpp +++ b/include/lfmc/pipeline/pipeline.hpp @@ -49,7 +49,7 @@ template NS> class Pipeline { return estimator_->result(); } - + const Estimator* get_estimator() const noexcept { return estimator_.get(); } @@ -57,7 +57,6 @@ template NS> class Pipeline { Estimator* get_estimator() noexcept { return estimator_.get(); } - }; } // namespace lfmc diff --git a/include/lfmc/strategy/multi_strategy_runner.hpp b/include/lfmc/strategy/multi_strategy_runner.hpp index 96cd932..cbff478 100644 --- a/include/lfmc/strategy/multi_strategy_runner.hpp +++ b/include/lfmc/strategy/multi_strategy_runner.hpp @@ -10,20 +10,18 @@ namespace lfmc { -template NS> -class MultiStrategyRunner { -private: +template NS> class MultiStrategyRunner { + private: std::vector>> strategies; std::vector threads; -public: + public: void add_strategy(std::unique_ptr> strategy) { strategies.push_back(std::move(strategy)); } // Run all strategies for a warmup period - std::expected run_warmup(size_t steps, double T, - size_t warmup_iterations) { + std::expected run_warmup(size_t steps, double T, size_t warmup_iterations) { threads.clear(); threads.reserve(strategies.size()); @@ -52,11 +50,11 @@ class MultiStrategyRunner { std::vector get_all_metrics() const { std::vector metrics; metrics.reserve(strategies.size()); - + for (const auto& strategy : strategies) { metrics.push_back(strategy->get_metrics()); } - + return metrics; } @@ -69,8 +67,8 @@ class MultiStrategyRunner { return std::min_element(metrics.begin(), metrics.end()) - metrics.begin(); } - size_t strategy_count() const noexcept { - return strategies.size(); + size_t strategy_count() const noexcept { + return strategies.size(); } }; diff --git a/include/lfmc/strategy/strategy_factory.hpp b/include/lfmc/strategy/strategy_factory.hpp index bdf2fbf..3b7b0e3 100644 --- a/include/lfmc/strategy/strategy_factory.hpp +++ b/include/lfmc/strategy/strategy_factory.hpp @@ -9,8 +9,8 @@ #include "lfmc/pipeline/pipeline.hpp" #include "lfmc/random_source/antithetic_random_source.hpp" #include "lfmc/random_source/pseudo_random_source.hpp" -#include "lfmc/strategy/strategy_runner.hpp" #include "lfmc/stochastic_process/stochastic_process.hpp" +#include "lfmc/strategy/strategy_runner.hpp" #include #include @@ -18,56 +18,46 @@ namespace lfmc { -template NS> -class StrategyFactory { -public: +template NS> class StrategyFactory { + public: static std::unique_ptr> - create_pseudo_random_strategy(SP process, NS scheme, - std::unique_ptr payoff, - unsigned seed = std::random_device{}()) { + create_pseudo_random_strategy(SP process, NS scheme, std::unique_ptr payoff, + unsigned seed = std::random_device{}()) { auto rs = std::make_unique(seed); auto pg = std::make_unique>(process, scheme); auto est = std::make_unique(); - Pipeline pipeline(std::move(rs), std::move(pg), - std::move(payoff), std::move(est)); + Pipeline pipeline(std::move(rs), std::move(pg), std::move(payoff), std::move(est)); - return std::make_unique>( - std::move(pipeline), "PseudoRandom"); + return std::make_unique>(std::move(pipeline), "PseudoRandom"); } static std::unique_ptr> - create_antithetic_strategy(SP process, NS scheme, - std::unique_ptr payoff, + create_antithetic_strategy(SP process, NS scheme, std::unique_ptr payoff, unsigned seed = std::random_device{}()) { auto rs = std::make_unique(seed); auto pg = std::make_unique>(process, scheme); auto est = std::make_unique(); - Pipeline pipeline(std::move(rs), std::move(pg), - std::move(payoff), std::move(est)); + Pipeline pipeline(std::move(rs), std::move(pg), std::move(payoff), std::move(est)); - return std::make_unique>( - std::move(pipeline), "Antithetic"); + return std::make_unique>(std::move(pipeline), "Antithetic"); } static std::unique_ptr> - create_control_variate_strategy(SP process, NS scheme, - std::unique_ptr target_payoff, + create_control_variate_strategy(SP process, NS scheme, std::unique_ptr target_payoff, std::unique_ptr control_payoff, double control_expectation, unsigned seed = std::random_device{}()) { auto rs = std::make_unique(seed); auto pg = std::make_unique>(process, scheme); - auto po = std::make_unique( - std::move(target_payoff), std::move(control_payoff)); + auto po = std::make_unique(std::move(target_payoff), + std::move(control_payoff)); auto est = std::make_unique(control_expectation); - Pipeline pipeline(std::move(rs), std::move(pg), - std::move(po), std::move(est)); + Pipeline pipeline(std::move(rs), std::move(pg), std::move(po), std::move(est)); - return std::make_unique>( - std::move(pipeline), "ControlVariate"); + return std::make_unique>(std::move(pipeline), "ControlVariate"); } // Add more factory methods as we make/implement more VR strategies: diff --git a/include/lfmc/strategy/strategy_runner.hpp b/include/lfmc/strategy/strategy_runner.hpp index 5e63ae6..7dfc363 100644 --- a/include/lfmc/strategy/strategy_runner.hpp +++ b/include/lfmc/strategy/strategy_runner.hpp @@ -1,8 +1,8 @@ #pragma once +#include "lfmc/estimator/control_variate_estimator.hpp" #include "lfmc/estimator/estimator.hpp" #include "lfmc/estimator/monte_carlo_estimator.hpp" -#include "lfmc/estimator/control_variate_estimator.hpp" #include "lfmc/pipeline/pipeline.hpp" #include "lfmc/strategy/strategy_metrics.hpp" #include "lfmc/timing/timing.hpp" @@ -13,18 +13,17 @@ namespace lfmc { -template NS> -class StrategyRunner { -private: +template NS> class StrategyRunner { + private: Pipeline pipeline; std::string name; Timer timer; - + std::atomic current_mean{0.0}; std::atomic current_variance{std::numeric_limits::max()}; std::atomic samples_processed{0}; -public: + public: StrategyRunner(Pipeline pipeline, std::string name) : pipeline(std::move(pipeline)), name(std::move(name)) {} @@ -34,10 +33,9 @@ class StrategyRunner { if (!result) { return std::unexpected(result.error()); } - - + auto* estimator = pipeline.get_estimator(); - + // had to implement this, it wasn't in the base class, but need it to update the metrics if (auto* mc_est = dynamic_cast(estimator)) { current_mean.store(mc_est->mean(), std::memory_order_relaxed); @@ -50,23 +48,24 @@ class StrategyRunner { current_variance.store(cv_est->variance(), std::memory_order_relaxed); samples_processed.store(cv_est->sample_count(), std::memory_order_relaxed); } - + return {}; } StrategyMetrics get_metrics() const noexcept { - return StrategyMetrics{ - .name = name, - .mean = current_mean.load(std::memory_order_relaxed), - .variance = current_variance.load(std::memory_order_relaxed), - .std_error = std::sqrt(current_variance.load(std::memory_order_relaxed) / - samples_processed.load(std::memory_order_relaxed)), - .samples = samples_processed.load(std::memory_order_relaxed), - .elapsed_ms = timer.elapsedMilliseconds() - }; + return StrategyMetrics{.name = name, + .mean = current_mean.load(std::memory_order_relaxed), + .variance = current_variance.load(std::memory_order_relaxed), + .std_error = + std::sqrt(current_variance.load(std::memory_order_relaxed) / + samples_processed.load(std::memory_order_relaxed)), + .samples = samples_processed.load(std::memory_order_relaxed), + .elapsed_ms = timer.elapsedMilliseconds()}; } - const std::string& get_name() const noexcept { return name; } + const std::string& get_name() const noexcept { + return name; + } }; } // namespace lfmc \ No newline at end of file diff --git a/src/estimator/control_variate_estimator.cpp b/src/estimator/control_variate_estimator.cpp index abc7a39..d07241b 100644 --- a/src/estimator/control_variate_estimator.cpp +++ b/src/estimator/control_variate_estimator.cpp @@ -68,42 +68,47 @@ std::expected ControlVariateEstimator::merge(Estimator const& } double ControlVariateEstimator::mean() const noexcept { - if (count == 0) return 0.0; - + if (count == 0) + return 0.0; + double n = static_cast(count); double mean_x = sum_x / n; double mean_y = sum_y / n; - + double cov_xy = (sum_xy / n) - mean_x * mean_y; double var_y = (sum_yy / n) - mean_y * mean_y; - - if (var_y == 0.0) return mean_x; - + + if (var_y == 0.0) + return mean_x; + double beta = cov_xy / var_y; return mean_x - beta * (mean_y - control_expectation_); } double ControlVariateEstimator::variance() const noexcept { - if (count < 2) return std::numeric_limits::max(); - + if (count < 2) + return std::numeric_limits::max(); + double n = static_cast(count); double mean_x = sum_x / n; double mean_y = sum_y / n; - + double var_x = (sum_xx / n) - mean_x * mean_x; double var_y = (sum_yy / n) - mean_y * mean_y; double cov_xy = (sum_xy / n) - mean_x * mean_y; - - if (var_y == 0.0) return var_x; - + + if (var_y == 0.0) + return var_x; + double beta = cov_xy / var_y; - + // Var(X - β(Y - E[Y])) = Var(X) + β²Var(Y) - 2βCov(X,Y) return var_x + beta * beta * var_y - 2.0 * beta * cov_xy; } double ControlVariateEstimator::std_error() const noexcept { - if (count < 2) return std::numeric_limits::max(); + if (count < 2) + return std::numeric_limits::max(); return std::sqrt(variance() / static_cast(count)); } diff --git a/src/estimator/monte_carlo_estimator.cpp b/src/estimator/monte_carlo_estimator.cpp index 6e2e2b9..d45a6b1 100644 --- a/src/estimator/monte_carlo_estimator.cpp +++ b/src/estimator/monte_carlo_estimator.cpp @@ -1,8 +1,8 @@ #include "lfmc/estimator/monte_carlo_estimator.hpp" +#include // ← ADD THIS for std::sqrt +#include // ← ADD THIS for std::numeric_limits #include -#include // ← ADD THIS for std::sqrt -#include // ← ADD THIS for std::numeric_limits namespace lfmc { std::expected @@ -21,19 +21,20 @@ MonteCarloEstimator::add_payoffs(const std::vector& payoffs) { return {}; } - double MonteCarloEstimator::mean() const noexcept { return count > 0 ? sum / static_cast(count) : 0.0; } double MonteCarloEstimator::variance() const noexcept { - if (count < 2) return std::numeric_limits::max(); + if (count < 2) + return std::numeric_limits::max(); double m = mean(); - return (sum_sq / static_cast(count)) - (m * m); // ← Add explicit cast + return (sum_sq / static_cast(count)) - (m * m); // ← Add explicit cast } double MonteCarloEstimator::std_error() const noexcept { - if (count < 2) return std::numeric_limits::max(); + if (count < 2) + return std::numeric_limits::max(); return std::sqrt(variance() / static_cast(count)); } diff --git a/src/stochastic_process/geometric_brownian_motion.cpp b/src/stochastic_process/geometric_brownian_motion.cpp index 4ab7c62..172b265 100644 --- a/src/stochastic_process/geometric_brownian_motion.cpp +++ b/src/stochastic_process/geometric_brownian_motion.cpp @@ -9,13 +9,12 @@ double GeometricBrownianMotion::initial() const noexcept { return x0_; } - double GeometricBrownianMotion::drift(double x, double) const noexcept { - return mu_ * x; // Use first parameter + return mu_ * x; // Use first parameter } double GeometricBrownianMotion::diffusion(double x, double) const noexcept { - return sigma_ * x; // Use first parameter + return sigma_ * x; // Use first parameter } double GeometricBrownianMotion::mu() const noexcept { diff --git a/tests/test_multi_strategy.cpp b/tests/test_multi_strategy.cpp index aa10cc5..c4b24d0 100644 --- a/tests/test_multi_strategy.cpp +++ b/tests/test_multi_strategy.cpp @@ -14,35 +14,30 @@ using namespace lfmc; TEST_CASE("Multi-strategy runner compares variance reduction techniques") { GeometricBrownianMotion gbm(0.05, 0.20, 100.0); EulerMaruyama euler; - + constexpr size_t steps = 252; constexpr double T = 1.0; constexpr size_t warmup_iterations = 100; - MultiStrategyRunner> runner; + MultiStrategyRunner> runner; // Add pseudo-random strategy runner.add_strategy( - StrategyFactory>:: - create_pseudo_random_strategy( - gbm, euler, std::make_unique(100.0), 42u)); + StrategyFactory>:: + create_pseudo_random_strategy(gbm, euler, std::make_unique(100.0), 42u)); // Add antithetic variates strategy runner.add_strategy( - StrategyFactory>:: - create_antithetic_strategy( - gbm, euler, std::make_unique(100.0), 42u)); + StrategyFactory>:: + create_antithetic_strategy(gbm, euler, std::make_unique(100.0), 42u)); auto result = runner.run_warmup(steps, T, warmup_iterations); - + REQUIRE(result.has_value()); REQUIRE(runner.strategy_count() == 2); auto metrics = runner.get_all_metrics(); - + REQUIRE(metrics.size() == 2); REQUIRE(metrics[0].samples > 0); REQUIRE(metrics[1].samples > 0); @@ -50,9 +45,9 @@ TEST_CASE("Multi-strategy runner compares variance reduction techniques") { REQUIRE(metrics[1].variance > 0.0); size_t best_idx = runner.best_strategy_index(); - INFO("Best strategy: " << metrics[best_idx].name - << " with variance: " << metrics[best_idx].variance); - + INFO("Best strategy: " << metrics[best_idx].name + << " with variance: " << metrics[best_idx].variance); + REQUIRE(best_idx < metrics.size()); } From b81009fff31f029190b444462bdb1e2fc770406f Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Mon, 23 Mar 2026 12:20:56 -0400 Subject: [PATCH 18/19] refactor: match code to fit into the engine/worker model --- include/lfmc/engine/engine.hpp | 87 ++++++++++++++++--- include/lfmc/pipeline/pipeline_builder.hpp | 8 +- .../lfmc/strategy/multi_strategy_runner.hpp | 75 ---------------- include/lfmc/strategy/strategy_factory.hpp | 63 ++++++++++---- include/lfmc/strategy/strategy_metrics.hpp | 18 ++-- ...trategy_runner.hpp => strategy_worker.hpp} | 23 ++--- include/lfmc/strategy/types.hpp | 22 +++++ tests/CMakeLists.txt | 2 +- tests/test_convergency.cpp | 12 +-- ...est_multi_strategy.cpp => test_engine.cpp} | 46 ++++++---- 10 files changed, 202 insertions(+), 154 deletions(-) delete mode 100644 include/lfmc/strategy/multi_strategy_runner.hpp rename include/lfmc/strategy/{strategy_runner.hpp => strategy_worker.hpp} (80%) create mode 100644 include/lfmc/strategy/types.hpp rename tests/{test_multi_strategy.cpp => test_engine.cpp} (51%) diff --git a/include/lfmc/engine/engine.hpp b/include/lfmc/engine/engine.hpp index 28262dd..a92575a 100644 --- a/include/lfmc/engine/engine.hpp +++ b/include/lfmc/engine/engine.hpp @@ -1,21 +1,80 @@ #pragma once -#include "lfmc/pipeline/pipeline.hpp" -#include "lfmc/pipeline/pipeline_builder.hpp" +#include "lfmc/strategy/strategy_metrics.hpp" +#include "lfmc/strategy/strategy_worker.hpp" + +#include +#include +#include +#include + +// TODO I really don't enjoy the fact that everything is past the path generator is templated and +// therefore must be in all the header files. Is that necessary? Or can we do better... namespace lfmc { -// class Engine { -// private: -// Pipeline> pipeline_; -// -// public: -// Engine(Pipeline> pipeline) -// : pipeline_(std::move(pipeline)) {} -// -// std::expected run() { -// return pipeline_.run(); -// } -// }; +template NS> class Engine { + private: + std::vector>> strategies; + std::vector threads; + + public: + void add_strategy(std::unique_ptr> strategy) { + strategies.push_back(std::move(strategy)); + } + + // Run all strategies for a warmup period + std::expected run_warmup(size_t steps, double T, size_t warmup_iterations) { + threads.clear(); + threads.reserve(strategies.size()); + + for (auto& strategy : strategies) { + threads.emplace_back([&strategy, steps, T, warmup_iterations]() { + for (size_t i = 0; i < warmup_iterations; ++i) { + auto result = strategy->run_batch(steps, T); + if (!result) { + return; // Just exit the thread on error, we can check metrics later to see + // if it failed + } + } + }); + } + + for (auto& thread : threads) { + if (thread.joinable()) { + thread.join(); + } + } + + return {}; + } + + // TODO run main simulation loop + + // Get current metrics + std::vector get_all_metrics() const { + std::vector metrics; + metrics.reserve(strategies.size()); + + for (const auto& strategy : strategies) { + metrics.push_back(strategy->get_metrics()); + } + + return metrics; + } + + // Find best strat + std::expected get_best_strategy_index() const { + auto metrics = get_all_metrics(); + if (metrics.empty()) { + return std::unexpected("No metrics available"); + } + return std::min_element(metrics.begin(), metrics.end()) - metrics.begin(); + } + + size_t strategy_count() const noexcept { + return strategies.size(); + } +}; } // namespace lfmc diff --git a/include/lfmc/pipeline/pipeline_builder.hpp b/include/lfmc/pipeline/pipeline_builder.hpp index ba0566b..5751aa4 100644 --- a/include/lfmc/pipeline/pipeline_builder.hpp +++ b/include/lfmc/pipeline/pipeline_builder.hpp @@ -1,10 +1,12 @@ #pragma once #include "lfmc/estimator/estimator.hpp" +#include "lfmc/estimator/monte_carlo_estimator.hpp" #include "lfmc/numerical_scheme/numerical_scheme.hpp" #include "lfmc/path_generator/path_generator.hpp" #include "lfmc/payoff/payoff.hpp" #include "lfmc/pipeline/pipeline.hpp" +#include "lfmc/random_source/pseudo_random_source.hpp" #include "lfmc/random_source/random_source.hpp" #include "lfmc/stochastic_process/stochastic_process.hpp" @@ -18,7 +20,8 @@ template NS> class PipelineBuilder { std::unique_ptr estimator_; public: - PipelineBuilder& random_source(std::unique_ptr rs) { + PipelineBuilder& + random_source(std::unique_ptr rs = std::make_unique()) { random_source_ = std::move(rs); return *this; } @@ -33,7 +36,8 @@ template NS> class PipelineBuilder { return *this; } - PipelineBuilder& estimator(std::unique_ptr e) { + PipelineBuilder& + estimator(std::unique_ptr e = std::make_unique()) { estimator_ = std::move(e); return *this; } diff --git a/include/lfmc/strategy/multi_strategy_runner.hpp b/include/lfmc/strategy/multi_strategy_runner.hpp deleted file mode 100644 index cbff478..0000000 --- a/include/lfmc/strategy/multi_strategy_runner.hpp +++ /dev/null @@ -1,75 +0,0 @@ -#pragma once - -#include "lfmc/strategy/strategy_metrics.hpp" -#include "lfmc/strategy/strategy_runner.hpp" - -#include -#include -#include -#include - -namespace lfmc { - -template NS> class MultiStrategyRunner { - private: - std::vector>> strategies; - std::vector threads; - - public: - void add_strategy(std::unique_ptr> strategy) { - strategies.push_back(std::move(strategy)); - } - - // Run all strategies for a warmup period - std::expected run_warmup(size_t steps, double T, size_t warmup_iterations) { - threads.clear(); - threads.reserve(strategies.size()); - - for (auto& strategy : strategies) { - threads.emplace_back([&strategy, steps, T, warmup_iterations]() { - for (size_t i = 0; i < warmup_iterations; ++i) { - auto result = strategy->run_batch(steps, T); - if (!result) { - // Error occurred - need this to get log - return; - } - } - }); - } - - for (auto& thread : threads) { - if (thread.joinable()) { - thread.join(); - } - } - - return {}; - } - - // Get current metricss - std::vector get_all_metrics() const { - std::vector metrics; - metrics.reserve(strategies.size()); - - for (const auto& strategy : strategies) { - metrics.push_back(strategy->get_metrics()); - } - - return metrics; - } - - // Find best strat - size_t best_strategy_index() const { - auto metrics = get_all_metrics(); - if (metrics.empty()) { - return 0; - } - return std::min_element(metrics.begin(), metrics.end()) - metrics.begin(); - } - - size_t strategy_count() const noexcept { - return strategies.size(); - } -}; - -} // namespace lfmc \ No newline at end of file diff --git a/include/lfmc/strategy/strategy_factory.hpp b/include/lfmc/strategy/strategy_factory.hpp index 3b7b0e3..dda5560 100644 --- a/include/lfmc/strategy/strategy_factory.hpp +++ b/include/lfmc/strategy/strategy_factory.hpp @@ -1,50 +1,67 @@ #pragma once #include "lfmc/estimator/control_variate_estimator.hpp" -#include "lfmc/estimator/monte_carlo_estimator.hpp" -#include "lfmc/numerical_scheme/euler_maruyama.hpp" #include "lfmc/path_generator/path_generator.hpp" #include "lfmc/payoff/control_variate_payoffs.hpp" #include "lfmc/payoff/payoff.hpp" -#include "lfmc/pipeline/pipeline.hpp" +#include "lfmc/pipeline/pipeline_builder.hpp" #include "lfmc/random_source/antithetic_random_source.hpp" #include "lfmc/random_source/pseudo_random_source.hpp" #include "lfmc/stochastic_process/stochastic_process.hpp" -#include "lfmc/strategy/strategy_runner.hpp" +#include "lfmc/strategy/strategy_worker.hpp" +#include "lfmc/strategy/types.hpp" +#include #include #include -#include namespace lfmc { template NS> class StrategyFactory { public: - static std::unique_ptr> + static std::expected>, std::string> create_pseudo_random_strategy(SP process, NS scheme, std::unique_ptr payoff, unsigned seed = std::random_device{}()) { auto rs = std::make_unique(seed); auto pg = std::make_unique>(process, scheme); - auto est = std::make_unique(); - Pipeline pipeline(std::move(rs), std::move(pg), std::move(payoff), std::move(est)); + auto pipeline = PipelineBuilder() + .random_source(std::move(rs)) + .path_generator(std::move(pg)) + .payoff(std::move(payoff)) + .estimator() + .build(); - return std::make_unique>(std::move(pipeline), "PseudoRandom"); + if (!pipeline) { + return std::unexpected("Failed to build pipeline: " + pipeline.error()); + } + + return std::make_unique>(std::move(pipeline.value()), + Strategy::PseudoRandom); } - static std::unique_ptr> + static std::expected>, std::string> create_antithetic_strategy(SP process, NS scheme, std::unique_ptr payoff, unsigned seed = std::random_device{}()) { auto rs = std::make_unique(seed); auto pg = std::make_unique>(process, scheme); - auto est = std::make_unique(); - Pipeline pipeline(std::move(rs), std::move(pg), std::move(payoff), std::move(est)); + auto pipeline = PipelineBuilder() + .random_source(std::move(rs)) + .path_generator(std::move(pg)) + .payoff(std::move(payoff)) + .estimator() + .build(); + + if (!pipeline) { + return std::unexpected("Failed to build pipeline: " + pipeline.error()); + } - return std::make_unique>(std::move(pipeline), "Antithetic"); + return std::make_unique>(std::move(pipeline.value()), + Strategy::Antithetic); } - static std::unique_ptr> + static std::expected>, std::string> create_control_variate_strategy(SP process, NS scheme, std::unique_ptr target_payoff, std::unique_ptr control_payoff, double control_expectation, @@ -55,15 +72,25 @@ template NS> class StrategyFactory { std::move(control_payoff)); auto est = std::make_unique(control_expectation); - Pipeline pipeline(std::move(rs), std::move(pg), std::move(po), std::move(est)); + auto pipeline = PipelineBuilder() + .random_source(std::move(rs)) + .path_generator(std::move(pg)) + .payoff(std::move(po)) + .estimator(std::move(est)) + .build(); + + if (!pipeline) { + return std::unexpected("Failed to build pipeline: " + pipeline.error()); + } - return std::make_unique>(std::move(pipeline), "ControlVariate"); + return std::make_unique>(std::move(pipeline.value()), + Strategy::ControlVariate); } // Add more factory methods as we make/implement more VR strategies: // - create_quasi_monte_carlo_strategy() // - create_importance_sampling_strategy() - // etc. + // etc., and add it to the enum }; -} // namespace lfmc \ No newline at end of file +} // namespace lfmc diff --git a/include/lfmc/strategy/strategy_metrics.hpp b/include/lfmc/strategy/strategy_metrics.hpp index f1ed6d8..1d7053e 100644 --- a/include/lfmc/strategy/strategy_metrics.hpp +++ b/include/lfmc/strategy/strategy_metrics.hpp @@ -1,21 +1,29 @@ #pragma once +#include "lfmc/strategy/types.hpp" + +#include // For operator<=> #include -#include +#include namespace lfmc { struct StrategyMetrics { - std::string name; + Strategy strategy; double mean; double variance; double std_error; std::size_t samples; long long elapsed_ms; - bool operator<(const StrategyMetrics& other) const noexcept { - return variance < other.variance; + // Define ordering based on variance + auto operator<=>(const StrategyMetrics& other) const noexcept { + return variance <=> other.variance; } }; -} // namespace lfmc \ No newline at end of file +inline std::ostream& operator<<(std::ostream& os, const Strategy& s) { + return os << to_string(s); // or custom formatting +} + +} // namespace lfmc diff --git a/include/lfmc/strategy/strategy_runner.hpp b/include/lfmc/strategy/strategy_worker.hpp similarity index 80% rename from include/lfmc/strategy/strategy_runner.hpp rename to include/lfmc/strategy/strategy_worker.hpp index 7dfc363..c2af49e 100644 --- a/include/lfmc/strategy/strategy_runner.hpp +++ b/include/lfmc/strategy/strategy_worker.hpp @@ -1,22 +1,21 @@ #pragma once #include "lfmc/estimator/control_variate_estimator.hpp" -#include "lfmc/estimator/estimator.hpp" #include "lfmc/estimator/monte_carlo_estimator.hpp" #include "lfmc/pipeline/pipeline.hpp" #include "lfmc/strategy/strategy_metrics.hpp" +#include "lfmc/strategy/types.hpp" #include "lfmc/timing/timing.hpp" #include -#include #include namespace lfmc { -template NS> class StrategyRunner { +template NS> class StrategyWorker { private: Pipeline pipeline; - std::string name; + Strategy strategy; Timer timer; std::atomic current_mean{0.0}; @@ -24,8 +23,8 @@ template NS> class StrategyRunner { std::atomic samples_processed{0}; public: - StrategyRunner(Pipeline pipeline, std::string name) - : pipeline(std::move(pipeline)), name(std::move(name)) {} + StrategyWorker(Pipeline pipeline, Strategy strategy) + : pipeline(std::move(pipeline)), strategy(std::move(strategy)) {} // Run a single batch of simulations and update metrics I made in the other metrics file std::expected run_batch(size_t steps, double T) { @@ -36,7 +35,9 @@ template NS> class StrategyRunner { auto* estimator = pipeline.get_estimator(); - // had to implement this, it wasn't in the base class, but need it to update the metrics + // TODO possibly move statistics into base class for exposure so we don't need to dynamic + // cast had to implement this, it wasn't in the base class, but need it to update the + // metrics if (auto* mc_est = dynamic_cast(estimator)) { current_mean.store(mc_est->mean(), std::memory_order_relaxed); current_variance.store(mc_est->variance(), std::memory_order_relaxed); @@ -53,7 +54,7 @@ template NS> class StrategyRunner { } StrategyMetrics get_metrics() const noexcept { - return StrategyMetrics{.name = name, + return StrategyMetrics{.strategy = strategy, .mean = current_mean.load(std::memory_order_relaxed), .variance = current_variance.load(std::memory_order_relaxed), .std_error = @@ -63,9 +64,9 @@ template NS> class StrategyRunner { .elapsed_ms = timer.elapsedMilliseconds()}; } - const std::string& get_name() const noexcept { - return name; + const Strategy& get_strategy() const noexcept { + return strategy; } }; -} // namespace lfmc \ No newline at end of file +} // namespace lfmc diff --git a/include/lfmc/strategy/types.hpp b/include/lfmc/strategy/types.hpp new file mode 100644 index 0000000..d97aa0b --- /dev/null +++ b/include/lfmc/strategy/types.hpp @@ -0,0 +1,22 @@ +#pragma once + +namespace lfmc { + +// TODO maybe not the best way to do this, but the easiest and cleanest for now +enum class Strategy { PseudoRandom, Antithetic, ControlVariate }; + +// Helper function to convert strategy enum to string for logging and metrics +inline const char* to_string(Strategy strategy) { + switch (strategy) { + case Strategy::PseudoRandom: + return "PseudoRandom"; + case Strategy::Antithetic: + return "Antithetic"; + case Strategy::ControlVariate: + return "ControlVariate"; + default: + return "Unknown"; + } +} + +} // namespace lfmc diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 57e260e..0608241 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,7 +22,7 @@ set(TEST_SOURCES test_scheme.cpp test_pipeline.cpp test_convergency.cpp - test_multi_strategy.cpp + test_engine.cpp ) add_executable(tests ${TEST_SOURCES}) diff --git a/tests/test_convergency.cpp b/tests/test_convergency.cpp index 028f5f3..a8ce796 100644 --- a/tests/test_convergency.cpp +++ b/tests/test_convergency.cpp @@ -19,9 +19,7 @@ using namespace lfmc; using Catch::Matchers::WithinAbs; -// ---------------------------------------------------------------- // Black-Scholes closed-form helpers for ground truth -// ---------------------------------------------------------------- namespace bs { static double norm_cdf(double x) { @@ -64,10 +62,8 @@ double up_and_out_call(double S, double K, double H, double r, double sigma, dou } // namespace bs -// ---------------------------------------------------------------- -// Convergence runner: runs pipeline at increasing sample counts -// and prints a table of results vs ground truth -// ---------------------------------------------------------------- +// Convergence runner: runs pipeline at increasing sample counts and prints a table of results vs +// ground truth struct ConvergenceResult { std::size_t samples; double estimate; @@ -124,9 +120,7 @@ run_convergence(const std::string& label, PayoffFactory make_payoff, double grou return results; } -// ---------------------------------------------------------------- // Shared parameters -// ---------------------------------------------------------------- static constexpr double S0 = 100.0; static constexpr double K = 100.0; static constexpr double B_UP = 120.0; // Up-and-out barrier @@ -138,9 +132,7 @@ static constexpr int STEPS = 252; static const std::vector TIERS = {1000, 5000, 10000, 50000, 100000, 500000}; -// ---------------------------------------------------------------- // Tests -// ---------------------------------------------------------------- TEST_CASE("Asian Call convergence", "[exotic][convergence][asian]") { double truth = bs::geometric_asian_call(S0, K, MU, SIGMA, T, STEPS); diff --git a/tests/test_multi_strategy.cpp b/tests/test_engine.cpp similarity index 51% rename from tests/test_multi_strategy.cpp rename to tests/test_engine.cpp index c4b24d0..a39dc15 100644 --- a/tests/test_multi_strategy.cpp +++ b/tests/test_engine.cpp @@ -1,7 +1,7 @@ +#include "lfmc/engine/engine.hpp" #include "lfmc/numerical_scheme/euler_maruyama.hpp" #include "lfmc/payoff/european_payoffs.hpp" #include "lfmc/stochastic_process/geometric_brownian_motion.hpp" -#include "lfmc/strategy/multi_strategy_runner.hpp" #include "lfmc/strategy/strategy_factory.hpp" #include @@ -11,7 +11,7 @@ using namespace lfmc; // TODO add more test cases for different option types, more strategies, error handling, etc. -TEST_CASE("Multi-strategy runner compares variance reduction techniques") { +TEST_CASE("Engine compares variance reduction techniques", "[engine]") { GeometricBrownianMotion gbm(0.05, 0.20, 100.0); EulerMaruyama euler; @@ -19,24 +19,32 @@ TEST_CASE("Multi-strategy runner compares variance reduction techniques") { constexpr double T = 1.0; constexpr size_t warmup_iterations = 100; - MultiStrategyRunner> runner; + Engine> engine; // Add pseudo-random strategy - runner.add_strategy( + auto pseudo_random_strategy_result = StrategyFactory>:: - create_pseudo_random_strategy(gbm, euler, std::make_unique(100.0), 42u)); + create_pseudo_random_strategy(gbm, euler, std::make_unique(100.0), 42u); + if (!pseudo_random_strategy_result) { + FAIL("Failed to create pseudo-random strategy: " << pseudo_random_strategy_result.error()); + } + engine.add_strategy(std::move(pseudo_random_strategy_result.value())); // Add antithetic variates strategy - runner.add_strategy( + auto antithetic_strategy_result = StrategyFactory>:: - create_antithetic_strategy(gbm, euler, std::make_unique(100.0), 42u)); + create_antithetic_strategy(gbm, euler, std::make_unique(100.0), 42u); + if (!antithetic_strategy_result) { + FAIL("Failed to create antithetic strategy: " << antithetic_strategy_result.error()); + } + engine.add_strategy(std::move(antithetic_strategy_result.value())); - auto result = runner.run_warmup(steps, T, warmup_iterations); + auto result = engine.run_warmup(steps, T, warmup_iterations); REQUIRE(result.has_value()); - REQUIRE(runner.strategy_count() == 2); + REQUIRE(engine.strategy_count() == 2); - auto metrics = runner.get_all_metrics(); + auto metrics = engine.get_all_metrics(); REQUIRE(metrics.size() == 2); REQUIRE(metrics[0].samples > 0); @@ -44,21 +52,23 @@ TEST_CASE("Multi-strategy runner compares variance reduction techniques") { REQUIRE(metrics[0].variance > 0.0); REQUIRE(metrics[1].variance > 0.0); - size_t best_idx = runner.best_strategy_index(); - INFO("Best strategy: " << metrics[best_idx].name - << " with variance: " << metrics[best_idx].variance); + auto best_idx = engine.get_best_strategy_index(); + if (!best_idx) { + FAIL("Failed to get best strategy index: " << best_idx.error()); + } + size_t idx = best_idx.value(); - REQUIRE(best_idx < metrics.size()); + REQUIRE(idx < metrics.size()); } -TEST_CASE("Multi-strategy runner with control variates") { +TEST_CASE("Multi-strategy engine with control variates") { // TODO implement once control variate bugs are fixed } -TEST_CASE("Multi-strategy runner handles empty strategy list") { +TEST_CASE("Multi-strategy engine handles empty strategy list") { // TODO test error handling for no strategies added } -TEST_CASE("Multi-strategy runner can run multiple warmup cycles") { +TEST_CASE("Multi-strategy engine can run multiple warmup cycles") { // TODO test running warmup multiple times and verify metrics update -} \ No newline at end of file +} From 6bb5f16386fb98684b27b47dfb2c2f2c21565d01 Mon Sep 17 00:00:00 2001 From: Oliver Deng Date: Mon, 23 Mar 2026 12:26:08 -0400 Subject: [PATCH 19/19] refactor: small details --- include/lfmc/engine/engine.hpp | 3 ++- include/lfmc/estimator/control_variate_estimator.hpp | 1 - include/lfmc/strategy/strategy_metrics.hpp | 7 +------ tests/test_engine.cpp | 3 +++ 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/include/lfmc/engine/engine.hpp b/include/lfmc/engine/engine.hpp index a92575a..4a04490 100644 --- a/include/lfmc/engine/engine.hpp +++ b/include/lfmc/engine/engine.hpp @@ -49,7 +49,8 @@ template NS> class Engine { return {}; } - // TODO run main simulation loop + // TODO run main simulation loop - is warmup function above necessary or is that just what this + // is? // Get current metrics std::vector get_all_metrics() const { diff --git a/include/lfmc/estimator/control_variate_estimator.hpp b/include/lfmc/estimator/control_variate_estimator.hpp index c79ee7a..355b529 100644 --- a/include/lfmc/estimator/control_variate_estimator.hpp +++ b/include/lfmc/estimator/control_variate_estimator.hpp @@ -5,7 +5,6 @@ #include #include -#include #include #include diff --git a/include/lfmc/strategy/strategy_metrics.hpp b/include/lfmc/strategy/strategy_metrics.hpp index 1d7053e..d154580 100644 --- a/include/lfmc/strategy/strategy_metrics.hpp +++ b/include/lfmc/strategy/strategy_metrics.hpp @@ -4,7 +4,6 @@ #include // For operator<=> #include -#include namespace lfmc { @@ -13,7 +12,7 @@ struct StrategyMetrics { double mean; double variance; double std_error; - std::size_t samples; + size_t samples; long long elapsed_ms; // Define ordering based on variance @@ -22,8 +21,4 @@ struct StrategyMetrics { } }; -inline std::ostream& operator<<(std::ostream& os, const Strategy& s) { - return os << to_string(s); // or custom formatting -} - } // namespace lfmc diff --git a/tests/test_engine.cpp b/tests/test_engine.cpp index a39dc15..e7cac89 100644 --- a/tests/test_engine.cpp +++ b/tests/test_engine.cpp @@ -39,6 +39,7 @@ TEST_CASE("Engine compares variance reduction techniques", "[engine]") { } engine.add_strategy(std::move(antithetic_strategy_result.value())); + // Run warmup and check metrics auto result = engine.run_warmup(steps, T, warmup_iterations); REQUIRE(result.has_value()); @@ -59,6 +60,8 @@ TEST_CASE("Engine compares variance reduction techniques", "[engine]") { size_t idx = best_idx.value(); REQUIRE(idx < metrics.size()); + + // TODO run main loop } TEST_CASE("Multi-strategy engine with control variates") {