Restructure C++ core into cpp module and package bindings.

Move the pricing engine sources out of src/ into cpp/, add the closed-form engine and pybind wiring, and align tests/build targets with the new project layout.

Made-with: Cursor
This commit is contained in:
David Doebel
2026-04-02 16:30:33 +02:00
parent 61df0b425d
commit 087a2f0d74
53 changed files with 803 additions and 195 deletions

49
cpp/BSWrapper.cpp Normal file
View File

@@ -0,0 +1,49 @@
//
// Created by David Doebel on 27.03.2026.
//
#include "BSWrapper.hpp"
#include "BlackScholesClosedFormEngine.hpp"
#include "BlackScholesProcess.hpp"
#include "Instrument.hpp"
#include "Option.hpp"
#include "FlatVolatilitySurface.hpp"
#include "FlatYieldCurve.hpp"
#include <cassert>
#include <iostream>
class FlatYieldCurve;
double BSWrapper::bs_price_wrapper(double S, double K, double T, double r, double sigma,
bool is_call) {
std::shared_ptr<FlatYieldCurve> flat_curve = std::make_shared<FlatYieldCurve>(r);
auto flat_vol_surface = std::make_shared<FlatVolatilitySurface>(sigma);
MarketData data(S,flat_curve, flat_vol_surface);
std::unique_ptr<BlackScholesProcess> process = std::make_unique<BlackScholesProcess>(data);
std::unique_ptr<BlackScholesClosedFormEngine> pricing_engine =
std::make_unique<BlackScholesClosedFormEngine>(std::move(process));
std::unique_ptr<Payoff> payoff;
if (is_call)
payoff = std::make_unique<CallPayoff>(K);
else payoff = std::make_unique<PutPayoff>(K);
EuropeanExercise exercise(T);
VanillaOption option(T,std::make_unique<EuropeanExercise>(exercise),
std::move(payoff),std::move(pricing_engine));
return option.price();
}
std::vector<double> BSWrapper::batch_bs_price_wrapper(const std::vector<double> &S, const std::vector<double> &K,
const std::vector<double> &T, const std::vector<double> &r, const std::vector<double> &sigma,
const std::vector<bool> &is_call) {
assert(K.size() == S.size() && K.size() == T.size() && K.size() == r.size() && K.size() ==
sigma.size() && K.size() == is_call.size());
std::size_t n = K.size();
std::vector<double> result(n);
for (std::size_t i = 0; i < n; ++i) {
result[i] = bs_price_wrapper(S[i], K[i], T[i], r[i], sigma[i], is_call[i]);
if (i % 100 == 0)
std::cout << "i = " << i << " result = " << result[i] << std::endl; // ( i % 1000 == 0)
}
return result;
}

24
cpp/BSWrapper.hpp Normal file
View File

@@ -0,0 +1,24 @@
/**
* @file BSWrapper.hpp
* @brief BlackScholes vanilla price (closed form; used from Python / implied vol).
*/
#ifndef QUANTENGINE_BSWRAPPER_HPP
#define QUANTENGINE_BSWRAPPER_HPP
#include <vector>
/**
* @brief Static helpers wrapping scalar and batch pricing.
*/
class BSWrapper {
public:
BSWrapper() = delete;
static double bs_price_wrapper(double S, double K, double T, double r, double sigma, bool is_call);
static std::vector<double> batch_bs_price_wrapper(const std::vector<double>& S, const std::vector<double>& K,
const std::vector<double>& T, const std::vector<double>& r, const std::vector<double>& sigma,
const std::vector<bool>& is_call);
};
#endif //QUANTENGINE_BSWRAPPER_HPP

View File

@@ -0,0 +1,69 @@
/**
* @file BlackScholesClosedFormEngine.cpp
* @brief BlackScholes closed-form pricing (calls, puts, cash-or-nothing digital).
*/
#include "BlackScholesClosedFormEngine.hpp"
#include "Instrument.hpp"
#include "Payoff.hpp"
#include <cmath>
#include <stdexcept>
namespace {
double norm_cdf(double x) {
return 0.5 * (1.0 + std::erf(x / std::sqrt(2.0)));
}
} // namespace
double BlackScholesClosedFormEngine::calculate(const Instrument &instrument) const {
if (instrument.exerciseType() != Exercise::Type::European) {
throw std::invalid_argument("BlackScholesClosedFormEngine: European exercise only");
}
const double T = instrument.maturity();
const MarketData &md = process_->data();
const double S = md.spot();
double K = instrument.payoff().strike();
const PayoffKind pk = instrument.payoff().kind();
if (T <= 0.0) {
return instrument.payoff()(S);
}
const double r = md.yield_curve().zeroRate(T);
const double sigma = md.volatility_surface().sigma(K, T);
if (sigma <= 0.0) {
throw std::invalid_argument("BlackScholesClosedFormEngine: volatility must be positive");
}
const double disc = md.yield_curve().discount(T);
const double sqrtT = std::sqrt(T);
const double sig_sqrtT = sigma * sqrtT;
if (sig_sqrtT < 1e-15) {
const double forward = S * std::exp(r * T);
switch (pk) {
case PayoffKind::Call:
return disc * std::max(0.0, forward - K);
case PayoffKind::Put:
return disc * std::max(0.0, K - forward);
case PayoffKind::Digital:
return (forward > K) ? disc : 0.0;
}
}
const double d1 = (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) / sig_sqrtT;
const double d2 = d1 - sig_sqrtT;
switch (pk) {
case PayoffKind::Call:
return S * norm_cdf(d1) - K * disc * norm_cdf(d2);
case PayoffKind::Put:
return K * disc * norm_cdf(-d2) - S * norm_cdf(-d1);
case PayoffKind::Digital:
return disc * norm_cdf(d2);
}
throw std::logic_error("BlackScholesClosedFormEngine: unhandled PayoffKind");
}

