Skip to content

Commit eefa7a7

Browse files
committed
feat: finished refactor and added control variates
1 parent b22c6d9 commit eefa7a7

20 files changed

Lines changed: 406 additions & 197 deletions

include/lfmc/estimator.hpp

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
#include <expected>
66
#include <string>
7+
#include <vector>
78

89
namespace lfmc {
910

1011
class Estimator {
1112
public:
12-
virtual std::expected<void, std::string> add_payoffs(const Payoffs& payoffs) = 0;
13+
virtual std::expected<void, std::string> add_payoffs(const std::vector<Payoffs>& payoffs) = 0;
1314
virtual bool converged() const = 0;
1415
virtual std::expected<double, std::string> result() const = 0;
1516
// virtual void merge(Estimator const& other) = 0;
@@ -22,7 +23,29 @@ class MonteCarloEstimator : public Estimator {
2223
std::size_t count = 0;
2324

2425
public:
25-
std::expected<void, std::string> add_payoffs(const Payoffs& payoffs) override;
26+
std::expected<void, std::string> add_payoffs(const std::vector<Payoffs>& payoffs) override;
27+
bool converged() const override;
28+
std::expected<double, std::string> result() const override;
29+
// void merge(Estimator const& other) override;
30+
};
31+
32+
// TODO if control is analytically known, can put it at payoff level
33+
class ControlVariateEstimator : public Estimator {
34+
private:
35+
std::size_t count = 0;
36+
37+
double sum_x = 0.0; // Sum of original payoffs
38+
double sum_y = 0.0; // Sum of control variate values
39+
double sum_xy = 0.0; // Sum of products of payoffs and control variate
40+
double sum_yy = 0.0; // Sum of squares of control variate values
41+
42+
double control_expectation_ = 0.0; // Expected value of control variate (known analytically)
43+
44+
public:
45+
explicit ControlVariateEstimator(double control_expectation)
46+
: control_expectation_(control_expectation) {}
47+
48+
std::expected<void, std::string> add_payoffs(const std::vector<Payoffs>& payoffs) override;
2649
bool converged() const override;
2750
std::expected<double, std::string> result() const override;
2851
// void merge(Estimator const& other) override;

include/lfmc/numerical_scheme.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#pragma once
22

3+
#include "lfmc/stochastic_process.hpp"
4+
5+
#include <cmath>
36
#include <concepts>
47

58
namespace lfmc {
@@ -10,6 +13,15 @@ concept NumericalScheme =
1013
{ s.step(p, t, x, dt, z) } -> std::convertible_to<double>;
1114
};
1215

16+
template <StochasticProcess P> class EulerMaruyama {
17+
public:
18+
double step(P const& process, double t, double x, double dt, double z) const noexcept {
19+
double drift = process.drift(t, x);
20+
double diffusion = process.diffusion(t, x);
21+
return x + drift * dt + diffusion * std::sqrt(dt) * z;
22+
}
23+
};
24+
1325
/**
1426
* @brief Exact simulation for Geometric Brownian Motion.
1527
*

include/lfmc/path_generator.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class PathGenerator {
3535
path.push_back(x);
3636
t += dt;
3737
}
38+
paths.push_back(std::move(path));
3839
}
3940

4041
return paths;

include/lfmc/payoff.hpp

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
#include "lfmc/types.hpp"
44

5+
#include <expected>
6+
#include <memory>
7+
#include <vector>
8+
59
namespace lfmc {
610

711
class Payoff {
812
public:
9-
virtual Payoffs generate_payoffs(const std::vector<Path>& paths) const = 0;
13+
virtual std::expected<std::vector<Payoffs>, std::string>
14+
generate_payoffs(const std::vector<Path>& paths) const = 0;
1015
virtual ~Payoff() = default;
1116
};
1217

@@ -17,7 +22,8 @@ class EuropeanCall : public Payoff {
1722
public:
1823
explicit EuropeanCall(double strike);
1924

20-
Payoffs generate_payoffs(const std::vector<Path>& paths) const override;
25+
std::expected<std::vector<Payoffs>, std::string>
26+
generate_payoffs(const std::vector<Path>& paths) const override;
2127
};
2228

2329
class EuropeanPut : public Payoff {
@@ -27,7 +33,24 @@ class EuropeanPut : public Payoff {
2733
public:
2834
explicit EuropeanPut(double strike);
2935

30-
Payoffs generate_payoffs(const std::vector<Path>& paths) const override;
36+
std::expected<std::vector<Payoffs>, std::string>
37+
generate_payoffs(const std::vector<Path>& paths) const override;
38+
};
39+
40+
// TODO subpar in terms of architecture because we have both control variate payoff and estimator,
41+
// but for now it's simpler to implement it this way - can enforce with a factory. Can refactor
42+
// later if needed.
43+
class ControlVariatePayoff : public Payoff {
44+
private:
45+
std::unique_ptr<Payoff> target_payoff_;
46+
std::unique_ptr<Payoff> control_payoff_;
47+
48+
public:
49+
explicit ControlVariatePayoff(std::unique_ptr<Payoff> target_payoff,
50+
std::unique_ptr<Payoff> control_payoff);
51+
52+
std::expected<std::vector<Payoffs>, std::string>
53+
generate_payoffs(const std::vector<Path>& paths) const override;
3154
};
3255

3356
} // namespace lfmc

include/lfmc/pipeline.hpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,25 @@ template <StochasticProcess SP, NumericalScheme<SP> NS> class Pipeline {
2626

2727
std::expected<double, std::string> run(size_t steps, double T) {
2828
while (!estimator_->converged()) {
29+
// TODO make all the return types std::expected? IDK if it's a good idea for hot loops
30+
// like this but it would make error handling easier and more consistent. For now just
31+
// return
2932
auto normals = random_source_->generate_normals(steps);
33+
if (normals.empty()) {
34+
return std::unexpected("Failed to generate random normals");
35+
}
36+
3037
auto paths = path_generator_->generate_paths(normals, steps, T);
38+
if (paths.empty()) {
39+
return std::unexpected("Failed to generate paths");
40+
}
41+
3142
auto payoffs = payoff_->generate_payoffs(paths);
32-
auto result = estimator_->add_payoffs(payoffs);
43+
if (!payoffs) {
44+
return std::unexpected("Failed to generate payoffs");
45+
}
3346

47+
auto result = estimator_->add_payoffs(payoffs.value());
3448
if (!result) {
3549
return std::unexpected(result.error());
3650
}

include/lfmc/random_source.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,18 @@ class PseudoRandomSource : public RandomSource {
2626
void seed(unsigned seed);
2727
};
2828

29+
class AntitheticRandomSource : public RandomSource {
30+
private:
31+
std::mt19937 rng_;
32+
std::normal_distribution<double> dist_;
33+
bool toggle_ = false;
34+
35+
public:
36+
AntitheticRandomSource(unsigned seed = std::random_device{}());
37+
38+
std::vector<Normals> generate_normals(size_t steps, size_t) override;
39+
40+
void seed(unsigned seed);
41+
};
42+
2943
} // namespace lfmc

include/lfmc/stochastic_process.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ class GeometricBrownianMotion {
2929
double initial() const noexcept;
3030
double drift(double, double x) const noexcept;
3131
double diffusion(double, double x) const noexcept;
32+
33+
double mu() const noexcept;
34+
double sigma() const noexcept;
3235
};
3336

3437
} // namespace lfmc

src/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# Source files for the lfmc library
22
set(LFMC_SOURCES
3-
euler_maruyama.cpp
43
european_payoffs.cpp
54
geometric_brownian_motion.cpp
65
monte_carlo_estimator.cpp
6+
control_variate_estimator.cpp
7+
control_variate_payoffs.cpp
78
pseudo_random_source.cpp
9+
antithetic_random_source.cpp
10+
timing.cpp
811
)
912

1013
# Create library

src/antithetic_random_source.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#include "lfmc/random_source.hpp"
2+
3+
namespace lfmc {
4+
AntitheticRandomSource::AntitheticRandomSource(unsigned seed) : rng_(seed), dist_(0.0, 1.0) {}
5+
6+
std::vector<Normals> AntitheticRandomSource::generate_normals(size_t steps, size_t samples) {
7+
std::vector<Normals> result(samples, Normals(steps));
8+
for (size_t i = 0; i < samples; ++i) {
9+
Normals normals(steps);
10+
for (size_t j = 0; j < steps; ++j) {
11+
double z = dist_(rng_);
12+
normals[j] = toggle_ ? -z : z;
13+
}
14+
result[i] = std::move(normals);
15+
toggle_ = !toggle_; // Alternate between normal and antithetic
16+
}
17+
return result;
18+
}
19+
20+
void AntitheticRandomSource::seed(unsigned seed) {
21+
rng_.seed(seed);
22+
toggle_ = false; // Reset toggle when reseeding
23+
}
24+
25+
} // namespace lfmc

src/control_variate_estimator.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#include "lfmc/estimator.hpp"
2+
3+
namespace lfmc {
4+
5+
std::expected<void, std::string>
6+
ControlVariateEstimator::add_payoffs(const std::vector<Payoffs>& payoffs) {
7+
if (payoffs.empty()) {
8+
return std::unexpected("No payoffs provided to add to the estimator.");
9+
}
10+
11+
for (const auto& row : payoffs) {
12+
double x = row[0];
13+
double y = row[1];
14+
15+
sum_x += x;
16+
sum_y += y;
17+
sum_xy += x * y;
18+
sum_yy += y * y;
19+
++count;
20+
}
21+
22+
return {};
23+
}
24+
25+
bool ControlVariateEstimator::converged() const {
26+
return count >= 10000; // Placeholder convergence criterion
27+
}
28+
29+
std::expected<double, std::string> ControlVariateEstimator::result() const {
30+
if (count <= 0) {
31+
return std::unexpected("No samples added to the estimator.");
32+
}
33+
if (!converged()) {
34+
return std::unexpected("Estimator has not yet converged.");
35+
}
36+
37+
double n = static_cast<double>(count);
38+
39+
double mean_x = sum_x / n;
40+
double mean_y = sum_y / n;
41+
42+
double cov_xy = (sum_xy / n) - mean_x * mean_y;
43+
double var_y = (sum_yy / n) - mean_y * mean_y;
44+
45+
if (var_y == 0.0)
46+
return std::unexpected("Zero variance in control variate");
47+
48+
double beta = cov_xy / var_y;
49+
50+
return mean_x - beta * (mean_y - control_expectation_);
51+
}
52+
53+
} // namespace lfmc

0 commit comments

Comments
 (0)