From ee29f44f298a72020969214005cff50aaa9d8874 Mon Sep 17 00:00:00 2001 From: Shashank Mukkera Date: Wed, 25 Feb 2026 20:00:18 -0500 Subject: [PATCH 1/2] Add Markov chain module and tests - Add include/finmath/MarkovChains/markov_chain.h and implementation - Add test/MarkovChains/C++/markov_chain_test.cpp - Include Markov chain in finmath.h and CMakeLists.txt test suite Made-with: Cursor --- CMakeLists.txt | 5 +++++ include/finmath/MarkovChains/markov_chain.h | 17 +++++++++++++++++ include/finmath/finmath.h | 1 + src/cpp/MarkovChains/markov_chain.cpp | 12 ++++++++++++ test/MarkovChains/C++/markov_chain_test.cpp | 20 ++++++++++++++++++++ 5 files changed, 55 insertions(+) create mode 100644 include/finmath/MarkovChains/markov_chain.h create mode 100644 src/cpp/MarkovChains/markov_chain.cpp create mode 100644 test/MarkovChains/C++/markov_chain_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7948d45..794405b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,9 @@ target_link_libraries(rolling_std_dev_test finmath_library) add_executable(bellman_arbitrage_test test/GraphAlgos/bellman_arbitrage_test.cpp) target_link_libraries(bellman_arbitrage_test finmath_library) +add_executable(markov_chain_test test/MarkovChains/C++/markov_chain_test.cpp) +target_link_libraries(markov_chain_test finmath_library) + # Test runner add_executable(run_all_tests test/test_runner.cpp) @@ -59,6 +62,7 @@ add_test(NAME CompoundInterestTest COMMAND compound_interest_test) add_test(NAME RSITest COMMAND rsi_test) add_test(NAME RollingStdDevTest COMMAND rolling_std_dev_test) add_test(NAME BellmanArbitrageTest COMMAND bellman_arbitrage_test) +add_test(NAME MarkovChainTest COMMAND markov_chain_test) # Add a custom target to run all tests add_custom_target(build_and_test @@ -67,6 +71,7 @@ add_custom_target(build_and_test COMMAND ${CMAKE_COMMAND} --build . --target compound_interest_test COMMAND ${CMAKE_COMMAND} --build . --target rsi_test COMMAND ${CMAKE_COMMAND} --build . --target bellman_arbitrage_test + COMMAND ${CMAKE_COMMAND} --build . --target markov_chain_test COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) diff --git a/include/finmath/MarkovChains/markov_chain.h b/include/finmath/MarkovChains/markov_chain.h new file mode 100644 index 0000000..1dc20fd --- /dev/null +++ b/include/finmath/MarkovChains/markov_chain.h @@ -0,0 +1,17 @@ +#ifndef MARKOV_CHAIN_H +#define MARKOV_CHAIN_H + +#include +#include +#include + +// TODO: Implement Markov Chain functionality +// Potential features: +// - Transition matrix operations +// - State space modeling +// - Steady-state calculations +// - N-step transition probabilities +// - First passage times +// - Absorbing states analysis + +#endif // MARKOV_CHAIN_H diff --git a/include/finmath/finmath.h b/include/finmath/finmath.h index 33bc7a0..f3c11da 100644 --- a/include/finmath/finmath.h +++ b/include/finmath/finmath.h @@ -9,6 +9,7 @@ #include "finmath/TimeSeries/rsi.h" #include "finmath/TimeSeries/ema.h" #include "finmath/TimeSeries/rolling_std_dev.h" +#include "finmath/MarkovChains/markov_chain.h" // Include other headers as needed #endif // FINMATH_H diff --git a/src/cpp/MarkovChains/markov_chain.cpp b/src/cpp/MarkovChains/markov_chain.cpp new file mode 100644 index 0000000..9ccf626 --- /dev/null +++ b/src/cpp/MarkovChains/markov_chain.cpp @@ -0,0 +1,12 @@ +#include "finmath/MarkovChains/markov_chain.h" +#include +#include + +// TODO: Implement Markov Chain functions + +// Example function signatures (placeholder): +// +// std::vector> compute_transition_matrix(const std::vector& states); +// std::vector compute_steady_state(const std::vector>& transition_matrix); +// std::vector> matrix_power(const std::vector>& matrix, int n); +// double compute_first_passage_time(const std::vector>& transition_matrix, int from_state, int to_state); diff --git a/test/MarkovChains/C++/markov_chain_test.cpp b/test/MarkovChains/C++/markov_chain_test.cpp new file mode 100644 index 0000000..9117af9 --- /dev/null +++ b/test/MarkovChains/C++/markov_chain_test.cpp @@ -0,0 +1,20 @@ +#include "finmath/MarkovChains/markov_chain.h" +#include +#include +#include + +// TODO: Implement test cases for Markov Chain functionality + +int main() { + std::cout << "Markov Chain tests - TODO: Implement" << std::endl; + + // Example test structure: + // - Test transition matrix creation + // - Test steady state calculations + // - Test matrix operations + // - Test first passage times + // - Test absorbing states + + std::cout << "All Markov Chain tests passed (placeholder)." << std::endl; + return 0; +} From 57c913a576b37746fa725901f932bb49f822b560 Mon Sep 17 00:00:00 2001 From: Shashank Mukkera Date: Tue, 31 Mar 2026 17:57:04 -0400 Subject: [PATCH 2/2] Add returns and rolling z-score Made-with: Cursor --- CMakeLists.txt | 2 + include/finmath/TimeSeries/returns.h | 10 ++ include/finmath/TimeSeries/rolling_zscore.h | 9 ++ src/cpp/TimeSeries/returns.cpp | 96 ++++++++++++++++ src/cpp/TimeSeries/rolling_zscore.cpp | 115 ++++++++++++++++++++ src/python_bindings.cpp | 16 +++ test/TimeSeries/returns_test.cpp | 39 +++++++ test/TimeSeries/rolling_zscore_test.cpp | 53 +++++++++ tests/test_timeseries.py | 26 +++++ 9 files changed, 366 insertions(+) create mode 100644 include/finmath/TimeSeries/returns.h create mode 100644 include/finmath/TimeSeries/rolling_zscore.h create mode 100644 src/cpp/TimeSeries/returns.cpp create mode 100644 src/cpp/TimeSeries/rolling_zscore.cpp create mode 100644 test/TimeSeries/returns_test.cpp create mode 100644 test/TimeSeries/rolling_zscore_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d4d930..8163b87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,8 @@ add_cpp_test_labeled(BlackScholesTest test/OptionPricing/black_scholes_test.cpp add_cpp_test_labeled(BinomialOptionPricingTest test/OptionPricing/binomial_option_pricing_test.cpp "OptionPricing;Unit") add_cpp_test_labeled(RSITest test/TimeSeries/rsi_test.cpp "TimeSeries;Unit") add_cpp_test_labeled(RollingStdDevTest test/TimeSeries/rolling_std_dev_test.cpp "TimeSeries;Unit") +add_cpp_test_labeled(ReturnsTest test/TimeSeries/returns_test.cpp "TimeSeries;Unit") +add_cpp_test_labeled(RollingZScoreTest test/TimeSeries/rolling_zscore_test.cpp "TimeSeries;Unit") add_cpp_test_labeled(BellmanArbitrageTest test/GraphAlgos/bellman_arbitrage_test.cpp "GraphAlgos;Unit") add_cpp_test_labeled(MarkovChainTest test/MarkovChains/C++/markov_chain_test.cpp "MarkovChains;Unit") diff --git a/include/finmath/TimeSeries/returns.h b/include/finmath/TimeSeries/returns.h new file mode 100644 index 0000000..66090c0 --- /dev/null +++ b/include/finmath/TimeSeries/returns.h @@ -0,0 +1,10 @@ +#ifndef FINMATH_TIME_SERIES_RETURNS_H +#define FINMATH_TIME_SERIES_RETURNS_H + +#include + +// Element 0 is 0.0; element i is return from prices[i-1] to prices[i]. +std::vector log_returns(const std::vector& prices); +std::vector pct_returns(const std::vector& prices); + +#endif // FINMATH_TIME_SERIES_RETURNS_H diff --git a/include/finmath/TimeSeries/rolling_zscore.h b/include/finmath/TimeSeries/rolling_zscore.h new file mode 100644 index 0000000..ea49bbf --- /dev/null +++ b/include/finmath/TimeSeries/rolling_zscore.h @@ -0,0 +1,9 @@ +#ifndef FINMATH_TIME_SERIES_ROLLING_ZSCORE_H +#define FINMATH_TIME_SERIES_ROLLING_ZSCORE_H + +#include + +std::vector rolling_mean(size_t window_size, const std::vector& data); +std::vector rolling_zscore(size_t window_size, const std::vector& data); + +#endif // FINMATH_TIME_SERIES_ROLLING_ZSCORE_H diff --git a/src/cpp/TimeSeries/returns.cpp b/src/cpp/TimeSeries/returns.cpp new file mode 100644 index 0000000..a593133 --- /dev/null +++ b/src/cpp/TimeSeries/returns.cpp @@ -0,0 +1,96 @@ +#include "finmath/TimeSeries/returns.h" + +#include + +#include +#include + +namespace py = pybind11; + +std::vector log_returns(const std::vector& prices) +{ + if (prices.empty()) { + return {}; + } + + std::vector out(prices.size(), 0.0); + for (size_t i = 1; i < prices.size(); ++i) { + const double prev = prices[i - 1]; + const double curr = prices[i]; + if (prev <= 0.0 || curr <= 0.0) { + throw std::invalid_argument("log_returns requires all prices > 0"); + } + out[i] = std::log(curr / prev); + } + return out; +} + +std::vector pct_returns(const std::vector& prices) +{ + if (prices.empty()) { + return {}; + } + + std::vector out(prices.size(), 0.0); + for (size_t i = 1; i < prices.size(); ++i) { + const double prev = prices[i - 1]; + const double curr = prices[i]; + if (prev == 0.0) { + throw std::invalid_argument("pct_returns requires previous price != 0"); + } + out[i] = (curr / prev) - 1.0; + } + return out; +} + +std::vector log_returns_np(py::array_t prices_arr) +{ + py::buffer_info buf_info = prices_arr.request(); + if (buf_info.ndim != 1) { + throw std::runtime_error("Input array must be 1-dimensional."); + } + + const size_t n = buf_info.size; + const double* prices_ptr = static_cast(buf_info.ptr); + + if (n == 0) { + return {}; + } + + std::vector out(n, 0.0); + for (size_t i = 1; i < n; ++i) { + const double prev = prices_ptr[i - 1]; + const double curr = prices_ptr[i]; + if (prev <= 0.0 || curr <= 0.0) { + throw std::runtime_error("log_returns requires all prices > 0"); + } + out[i] = std::log(curr / prev); + } + return out; +} + +std::vector pct_returns_np(py::array_t prices_arr) +{ + py::buffer_info buf_info = prices_arr.request(); + if (buf_info.ndim != 1) { + throw std::runtime_error("Input array must be 1-dimensional."); + } + + const size_t n = buf_info.size; + const double* prices_ptr = static_cast(buf_info.ptr); + + if (n == 0) { + return {}; + } + + std::vector out(n, 0.0); + for (size_t i = 1; i < n; ++i) { + const double prev = prices_ptr[i - 1]; + const double curr = prices_ptr[i]; + if (prev == 0.0) { + throw std::runtime_error("pct_returns requires previous price != 0"); + } + out[i] = (curr / prev) - 1.0; + } + return out; +} diff --git a/src/cpp/TimeSeries/rolling_zscore.cpp b/src/cpp/TimeSeries/rolling_zscore.cpp new file mode 100644 index 0000000..c2f70a9 --- /dev/null +++ b/src/cpp/TimeSeries/rolling_zscore.cpp @@ -0,0 +1,115 @@ +#include "finmath/TimeSeries/rolling_zscore.h" + +#include + +#include +#include +#include + +namespace py = pybind11; + +std::vector rolling_mean(size_t window_size, const std::vector& data) +{ + if (window_size == 0) { + throw std::invalid_argument("Window size cannot be zero."); + } + + std::vector result(data.size(), 0.0); + if (data.empty()) { + return result; + } + + if (window_size > data.size()) { + window_size = data.size(); + } + + double sum = 0.0; + for (size_t i = 0; i < window_size; ++i) { + sum += data[i]; + } + result[window_size - 1] = sum / static_cast(window_size); + + for (size_t i = window_size; i < data.size(); ++i) { + sum += data[i] - data[i - window_size]; + result[i] = sum / static_cast(window_size); + } + + return result; +} + +std::vector rolling_zscore(size_t window_size, const std::vector& data) +{ + if (window_size == 0) { + throw std::invalid_argument("Window size cannot be zero."); + } + + std::vector result(data.size(), 0.0); + if (data.empty()) { + return result; + } + + if (window_size > data.size()) { + window_size = data.size(); + } + + double sum = 0.0; + double sumsq = 0.0; + for (size_t i = 0; i < window_size; ++i) { + sum += data[i]; + sumsq += data[i] * data[i]; + } + + auto write = [&](size_t idx) { + const double mean = sum / static_cast(window_size); + const double ex2 = sumsq / static_cast(window_size); + double var = ex2 - mean * mean; + if (var < 0.0) { + var = 0.0; + } + const double std = std::sqrt(var); + if (std == 0.0) { + result[idx] = 0.0; + return; + } + result[idx] = (data[idx] - mean) / std; + }; + + write(window_size - 1); + for (size_t i = window_size; i < data.size(); ++i) { + const double out = data[i - window_size]; + const double in = data[i]; + sum += in - out; + sumsq += in * in - out * out; + write(i); + } + + return result; +} + +std::vector rolling_mean_np(py::array_t data_arr, size_t window_size) +{ + py::buffer_info buf_info = data_arr.request(); + if (buf_info.ndim != 1) { + throw std::runtime_error("Input array must be 1-dimensional."); + } + + const size_t n = buf_info.size; + const double* ptr = static_cast(buf_info.ptr); + + std::vector data(ptr, ptr + n); + return rolling_mean(window_size, data); +} + +std::vector rolling_zscore_np(py::array_t data_arr, size_t window_size) +{ + py::buffer_info buf_info = data_arr.request(); + if (buf_info.ndim != 1) { + throw std::runtime_error("Input array must be 1-dimensional."); + } + + const size_t n = buf_info.size; + const double* ptr = static_cast(buf_info.ptr); + + std::vector data(ptr, ptr + n); + return rolling_zscore(window_size, data); +} diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index 1dbc0c1..bf06847 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -13,6 +13,8 @@ #include "finmath/TimeSeries/rsi_simd.h" #include "finmath/TimeSeries/ema.h" #include "finmath/TimeSeries/ema_simd.h" +#include "finmath/TimeSeries/returns.h" +#include "finmath/TimeSeries/rolling_zscore.h" #include "finmath/Helper/simd_helper.h" #include "finmath/GraphAlgos/bellman_arbitrage.h" @@ -22,6 +24,10 @@ namespace py = pybind11; std::vector rolling_volatility_np(py::array_t prices_arr, size_t window_size); std::vector simple_moving_average_np(py::array_t data_arr, size_t window_size); std::vector compute_smoothed_rsi_np(py::array_t prices_arr, size_t window_size); +std::vector log_returns_np(py::array_t prices_arr); +std::vector pct_returns_np(py::array_t prices_arr); +std::vector rolling_mean_np(py::array_t data_arr, size_t window_size); +std::vector rolling_zscore_np(py::array_t data_arr, size_t window_size); PYBIND11_MODULE(finmath, m) { @@ -83,6 +89,16 @@ PYBIND11_MODULE(finmath, m) m.def("ema_smoothing_simd", &compute_ema_with_smoothing_simd, "Exponential Moving Average - Smoothing Factor (SIMD-optimized, zero-copy NumPy)", py::arg("prices"), py::arg("smoothing_factor")); + // Returns + z-score utilities + m.def("log_returns", &log_returns, "Log returns (List input)", py::arg("prices")); + m.def("log_returns", &log_returns_np, "Log returns (NumPy/Pandas input)", py::arg("prices")); + m.def("pct_returns", &pct_returns, "Percent returns (List input)", py::arg("prices")); + m.def("pct_returns", &pct_returns_np, "Percent returns (NumPy/Pandas input)", py::arg("prices")); + m.def("rolling_mean", &rolling_mean, "Rolling mean (List input)", py::arg("window_size"), py::arg("data")); + m.def("rolling_mean", &rolling_mean_np, "Rolling mean (NumPy/Pandas input)", py::arg("data"), py::arg("window_size")); + m.def("rolling_zscore", &rolling_zscore, "Rolling z-score (List input)", py::arg("window_size"), py::arg("data")); + m.def("rolling_zscore", &rolling_zscore_np, "Rolling z-score (NumPy/Pandas input)", py::arg("data"), py::arg("window_size")); + // Utility function to get SIMD backend m.def("get_simd_backend", &finmath::simd::get_simd_backend, "Get the active SIMD backend (AVX, SSE, NEON, or Scalar)"); diff --git a/test/TimeSeries/returns_test.cpp b/test/TimeSeries/returns_test.cpp new file mode 100644 index 0000000..3fe531b --- /dev/null +++ b/test/TimeSeries/returns_test.cpp @@ -0,0 +1,39 @@ +#include +#include +#include +#include + +#include "finmath/TimeSeries/returns.h" + +static bool almost_equal(double a, double b, double tol) +{ + return std::abs(a - b) <= tol * std::max(std::abs(a), std::abs(b)); +} + +int main() +{ + const double tol = 1e-9; + + // pct_returns: simple known ratios + { + std::vector prices{100.0, 110.0, 99.0}; + auto r = pct_returns(prices); + assert(r.size() == prices.size()); + assert(r[0] == 0.0); + assert(almost_equal(r[1], 0.10, tol)); + assert(almost_equal(r[2], -0.10, tol)); + } + + // log_returns: exp/log identity checks + { + std::vector prices{100.0, 110.0, 99.0}; + auto r = log_returns(prices); + assert(r.size() == prices.size()); + assert(r[0] == 0.0); + assert(almost_equal(std::exp(r[1]), prices[1] / prices[0], tol)); + assert(almost_equal(std::exp(r[2]), prices[2] / prices[1], tol)); + } + + std::cout << "Returns Tests Pass!" << std::endl; + return 0; +} diff --git a/test/TimeSeries/rolling_zscore_test.cpp b/test/TimeSeries/rolling_zscore_test.cpp new file mode 100644 index 0000000..54dbd7d --- /dev/null +++ b/test/TimeSeries/rolling_zscore_test.cpp @@ -0,0 +1,53 @@ +#include +#include +#include +#include + +#include "finmath/TimeSeries/rolling_zscore.h" + +static bool almost_equal(double a, double b, double tol) +{ + return std::abs(a - b) <= tol * std::max(std::abs(a), std::abs(b)); +} + +int main() +{ + const double tol = 1e-6; + + // Constant series => std=0 => zscore 0 + { + std::vector x{5.0, 5.0, 5.0, 5.0}; + auto z = rolling_zscore(3, x); + assert(z.size() == x.size()); + assert(z[0] == 0.0); + assert(z[1] == 0.0); + assert(z[2] == 0.0); + assert(z[3] == 0.0); + } + + // Increasing series: validate last point against computed mean/std of last window + { + std::vector x{1.0, 2.0, 3.0, 4.0, 5.0}; + const size_t w = 3; + auto z = rolling_zscore(w, x); + assert(z.size() == x.size()); + + const double mean = (3.0 + 4.0 + 5.0) / 3.0; + const double ex2 = (3.0 * 3.0 + 4.0 * 4.0 + 5.0 * 5.0) / 3.0; + const double var = ex2 - mean * mean; + const double std = std::sqrt(var); + const double expected = (5.0 - mean) / std; + assert(almost_equal(z.back(), expected, tol)); + } + + // rolling_mean: last value check + { + std::vector x{1.0, 2.0, 3.0, 4.0, 5.0}; + auto m = rolling_mean(2, x); + assert(m.size() == x.size()); + assert(almost_equal(m.back(), 4.5, tol)); + } + + std::cout << "Rolling Z-Score Tests Pass!" << std::endl; + return 0; +} diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index f5ec83b..2939e9f 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -74,3 +74,29 @@ def test_get_simd_backend(): backend = finmath.get_simd_backend() assert isinstance(backend, str) assert backend # non-empty + + +def test_log_returns_list(): + prices = [100.0, 110.0, 99.0] + r = finmath.log_returns(prices) + assert len(r) == len(prices) + assert r[0] == pytest.approx(0.0) + assert r[1] == pytest.approx(__import__("math").log(110.0 / 100.0)) + + +def test_pct_returns_list(): + prices = [100.0, 110.0, 99.0] + r = finmath.pct_returns(prices) + assert len(r) == len(prices) + assert r[0] == pytest.approx(0.0) + assert r[1] == pytest.approx(0.10) + assert r[2] == pytest.approx(-0.10) + + +def test_rolling_zscore_list(): + x = [1.0, 2.0, 3.0, 4.0, 5.0] + z = finmath.rolling_zscore(3, x) + assert len(z) == len(x) + assert z[0] == pytest.approx(0.0) + assert z[1] == pytest.approx(0.0) + assert z[-1] != 0.0