View File

@@ -0,0 +1,22 @@
/**
* @file BlackScholesClosedFormEngine.hpp
* @brief Risk-neutral BlackScholes formula for European payoffs under GBM (flat or surface inputs via @ref MarketData).
*/
#ifndef QUANTENGINE_BLACKSCHOLESCLOSEDFORMENGINE_HPP
#define QUANTENGINE_BLACKSCHOLESCLOSEDFORMENGINE_HPP
#include "PricingEngine.hpp"
/**
* @brief Analytic European vanilla / digital prices using @f$r@f$ and @f$\sigma(K,T)@f$ from the embedded processs @ref MarketData.
*/
class BlackScholesClosedFormEngine : public PricingEngine {
public:
explicit BlackScholesClosedFormEngine(std::unique_ptr<StochasticProcess> process)
: PricingEngine(std::move(process)) {}
double calculate(const Instrument &instrument) const override;
};
#endif // QUANTENGINE_BLACKSCHOLESCLOSEDFORMENGINE_HPP

View File

@@ -0,0 +1,24 @@
/**
* @file BlackScholesProcess.cpp
* @brief BlackScholes GBM drift, diffusion, and step.
*/
#include "BlackScholesProcess.hpp"
double BlackScholesProcess::drift(double t, double s) {
double r = this->data().yield_curve().zeroRate(t);
return r * s;
}
double BlackScholesProcess::diffusion(double t, double s) {
double sigma = this->data().volatility_surface().sigma(s,t);
return sigma*s;
}
double BlackScholesProcess::step(double t, double s, double dt, double dW) {
double r = this->data().yield_curve().zeroRate(t);
double sigma = this->data().volatility_surface().sigma(s,t);
return s*exp((r-0.5*sigma*sigma)*dt + sigma*sqrt(dt)*dW);
}

View File

@@ -0,0 +1,26 @@
/**
* @file BlackScholesProcess.hpp
* @brief Geometric Brownian motion with yield and volatility surfaces.
*/
#ifndef QUANTENGINE_BLACKSCHOLESPROCESS_HPP
#define QUANTENGINE_BLACKSCHOLESPROCESS_HPP
#include "StochasticProcess.hpp"
/**
* @brief GBM: drift @f$r_t S@f$, diffusion @f$\sigma(S,t) S@f$, exact log-step.
*/
class BlackScholesProcess : public StochasticProcess{
public:
explicit BlackScholesProcess(MarketData data) : StochasticProcess(std::move(data)){}
double drift(double t, double s) override;
double diffusion(double t, double s) override;
double step(double t, double s, double dt, double dW) override;
};
#endif //QUANTENGINE_BLACKSCHOLESPROCESS_HPP

65
cpp/CMakeLists.txt Normal file
View File

