#include #include #include "raid/estimators/realized.hpp" #include "raid/models/garch.hpp" #include "raid/core/rng.hpp" #include "raid/data/io.hpp" #include "raid/data/returns.hpp" #include "raid/data/resample.hpp" #include using namespace raid; using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinRel; TEST_CASE("Realized measures on a known segment", "[realized]") { std::vector r = {0.01, -0.02, 0.015, -0.005, 0.0}; std::vector off = {0, 5}; backend::Batch b{r, off}; est::RealizedOptions o; o.plan.backend = Backend::cpu; auto m = est::realized_measures_batch(b, o); CHECK_THAT(m.rv[0], WithinRel(0.0001 + 0.0004 + 0.000225 + 0.000025, 1e-14)); CHECK_THAT(m.rsv_neg[0], WithinRel(0.0004 + 0.000025, 1e-14)); CHECK_THAT(m.rsv_pos[0], WithinRel(0.0001 + 0.000225, 1e-14)); const double adj = 0.01 * 0.02 + 0.02 * 0.015 + 0.015 * 0.005 + 0.0; CHECK_THAT(m.bpv[0], WithinRel((M_PI / 2.0) * (5.0 / 4.0) * adj, 1e-13)); CHECK_THAT(m.rq[0], WithinRel((5.0 / 3.0) * (1e-8 + 1.6e-7 + 5.0625e-8 + 6.25e-10), 1e-12)); CHECK_THAT(m.daily_return[0], WithinAbs(0.0, 1e-15)); CHECK(m.stats.backend == Backend::cpu); CHECK_THAT(est::mu_p(1.0), WithinRel(est::kMu1, 1e-14)); CHECK_THAT(est::mu_p(4.0 / 3.0), WithinRel(0.8308, 1e-3)); } TEST_CASE("Intraday returns respect session boundaries and the synthetic fixture validates", "[realized][data]") { auto bars = data::read_bars_csv(std::string(RAID_SOURCE_DIR) + "/datasets/fixtures/synthetic_bars_1m.csv"); REQUIRE(bars.count("SYN1")); auto& b = bars["SYN1"]; MarketCalendar cal; auto q = b.validate(cal); CHECK(q.n_sessions == 5); CHECK_FALSE(q.has_errors()); CHECK(q.coverage > 0.95); auto reg = b.filter_session(cal, SessionPolicy::regular); auto ir = data::intraday_returns(reg); CHECK(ir.session_offsets.size() == 6); CHECK(static_cast(ir.session_offsets[1]) == reg.session_closes(0).size() - 1); CHECK(std::isnan(ir.overnight[0])); CHECK(std::isfinite(ir.overnight[1])); auto agg = data::aggregate(reg, Interval::m5, cal); CHECK(agg.n_sessions() == 5); CHECK(agg.session_closes(0).size() <= 79); CHECK_THAT(agg.close[static_cast(agg.session_offsets[1]) - 1], WithinRel(reg.close[static_cast(reg.session_offsets[1]) - 1], 1e-15)); f64 v5 = 0; for (usize i = 0; i < agg.session_closes(0).size(); ++i) v5 += agg.volume[i]; f64 v1 = 0; for (usize i = 0; i < reg.session_closes(0).size(); ++i) v1 += reg.volume[i]; CHECK_THAT(v5, WithinRel(v1, 1e-12)); } TEST_CASE("GARCH likelihood, transforms and fit on simulated data", "[garch]") { using namespace models; Rng rng(2024, 0); const usize n = 3000; std::vector r(n); f64 s2 = 0.04 / (1 - 0.05 - 0.9), e = 0; for (usize t = 0; t < n; ++t) { s2 = 0.04 + 0.05 * e * e + 0.9 * s2; e = std::sqrt(s2) * rng.normal(); r[t] = 0.01 + e; } std::vector th = {0.01, 0.04, 0.05, 0.9}; CHECK(garch_params_valid(th, GarchModel::garch, GarchDist::gaussian)); std::vector bad = {0.0, 0.04, 0.5, 0.6}; CHECK_FALSE(garch_params_valid(bad, GarchModel::garch, GarchDist::gaussian)); auto u = garch_to_unconstrained(th, GarchModel::garch, GarchDist::gaussian); auto back = garch_from_unconstrained(u, GarchModel::garch, GarchDist::gaussian); for (usize j = 0; j < 4; ++j) CHECK_THAT(back[j], WithinRel(th[j], 1e-8)); const f64 nll = garch_nll(r, th, GarchModel::garch, GarchDist::gaussian); CHECK(std::isfinite(nll)); std::vector neg = {0.0, -1.0, 0.05, 0.9}; CHECK(std::isinf(garch_nll(r, neg, GarchModel::garch, GarchDist::gaussian))); GarchFitOptions o; o.forecast_horizon = 5; auto fit = garch_fit(r, o); REQUIRE(fit.ok()); CHECK(fit.loss <= nll + 1e-6); CHECK_THAT(fit.coefficients[3], WithinAbs(0.9, 0.06)); CHECK_THAT(fit.coefficients[2], WithinAbs(0.05, 0.03)); CHECK(fit.inference->std_errors[3] > 0); auto fc = fit.extra["forecast_variance"].get>(); CHECK(fc.size() == 5); CHECK(fc[0] > 0); // GJR & EGARCH & t fits must at least converge on this series o.model = GarchModel::gjr; o.forecast_horizon = 0; CHECK(garch_fit(r, o).ok()); o.model = GarchModel::egarch; auto ef = garch_fit(r, o); REQUIRE(ef.ok()); CHECK_THAT(ef.coefficients[4], WithinAbs(0.95, 0.1)); o.model = GarchModel::garch; o.dist = GarchDist::student_t; auto tf = garch_fit(r, o); CHECK(tf.ok()); CHECK(tf.coefficients.back() > 2.0); } TEST_CASE("Non-convergence is never reported as success", "[garch]") { using namespace models; std::vector r(100, 0.0); r[50] = 1.0; // degenerate series GarchFitOptions o; o.max_iter = 1; auto fit = garch_fit(r, o); CHECK_FALSE(fit.ok()); }