diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f44252..4abb5df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,7 +111,10 @@ 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") add_cpp_test_labeled(BondPricingTest test/FixedIncome/bond_pricing_test.cpp "FixedIncome;Unit") add_cpp_test_labeled(AnnuityTest test/InterestAndAnnuities/annuity_test.cpp "InterestAndAnnuities;Unit") add_cpp_test_labeled(CashFlowTest test/InterestAndAnnuities/cash_flow_test.cpp "InterestAndAnnuities;Unit") 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/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/include/finmath/finmath.h b/include/finmath/finmath.h index f3f8108..673a395 100644 --- a/include/finmath/finmath.h +++ b/include/finmath/finmath.h @@ -9,5 +9,6 @@ #include "finmath/TimeSeries/rsi.h" #include "finmath/TimeSeries/ema.h" #include "finmath/TimeSeries/rolling_std_dev.h" +#include "finmath/MarkovChains/markov_chain.h" #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/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 d348390..4a273fe 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -17,6 +17,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" #include "finmath/FixedIncome/bond_pricing.h" @@ -27,6 +29,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) { @@ -88,6 +94,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/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; +} 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