1// RAID vs R (highfrequency 1.0.3, rugarch 1.5.6) on the committed fixtures — values from scripts/reference_r.R.2#include <catch2/catch_test_macros.hpp>3#include <catch2/matchers/catch_matchers_floating_point.hpp>4#include <nlohmann/json.hpp>5#include <fstream>6#include "raid/data/io.hpp"7#include "raid/data/returns.hpp"8#include "raid/estimators/realized.hpp"9#include "raid/estimators/har.hpp"10#include "raid/models/garch.hpp"11using namespace raid; using json = nlohmann::json; using Catch::Matchers::WithinRel; using Catch::Matchers::WithinAbs;1213static json load() { std::ifstream f(std::string(RAID_SOURCE_DIR) + "/datasets/fixtures/reference_r.json"); REQUIRE(f.good()); json j; f >> j; return j; }1415TEST_CASE("Realized measures match highfrequency (rCov, rBPCov, rSVar, rQuar)", "[reference][r][realized]") {16 auto R = load(); auto bars = data::read_bars_csv(std::string(RAID_SOURCE_DIR) + "/datasets/fixtures/synthetic_bars_1m.csv"); MarketCalendar cal;17 for (auto& e : R["realized"]) {18 const std::string sym = e["symbol"], sess = e["session"]; auto b = bars.at(sym); b.normalize(); auto reg = b.filter_session(cal, SessionPolicy::regular); auto ir = data::intraday_returns(reg);19 usize k = 0; while (k < ir.session_dates.size() && ir.session_dates[k].iso() != sess) ++k; REQUIRE(k < ir.session_dates.size());20 est::RealizedOptions o; o.plan.backend = Backend::cpu; o.bpv_finite_sample = false; auto m = est::realized_measures(ir, o); // highfrequency's rBPCov with makeReturns=FALSE applies no M/(M−1) factor21 INFO(sym << " " << sess); CHECK(m.n_returns[k] == e["n_returns"].get<usize>());22 CHECK_THAT(m.rv[k], WithinRel(e["rv"].get<double>(), 1e-10)); CHECK_THAT(m.bpv[k], WithinRel(e["bpv"].get<double>(), 1e-10));23 CHECK_THAT(m.rsv_neg[k], WithinRel(e["rsv"][0].get<double>(), 1e-10)); CHECK_THAT(m.rsv_pos[k], WithinRel(e["rsv"][1].get<double>(), 1e-10));24 // highfrequency::rQuar uses N = (number of returns + 1)/3 (number of prices); RAID uses M/3 (BPQ 2016, BNS 2002): convert25 const double M = static_cast<double>(m.n_returns[k]); CHECK_THAT(m.rq[k] * (M + 1.0) / M, WithinRel(e["rq"].get<double>(), 1e-10));26 }27}28TEST_CASE("HAR-RV coefficients match highfrequency::HARmodel (h = 1)", "[reference][r][har]") {29 auto R = load(); std::vector<double> rv, rq; { std::ifstream f(std::string(RAID_SOURCE_DIR) + "/datasets/fixtures/har_rv.csv"); std::string line; std::getline(f, line); while (std::getline(f, line)) { auto c = line.find(','); rv.push_back(std::stod(line.substr(0, c))); rq.push_back(std::stod(line.substr(c + 1))); } }30 est::HarSpec s; s.horizon = 1; auto e = est::har_fit(rv, s); auto h1 = R["har_h1"];31 CHECK(e.n_obs == h1["n"].get<usize>()); for (int j = 0; j < 4; ++j) CHECK_THAT(e.coefficients[j], WithinRel(h1["coef"][j].get<double>(), 1e-8)); CHECK_THAT(e.loss, WithinRel(h1["ssr"].get<double>(), 1e-8));32 // h = 5: highfrequency uses the h-day *average* RV as the dependent variable (same as our HarTarget::average)33 s.horizon = 5; auto e5 = est::har_fit(rv, s); auto h5 = R["har_h5"]; INFO("n ours " << e5.n_obs << " R " << h5["n"].get<usize>());34 if (e5.n_obs == h5["n"].get<usize>()) { for (int j = 0; j < 4; ++j) CHECK_THAT(e5.coefficients[j], WithinRel(h5["coef"][j].get<double>(), 1e-6)); }35 else WARN("HARmodel h=5 sample size differs (" + std::to_string(h5["n"].get<usize>()) + " vs " + std::to_string(e5.n_obs) + "): alignment convention documented in the registry");36 // HARQ: highfrequency's RQ1 term is sqrt(RQ_t) * RV_t (BPQ 2016); coefficient scale must agree37 est::HarSpec q; q.harq = true; auto eq = est::har_fit(rv, q, rq); auto hq = R["harq_h1"]; INFO("harq n ours " << eq.n_obs << " R " << hq["n"].get<usize>());38 if (eq.n_obs == hq["n"].get<usize>()) for (int j = 0; j < 5; ++j) CHECK_THAT(eq.coefficients[j], WithinRel(hq["coef"][j].get<double>(), 1e-6));39}40TEST_CASE("GARCH family matches rugarch within tolerance (parameter mapping documented)", "[reference][r][garch]") {41 auto R = load(); std::vector<double> r; { std::ifstream f(std::string(RAID_SOURCE_DIR) + "/datasets/fixtures/garch_returns.csv"); std::string line; std::getline(f, line); while (std::getline(f, line)) r.push_back(std::stod(line)); }42 using namespace models;43 auto check = [&](const char* key, GarchModel m, GarchDist d, std::vector<std::pair<int, const char*>> map) {44 GarchFitOptions o; o.model = m; o.dist = d; auto fit = garch_fit(r, o); INFO(key); REQUIRE(fit.ok()); auto g = R["garch"][key];45 // rugarch initialises σ² with the sample variance (as RAID) → log-likelihoods should agree to 1e-4 relative46 CHECK_THAT(-fit.loss, WithinRel(g["loglik"].get<double>(), 2e-4));47 for (auto& [j, name] : map) { const double ours = fit.coefficients[static_cast<usize>(j)], theirs = g["coef"][name].get<double>(); INFO(name << " ours " << ours << " rugarch " << theirs); CHECK((std::abs(ours - theirs) <= 0.05 * std::abs(theirs) + 0.01)); }48 return fit; };49 check("sGARCH.norm", GarchModel::garch, GarchDist::gaussian, {{0, "mu"}, {1, "omega"}, {2, "alpha1"}, {3, "beta1"}});50 check("gjrGARCH.norm", GarchModel::gjr, GarchDist::gaussian, {{0, "mu"}, {1, "omega"}, {2, "alpha1"}, {3, "gamma1"}, {4, "beta1"}});51 // rugarch eGARCH: log σ² = ω + α z + γ(|z| − E|z|) + β log σ² ⇒ rugarch alpha1 = RAID gamma (sign), rugarch gamma1 = RAID alpha (magnitude)52 check("eGARCH.norm", GarchModel::egarch, GarchDist::gaussian, {{0, "mu"}, {1, "omega"}, {2, "gamma1"}, {3, "alpha1"}, {4, "beta1"}});53 auto t = check("sGARCH.std", GarchModel::garch, GarchDist::student_t, {{0, "mu"}, {1, "omega"}, {2, "alpha1"}, {3, "beta1"}});54 CHECK(t.coefficients.back() > 30.0); // rugarch shape → 100 (its upper bound); RAID ν → its own upper bound (500): Gaussian data, ν unidentified55}56