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:
@@ -4,31 +4,53 @@ project(QuantEngine)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_FLAGS "-O3 -march=native")
|
||||
|
||||
option(BUILD_TESTING "Build GoogleTest target and tests" ON)
|
||||
|
||||
set(PYBIND11_FINDPYTHON ON)
|
||||
find_package(Python3 REQUIRED COMPONENTS Interpreter Development.Module)
|
||||
find_package(Eigen3 REQUIRED)
|
||||
find_package(pybind11 CONFIG REQUIRED)
|
||||
#find_package(PostgreSQL REQUIRED)
|
||||
#find_package(PkgConfig REQUIRED)
|
||||
#pkg_check_modules(PQXX REQUIRED IMPORTED_TARGET libpqxx)
|
||||
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(cpp)
|
||||
|
||||
# Testing
|
||||
enable_testing()
|
||||
find_package(Doxygen OPTIONAL_COMPONENTS dot)
|
||||
if(DOXYGEN_FOUND)
|
||||
add_custom_target(
|
||||
docs
|
||||
COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_SOURCE_DIR}/docs/Doxyfile
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMENT "Generating API documentation (HTML in docs/html)"
|
||||
VERBATIM)
|
||||
endif()
|
||||
|
||||
include(FetchContent)
|
||||
install(FILES "${CMAKE_SOURCE_DIR}/qengine/__init__.py" DESTINATION qengine)
|
||||
install(TARGETS qengine_cpp
|
||||
LIBRARY DESTINATION qengine
|
||||
RUNTIME DESTINATION qengine)
|
||||
|
||||
FetchContent_Declare(
|
||||
if(BUILD_TESTING)
|
||||
enable_testing()
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
)
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
|
||||
add_executable(qengine_tests
|
||||
add_executable(qengine_tests
|
||||
tests/test_black_scholes.cpp
|
||||
tests/stubs/FlatYieldCurve.cpp
|
||||
tests/stubs/FlatVolatilitySurface.cpp)
|
||||
|
||||
target_link_libraries(qengine_tests qengine GTest::gtest_main)
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(qengine_tests)
|
||||
target_include_directories(qengine_tests PRIVATE ${CMAKE_SOURCE_DIR}/tests)
|
||||
target_link_libraries(qengine_tests qengine_core GTest::gtest_main)
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(qengine_tests)
|
||||
endif()
|
||||
|
||||
49
cpp/BSWrapper.cpp
Normal file
49
cpp/BSWrapper.cpp
Normal 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
24
cpp/BSWrapper.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @file BSWrapper.hpp
|
||||
* @brief Black–Scholes 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
|
||||
69
cpp/BlackScholesClosedFormEngine.cpp
Normal file
69
cpp/BlackScholesClosedFormEngine.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file BlackScholesClosedFormEngine.cpp
|
||||
* @brief Black–Scholes 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");
|
||||
}
|
||||
22
cpp/BlackScholesClosedFormEngine.hpp
Normal file
22
cpp/BlackScholesClosedFormEngine.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @file BlackScholesClosedFormEngine.hpp
|
||||
* @brief Risk-neutral Black–Scholes 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 process’s @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
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file BlackScholesProcess.cpp
|
||||
* @brief Black–Scholes GBM drift, diffusion, and step.
|
||||
*/
|
||||
|
||||
#include "BlackScholesProcess.hpp"
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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)){}
|
||||
65
cpp/CMakeLists.txt
Normal file
65
cpp/CMakeLists.txt
Normal 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}")
|
||||
@@ -1,25 +1,30 @@
|
||||
//
|
||||
// Created by David Doebel on 13.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file DBIngest.cpp
|
||||
* @brief Database connection and placeholder update routines.
|
||||
*/
|
||||
|
||||
#include "DBIngest.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
// Queries
|
||||
// Query for selecting the volatility surface parameters
|
||||
std::string vol_surface_query = ""
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
|
||||
|
||||
|
||||
#include <sstream>
|
||||
|
||||
bool DBIngest::connect() {
|
||||
connection_ = pqxx::connection("dbname=options_db user=quant_user port = 5432 host = localhost password = strong_password" );
|
||||
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";
|
||||
@@ -31,6 +36,7 @@ bool DBIngest::connect() {
|
||||
|
||||
bool DBIngest::disconnect() {
|
||||
connection_.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DBIngest::update(VolatilitySurface &surface) {
|
||||
@@ -48,8 +54,11 @@ bool DBIngest::update(VolatilitySurface &surface) {
|
||||
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;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 13.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file DBIngest.hpp
|
||||
* @brief PostgreSQL helpers to load market objects (work in progress).
|
||||
*/
|
||||
|
||||
#ifndef QUANTENGINE_DBINGEST_HPP
|
||||
#define QUANTENGINE_DBINGEST_HPP
|
||||
@@ -10,6 +11,9 @@
|
||||
#include "VolatilitySurface.hpp"
|
||||
#include "YieldCurve.hpp"
|
||||
|
||||
/**
|
||||
* @brief Connects to Postgres via libpqxx and queries quotes for surface building.
|
||||
*/
|
||||
class DBIngest {
|
||||
|
||||
bool connect();
|
||||
6
cpp/Exercise.cpp
Normal file
6
cpp/Exercise.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @file Exercise.cpp
|
||||
* @brief @ref Exercise translation unit (interface only).
|
||||
*/
|
||||
|
||||
#include "Exercise.hpp"
|
||||
@@ -1,11 +1,15 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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;
|
||||
@@ -22,7 +26,9 @@ protected:
|
||||
|
||||
};
|
||||
|
||||
/** @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);
|
||||
@@ -35,7 +41,9 @@ 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);
|
||||
5
cpp/FlatVolatilitySurface.cpp
Normal file
5
cpp/FlatVolatilitySurface.cpp
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @file FlatVolatilitySurface.cpp
|
||||
* @brief Ensures link visibility for @ref FlatVolatilitySurface.
|
||||
*/
|
||||
#include "FlatVolatilitySurface.hpp"
|
||||
21
cpp/FlatVolatilitySurface.hpp
Normal file
21
cpp/FlatVolatilitySurface.hpp
Normal 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
5
cpp/FlatYieldCurve.cpp
Normal 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
22
cpp/FlatYieldCurve.hpp
Normal 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
|
||||
93
cpp/ImpliedVolatility/Pybind.cpp
Normal file
93
cpp/ImpliedVolatility/Pybind.cpp
Normal 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"));
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file Instrument.cpp
|
||||
* @brief @ref Instrument implementation.
|
||||
*/
|
||||
|
||||
#include "Instrument.hpp"
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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;
|
||||
@@ -24,6 +29,9 @@ public:
|
||||
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_;
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file MarketData.cpp
|
||||
* @brief @ref MarketData accessors.
|
||||
*/
|
||||
|
||||
#include "MarketData.hpp"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file MarketData.hpp
|
||||
* @brief Spot, discount curve, and volatility surface bundle.
|
||||
*/
|
||||
|
||||
#ifndef QUANTENGINE_MARKETDATA_HPP
|
||||
#define QUANTENGINE_MARKETDATA_HPP
|
||||
@@ -8,6 +9,9 @@
|
||||
#include "VolatilitySurface.hpp"
|
||||
#include <memory>
|
||||
|
||||
/**
|
||||
* @brief Immutable snapshot of inputs needed to simulate or price.
|
||||
*/
|
||||
class MarketData {
|
||||
public:
|
||||
MarketData() = delete;
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file MonteCarloEngine.cpp
|
||||
* @brief Monte Carlo mean estimator with discounting.
|
||||
*/
|
||||
|
||||
#include "MonteCarloEngine.hpp"
|
||||
#include <iostream>
|
||||
@@ -1,13 +1,16 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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;
|
||||
8
cpp/NewtonSolver.cpp
Normal file
8
cpp/NewtonSolver.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @file NewtonSolver.cpp
|
||||
* @brief Placeholder translation unit for @ref NewtonSolver.
|
||||
*/
|
||||
|
||||
#include "NewtonSolver.hpp"
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
//
|
||||
// Created by David Doebel on 13.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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) {
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file Option.cpp
|
||||
* @brief @ref Option implementation.
|
||||
*/
|
||||
|
||||
#include "Option.hpp"
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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;
|
||||
@@ -17,10 +21,13 @@ public:
|
||||
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;
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file Payoff.cpp
|
||||
* @brief Payoff function implementations.
|
||||
*/
|
||||
|
||||
#include "Payoff.hpp"
|
||||
#include <algorithm>
|
||||
@@ -1,11 +1,19 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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:
|
||||
|
||||
@@ -14,35 +22,42 @@ public:
|
||||
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_;
|
||||
};
|
||||
6
cpp/PricingEngine.cpp
Normal file
6
cpp/PricingEngine.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @file PricingEngine.cpp
|
||||
* @brief @ref PricingEngine translation unit (interface only).
|
||||
*/
|
||||
|
||||
#include "PricingEngine.hpp"
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file PricingEngine.hpp
|
||||
* @brief Abstract pricer for @ref Instrument given a stochastic model.
|
||||
*/
|
||||
|
||||
#ifndef QUANTENGINE_PRICINGENGINE_HPP
|
||||
#define QUANTENGINE_PRICINGENGINE_HPP
|
||||
@@ -10,6 +11,9 @@
|
||||
|
||||
class Instrument;
|
||||
|
||||
/**
|
||||
* @brief Computes model price of an instrument (e.g. Monte Carlo, PDE, closed form).
|
||||
*/
|
||||
class PricingEngine {
|
||||
public:
|
||||
PricingEngine() = default;
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file RandomGenerator.cpp
|
||||
* @brief @ref MersenneTwister implementation.
|
||||
*/
|
||||
|
||||
#include "RandomGenerator.hpp"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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;
|
||||
@@ -14,6 +16,7 @@ public:
|
||||
virtual std::vector<double> nextGaussianVector(std::size_t n) = 0;
|
||||
};
|
||||
|
||||
/** @brief @c std::mt19937 with normal distribution. */
|
||||
class MersenneTwister : public RandomGenerator {
|
||||
public:
|
||||
MersenneTwister() = default;
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
/**
|
||||
* @file Statistics.cpp
|
||||
* @brief Streaming moment and extrema updates.
|
||||
*/
|
||||
|
||||
#include "Statistics.hpp"
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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.) {}
|
||||
6
cpp/StochasticProcess.cpp
Normal file
6
cpp/StochasticProcess.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @file StochasticProcess.cpp
|
||||
* @brief @ref StochasticProcess translation unit (interface only).
|
||||
*/
|
||||
|
||||
#include "StochasticProcess.hpp"
|
||||
@@ -1,12 +1,16 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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;
|
||||
6
cpp/VolatilitySurface.cpp
Normal file
6
cpp/VolatilitySurface.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @file VolatilitySurface.cpp
|
||||
* @brief @ref VolatilitySurface translation unit (interface only).
|
||||
*/
|
||||
|
||||
#include "VolatilitySurface.hpp"
|
||||
28
cpp/VolatilitySurface.hpp
Normal file
28
cpp/VolatilitySurface.hpp
Normal 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
6
cpp/YieldCurve.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @file YieldCurve.cpp
|
||||
* @brief @ref YieldCurve translation unit (interface only).
|
||||
*/
|
||||
|
||||
#include "YieldCurve.hpp"
|
||||
@@ -1,11 +1,14 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
/**
|
||||
* @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;
|
||||
50
docs/Doxyfile
Normal file
50
docs/Doxyfile
Normal file
@@ -0,0 +1,50 @@
|
||||
# Doxygen configuration for QuantEngine (option_pricing).
|
||||
# Run from repo root: doxygen docs/Doxyfile
|
||||
# Or: cmake --build build --target docs
|
||||
|
||||
PROJECT_NAME = QuantEngine
|
||||
PROJECT_BRIEF = "Monte Carlo option pricing, market data abstractions, and Python bindings"
|
||||
|
||||
OUTPUT_DIRECTORY = docs/html
|
||||
CREATE_SUBDIRS = NO
|
||||
ALLOW_UNICODE_NAMES = YES
|
||||
|
||||
JAVADOC_AUTOBRIEF = YES
|
||||
QT_AUTOBRIEF = NO
|
||||
OPTIMIZE_OUTPUT_FOR_CPLUSPLUS = YES
|
||||
|
||||
FULL_PATH_NAMES = YES
|
||||
STRIP_FROM_PATH =
|
||||
|
||||
QUIET = NO
|
||||
WARNINGS = YES
|
||||
WARN_IF_UNDOCUMENTED = NO
|
||||
WARN_NO_PARAMDOC = NO
|
||||
|
||||
INPUT = cpp
|
||||
INPUT_ENCODING = UTF-8
|
||||
FILE_PATTERNS = *.cpp *.hpp *.h
|
||||
RECURSIVE = YES
|
||||
|
||||
EXCLUDE_PATTERNS =
|
||||
EXCLUDE_SYMBOLS =
|
||||
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = .
|
||||
HTML_COLORSTYLE_HUE = 220
|
||||
GENERATE_LATEX = NO
|
||||
|
||||
SEARCHENGINE = YES
|
||||
|
||||
SOURCE_BROWSER = YES
|
||||
REFERENCED_BY_RELATION = YES
|
||||
REFERENCES_RELATION = YES
|
||||
|
||||
ALPHABETICAL_INDEX = YES
|
||||
ENABLE_PREPROCESSING = YES
|
||||
MACRO_EXPANSION = NO
|
||||
|
||||
CLASS_DIAGRAMS = YES
|
||||
HAVE_DOT = NO
|
||||
|
||||
PREDEFINED = DOXYGEN
|
||||
5
qengine/__init__.py
Normal file
5
qengine/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Qengine: quant pricing backend (native extension in qengine.qengine)."""
|
||||
|
||||
from .qengine import bs_price
|
||||
|
||||
__all__ = ["bs_price"]
|
||||
65
scripts/test_qengine_bindings.py
Normal file
65
scripts/test_qengine_bindings.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Smoke test: use an installed `qengine` package (pip install .) or a dev build (cmake -> qengine/*.so)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Running `python scripts/this.py` puts `scripts/` on sys.path, not the repo root
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(_REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO_ROOT))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
import qengine
|
||||
except ImportError as e:
|
||||
print(
|
||||
f"Import failed ({e}). Install the package (pip install .) or build with CMake so "
|
||||
"qengine/qengine.*.so exists next to qengine/__init__.py.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
call = qengine.bs_price(100.0, 100.0, 1.0, 0.05, 0.2, True)
|
||||
put = qengine.bs_price(100.0, 100.0, 1.0, 0.05, 0.2, False)
|
||||
batch_list = qengine.bs_price(
|
||||
[100.0, 100.0],
|
||||
[100.0, 110.0],
|
||||
[1.0, 1.0],
|
||||
[0.05, 0.05],
|
||||
[0.2, 0.2],
|
||||
[True, False],
|
||||
)
|
||||
|
||||
assert math.isfinite(call) and math.isfinite(put)
|
||||
assert len(batch_list) == 2 and all(math.isfinite(x) for x in batch_list)
|
||||
|
||||
print("qengine.bs_price (call):", call)
|
||||
print("qengine.bs_price (put):", put)
|
||||
print("qengine.bs_price (list batch):", list(batch_list))
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("ok: overloads callable (NumPy not installed; skipped ndarray batch test).")
|
||||
return 0
|
||||
|
||||
s = np.array([100.0, 100.0], dtype=np.float64)
|
||||
k = np.array([100.0, 110.0], dtype=np.float64)
|
||||
t = np.array([1.0, 1.0], dtype=np.float64)
|
||||
r = np.array([0.05, 0.05], dtype=np.float64)
|
||||
sig = np.array([0.2, 0.2], dtype=np.float64)
|
||||
opt = np.array([True, False], dtype=bool)
|
||||
batch_np = qengine.bs_price(s, k, t, r, sig, opt)
|
||||
assert len(batch_np) == 2 and all(math.isfinite(float(x)) for x in batch_np)
|
||||
print("qengine.bs_price (ndarray batch):", [float(x) for x in batch_np])
|
||||
print("ok: overloads callable.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,41 +0,0 @@
|
||||
add_library(qengine
|
||||
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
|
||||
BlackScholesProcess.cpp
|
||||
BlackScholesProcess.hpp
|
||||
DBIngest.cpp
|
||||
DBIngest.hpp
|
||||
GaussSolver.cpp
|
||||
GaussSolver.hpp
|
||||
|
||||
|
||||
)
|
||||
|
||||
target_include_directories(qengine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_include_directories(qengine PRIVATE
|
||||
/opt/homebrew/include
|
||||
)
|
||||
target_link_libraries(qengine Eigen3::Eigen)
|
||||
target_link_libraries(qengine pqxx pq)
|
||||
@@ -1,5 +0,0 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
|
||||
#include "Exercise.hpp"
|
||||
@@ -1,7 +0,0 @@
|
||||
//
|
||||
// Created by David Doebel on 13.03.2026.
|
||||
//
|
||||
|
||||
#include "NewtonSolver.hpp"
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
|
||||
#include "PricingEngine.hpp"
|
||||
@@ -1,5 +0,0 @@
|
||||
//
|
||||
// Created by David Doebel on 05.03.2026.
|
||||
//
|
||||
|
||||
#include "StochasticProcess.hpp"
|
||||
@@ -1,5 +0,0 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
|
||||
#include "VolatilitySurface.hpp"
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
|
||||
#ifndef QUANTENGINE_VOLATILITYSURFACE_HPP
|
||||
#define QUANTENGINE_VOLATILITYSURFACE_HPP
|
||||
|
||||
|
||||
class VolatilitySurface {
|
||||
public:
|
||||
virtual ~VolatilitySurface() = default;
|
||||
virtual double sigma(double K, double T) const = 0;
|
||||
private:
|
||||
|
||||
};
|
||||
|
||||
|
||||
#endif //QUANTENGINE_VOLATILITYSURFACE_HPP
|
||||
@@ -1,5 +0,0 @@
|
||||
//
|
||||
// Created by David Doebel on 06.03.2026.
|
||||
//
|
||||
|
||||
#include "YieldCurve.hpp"
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "BlackScholesClosedFormEngine.hpp"
|
||||
#include "BlackScholesProcess.hpp"
|
||||
#include "MonteCarloEngine.hpp"
|
||||
#include "Instrument.hpp"
|
||||
@@ -51,3 +52,28 @@ TEST(BlackScholesProcess, ExpectedValue) {
|
||||
ASSERT_NEAR(callPrice, callGT, tol);
|
||||
ASSERT_NEAR(putPrice, putGT, tol);
|
||||
}
|
||||
|
||||
TEST(BlackScholesClosedForm, MatchesReference) {
|
||||
const double K = 100.0;
|
||||
const double T = 1.0;
|
||||
|
||||
const MarketData marketData(
|
||||
100.0,
|
||||
std::make_shared<FlatYieldCurve>(0.01),
|
||||
std::make_shared<FlatVolatilitySurface>(0.2));
|
||||
|
||||
auto processCall = std::make_unique<BlackScholesProcess>(marketData);
|
||||
auto processPut = std::make_unique<BlackScholesProcess>(marketData);
|
||||
|
||||
auto analyticCall = std::make_unique<BlackScholesClosedFormEngine>(std::move(processCall));
|
||||
auto analyticPut = std::make_unique<BlackScholesClosedFormEngine>(std::move(processPut));
|
||||
|
||||
Instrument callInstr(T, std::make_unique<CallPayoff>(K), std::move(analyticCall));
|
||||
Instrument putInstr(T, std::make_unique<PutPayoff>(K), std::move(analyticPut));
|
||||
|
||||
const double callGT = 8.4333186901;
|
||||
const double putGT = 7.4383020650;
|
||||
|
||||
ASSERT_NEAR(callInstr.price(), callGT, 1e-9);
|
||||
ASSERT_NEAR(putInstr.price(), putGT, 1e-9);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user