@@ -0,0 +1,65 @@
add_library(qengine_core
Instrument.cpp
Instrument.hpp
Payoff.cpp
Payoff.hpp
Option.cpp
Option.hpp
PricingEngine.cpp
PricingEngine.hpp
MonteCarloEngine.cpp
MonteCarloEngine.hpp
StochasticProcess.cpp
StochasticProcess.hpp
Exercise.cpp
Exercise.hpp
MarketData.cpp
MarketData.hpp
YieldCurve.cpp
YieldCurve.hpp
VolatilitySurface.cpp
VolatilitySurface.hpp
RandomGenerator.cpp
RandomGenerator.hpp
Statistics.cpp
Statistics.hpp
BlackScholesClosedFormEngine.cpp
BlackScholesClosedFormEngine.hpp
BlackScholesProcess.cpp
BlackScholesProcess.hpp
DBIngest.cpp
DBIngest.hpp
BSWrapper.cpp
BSWrapper.hpp
NewtonSolver.cpp
NewtonSolver.hpp
)
target_include_directories(qengine_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(qengine_core PRIVATE
/opt/homebrew/include
)
find_library(PQXX_LIB pqxx PATHS /opt/homebrew/lib /usr/local/lib /usr/lib)
find_library(PQ_LIB pq PATHS /opt/homebrew/opt/libpq/lib /opt/homebrew/lib /usr/local/lib /usr/lib)
if(NOT PQXX_LIB OR NOT PQ_LIB)
message(FATAL_ERROR "Could not find libpqxx and/or libpq (install via Homebrew: brew install libpqxx libpq)")
endif()
target_link_libraries(qengine_core Eigen3::Eigen)
target_link_libraries(qengine_core ${PQXX_LIB} ${PQ_LIB})
# Python import path: package qengine, extension submodule qengine (file qengine/qengine*.so)
pybind11_add_module(qengine_cpp MODULE ImpliedVolatility/Pybind.cpp)
set_target_properties(qengine_cpp PROPERTIES OUTPUT_NAME qengine)
target_link_libraries(qengine_cpp PRIVATE qengine_core)
# Place the module next to qengine/__init__.py so `import qengine` works from the repo root
set(_qengine_py_pkg "${CMAKE_SOURCE_DIR}/qengine")
set_target_properties(qengine_cpp PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${_qengine_py_pkg}"
LIBRARY_OUTPUT_DIRECTORY_RELEASE "${_qengine_py_pkg}"
LIBRARY_OUTPUT_DIRECTORY_DEBUG "${_qengine_py_pkg}"
RUNTIME_OUTPUT_DIRECTORY "${_qengine_py_pkg}"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${_qengine_py_pkg}"
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${_qengine_py_pkg}")

64
cpp/DBIngest.cpp Normal file
View File

@@ -0,0 +1,64 @@
/**
* @file DBIngest.cpp
* @brief Database connection and placeholder update routines.
*/
#include "DBIngest.hpp"
#include <cstdlib>
#include <iostream>
#include <sstream>
bool DBIngest::connect() {
const char* db_name = std::getenv("DB_NAME");
const char* db_user = std::getenv("DB_USER");
const char* db_password = std::getenv("DB_PASSWORD");
const char* db_host = std::getenv("DB_HOST");
const char* db_port = std::getenv("DB_PORT");
std::ostringstream conn_str;
conn_str
<< "dbname=" << (db_name ? db_name : "options_db")
<< " user=" << (db_user ? db_user : "quant_user")
<< " host=" << (db_host ? db_host : "localhost")
<< " port=" << (db_port ? db_port : "5432")
<< " password=" << (db_password ? db_password : "");
connection_ = pqxx::connection(conn_str.str());
if(connection_.is_open()) {
std::cout << "Connected\n";
return true;
}
std::cout << "Not connected\n";
return false;
}
bool DBIngest::disconnect() {
connection_.close();
return true;
}
bool DBIngest::update(VolatilitySurface &surface) {
std::string vol_surface_query = "SELECT c.strike, c.expiration_date, q.mid, u.price "
"FROM option_quotes q"
"JOIN option_contracts c "
"ON q.contract_id = c.id "
"JOIN underlying_prices u"
"ON u.underlying_id = c.underlying_id"
"WHERE q.timestamp = ("
"SELECT MAX(timestamp) FROM option_quotes"
")";
pqxx::work work(connection_);
pqxx::result result = work.exec(vol_surface_query);
for (auto row : result) {
std::cout << row[0] << " " << row[1] << " " << row[2] << " " << row[3] << std::endl;
}
(void)surface;
return false;
}
bool DBIngest::update(YieldCurve &yield_curve) {
(void)yield_curve;
return false;
}

28
cpp/DBIngest.hpp Normal file
View File

@@ -0,0 +1,28 @@
/**
* @file DBIngest.hpp
* @brief PostgreSQL helpers to load market objects (work in progress).
*/
#ifndef QUANTENGINE_DBINGEST_HPP
#define QUANTENGINE_DBINGEST_HPP
#include <pqxx/pqxx>
#include "VolatilitySurface.hpp"
#include "YieldCurve.hpp"
/**
* @brief Connects to Postgres via libpqxx and queries quotes for surface building.
*/
class DBIngest {
bool connect();
bool disconnect();
bool update(VolatilitySurface& surface);
bool update(YieldCurve& yield_curve);
private:
pqxx::connection connection_;
};
#endif //QUANTENGINE_DBINGEST_HPP

6
cpp/Exercise.cpp Normal file
View File

@@ -0,0 +1,6 @@
/**
* @file Exercise.cpp
* @brief @ref Exercise translation unit (interface only).
*/
#include "Exercise.hpp"

61
cpp/Exercise.hpp Normal file
View File

@@ -0,0 +1,61 @@
/**
* @file Exercise.hpp
* @brief Exercise style (European, American, Bermudan) and exercise times.
*/
#ifndef QUANTENGINE_EXERCISE_HPP
#define QUANTENGINE_EXERCISE_HPP
#include <vector>
/**
* @brief Describes when the holder may exercise (metadata for pricing engines).
*/
class Exercise {
public:
Exercise() = default;
virtual ~Exercise() = default;
enum class Type {
European,
American,
Bermudan
};
virtual Type type() const = 0;
protected:
std::vector<double> exercise_times_;
};
/** @brief Single exercise at maturity. */
class EuropeanExercise : public Exercise {
public:
EuropeanExercise() : type_(Type::European) {};
EuropeanExercise(double maturity) : type_(Type::European){
exercise_times_.push_back(maturity);
}
~EuropeanExercise() override = default;
[[nodiscard]] Type type() const override {
return type_;
}
private:
Type type_;
};
/** @brief Continuous American exercise from @f$t=0@f$ to maturity (placeholder grid). */
class AmericanExercise : public Exercise{
public:
AmericanExercise() : type_(Type::American) {};
AmericanExercise(double maturity) : type_(Type::American) {
exercise_times_.push_back(0);
exercise_times_.push_back(maturity);
}
[[nodiscard]] Type type() const override {
return type_;
}
private:
Type type_;
};
#endif //QUANTENGINE_EXERCISE_HPP

View File

@@ -0,0 +1,5 @@
/**
* @file FlatVolatilitySurface.cpp
* @brief Ensures link visibility for @ref FlatVolatilitySurface.
*/
#include "FlatVolatilitySurface.hpp"

View File

@@ -0,0 +1,21 @@
/**
* @file FlatVolatilitySurface.hpp
* @brief Constant implied volatility surface.
*/
#ifndef QUANTENGINE_FLATVOLATILITYSURFACE_HPP
#define QUANTENGINE_FLATVOLATILITYSURFACE_HPP
#include "VolatilitySurface.hpp"
/**
* @brief @f$\sigma(K,T)\equiv\sigma_0@f$.
*/
class FlatVolatilitySurface : public VolatilitySurface {
public:
explicit FlatVolatilitySurface(double sigma = 0.2) : sigma_(sigma) {}
double sigma(double K, double T) const override {return sigma_;}
private:
double sigma_;
};
#endif

5
cpp/FlatYieldCurve.cpp Normal file
View File

@@ -0,0 +1,5 @@
/**
* @file FlatYieldCurve.cpp
* @brief Ensures link visibility for @ref FlatYieldCurve (inline methods in header).
*/
#include "FlatYieldCurve.hpp"

22
cpp/FlatYieldCurve.hpp Normal file
View File

@@ -0,0 +1,22 @@
/**
* @file FlatYieldCurve.hpp
* @brief Constant zero rate yield curve.
*/
#ifndef QUANTENGINE_FLATYIELDCURVE_HPP
#define QUANTENGINE_FLATYIELDCURVE_HPP
#include "YieldCurve.hpp"
#include <cmath>
/**
* @brief @f$P(t)=e^{-r t}@f$, @f$f(t)\equiv r@f$.
*/
class FlatYieldCurve : public YieldCurve{
public:
explicit FlatYieldCurve(double rate = 0.01) : rate_(rate) {}
double discount(double t) const override {return std::exp(-rate_ * t); };
double zeroRate(double t) const override {return rate_; }
private:
double rate_ = 0.01;
};
#endif

View File

@@ -0,0 +1,93 @@
/**
* @file Pybind.cpp
* @brief pybind11 module @c qengine exposing @ref BSWrapper::bs_price_wrapper overloads.
*/
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <vector>
#include "BSWrapper.hpp"
namespace py = pybind11;
namespace {
std::vector<double> to_vector_double(const py::array_t<double> &a) {
py::buffer_info info = a.request();
if (info.ndim != 1) {
throw std::runtime_error("expected 1-D ndarray for S, K, T, r, sigma");
}
const auto *p = static_cast<const double *>(info.ptr);
const ssize_t n = info.shape[0];
return std::vector<double>(p, p + n);
}
std::vector<bool> to_vector_bool_1d(const py::array_t<bool> &a) {
py::buffer_info info = a.request();
if (info.ndim != 1) {
throw std::runtime_error("expected 1-D ndarray for is_call");
}
if (info.itemsize != 1) {
throw std::runtime_error("is_call: expected a boolean ndarray (dtype=bool)");
}
const ssize_t n = info.shape[0];
const auto *p = static_cast<const std::uint8_t *>(info.ptr);
std::vector<bool> out(static_cast<size_t>(n));
for (ssize_t i = 0; i < n; ++i) {
out[static_cast<size_t>(i)] = (p[i] != 0);
}
return out;
}
void check_same_length(size_t n, size_t k, const char *name) {
if (n != k) {
throw std::runtime_error(std::string("length mismatch for ") + name);
}
}
} // namespace
PYBIND11_MODULE(qengine, m) {
m.doc() = "Binding for the Black Scholes model";
m.def(
"bs_price",
[](double S, double K, double T, double r, double sigma, bool is_call) {
return BSWrapper::bs_price_wrapper(S, K, T, r, sigma, is_call);
},
py::arg("S"), py::arg("K"), py::arg("T"), py::arg("r"), py::arg("sigma"), py::arg("is_call"));
m.def(
"bs_price",
[](py::array_t<double> S, py::array_t<double> K, py::array_t<double> T, py::array_t<double> r,
py::array_t<double> sigma, py::array_t<bool> is_call) {
std::vector<double> vS = to_vector_double(S);
std::vector<double> vK = to_vector_double(K);
std::vector<double> vT = to_vector_double(T);
std::vector<double> vr = to_vector_double(r);
std::vector<double> vsig = to_vector_double(sigma);
std::vector<bool> vC = to_vector_bool_1d(is_call);
const size_t n = vS.size();
check_same_length(n, vK.size(), "K");
check_same_length(n, vT.size(), "T");
check_same_length(n, vr.size(), "r");
check_same_length(n, vsig.size(), "sigma");
check_same_length(n, vC.size(), "is_call");
return BSWrapper::batch_bs_price_wrapper(vS, vK, vT, vr, vsig, vC);
},
py::arg("S"), py::arg("K"), py::arg("T"), py::arg("r"), py::arg("sigma"), py::arg("is_call"));
m.def(
"bs_price",
[](const std::vector<double> &S, const std::vector<double> &K, const std::vector<double> &T,
const std::vector<double> &r, const std::vector<double> &sigma, const std::vector<bool> &is_call) {
return BSWrapper::batch_bs_price_wrapper(S, K, T, r, sigma, is_call);
},
py::arg("S"), py::arg("K"), py::arg("T"), py::arg("r"), py::arg("sigma"), py::arg("is_call"));
}

18
cpp/Instrument.cpp Normal file
View File

@@ -0,0 +1,18 @@
/**
* @file Instrument.cpp
* @brief @ref Instrument implementation.
*/
#include "Instrument.hpp"
Instrument::Instrument(double maturity, std::unique_ptr<Payoff> payoff,
std::unique_ptr<PricingEngine> engine) : maturity_(maturity), payoff_(std::move(payoff)), engine_
(std::move(engine)){
}
double Instrument::price() const {
return engine_->calculate(*this);
}

42
cpp/Instrument.hpp Normal file
View File

@@ -0,0 +1,42 @@
/**
* @file Instrument.hpp
* @brief Generic derivative instrument: payoff plus pricing engine.
*/
#ifndef QUANTENGINE_INSTRUMENT_HPP
#define QUANTENGINE_INSTRUMENT_HPP
#include "Exercise.hpp"
#include "Payoff.hpp"
#include "PricingEngine.hpp"
#include <memory>
class PricingEngine;
/**
* @brief Represents a tradeable claim priced via a @ref PricingEngine.
*/
class Instrument {
public:
Instrument() = default;
Instrument(double maturity, std::unique_ptr<Payoff> payoff, std::unique_ptr<PricingEngine> engine);
double price() const;
[[nodiscard]] double maturity() const {
return maturity_;
}
[[nodiscard]] Payoff& payoff() const {
return *payoff_;
}
/** @brief Base @ref Instrument is treated as European unless overridden by @ref Option. */
[[nodiscard]] virtual Exercise::Type exerciseType() const { return Exercise::Type::European; }
protected:
double maturity_;
std::unique_ptr<Payoff> payoff_;
std::unique_ptr<PricingEngine> engine_;
};
#endif //QUANTENGINE_INSTRUMENT_HPP

10
cpp/MarketData.cpp Normal file
View File

@@ -0,0 +1,10 @@
/**
* @file MarketData.cpp
* @brief @ref MarketData accessors.
*/
#include "MarketData.hpp"
double MarketData::spot() const { return spot_; }
const YieldCurve& MarketData::yield_curve() const { return *yield_curve_; }
const VolatilitySurface& MarketData::volatility_surface() const { return *volatility_surface_; }

37
cpp/MarketData.hpp Normal file
View File

@@ -0,0 +1,37 @@
/**
* @file MarketData.hpp
* @brief Spot, discount curve, and volatility surface bundle.
*/
#ifndef QUANTENGINE_MARKETDATA_HPP
#define QUANTENGINE_MARKETDATA_HPP
#include "YieldCurve.hpp"
#include "VolatilitySurface.hpp"
#include <memory>
/**
* @brief Immutable snapshot of inputs needed to simulate or price.
*/
class MarketData {
public:
MarketData() = delete;
MarketData(double spot, std::shared_ptr<const YieldCurve> yield_curve,
std::shared_ptr<const VolatilitySurface> volatility_surface)
: spot_(spot),
yield_curve_(std::move(yield_curve)),
volatility_surface_(std::move(volatility_surface)) {
}
double spot() const;
const YieldCurve& yield_curve() const;
const VolatilitySurface& volatility_surface() const;
private:
double spot_;
std::shared_ptr<const YieldCurve> yield_curve_;
std::shared_ptr<const VolatilitySurface> volatility_surface_;
};
#endif //QUANTENGINE_MARKETDATA_HPP

25
cpp/MonteCarloEngine.cpp Normal file
View File

@@ -0,0 +1,25 @@
/**
* @file MonteCarloEngine.cpp
* @brief Monte Carlo mean estimator with discounting.
*/
#include "MonteCarloEngine.hpp"
#include <iostream>
#include "Instrument.hpp"
#include "Statistics.hpp"
double MonteCarloEngine::calculate(const Instrument &instrument) const {
// parameters
double T = instrument.maturity();
double spot = process_->data().spot();
Statistics stats;
auto rNumbers = rng_->nextGaussianVector(numPaths_);
std::vector<double> payoffs(numPaths_);
for (std::size_t i = 0; i < numPaths_; ++i) {
double terminalPrice = process_->step(0.0,spot,T,rNumbers[i]);
double payoff = instrument.payoff()(terminalPrice);
stats.dump(payoff);
}
return stats.mean() * process_->data().yield_curve().discount(T);
}

26
cpp/MonteCarloEngine.hpp Normal file
View File

@@ -0,0 +1,26 @@
/**
* @file MonteCarloEngine.hpp
* @brief Monte Carlo pricing using a @ref StochasticProcess and @ref RandomGenerator.
*/
#ifndef QUANTENGINE_MONTECARLOENGINE_HPP
#define QUANTENGINE_MONTECARLOENGINE_HPP
#include "PricingEngine.hpp"
#include "RandomGenerator.hpp"
/**
* @brief Simple path simulation: one Euler/exact step to horizon, average discounted payoff.
*/
class MonteCarloEngine : public PricingEngine{
public:
MonteCarloEngine() = default;
MonteCarloEngine(int numPaths, std::unique_ptr<StochasticProcess> process, std::shared_ptr<RandomGenerator> rng):
numPaths_(numPaths), PricingEngine(std::move(process)), rng_(std::move(rng)) {}
double calculate(const Instrument& instrument) const override;
private:
int numPaths_;
std::shared_ptr<RandomGenerator> rng_;
};
#endif //QUANTENGINE_MONTECARLOENGINE_HPP

8
cpp/NewtonSolver.cpp Normal file
View File

@@ -0,0 +1,8 @@
/**
* @file NewtonSolver.cpp
* @brief Placeholder translation unit for @ref NewtonSolver.
*/
#include "NewtonSolver.hpp"

30
cpp/NewtonSolver.hpp Normal file
View File

@@ -0,0 +1,30 @@
/**
* @file NewtonSolver.hpp
* @brief Generic Newton iteration helper (incomplete / reserved for solvers).
*/
#ifndef QUANTENGINE_GAUSSSOLVER_HPP
#define QUANTENGINE_GAUSSSOLVER_HPP
#include <functional>
/**
* @brief Template Newton step loop with relative/absolute tolerances.
*/
class NewtonSolver {
template<typename F, typename DFinv, typename T>
bool solve(F&& func, DFinv&& dfinv,T x0 , double rtol, double atol) {
T x = x0;
int i = 0;
T increment;
do {
increment = dfinv(x) * func(x);
x -= increment;
++i;
} while (i < 1000 && std::abs(increment)/ std::abs(x) > rtol && std::abs(increment) > atol);
}
};
#endif //QUANTENGINE_GAUSSSOLVER_HPP

11
cpp/Option.cpp Normal file
View File

@@ -0,0 +1,11 @@
/**
* @file Option.cpp
* @brief @ref Option implementation.
*/
#include "Option.hpp"
Option::Option(double maturity, std::unique_ptr<Exercise> exercise, std::unique_ptr<Payoff> payoff,
std::unique_ptr<PricingEngine> engine) : Instrument(maturity, std::move(payoff),
std::move(engine)), exercise_(std::move(exercise)){
}

40
cpp/Option.hpp Normal file
View File

@@ -0,0 +1,40 @@
/**
* @file Option.hpp
* @brief Option instrument with exercise style (@ref Exercise).
*/
#ifndef QUANTENGINE_OPTION_HPP
#define QUANTENGINE_OPTION_HPP
#include "Instrument.hpp"
#include "Exercise.hpp"
/**
* @brief Extends @ref Instrument with exercise schedule / style metadata.
*/
class Option : public Instrument{
public:
Option() = default;
virtual ~Option() = default;
Option(double maturity, std::unique_ptr<Exercise> exercise,
std::unique_ptr<Payoff> payoff, std::unique_ptr<PricingEngine> engine);
[[nodiscard]] Exercise& exercise() const {
return *exercise_;
}
[[nodiscard]] Exercise::Type exerciseType() const override { return exercise_->type(); }
protected:
std::unique_ptr<Exercise> exercise_;
};
/** @brief Plain-vanilla option using the base @ref Option constructor. */
class VanillaOption : public Option {
public:
using Option::Option;
};
#endif //QUANTENGINE_OPTION_HPP

19
cpp/Payoff.cpp Normal file
View File

@@ -0,0 +1,19 @@
/**
* @file Payoff.cpp
* @brief Payoff function implementations.
*/
#include "Payoff.hpp"
#include <algorithm>
double CallPayoff::operator()(double S) {
return std::max(0., S - strike_);
}
double PutPayoff::operator()(double S) {
return std::max(0., strike_ - S);
}
double DigitalPayoff::operator()(double S) {
return S > strike_ ? 1. : 0.;
}

66
cpp/Payoff.hpp Normal file
View File

@@ -0,0 +1,66 @@
/**
* @file Payoff.hpp
* @brief Payoff interface and standard European payoffs (call, put, digital).
*/
#ifndef QUANTENGINE_PAYOFF_HPP
#define QUANTENGINE_PAYOFF_HPP
/**
* @brief Standard payoff shapes for routing (e.g. analytic vs Monte Carlo).
*/
enum class PayoffKind { Call, Put, Digital };
/**
* @brief Terminal payoff as a function of spot @f$S_T@f$.
*/
class Payoff {
public:
Payoff() = default;
virtual ~Payoff() = default;
virtual double operator()(double S) = 0;
virtual double strike() = 0;
[[nodiscard]] virtual PayoffKind kind() const = 0;
};
/** @brief Standard European call @f$\max(S-K,0)@f$. */
class CallPayoff : public Payoff {
public:
CallPayoff() = default;
CallPayoff(double strike) : strike_(strike) {}
double operator()(double S) override;
double strike() override {return strike_;}
[[nodiscard]] PayoffKind kind() const override { return PayoffKind::Call; }
private:
double strike_;
};
/** @brief Standard European put @f$\max(K-S,0)@f$. */
class PutPayoff : public Payoff {
public:
PutPayoff() = default;
PutPayoff(double strike) : strike_(strike) {}
double operator()(double S) override;
double strike() override {return strike_;}
[[nodiscard]] PayoffKind kind() const override { return PayoffKind::Put; }
private:
double strike_;
};
/** @brief Digital (cash-or-nothing style) payoff @f$1_{S>K}@f$. */
class DigitalPayoff : public Payoff {
public:
DigitalPayoff() = default;
DigitalPayoff(double strike) : strike_(strike) {}
double operator()(double S) override;
double strike() override {return strike_;}
[[nodiscard]] PayoffKind kind() const override { return PayoffKind::Digital; }
private:
double strike_;
};
#endif //QUANTENGINE_PAYOFF_HPP

6
cpp/PricingEngine.cpp Normal file
View File

@@ -0,0 +1,6 @@
/**
* @file PricingEngine.cpp
* @brief @ref PricingEngine translation unit (interface only).
*/
#include "PricingEngine.hpp"

30
cpp/PricingEngine.hpp Normal file
View File

@@ -0,0 +1,30 @@
/**
* @file PricingEngine.hpp
* @brief Abstract pricer for @ref Instrument given a stochastic model.
*/
#ifndef QUANTENGINE_PRICINGENGINE_HPP
#define QUANTENGINE_PRICINGENGINE_HPP
#include <memory>
#include "StochasticProcess.hpp"
class Instrument;
/**
* @brief Computes model price of an instrument (e.g. Monte Carlo, PDE, closed form).
*/
class PricingEngine {
public:
PricingEngine() = default;
PricingEngine(std::unique_ptr<StochasticProcess> process) : process_(std::move(process)){}
virtual ~PricingEngine() = default;
virtual double calculate(const Instrument& instrument) const = 0;
protected:
std::unique_ptr<StochasticProcess> process_;
};
#endif //QUANTENGINE_PRICINGENGINE_HPP

19
cpp/RandomGenerator.cpp Normal file
View File

@@ -0,0 +1,19 @@
/**
* @file RandomGenerator.cpp
* @brief @ref MersenneTwister implementation.
*/
#include "RandomGenerator.hpp"
double MersenneTwister::nextGaussian() {
return distr_(generator_);
}
std::vector<double> MersenneTwister::nextGaussianVector(std::size_t n) {
std::vector<double> v(n);
for (auto& e : v) {
e = nextGaussian();
}
return v;
}

31
cpp/RandomGenerator.hpp Normal file
View File

@@ -0,0 +1,31 @@
/**
* @file RandomGenerator.hpp
* @brief Random numbers for Monte Carlo (Gaussian draws).
*/
#ifndef QUANTENGINE_RANDOMGENERATOR_HPP
#define QUANTENGINE_RANDOMGENERATOR_HPP
#include <random>
/** @brief Interface for standard normal variates. */
class RandomGenerator {
public:
RandomGenerator() = default;
virtual ~RandomGenerator() = default;
virtual double nextGaussian() = 0;
virtual std::vector<double> nextGaussianVector(std::size_t n) = 0;
};
/** @brief @c std::mt19937 with normal distribution. */
class MersenneTwister : public RandomGenerator {
public:
MersenneTwister() = default;
double nextGaussian() override;
std::vector<double> nextGaussianVector(std::size_t n) override;
private:
std::mt19937 generator_;
std::normal_distribution<> distr_ {0.0, 1.0};
};
#endif //QUANTENGINE_RANDOMGENERATOR_HPP

53
cpp/Statistics.cpp Normal file
View File

@@ -0,0 +1,53 @@
/**
* @file Statistics.cpp
* @brief Streaming moment and extrema updates.
*/
#include "Statistics.hpp"
void Statistics::dump(double value) {
for (std::size_t i = 0; i < 3; ++i) {
moments_[i] += std::pow(value, i+1);
}
++n;
max_ = std::max(max_, value);
min_ = std::min(min_, value);
}
void Statistics::clear() {
n = 0;
moments_ = {0.,0.,0.};
}
double Statistics::mean() {
return moments_[0]/n;
}
double Statistics::variance() {
return moments_[1]/n - std::pow(mean(), 2);
}
double Statistics::standardDeviation() {
return std::sqrt(variance());
}
double Statistics::skewness() {
return moments_[2]/std::pow(n, 3);
}
double Statistics::max() {
return max_;
}
double Statistics::min() {
return min_;
}
double Statistics::sum() {
return moments_[0];
}
double Statistics::count() {
return n;
}

33
cpp/Statistics.hpp Normal file
View File

@@ -0,0 +1,33 @@
/**
* @file Statistics.hpp
* @brief Online sample moments for Monte Carlo diagnostics.
*/
#ifndef QUANTENGINE_STATISTICS_HPP
#define QUANTENGINE_STATISTICS_HPP
#include <vector>
/**
* @brief Accumulates count, mean/variance-related sums, and running min/max.
*/
class Statistics {
public:
Statistics() : moments_({0., 0., 0.}), n(0), max_(0.), min_(0.) {}
void dump(double value);
void clear();
double mean();
double variance();
double standardDeviation();
double skewness();
double max();
double min();
double sum();
double count();
private:
std::vector<double> moments_;
std::size_t n;
double max_, min_;
};
#endif //QUANTENGINE_STATISTICS_HPP

View File

@@ -0,0 +1,6 @@
/**
* @file StochasticProcess.cpp
* @brief @ref StochasticProcess translation unit (interface only).
*/
#include "StochasticProcess.hpp"

32
cpp/StochasticProcess.hpp Normal file
View File

@@ -0,0 +1,32 @@
/**
* @file StochasticProcess.hpp
* @brief Interface for SDE drift, diffusion, and time stepping.
*/
#ifndef QUANTENGINE_STOCHASTICPROCESS_HPP
#define QUANTENGINE_STOCHASTICPROCESS_HPP
#include "MarketData.hpp"
#include <memory>
/**
* @brief Stochastic model for the underlying, driven by @ref MarketData.
*/
class StochasticProcess {
public:
StochasticProcess() = delete;
explicit StochasticProcess(MarketData data) : data_(std::move(data)){}
virtual ~StochasticProcess() = default;
virtual double drift(double t, double s) = 0;
virtual double diffusion(double t, double s) = 0;
virtual double step(double t, double s, double dt, double dW) = 0;
const MarketData& data() const {return data_;}
private:
MarketData data_;
};
#endif //QUANTENGINE_STOCHASTICPROCESS_HPP

View File

@@ -0,0 +1,6 @@
/**
* @file VolatilitySurface.cpp
* @brief @ref VolatilitySurface translation unit (interface only).
*/
#include "VolatilitySurface.hpp"

28
cpp/VolatilitySurface.hpp Normal file
View File

@@ -0,0 +1,28 @@
/**
* @file VolatilitySurface.hpp
* @brief Implied volatility as a function of strike and expiry.
*/
#ifndef QUANTENGINE_VOLATILITYSURFACE_HPP
#define QUANTENGINE_VOLATILITYSURFACE_HPP
#include <vector>
/**
* @brief Local/vol surface @f$\sigma(K,T)@f$ used by simulation.
*/
class VolatilitySurface {
public:
virtual ~VolatilitySurface() = default;
virtual double sigma(double K, double T) const = 0;
private:
};
class SVI : public VolatilitySurface {
public:
SVI() = default;
SVI(std::vector<double> K, std::vector<double> rho, std::vector<double> S, std::vector<double> T);
};
#endif //QUANTENGINE_VOLATILITYSURFACE_HPP

6
cpp/YieldCurve.cpp Normal file
View File

@@ -0,0 +1,6 @@
/**
* @file YieldCurve.cpp
* @brief @ref YieldCurve translation unit (interface only).
*/
#include "YieldCurve.hpp"

40
cpp/YieldCurve.hpp Normal file
View File

@@ -0,0 +1,40 @@
/**
* @file YieldCurve.hpp
* @brief Abstract yield curve: discount factors and zero rates.
*/
#ifndef QUANTENGINE_YIELDCURVE_HPP
#define QUANTENGINE_YIELDCURVE_HPP
/**
* @brief Risk-free rate term structure for discounting and risk-neutral drift.
*/
class YieldCurve {
public:
YieldCurve() = default;
YieldCurve(const YieldCurve &other) {
}
YieldCurve(YieldCurve &&other) noexcept {
}
YieldCurve & operator=(const YieldCurve &other) {
if (this == &other)
return *this;
return *this;
}
YieldCurve & operator=(YieldCurve &&other) noexcept {
if (this == &other)
return *this;
return *this;
}
virtual ~YieldCurve() = default;
virtual double discount(double t) const = 0;
virtual double zeroRate(double t) const = 0;
};
#endif //QUANTENGINE_YIELDCURVE_HPP