WP40 RAID: Phase 0 audit + Phase 1 foundation (C++20 core, CPU backend, calendar, OLS/robust SE, realized measures, GARCH QML, nanobind, CLI, tests, docs)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
102 changed files +15,235 −21
added
.github/workflows/ci.yml
+22 −0
@@ -0,0 +1,22 @@ | ||
| 1 | +name: ci | |
| 2 | +on: [push, pull_request] | |
| 3 | +jobs: | |
| 4 | + cpu-linux: | |
| 5 | + runs-on: ubuntu-24.04 | |
| 6 | + steps: | |
| 7 | + - uses: actions/checkout@v4 | |
| 8 | + - run: sudo apt-get update && sudo apt-get install -y cmake ninja-build libeigen3-dev nlohmann-json3-dev catch2 libcurl4-openssl-dev | |
| 9 | + - run: cmake --preset cpu-only -DRAID_WERROR=OFF | |
| 10 | + - run: cmake --build --preset cpu-only | |
| 11 | + - run: ctest --preset cpu-only | |
| 12 | + - run: bash scripts/check_no_secrets.sh | |
| 13 | + macos-metal: | |
| 14 | + runs-on: macos-15 | |
| 15 | + steps: | |
| 16 | + - uses: actions/checkout@v4 | |
| 17 | + - run: brew install cmake ninja eigen apache-arrow catch2 nlohmann-json | |
| 18 | + - run: cmake --preset macos-release | |
| 19 | + - run: cmake --build --preset macos-release | |
| 20 | + - run: ctest --preset macos-release | |
| 21 | + - run: ./build/macos-release/raid devices | |
| 22 | + - run: ./build/macos-release/raid validate datasets/fixtures/synthetic_bars_1m.csv | |
modified
.gitignore
+8 −21
@@ -1,5 +1,4 @@ | ||
| 1 | −# | |
| 2 | −Build | |
| 1 | +# Build | |
| 3 | 2 | build/ |
| 4 | 3 | build-*/ |
| 5 | 4 | cmake-build-*/ |
@@ -13,38 +12,27 @@ wheelhouse/ | ||
| 13 | 12 | *.so |
| 14 | 13 | *.air |
| 15 | 14 | *.metallib |
| 16 | −# | |
| 17 | −Python | |
| 15 | +# Python | |
| 18 | 16 | __pycache__/ |
| 19 | 17 | *.pyc |
| 20 | 18 | .pytest_cache/ |
| 21 | 19 | .hypothesis/ |
| 22 | 20 | .venv/ |
| 23 | 21 | .ruff_cache/ |
| 24 | −# | |
| 25 | −Data | |
| 26 | −— | |
| 27 | −FirstRate | |
| 28 | −private | |
| 29 | −license: | |
| 30 | −never | |
| 31 | −commit | |
| 32 | −real | |
| 33 | −bars | |
| 22 | +# Data — FirstRate private license: never commit real bars | |
| 34 | 23 | datasets/cache/ |
| 35 | 24 | datasets/real/ |
| 36 | 25 | data/ |
| 26 | +experiments/results/raw/ | |
| 37 | 27 | *.parquet |
| 28 | +*.arrow | |
| 38 | 29 | !datasets/fixtures/*.parquet |
| 39 | 30 | !datasets/synthetic/*.parquet |
| 40 | −experiments/results/raw/*.arrow | |
| 41 | −# | |
| 42 | −Secrets | |
| 31 | +# Secrets | |
| 43 | 32 | .env |
| 44 | 33 | *.key |
| 45 | 34 | api-key*.txt |
| 46 | −# | |
| 47 | −LaTeX | |
| 35 | +# LaTeX | |
| 48 | 36 | paper/*.aux |
| 49 | 37 | paper/*.log |
| 50 | 38 | paper/*.out |
@@ -58,8 +46,7 @@ paper/*.blg | ||
| 58 | 46 | paper/*.synctex.gz |
| 59 | 47 | paper/*.run.xml |
| 60 | 48 | paper/*.bcf |
| 61 | −# | |
| 62 | −macOS | |
| 49 | +# macOS | |
| 63 | 50 | .DS_Store |
| 64 | 51 | Icon? |
| 65 | 52 | logs/ |
added
CITATION.cff
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +cff-version: 1.2.0 | |
| 2 | +message: "If you use RAID, please cite the working paper." | |
| 3 | +title: "RAID: Research Architecture for Intraday Data" | |
| 4 | +version: 0.1.0 | |
| 5 | +date-released: 2026-11-02 | |
| 6 | +authors: | |
| 7 | + - family-names: Boucher | |
| 8 | + given-names: Simon-Pierre | |
| 9 | + email: simon-pierre.boucher@uqo.ca | |
| 10 | + affiliation: "Université du Québec en Outaouais, Département des sciences administratives" | |
| 11 | +license: MIT | |
| 12 | +repository-code: "https://git.spboucher.ai/wp40_uqo" | |
| 13 | +keywords: [high-frequency econometrics, realized volatility, GARCH, HAR, Apple Metal, GPU, C++20, reproducibility] | |
| 14 | +preferred-citation: | |
| 15 | + type: report | |
| 16 | + title: "RAID: A Metal-Accelerated C++ Framework for Reproducible High-Frequency Financial Econometrics" | |
| 17 | + authors: | |
| 18 | + - family-names: Boucher | |
| 19 | + given-names: Simon-Pierre | |
| 20 | + institution: | |
| 21 | + name: "Université du Québec en Outaouais" | |
| 22 | + number: "40" | |
| 23 | + year: 2026 | |
added
CMakeLists.txt
+189 −0
@@ -0,0 +1,189 @@ | ||
| 1 | +cmake_minimum_required(VERSION 3.28) | |
| 2 | +project(raid VERSION 0.1.0 LANGUAGES CXX) | |
| 3 | + | |
| 4 | +# --------------------------------------------------------------------------- | |
| 5 | +# RAID — Research Architecture for Intraday Data (UQO WP 40) | |
| 6 | +# C++20 core + CPU backend (portable) + optional Metal backend (macOS arm64). | |
| 7 | +# --------------------------------------------------------------------------- | |
| 8 | +set(CMAKE_CXX_STANDARD 20) | |
| 9 | +set(CMAKE_CXX_STANDARD_REQUIRED ON) | |
| 10 | +set(CMAKE_CXX_EXTENSIONS OFF) | |
| 11 | +set(CMAKE_POSITION_INDEPENDENT_CODE ON) | |
| 12 | +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) | |
| 13 | +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) | |
| 14 | + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) | |
| 15 | +endif() | |
| 16 | + | |
| 17 | +option(RAID_ENABLE_METAL "Build the Metal backend (macOS only)" ${APPLE}) | |
| 18 | +option(RAID_WITH_ARROW "Build the Arrow/Parquet reader (requires Apache Arrow C++)" ON) | |
| 19 | +option(RAID_WITH_ACCELERATE "Use Apple Accelerate BLAS/LAPACK through Eigen (macOS only)" ${APPLE}) | |
| 20 | +option(RAID_BUILD_TESTS "Build Catch2 tests" ON) | |
| 21 | +option(RAID_BUILD_BENCH "Build benchmark executables" ON) | |
| 22 | +option(RAID_BUILD_CLI "Build the raid command-line tool" ON) | |
| 23 | +option(RAID_BUILD_PYTHON "Build the nanobind Python extension" OFF) | |
| 24 | +option(RAID_NATIVE_ARCH "Tune for the host CPU (-mcpu=native)" ON) | |
| 25 | +option(RAID_WERROR "Treat warnings as errors" OFF) | |
| 26 | + | |
| 27 | +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") | |
| 28 | +include(GNUInstallDirs) | |
| 29 | + | |
| 30 | +# ---- dependencies ---------------------------------------------------------- | |
| 31 | +find_package(Eigen3 REQUIRED CONFIG) | |
| 32 | +find_package(nlohmann_json 3.11 REQUIRED CONFIG) | |
| 33 | +find_package(CURL REQUIRED) # system libcurl (macOS SDK / Linux) | |
| 34 | +find_package(Threads REQUIRED) | |
| 35 | +if(RAID_WITH_ARROW) | |
| 36 | + find_package(Arrow CONFIG QUIET) | |
| 37 | + find_package(Parquet CONFIG QUIET) | |
| 38 | + if(NOT Arrow_FOUND OR NOT Parquet_FOUND) | |
| 39 | + message(WARNING "RAID_WITH_ARROW=ON but Arrow/Parquet not found; building without Arrow") | |
| 40 | + set(RAID_WITH_ARROW OFF) | |
| 41 | + endif() | |
| 42 | +endif() | |
| 43 | + | |
| 44 | +# ---- compile options ------------------------------------------------------- | |
| 45 | +add_library(raid_options INTERFACE) | |
| 46 | +target_compile_options(raid_options INTERFACE | |
| 47 | + $<$<CXX_COMPILER_ID:Clang,AppleClang,GNU>:-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wno-sign-conversion> | |
| 48 | + $<$<AND:$<CXX_COMPILER_ID:Clang,AppleClang,GNU>,$<BOOL:${RAID_WERROR}>>:-Werror> | |
| 49 | + $<$<AND:$<CXX_COMPILER_ID:Clang,AppleClang,GNU>,$<CONFIG:Release>>:-O3> | |
| 50 | +) | |
| 51 | +if(RAID_NATIVE_ARCH AND CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64") | |
| 52 | + target_compile_options(raid_options INTERFACE -mcpu=native) | |
| 53 | +endif() | |
| 54 | +# Floating-point semantics: strict IEEE (no -ffast-math) so that CPU FP64 is a true reference. | |
| 55 | +target_compile_options(raid_options INTERFACE $<$<CXX_COMPILER_ID:Clang,AppleClang>:-ffp-model=precise>) | |
| 56 | + | |
| 57 | +# ---- core library ---------------------------------------------------------- | |
| 58 | +file(GLOB_RECURSE RAID_CORE_SOURCES CONFIGURE_DEPENDS | |
| 59 | + src/core/*.cpp src/data/*.cpp src/statistics/*.cpp src/estimators/*.cpp src/models/*.cpp backends/cpu/*.cpp) | |
| 60 | +if(NOT RAID_WITH_ARROW) | |
| 61 | + list(FILTER RAID_CORE_SOURCES EXCLUDE REGEX ".*parquet_arrow\\.cpp$") | |
| 62 | +endif() | |
| 63 | +add_library(raid ${RAID_CORE_SOURCES}) | |
| 64 | +add_library(raid::raid ALIAS raid) | |
| 65 | +target_include_directories(raid PUBLIC | |
| 66 | + $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> | |
| 67 | + $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>) | |
| 68 | +target_link_libraries(raid PUBLIC Eigen3::Eigen nlohmann_json::nlohmann_json Threads::Threads CURL::libcurl raid_options) | |
| 69 | +target_compile_definitions(raid PUBLIC RAID_VERSION="${PROJECT_VERSION}") | |
| 70 | +if(RAID_WITH_ARROW) | |
| 71 | + target_link_libraries(raid PUBLIC Arrow::arrow_shared Parquet::parquet_shared) | |
| 72 | + target_compile_definitions(raid PUBLIC RAID_WITH_ARROW=1) | |
| 73 | +endif() | |
| 74 | +if(APPLE AND RAID_WITH_ACCELERATE) | |
| 75 | + target_compile_definitions(raid PUBLIC RAID_WITH_ACCELERATE=1 EIGEN_USE_BLAS=1 ACCELERATE_NEW_LAPACK=1 ACCELERATE_LAPACK_ILP64=0) | |
| 76 | + target_link_libraries(raid PUBLIC "-framework Accelerate") | |
| 77 | +endif() | |
| 78 | + | |
| 79 | +# ---- Metal backend --------------------------------------------------------- | |
| 80 | +if(RAID_ENABLE_METAL) | |
| 81 | + if(NOT APPLE) | |
| 82 | + message(FATAL_ERROR "RAID_ENABLE_METAL requires macOS") | |
| 83 | + endif() | |
| 84 | + file(GLOB _raid_metal_probe CONFIGURE_DEPENDS backends/metal/host/*.mm) | |
| 85 | + if(NOT _raid_metal_probe) | |
| 86 | + message(STATUS "Metal backend sources not present yet (Phase 3): building without Metal") | |
| 87 | + set(RAID_ENABLE_METAL OFF) | |
| 88 | + endif() | |
| 89 | +endif() | |
| 90 | +if(RAID_ENABLE_METAL) | |
| 91 | + enable_language(OBJCXX) | |
| 92 | + set(CMAKE_OBJCXX_STANDARD 20) | |
| 93 | + file(GLOB RAID_METAL_HOST CONFIGURE_DEPENDS backends/metal/host/*.mm backends/metal/host/*.cpp) | |
| 94 | + file(GLOB RAID_METAL_SHADERS CONFIGURE_DEPENDS backends/metal/shaders/*.metal) | |
| 95 | + add_library(raid_metal ${RAID_METAL_HOST}) | |
| 96 | + target_link_libraries(raid_metal PUBLIC raid "-framework Metal" "-framework Foundation") | |
| 97 | + target_compile_options(raid_metal PRIVATE -fobjc-arc) | |
| 98 | + target_compile_definitions(raid PUBLIC RAID_ENABLE_METAL=1) | |
| 99 | + # Embed the MSL sources as a string (runtime compilation fallback / cold-compile benchmark) | |
| 100 | + set(RAID_MSL_BUNDLE "${CMAKE_CURRENT_BINARY_DIR}/raid_msl_bundle.cpp") | |
| 101 | + add_custom_command(OUTPUT ${RAID_MSL_BUNDLE} | |
| 102 | + COMMAND ${CMAKE_COMMAND} -DOUT=${RAID_MSL_BUNDLE} "-DSHADERS=${RAID_METAL_SHADERS}" -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedMSL.cmake | |
| 103 | + DEPENDS ${RAID_METAL_SHADERS} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedMSL.cmake COMMENT "Embedding MSL sources") | |
| 104 | + target_sources(raid_metal PRIVATE ${RAID_MSL_BUNDLE}) | |
| 105 | + # Optional offline metallib (requires the Metal Toolchain). Missing toolchain => runtime compile only. | |
| 106 | + find_program(RAID_XCRUN xcrun) | |
| 107 | + if(RAID_XCRUN AND RAID_METAL_SHADERS) | |
| 108 | + execute_process(COMMAND ${RAID_XCRUN} -sdk macosx metal --version RESULT_VARIABLE _mt_rc OUTPUT_QUIET ERROR_QUIET) | |
| 109 | + if(_mt_rc EQUAL 0) | |
| 110 | + set(RAID_METALLIB "${CMAKE_CURRENT_BINARY_DIR}/raid.metallib") | |
| 111 | + set(_airs) | |
| 112 | + foreach(s ${RAID_METAL_SHADERS}) | |
| 113 | + get_filename_component(n ${s} NAME_WE) | |
| 114 | + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${n}.air | |
| 115 | + COMMAND ${RAID_XCRUN} -sdk macosx metal -std=metal3.2 -O2 -c ${s} -o ${CMAKE_CURRENT_BINARY_DIR}/${n}.air | |
| 116 | + DEPENDS ${s} COMMENT "metal ${n}") | |
| 117 | + list(APPEND _airs ${CMAKE_CURRENT_BINARY_DIR}/${n}.air) | |
| 118 | + endforeach() | |
| 119 | + add_custom_command(OUTPUT ${RAID_METALLIB} COMMAND ${RAID_XCRUN} -sdk macosx metallib ${_airs} -o ${RAID_METALLIB} DEPENDS ${_airs} COMMENT "metallib") | |
| 120 | + add_custom_target(raid_metallib ALL DEPENDS ${RAID_METALLIB}) | |
| 121 | + add_dependencies(raid_metal raid_metallib) | |
| 122 | + target_compile_definitions(raid_metal PRIVATE RAID_METALLIB_PATH="${RAID_METALLIB}") | |
| 123 | + message(STATUS "Metal toolchain found: building raid.metallib") | |
| 124 | + else() | |
| 125 | + message(STATUS "Metal toolchain not found: runtime MSL compilation only") | |
| 126 | + endif() | |
| 127 | + endif() | |
| 128 | +endif() | |
| 129 | + | |
| 130 | +# ---- CLI -------------------------------------------------------------------- | |
| 131 | +if(RAID_BUILD_CLI) | |
| 132 | + add_executable(raid_cli cli/raid.cpp) | |
| 133 | + set_target_properties(raid_cli PROPERTIES OUTPUT_NAME raid) | |
| 134 | + target_link_libraries(raid_cli PRIVATE raid) | |
| 135 | + if(RAID_ENABLE_METAL) | |
| 136 | + target_link_libraries(raid_cli PRIVATE raid_metal) | |
| 137 | + endif() | |
| 138 | +endif() | |
| 139 | + | |
| 140 | +# ---- examples ----------------------------------------------------------------- | |
| 141 | +add_executable(example_minimal examples/cpp/minimal.cpp) | |
| 142 | +target_link_libraries(example_minimal PRIVATE raid) | |
| 143 | +if(RAID_ENABLE_METAL) | |
| 144 | + target_link_libraries(example_minimal PRIVATE raid_metal) | |
| 145 | +endif() | |
| 146 | + | |
| 147 | +# ---- tests ------------------------------------------------------------------ | |
| 148 | +if(RAID_BUILD_TESTS) | |
| 149 | + find_package(Catch2 3 CONFIG REQUIRED) | |
| 150 | + include(CTest) | |
| 151 | + include(Catch) | |
| 152 | + file(GLOB_RECURSE RAID_TEST_SOURCES CONFIGURE_DEPENDS tests/unit/*.cpp tests/property/*.cpp tests/reference/*.cpp tests/simulation/*.cpp tests/stress/*.cpp tests/integration/*.cpp) | |
| 153 | + add_executable(raid_tests ${RAID_TEST_SOURCES}) | |
| 154 | + target_link_libraries(raid_tests PRIVATE raid Catch2::Catch2WithMain) | |
| 155 | + target_compile_definitions(raid_tests PRIVATE RAID_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") | |
| 156 | + if(RAID_ENABLE_METAL) | |
| 157 | + target_link_libraries(raid_tests PRIVATE raid_metal) | |
| 158 | + endif() | |
| 159 | + catch_discover_tests(raid_tests) | |
| 160 | +endif() | |
| 161 | + | |
| 162 | +# ---- benchmarks --------------------------------------------------------------- | |
| 163 | +if(RAID_BUILD_BENCH) | |
| 164 | + file(GLOB RAID_BENCH_SOURCES CONFIGURE_DEPENDS benchmarks/micro/*.cpp benchmarks/end_to_end/*.cpp benchmarks/accuracy/*.cpp benchmarks/scalability/*.cpp) | |
| 165 | + foreach(b ${RAID_BENCH_SOURCES}) | |
| 166 | + get_filename_component(n ${b} NAME_WE) | |
| 167 | + add_executable(bench_${n} ${b}) | |
| 168 | + target_link_libraries(bench_${n} PRIVATE raid) | |
| 169 | + if(RAID_ENABLE_METAL) | |
| 170 | + target_link_libraries(bench_${n} PRIVATE raid_metal) | |
| 171 | + endif() | |
| 172 | + endforeach() | |
| 173 | +endif() | |
| 174 | + | |
| 175 | +# ---- Python ------------------------------------------------------------------- | |
| 176 | +if(RAID_BUILD_PYTHON) | |
| 177 | + add_subdirectory(bindings/python) | |
| 178 | +endif() | |
| 179 | + | |
| 180 | +# ---- install ----------------------------------------------------------------- | |
| 181 | +install(TARGETS raid raid_options EXPORT raidTargets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) | |
| 182 | +if(RAID_ENABLE_METAL) | |
| 183 | + install(TARGETS raid_metal EXPORT raidTargets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) | |
| 184 | +endif() | |
| 185 | +if(RAID_BUILD_CLI) | |
| 186 | + install(TARGETS raid_cli RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) | |
| 187 | +endif() | |
| 188 | +install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) | |
| 189 | +install(EXPORT raidTargets NAMESPACE raid:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/raid) | |
added
CMakePresets.json
+25 −0
@@ -0,0 +1,25 @@ | ||
| 1 | +{ | |
| 2 | + "version": 6, | |
| 3 | + "configurePresets": [ | |
| 4 | + { "name": "base", "hidden": true, "generator": "Ninja", "binaryDir": "${sourceDir}/build/${presetName}", | |
| 5 | + "cacheVariables": { "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } }, | |
| 6 | + { "name": "macos-release", "inherits": "base", "displayName": "macOS arm64 Release (CPU + Metal + Arrow)", | |
| 7 | + "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "RAID_ENABLE_METAL": "ON", "RAID_WITH_ARROW": "ON", "CMAKE_PREFIX_PATH": "/opt/homebrew" } }, | |
| 8 | + { "name": "macos-debug", "inherits": "macos-release", "displayName": "macOS Debug + sanitizers", | |
| 9 | + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer", "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined", "RAID_NATIVE_ARCH": "OFF" } }, | |
| 10 | + { "name": "cpu-only", "inherits": "base", "displayName": "Portable CPU-only build (Linux/macOS)", | |
| 11 | + "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "RAID_ENABLE_METAL": "OFF", "RAID_WITH_ARROW": "OFF", "RAID_WITH_ACCELERATE": "OFF", "RAID_NATIVE_ARCH": "OFF" } }, | |
| 12 | + { "name": "bundle", "inherits": "base", "displayName": "Self-contained bundle for cluster nodes (E7)", | |
| 13 | + "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "RAID_ENABLE_METAL": "ON", "RAID_WITH_ARROW": "OFF", "RAID_NATIVE_ARCH": "OFF", "RAID_BUILD_TESTS": "OFF", "CMAKE_OSX_DEPLOYMENT_TARGET": "26.0", "CMAKE_PREFIX_PATH": "/opt/homebrew", "CMAKE_CXX_FLAGS": "-mcpu=apple-m1" } } | |
| 14 | + ], | |
| 15 | + "buildPresets": [ | |
| 16 | + { "name": "macos-release", "configurePreset": "macos-release" }, | |
| 17 | + { "name": "macos-debug", "configurePreset": "macos-debug" }, | |
| 18 | + { "name": "cpu-only", "configurePreset": "cpu-only" }, | |
| 19 | + { "name": "bundle", "configurePreset": "bundle" } | |
| 20 | + ], | |
| 21 | + "testPresets": [ | |
| 22 | + { "name": "macos-release", "configurePreset": "macos-release", "output": { "outputOnFailure": true } }, | |
| 23 | + { "name": "cpu-only", "configurePreset": "cpu-only", "output": { "outputOnFailure": true } } | |
| 24 | + ] | |
| 25 | +} | |
added
README.md
+114 −0
@@ -0,0 +1,114 @@ | ||
| 1 | +<!-- Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca --> | |
| 2 | +# ⚡ RAID — Research Architecture for Intraday Data | |
| 3 | + | |
| 4 | +**UQO Working Paper No. 40** — *A Metal-Accelerated C++ Framework for Reproducible High-Frequency Financial Econometrics* | |
| 5 | + | |
| 6 | +[](paper/) | |
| 7 | +[](CMakeLists.txt) | |
| 8 | +[-000000?logo=apple&logoColor=white)](backends/metal/) | |
| 9 | +[](pyproject.toml) | |
| 10 | +[-4051b5)](docs/audit.md) | |
| 11 | +[](https://uqo.ca) | |
| 12 | +[](mailto:simon-pierre.boucher@uqo.ca) | |
| 13 | + | |
| 14 | +> **TL;DR** — RAID asks whether heterogeneous high-frequency econometric workloads (realized measures, rolling/batched | |
| 15 | +> regressions, HAR, GARCH-family QML, block bootstrap) can be turned into batched, fused, streaming computation on Apple | |
| 16 | +> Silicon **without** losing estimator accuracy, inference validity or reproducibility. It is a C++20 library with a FP64 | |
| 17 | +> CPU reference backend, an optional FP32 Metal backend (Apple GPUs have no FP64), nanobind Python bindings, a CLI, a | |
| 18 | +> FinData OS client, and a benchmark protocol that reports break-even points and the cases where the CPU wins. | |
| 19 | +> Status: Phase 1 (foundation) — see `RESEARCH_STATUS.md`. No performance numbers are claimed yet. | |
| 20 | + | |
| 21 | +--- | |
| 22 | + | |
| 23 | +## 📌 Status | |
| 24 | + | |
| 25 | +| Item | Value | | |
| 26 | +|---|---| | |
| 27 | +| Version | 0.1.0 (November 2026) — Phases 0–1 | | |
| 28 | +| SSRN | not yet submitted | | |
| 29 | +| Journal | targets: J. Financial Econometrics, J. Financial Data Science, J. Computational Finance | | |
| 30 | +| Code & data | CPU core + tests build and pass on macOS 27 / Xcode 27.2; synthetic fixtures only in git; real bars via FinData OS (private license) | | |
| 31 | +| Ledger | `RESEARCH_STATUS.md` · `docs/PROGRESS.md` · `docs/DECISIONS.md` · `docs/risks.md` | | |
| 32 | + | |
| 33 | +## 🎯 What this paper does | |
| 34 | + | |
| 35 | +Three falsifiable questions. **RQ1** At what data and batch sizes does Metal beat an optimized multicore CPU, once I/O, | |
| 36 | +shader compilation, synchronization and transfers are counted? **RQ2** Does batched estimation raise throughput while | |
| 37 | +preserving statistical coverage and convergence? **RQ3** Can an Arrow/Parquet-to-estimator pipeline materially reduce | |
| 38 | +copies, memory pressure and end-to-end latency? Negative results are published. Tier A methods (returns, calendar-aware | |
| 39 | +aggregation, RV/BPV/RSV/RQ/jumps/rCov, OLS/WLS/ridge with HC/HAC/cluster inference, HAR family, GARCH/GJR/EGARCH with | |
| 40 | +Gaussian/Student-t QML, block bootstrap) are validated against R `highfrequency`/`rugarch` and Python | |
| 41 | +`statsmodels`/`arch`; GPU results are validated against the FP64 CPU reference with per-method tolerances. | |
| 42 | + | |
| 43 | +## 🏆 Headline results | |
| 44 | + | |
| 45 | +None yet — benchmarks (E1–E7) run in Phase 6; every number in the paper will point to a record under | |
| 46 | +`experiments/results/` listed in `experiments/manifests/`. | |
| 47 | + | |
| 48 | +## 🗂 Repository layout | |
| 49 | + | |
| 50 | +``` | |
| 51 | +wp40_raid/ | |
| 52 | +├── include/raid/{core,data,statistics,estimators,models,backend}/ # public C++20 headers | |
| 53 | +├── src/… backends/cpu/ backends/metal/{host,shaders}/ # implementations (Metal: Phase 3) | |
| 54 | +├── bindings/python/ python/raid/ cli/ # nanobind module, Python API, `raid` CLI | |
| 55 | +├── tests/{unit,property,reference,simulation,stress,integration}/ # Catch2 (reference = vs statsmodels/arch/highfrequency) | |
| 56 | +├── benchmarks/ experiments/{configs,scripts,results,manifests}/ # harness + tracked artifacts (Phase 6) | |
| 57 | +├── datasets/{synthetic,fixtures}/ # synthetic only; real data cached outside git | |
| 58 | +├── docs/ paper/ scripts/ .github/workflows/ | |
| 59 | +``` | |
| 60 | + | |
| 61 | +## 🚀 Quick start | |
| 62 | + | |
| 63 | +```bash | |
| 64 | +brew install cmake ninja eigen apache-arrow catch2 nlohmann-json # macOS | |
| 65 | +cmake --preset macos-release && cmake --build --preset macos-release # CPU + Metal + Arrow | |
| 66 | +ctest --preset macos-release # unit + reference tests | |
| 67 | +./build/macos-release/raid devices | |
| 68 | +./build/macos-release/raid validate datasets/fixtures/synthetic_bars_1m.csv | |
| 69 | +./build/macos-release/raid estimate realized datasets/fixtures/synthetic_bars_1m.csv --backend auto | |
| 70 | +uv venv --python 3.12 .venv && uv pip install --python .venv/bin/python -e ".[dev]" # Python package (nanobind) | |
| 71 | +``` | |
| 72 | + | |
| 73 | +Linux / CPU-only: `cmake --preset cpu-only && cmake --build --preset cpu-only && ctest --preset cpu-only`. | |
| 74 | + | |
| 75 | +## 🗃 Data | |
| 76 | + | |
| 77 | +Market data come from the owner's **FinData OS** lakehouse (`https://findata.spboucher.ai/api/v1`, FirstRate Data under | |
| 78 | +a private-use license): 1-minute OHLCV bars (bar-open timestamps, extended 04:00–20:00 ET, regular 09:30–16:00 with the | |
| 79 | +16:00 closing print, zero-volume minutes absent at the source), dataset versions pinned via `X-Dataset-Version`. Raw | |
| 80 | +bars are never committed; `datasets/fixtures/` contains synthetic bars generated by `scripts/make_fixtures.py`. | |
| 81 | + | |
| 82 | +## 🔬 Methodology | |
| 83 | + | |
| 84 | +See `docs/methods/registry.md` (one entry per method: equations, conventions, reference software and tolerance) and | |
| 85 | +`docs/architecture/overview.md`. | |
| 86 | + | |
| 87 | +## 🖼 Figures & tables inventory | |
| 88 | + | |
| 89 | +To be generated in Phase 6–7 from `experiments/results/`. | |
| 90 | + | |
| 91 | +## ⚠️ Limitations | |
| 92 | + | |
| 93 | +Apple Metal has no FP64: GPU paths are FP32 with compensated reductions and FP64 combination on the CPU, and are only | |
| 94 | +valid where the documented tolerance is met. No tick/quote data: microstructure-noise and order-flow methods are out of | |
| 95 | +scope. Energy is reported only when measured. | |
| 96 | + | |
| 97 | +## 📖 Citation | |
| 98 | + | |
| 99 | +```bibtex | |
| 100 | +@techreport{boucher2026raid, | |
| 101 | + title = {RAID: A Metal-Accelerated C++ Framework for Reproducible High-Frequency Financial Econometrics}, | |
| 102 | + author = {Boucher, Simon-Pierre}, | |
| 103 | + institution = {Université du Québec en Outaouais, Département des sciences administratives}, | |
| 104 | + type = {UQO Working Paper}, number = {40}, year = {2026} | |
| 105 | +} | |
| 106 | +``` | |
| 107 | + | |
| 108 | +## 👤 Author & contact | |
| 109 | + | |
| 110 | +Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca — Département des sciences administratives, UQO. | |
| 111 | + | |
| 112 | +## 📝 Notes | |
| 113 | + | |
| 114 | +Directory name `wp40_raid` (charter requirement) instead of the usual slug; git remote `gitsrv:srv/git/wp40_uqo.git`. | |
added
RESEARCH_STATUS.md
+40 −0
@@ -0,0 +1,40 @@ | ||
| 1 | +# RESEARCH_STATUS.md — RAID (UQO WP 40) | |
| 2 | + | |
| 3 | +Last update: 2026-11-02 (Phase 1 passed). Legend: ✅ verified by tests/artifacts · 🧪 implemented, validation pending · | |
| 4 | +🧭 experimental · ⛔ blocked · ⏳ planned · ✖ not implemented (deliberately). | |
| 5 | + | |
| 6 | +## Phases | |
| 7 | +| Phase | Gate | Status | Evidence | | |
| 8 | +|---|---|---|---| | |
| 9 | +| 0 Audit | scope & feasibility memo | ✅ passed 2026-11-02 | `docs/audit.md`, `docs/capabilities.md`, `docs/novelty_matrix.md`, `docs/risks.md`, `docs/PROGRESS.md` | | |
| 10 | +| 1 Foundation | clean build + tests + docs | ✅ passed 2026-11-02 | `ctest` 26/26 (314 assertions), `pytest` 7/7, `docs/PROGRESS.md` Phase 1 | | |
| 11 | +| 2 Data | offline synthetic tests + opt-in live tests | ⏳ | — | | |
| 12 | +| 3 Numerical primitives | accuracy + measured break-even | ⏳ | — | | |
| 13 | +| 4 Econometrics (Tier A) | references + Monte Carlo | ⏳ | — | | |
| 14 | +| 5 Extensions (Tier B) | per-feature specs + tests | ⏳ | — | | |
| 15 | +| 6 Benchmarking E1–E7 | every claim traceable | ⏳ | — | | |
| 16 | +| 7 Manuscript | reproducible build, no placeholders | ⏳ | — | | |
| 17 | +| 8 Independent review | clean-checkout reproduction | ⏳ | — | | |
| 18 | + | |
| 19 | +## Methods registry (summary; full entries in `docs/methods/registry.md` from Phase 4) | |
| 20 | +| Method | Tier | CPU FP64 | Metal FP32 | Reference software | Status | | |
| 21 | +|---|---|---|---|---|---| | |
| 22 | +| returns (log/arith), lag/lead, rolling windows, calendar-aware aggregation | A | ✅ | ⏳ (Phase 3) | numpy; exchange_calendars (calendar exact 2000–2026) | ✅ CPU | | |
| 23 | +| RV, BPV, RSV±, RQ, jump proxy, TriPV, rCov/rCorr | A | ✅ (numpy truth 1e−12) | ⏳ (Phase 3) | highfrequency 1.0.3 (R comparison Phase 4) | ✅ CPU | | |
| 24 | +| OLS/WLS/ridge, HC0–3, HAC, cluster, rolling OLS | A | ✅ (statsmodels 1e−9) | ⏳ (sufficient stats, Phase 3) | statsmodels 0.15, scipy 1.18 | ✅ CPU | | |
| 25 | +| HAR-RV, log-HAR, HAR-X, HARQ | A | ⏳ | ⏳ | highfrequency `HARmodel`, statsmodels | ⏳ | | |
| 26 | +| GARCH(1,1), GJR, EGARCH (N, t), QMLE/Hessian/OPG SE, forecasts | A | ✅ (arch 8.0: loglik 5e−4, params 5 %) | ⏳ (batched likelihood, Phase 3) | arch 8.0 ✅, rugarch 1.5.6 (Phase 4) | ✅ CPU | | |
| 27 | +| Bootstrap (stationary / circular block), Monte Carlo | A | ⏳ | ⏳ | arch.bootstrap | ⏳ | | |
| 28 | +| VAR, GFEVD, Diebold–Yilmaz | B | ⏳ | ⏳ | statsmodels VAR | ⏳ | | |
| 29 | +| Realized GARCH, HEAVY, DCC, PCA/shrinkage, Kalman | B | ✖ until Tier A passes | | | ⏳ | | |
| 30 | +| Pre-averaging, realized kernels, MSRV, BNS test | C | ✖ (1-min bars) | | highfrequency | 🧭 only if validated | | |
| 31 | +| Hawkes, order-flow, LOB methods | C | ✖ | ✖ | — | ✖ no admissible data (bars only) | | |
| 32 | + | |
| 33 | +## Blockers | |
| 34 | +- None at Phase 0. Seven cluster nodes unreachable (m2m16, M2M32, M2M32b, M4M36, m4ma, m4mb, m4mc): E7 reports them as | |
| 35 | + not tested unless they come back. | |
| 36 | + | |
| 37 | +## Next safe action | |
| 38 | +Phase 2: FinData client (C++ libcurl: catalog, bars Arrow pages, exports with polling/download, provenance, cache, | |
| 39 | +token bucket, retries), `raid data` CLI, offline tests with a recorded OpenAPI/fixture server, opt-in live tests | |
| 40 | +(`RAID_LIVE=1`), aggregate-vs-FinData-5m check. | |
added
backends/cpu/cpu_backend.cpp
+115 −0
@@ -0,0 +1,115 @@ | ||
| 1 | +// CPU reference backend: FP64, Neumaier-compensated per-series reductions, parallel over series. | |
| 2 | +#include "raid/backend/backend.hpp" | |
| 3 | +#include "raid/core/thread_pool.hpp" | |
| 4 | +#include "raid/core/error.hpp" | |
| 5 | +#include "raid/statistics/reductions.hpp" | |
| 6 | +#include "raid/statistics/rolling.hpp" | |
| 7 | +#include "raid/models/garch.hpp" | |
| 8 | +#include <chrono> | |
| 9 | +#include <thread> | |
| 10 | +#include <cmath> | |
| 11 | +#ifdef __APPLE__ | |
| 12 | +#include <sys/sysctl.h> | |
| 13 | +#endif | |
| 14 | + | |
| 15 | +namespace raid::backend { | |
| 16 | + | |
| 17 | +namespace { | |
| 18 | +struct Timer { std::chrono::steady_clock::time_point t0 = std::chrono::steady_clock::now(); f64 s() const { return std::chrono::duration<f64>(std::chrono::steady_clock::now() - t0).count(); } }; | |
| 19 | +void fill(KernelStats* st, const char* k, const Timer& t, usize bin, usize bout) { if (!st) return; st->kernel = k; st->backend = raid::Backend::cpu; st->precision = Precision::fp64; st->wall_seconds = t.s(); st->bytes_in = bin; st->bytes_out = bout; } | |
| 20 | +} | |
| 21 | + | |
| 22 | +class CpuBackend final : public Backend { | |
| 23 | + public: | |
| 24 | + explicit CpuBackend(unsigned threads) : pool_(threads == 0 ? nullptr : std::make_unique<ThreadPool>(threads)) {} | |
| 25 | + std::string name() const override { return "cpu"; } | |
| 26 | + raid::Backend kind() const override { return raid::Backend::cpu; } | |
| 27 | + bool available() const override { return true; } | |
| 28 | + Device device() const override { | |
| 29 | + Device d; d.kind = raid::Backend::cpu; d.supports_fp64 = true; d.unified_memory = true; d.compute_units = static_cast<int>(std::thread::hardware_concurrency()); | |
| 30 | +#ifdef __APPLE__ | |
| 31 | + char buf[256]; size_t n = sizeof buf; if (sysctlbyname("machdep.cpu.brand_string", buf, &n, nullptr, 0) == 0) d.name = std::string(buf, n - 1); else d.name = "cpu"; | |
| 32 | + i64 mem = 0; n = sizeof mem; if (sysctlbyname("hw.memsize", &mem, &n, nullptr, 0) == 0) d.memory_bytes = static_cast<u64>(mem); | |
| 33 | +#else | |
| 34 | + d.name = "cpu"; | |
| 35 | +#endif | |
| 36 | + d.details = "threads=" + std::to_string(pool().size()); | |
| 37 | + return d; | |
| 38 | + } | |
| 39 | + ThreadPool& pool() const { return pool_ ? *pool_ : ThreadPool::global(); } | |
| 40 | + | |
| 41 | + template <class F> std::vector<f64> per_series(const Batch& b, F f) { | |
| 42 | + std::vector<f64> out(b.n_series()); | |
| 43 | + pool().parallel_for(b.n_series(), [&](usize i0, usize i1) { for (usize i = i0; i < i1; ++i) out[i] = f(b.series(i)); }); | |
| 44 | + return out; | |
| 45 | + } | |
| 46 | + | |
| 47 | + std::vector<f64> sum_squares(const Batch& b, KernelStats* st) override { | |
| 48 | + Timer t; auto r = per_series(b, [](std::span<const f64> x) { return stats::sum_squares(x); }); fill(st, "sum_squares", t, b.values.size_bytes(), r.size() * 8); return r; | |
| 49 | + } | |
| 50 | + std::vector<f64> sum_abs_adjacent(const Batch& b, KernelStats* st) override { | |
| 51 | + Timer t; auto r = per_series(b, [](std::span<const f64> x) { f64 s = 0, c = 0; for (usize i = 1; i < x.size(); ++i) { const f64 v = std::abs(x[i]) * std::abs(x[i - 1]), tt = s + v; if (std::abs(s) >= std::abs(v)) c += (s - tt) + v; else c += (v - tt) + s; s = tt; } return s + c; }); | |
| 52 | + fill(st, "sum_abs_adjacent", t, b.values.size_bytes(), r.size() * 8); return r; | |
| 53 | + } | |
| 54 | + void semivariances(const Batch& b, std::vector<f64>& neg, std::vector<f64>& pos, KernelStats* st) override { | |
| 55 | + Timer t; neg.assign(b.n_series(), 0); pos.assign(b.n_series(), 0); | |
| 56 | + pool().parallel_for(b.n_series(), [&](usize i0, usize i1) { for (usize i = i0; i < i1; ++i) { f64 sn = 0, cn = 0, sp = 0, cp = 0; for (f64 v : b.series(i)) { const f64 q = v * v; if (v < 0) { const f64 tt = sn + q; if (std::abs(sn) >= q) cn += (sn - tt) + q; else cn += (q - tt) + sn; sn = tt; } else if (v > 0) { const f64 tt = sp + q; if (std::abs(sp) >= q) cp += (sp - tt) + q; else cp += (q - tt) + sp; sp = tt; } } neg[i] = sn + cn; pos[i] = sp + cp; } }); | |
| 57 | + fill(st, "semivariances", t, b.values.size_bytes(), neg.size() * 16); | |
| 58 | + } | |
| 59 | + std::vector<f64> sum_fourth(const Batch& b, KernelStats* st) override { | |
| 60 | + Timer t; auto r = per_series(b, [](std::span<const f64> x) { f64 s = 0, c = 0; for (f64 v : x) { const f64 q = v * v * v * v, tt = s + q; if (std::abs(s) >= q) c += (s - tt) + q; else c += (q - tt) + s; s = tt; } return s + c; }); | |
| 61 | + fill(st, "sum_fourth", t, b.values.size_bytes(), r.size() * 8); return r; | |
| 62 | + } | |
| 63 | + std::vector<f64> rolling_sum_squares(const Batch& b, usize window, KernelStats* st) override { | |
| 64 | + Timer t; std::vector<f64> out(b.values.size()); | |
| 65 | + pool().parallel_for(b.n_series(), [&](usize i0, usize i1) { for (usize i = i0; i < i1; ++i) { auto x = b.series(i); std::vector<f64> sq(x.size()); for (usize j = 0; j < x.size(); ++j) sq[j] = x[j] * x[j]; auto r = stats::rolling_sum(sq, stats::RollingOptions{window, 0, 1024}); std::copy(r.begin(), r.end(), out.begin() + b.offsets[i]); } }); | |
| 66 | + fill(st, "rolling_sum_squares", t, b.values.size_bytes(), out.size() * 8); return out; | |
| 67 | + } | |
| 68 | + void regression_stats(std::span<const f64> x, std::span<const f64> y, std::span<const i64> offsets, usize k, std::vector<f64>& xtx, std::vector<f64>& xty, std::vector<f64>& yty, KernelStats* st) override { | |
| 69 | + Timer t; const usize ns = offsets.size() - 1; xtx.assign(ns * k * k, 0); xty.assign(ns * k, 0); yty.assign(ns, 0); | |
| 70 | + pool().parallel_for(ns, [&](usize i0, usize i1) { for (usize s = i0; s < i1; ++s) { const usize b0 = static_cast<usize>(offsets[s]), n = static_cast<usize>(offsets[s + 1] - offsets[s]); | |
| 71 | + std::vector<f64> acc(k * k, 0), accc(k * k, 0), ay(k, 0), ayc(k, 0); f64 syy = 0, cyy = 0; | |
| 72 | + for (usize r = 0; r < n; ++r) { const f64* xr = x.data() + (b0 + r) * k; const f64 yr = y[b0 + r]; | |
| 73 | + for (usize a = 0; a < k; ++a) { for (usize c = a; c < k; ++c) { const f64 v = xr[a] * xr[c], tt = acc[a * k + c] + v; if (std::abs(acc[a * k + c]) >= std::abs(v)) accc[a * k + c] += (acc[a * k + c] - tt) + v; else accc[a * k + c] += (v - tt) + acc[a * k + c]; acc[a * k + c] = tt; } | |
| 74 | + const f64 v = xr[a] * yr, tt = ay[a] + v; if (std::abs(ay[a]) >= std::abs(v)) ayc[a] += (ay[a] - tt) + v; else ayc[a] += (v - tt) + ay[a]; ay[a] = tt; } | |
| 75 | + const f64 q = yr * yr, tt = syy + q; if (std::abs(syy) >= q) cyy += (syy - tt) + q; else cyy += (q - tt) + syy; syy = tt; } | |
| 76 | + for (usize a = 0; a < k; ++a) for (usize c = a; c < k; ++c) { const f64 v = acc[a * k + c] + accc[a * k + c]; xtx[s * k * k + a * k + c] = v; xtx[s * k * k + c * k + a] = v; } | |
| 77 | + for (usize a = 0; a < k; ++a) xty[s * k + a] = ay[a] + ayc[a]; | |
| 78 | + yty[s] = syy + cyy; } }); | |
| 79 | + fill(st, "regression_stats", t, x.size_bytes() + y.size_bytes(), (xtx.size() + xty.size() + yty.size()) * 8); | |
| 80 | + } | |
| 81 | + std::vector<f64> garch_nll_batch(const Batch& returns, std::span<const f64> params, usize n_params, usize p, int model_code, f64 nu_fixed, KernelStats* st) override { | |
| 82 | + Timer t; const usize ns = returns.n_series(); std::vector<f64> out(ns * n_params); | |
| 83 | + pool().parallel_for(ns * n_params, [&](usize i0, usize i1) { for (usize idx = i0; idx < i1; ++idx) { const usize s = idx / n_params, q = idx % n_params; const f64* th = params.data() + (s * n_params + q) * p; out[idx] = models::garch_nll(returns.series(s), std::span<const f64>(th, p), static_cast<models::GarchModel>(model_code), nu_fixed); } }); | |
| 84 | + fill(st, "garch_nll_batch", t, returns.values.size_bytes() + params.size_bytes(), out.size() * 8); return out; | |
| 85 | + } | |
| 86 | + private: | |
| 87 | + std::unique_ptr<ThreadPool> pool_; | |
| 88 | +}; | |
| 89 | + | |
| 90 | +std::shared_ptr<Backend> make_cpu_backend(unsigned threads) { return std::make_shared<CpuBackend>(threads); } | |
| 91 | + | |
| 92 | +std::shared_ptr<Backend> resolve(const ExecutionPlan& plan, std::string* reason) { | |
| 93 | + auto why = [&](const std::string& s) { if (reason) *reason = s; }; | |
| 94 | + if (plan.backend == raid::Backend::cpu) { why("cpu requested"); return make_cpu_backend(plan.threads); } | |
| 95 | +#ifdef RAID_ENABLE_METAL | |
| 96 | + auto m = make_metal_backend(plan.precision); | |
| 97 | + if (m && m->available()) { why(plan.backend == raid::Backend::metal ? "metal requested" : "auto: metal available"); return m; } | |
| 98 | + if (plan.backend == raid::Backend::metal && !plan.allow_fallback) throw RaidError(ErrorCode::unsupported, "Metal backend requested but unavailable and fallback disabled"); | |
| 99 | + why("metal unavailable: falling back to cpu"); | |
| 100 | +#else | |
| 101 | + if (plan.backend == raid::Backend::metal && !plan.allow_fallback) throw RaidError(ErrorCode::unsupported, "RAID built without Metal support"); | |
| 102 | + why(plan.backend == raid::Backend::metal ? "built without Metal: falling back to cpu" : "auto: cpu (no Metal in this build)"); | |
| 103 | +#endif | |
| 104 | + return make_cpu_backend(plan.threads); | |
| 105 | +} | |
| 106 | + | |
| 107 | +std::vector<Device> list_devices() { | |
| 108 | + std::vector<Device> d; d.push_back(make_cpu_backend()->device()); | |
| 109 | +#ifdef RAID_ENABLE_METAL | |
| 110 | + if (auto m = make_metal_backend()) if (m->available()) d.push_back(m->device()); | |
| 111 | +#endif | |
| 112 | + return d; | |
| 113 | +} | |
| 114 | + | |
| 115 | +} // namespace raid::backend | |
added
bindings/python/CMakeLists.txt
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +find_package(Python 3.11 COMPONENTS Interpreter Development.Module REQUIRED) | |
| 2 | +execute_process(COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT) | |
| 3 | +find_package(nanobind CONFIG REQUIRED) | |
| 4 | +nanobind_add_module(_core NB_STATIC raid_py.cpp) | |
| 5 | +target_link_libraries(_core PRIVATE raid) | |
| 6 | +if(RAID_ENABLE_METAL) | |
| 7 | + target_link_libraries(_core PRIVATE raid_metal) | |
| 8 | +endif() | |
| 9 | +install(TARGETS _core LIBRARY DESTINATION raid) | |
added
bindings/python/raid_py.cpp
+102 −0
@@ -0,0 +1,102 @@ | ||
| 1 | +// nanobind bindings for RAID. Arrays are passed as NumPy views (no copy for contiguous float64/int64), the GIL is | |
| 2 | +// released around every heavy call, and results come back as dicts (JSON-like) plus NumPy arrays for vectors. | |
| 3 | +#include <nanobind/nanobind.h> | |
| 4 | +#include <nanobind/ndarray.h> | |
| 5 | +#include <nanobind/stl/string.h> | |
| 6 | +#include <nanobind/stl/vector.h> | |
| 7 | +#include <nanobind/stl/optional.h> | |
| 8 | +#include <nlohmann/json.hpp> | |
| 9 | +#include "raid/core/types.hpp" | |
| 10 | +#include "raid/core/error.hpp" | |
| 11 | +#include "raid/backend/backend.hpp" | |
| 12 | +#include "raid/estimators/ols.hpp" | |
| 13 | +#include "raid/estimators/realized.hpp" | |
| 14 | +#include "raid/models/garch.hpp" | |
| 15 | +#include "raid/data/bar_series.hpp" | |
| 16 | +#include "raid/data/calendar.hpp" | |
| 17 | +#include "raid/data/returns.hpp" | |
| 18 | + | |
| 19 | +namespace nb = nanobind; | |
| 20 | +using namespace raid; | |
| 21 | +using json = nlohmann::json; | |
| 22 | +using F64 = nb::ndarray<const double, nb::c_contig, nb::device::cpu>; | |
| 23 | +using I64 = nb::ndarray<const int64_t, nb::c_contig, nb::device::cpu>; | |
| 24 | + | |
| 25 | +namespace { | |
| 26 | +nb::object json_to_py(const json& j) { | |
| 27 | + switch (j.type()) { | |
| 28 | + case json::value_t::null: return nb::none(); | |
| 29 | + case json::value_t::boolean: return nb::bool_(j.get<bool>()); | |
| 30 | + case json::value_t::number_integer: return nb::int_(j.get<long long>()); | |
| 31 | + case json::value_t::number_unsigned: return nb::int_(j.get<unsigned long long>()); | |
| 32 | + case json::value_t::number_float: return nb::float_(j.get<double>()); | |
| 33 | + case json::value_t::string: return nb::str(j.get<std::string>().c_str()); | |
| 34 | + case json::value_t::array: { nb::list l; for (auto& v : j) l.append(json_to_py(v)); return l; } | |
| 35 | + case json::value_t::object: { nb::dict d; for (auto& [k, v] : j.items()) d[k.c_str()] = json_to_py(v); return d; } | |
| 36 | + default: return nb::none(); | |
| 37 | + } | |
| 38 | +} | |
| 39 | +nb::ndarray<nb::numpy, double> to_np(std::vector<double> v) { | |
| 40 | + auto* data = new std::vector<double>(std::move(v)); | |
| 41 | + nb::capsule owner(data, [](void* p) noexcept { delete static_cast<std::vector<double>*>(p); }); | |
| 42 | + return nb::ndarray<nb::numpy, double>(data->data(), {data->size()}, owner); | |
| 43 | +} | |
| 44 | +nb::ndarray<nb::numpy, int64_t> to_np_i64(std::vector<int64_t> v) { | |
| 45 | + auto* data = new std::vector<int64_t>(std::move(v)); | |
| 46 | + nb::capsule owner(data, [](void* p) noexcept { delete static_cast<std::vector<int64_t>*>(p); }); | |
| 47 | + return nb::ndarray<nb::numpy, int64_t>(data->data(), {data->size()}, owner); | |
| 48 | +} | |
| 49 | +} // namespace | |
| 50 | + | |
| 51 | +NB_MODULE(_core, m) { | |
| 52 | + m.doc() = "RAID compiled core (C++20, CPU + optional Metal)"; | |
| 53 | + m.attr("__version__") = RAID_VERSION; | |
| 54 | + nb::exception<RaidError>(m, "RaidError"); | |
| 55 | + | |
| 56 | + m.def("devices", []() { json out = json::array(); for (auto& d : backend::list_devices()) out.push_back(d.to_json()); return json_to_py(out); }); | |
| 57 | + | |
| 58 | + m.def("ols", [](F64 X, F64 y, const std::string& cov, int hac_lags, std::optional<I64> clusters, double ridge, std::vector<usize> unpen, std::optional<F64> weights, std::vector<std::string> names) { | |
| 59 | + RAID_REQUIRE(X.ndim() == 2 && y.ndim() == 1 && X.shape(0) == y.shape(0), invalid_argument, "X (n,k), y (n,)"); | |
| 60 | + const Eigen::Index n = static_cast<Eigen::Index>(X.shape(0)), k = static_cast<Eigen::Index>(X.shape(1)); | |
| 61 | + Eigen::Map<const Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>> Xm(X.data(), n, k); | |
| 62 | + Eigen::Map<const Eigen::VectorXd> ym(y.data(), n); | |
| 63 | + est::OlsOptions o; o.cov.type = stats::covtype_from_string(cov); o.cov.hac_lags = hac_lags; o.ridge_lambda = ridge; o.unpenalized = unpen; o.names = names; | |
| 64 | + std::vector<i64> cl; if (clusters) { cl.assign(clusters->data(), clusters->data() + clusters->shape(0)); o.cov.clusters = cl; } | |
| 65 | + std::vector<f64> w; if (weights) { w.assign(weights->data(), weights->data() + weights->shape(0)); o.weights = w; } | |
| 66 | + Eigen::MatrixXd Xd = Xm; json out; { nb::gil_scoped_release rel; auto r = est::ols(Xd, ym, o); out = r.estimate.to_json(); } | |
| 67 | + return json_to_py(out); | |
| 68 | + }, nb::arg("X"), nb::arg("y"), nb::arg("cov") = "classical", nb::arg("hac_lags") = -1, nb::arg("clusters") = nb::none(), nb::arg("ridge_lambda") = 0.0, nb::arg("unpenalized") = std::vector<usize>{}, nb::arg("weights") = nb::none(), nb::arg("names") = std::vector<std::string>{}); | |
| 69 | + | |
| 70 | + m.def("realized_measures", [](F64 r, I64 off, const std::string& backend_s, bool bpv_fs, bool deterministic) { | |
| 71 | + est::RealizedOptions o; o.plan.backend = backend_from_string(backend_s); o.bpv_finite_sample = bpv_fs; o.plan.deterministic = deterministic; | |
| 72 | + backend::Batch b{std::span<const f64>(r.data(), r.shape(0)), std::span<const i64>(off.data(), off.shape(0))}; | |
| 73 | + est::RealizedMeasures mres; { nb::gil_scoped_release rel; mres = est::realized_measures_batch(b, o); } | |
| 74 | + nb::dict d; d["rv"] = to_np(mres.rv); d["bpv"] = to_np(mres.bpv); d["rsv_neg"] = to_np(mres.rsv_neg); d["rsv_pos"] = to_np(mres.rsv_pos); d["rq"] = to_np(mres.rq); d["jump"] = to_np(mres.jump); d["tripv"] = to_np(mres.tripv); d["ret"] = to_np(mres.daily_return); | |
| 75 | + d["backend"] = std::string(to_string(mres.stats.backend)); d["precision"] = std::string(to_string(mres.stats.precision)); d["wall_seconds"] = mres.stats.wall_seconds; d["device_seconds"] = mres.stats.device_seconds; d["note"] = mres.stats.note; d["warnings"] = mres.warnings; | |
| 76 | + return d; | |
| 77 | + }, nb::arg("returns"), nb::arg("offsets"), nb::arg("backend") = "auto", nb::arg("bpv_finite_sample") = true, nb::arg("deterministic") = true); | |
| 78 | + | |
| 79 | + m.def("garch_fit", [](F64 r, const std::string& model, const std::string& dist, int horizon, const std::string& cov, int max_iter, double tol) { | |
| 80 | + models::GarchFitOptions o; o.model = model == "gjr" ? models::GarchModel::gjr : (model == "egarch" ? models::GarchModel::egarch : models::GarchModel::garch); | |
| 81 | + o.dist = dist == "student_t" || dist == "t" ? models::GarchDist::student_t : models::GarchDist::gaussian; o.forecast_horizon = horizon; o.cov = cov; o.max_iter = max_iter; o.tol = tol; | |
| 82 | + std::span<const f64> rs(r.data(), r.shape(0)); json out; { nb::gil_scoped_release rel; out = models::garch_fit(rs, o).to_json(); } | |
| 83 | + return json_to_py(out); | |
| 84 | + }, nb::arg("returns"), nb::arg("model") = "garch", nb::arg("dist") = "gaussian", nb::arg("horizon") = 0, nb::arg("cov") = "qmle", nb::arg("max_iter") = 500, nb::arg("tol") = 1e-8); | |
| 85 | + | |
| 86 | + m.def("validate_bars", [](I64 t, F64 o, F64 h, F64 l, F64 c, F64 v, const std::string& symbol, const std::string& interval, const std::string& session) { | |
| 87 | + BarSeries b; b.symbol = symbol; b.interval = interval_from_string(interval); const usize n = t.shape(0); b.reserve(n); | |
| 88 | + for (usize i = 0; i < n; ++i) b.push_back(Timestamp(t.data()[i]), o.data()[i], h.data()[i], l.data()[i], c.data()[i], v.data()[i]); | |
| 89 | + json out; { nb::gil_scoped_release rel; auto st = b.normalize(); MarketCalendar cal; auto q = b.validate(cal, session == "extended" ? SessionPolicy::extended : SessionPolicy::regular); q.duplicates = st.duplicates_removed; q.unsorted = st.reordered; out = q.to_json(); } | |
| 90 | + return json_to_py(out); | |
| 91 | + }); | |
| 92 | + | |
| 93 | + m.def("calendar_sessions", [](const std::string& a, const std::string& b) { MarketCalendar cal; std::vector<std::string> out; for (auto d : cal.sessions(date_from_iso(a), date_from_iso(b))) out.push_back(d.iso()); return out; }); | |
| 94 | + | |
| 95 | + m.def("intraday_returns", [](I64 t, F64 c, const std::string& kind) { | |
| 96 | + BarSeries b; b.interval = Interval::m1; const usize n = t.shape(0); b.reserve(n); | |
| 97 | + for (usize i = 0; i < n; ++i) b.push_back(Timestamp(t.data()[i]), c.data()[i], c.data()[i], c.data()[i], c.data()[i], 0.0); | |
| 98 | + data::IntradayReturns ir; { nb::gil_scoped_release rel; b.normalize(); MarketCalendar cal; auto reg = b.filter_session(cal, SessionPolicy::regular); ir = data::intraday_returns(reg, kind == "arithmetic" ? data::ReturnKind::arithmetic : data::ReturnKind::log); } | |
| 99 | + nb::dict d; d["r"] = to_np(ir.r); d["offsets"] = to_np_i64(ir.session_offsets); std::vector<std::string> s; for (auto dd : ir.session_dates) s.push_back(dd.iso()); d["sessions"] = s; d["overnight"] = to_np(ir.overnight); d["close_to_close"] = to_np(ir.close_to_close); d["open_to_close"] = to_np(ir.open_to_close); | |
| 100 | + return d; | |
| 101 | + }); | |
| 102 | +} | |
added
cli/raid.cpp
+120 −0
@@ -0,0 +1,120 @@ | ||
| 1 | +// raid — command-line tool: devices | validate | estimate | data | benchmark (Phase 1: devices, validate, estimate on CSV/Parquet). | |
| 2 | +#include <cstdio> | |
| 3 | +#include <cstring> | |
| 4 | +#include <iostream> | |
| 5 | +#include <fstream> | |
| 6 | +#include <string> | |
| 7 | +#include <vector> | |
| 8 | +#include <map> | |
| 9 | +#include <nlohmann/json.hpp> | |
| 10 | +#include "raid/core/types.hpp" | |
| 11 | +#include "raid/core/error.hpp" | |
| 12 | +#include "raid/core/manifest.hpp" | |
| 13 | +#include "raid/core/hash.hpp" | |
| 14 | +#include "raid/backend/backend.hpp" | |
| 15 | +#include "raid/data/io.hpp" | |
| 16 | +#include "raid/data/calendar.hpp" | |
| 17 | +#include "raid/data/returns.hpp" | |
| 18 | +#include "raid/estimators/realized.hpp" | |
| 19 | +#include "raid/estimators/ols.hpp" | |
| 20 | +#include "raid/models/garch.hpp" | |
| 21 | +#ifdef RAID_WITH_FINDATA | |
| 22 | +#include "raid/data/findata.hpp" | |
| 23 | +#endif | |
| 24 | + | |
| 25 | +using namespace raid; | |
| 26 | +using json = nlohmann::json; | |
| 27 | + | |
| 28 | +namespace { | |
| 29 | +void usage() { | |
| 30 | + std::fprintf(stderr, | |
| 31 | + "raid %s — Research Architecture for Intraday Data (UQO WP 40)\n" | |
| 32 | + " raid devices list backends/devices (JSON)\n" | |
| 33 | + " raid validate <bars.csv|.parquet> [--session regular|extended] quality report per symbol (JSON)\n" | |
| 34 | + " raid estimate realized <bars> [--backend auto|cpu|metal] [--bpv-no-fs] per-session realized measures\n" | |
| 35 | + " raid estimate garch <bars> [--model garch|gjr|egarch] [--dist gaussian|student_t] [--horizon H] [--returns daily|intraday]\n" | |
| 36 | + " raid estimate ols <csv y,x1,x2...> [--cov classical|hc0..hc3|hac|cluster] [--lags L]\n" | |
| 37 | + " raid data ... FinData access (Phase 2)\n" | |
| 38 | + " raid benchmark ... benchmark harness (Phase 3+)\n", RAID_VERSION); | |
| 39 | +} | |
| 40 | +std::string arg(const std::vector<std::string>& a, const std::string& key, const std::string& def = "") { for (usize i = 0; i + 1 < a.size(); ++i) if (a[i] == key) return a[i + 1]; return def; } | |
| 41 | +bool flag(const std::vector<std::string>& a, const std::string& key) { for (auto& s : a) if (s == key) return true; return false; } | |
| 42 | + | |
| 43 | +std::map<std::string, BarSeries> load_bars(const std::string& path) { | |
| 44 | + if (path.size() > 4 && path.substr(path.size() - 4) == ".csv") return data::read_bars_csv(path); | |
| 45 | +#ifdef RAID_WITH_ARROW | |
| 46 | + if (path.size() > 8 && path.substr(path.size() - 8) == ".parquet") return data::read_bars_parquet(path).series; | |
| 47 | + if (path.size() > 6 && path.substr(path.size() - 6) == ".arrow") return data::read_bars_arrow_ipc(path).series; | |
| 48 | +#endif | |
| 49 | + throw RaidError(ErrorCode::io, "unsupported input format (csv" | |
| 50 | +#ifdef RAID_WITH_ARROW | |
| 51 | + ", parquet, arrow" | |
| 52 | +#endif | |
| 53 | + "): " + path); | |
| 54 | +} | |
| 55 | + | |
| 56 | +int cmd_devices() { | |
| 57 | + json out = json::array(); for (auto& d : backend::list_devices()) out.push_back(d.to_json()); | |
| 58 | + json j = {{"devices", out}, {"host", host_info()}}; std::cout << j.dump(1) << "\n"; return 0; | |
| 59 | +} | |
| 60 | + | |
| 61 | +int cmd_validate(const std::vector<std::string>& a) { | |
| 62 | + if (a.empty()) { usage(); return 2; } | |
| 63 | + auto bars = load_bars(a[0]); MarketCalendar cal; | |
| 64 | + const auto policy = arg(a, "--session", "regular") == "extended" ? SessionPolicy::extended : SessionPolicy::regular; | |
| 65 | + json out = json::array(); | |
| 66 | + for (auto& [sym, b] : bars) { auto bb = b; auto st = bb.normalize(); auto q = bb.validate(cal, policy); q.duplicates = st.duplicates_removed; q.unsorted = st.reordered; out.push_back(q.to_json()); } | |
| 67 | + std::cout << json({{"file", a[0]}, {"sha256", Sha256::of_file(a[0])}, {"reports", out}}).dump(1) << "\n"; return 0; | |
| 68 | +} | |
| 69 | + | |
| 70 | +int cmd_estimate(const std::vector<std::string>& a) { | |
| 71 | + if (a.size() < 2) { usage(); return 2; } | |
| 72 | + const std::string what = a[0], path = a[1]; | |
| 73 | + ExecutionPlan plan; plan.backend = backend_from_string(arg(a, "--backend", "auto")); | |
| 74 | + if (what == "realized") { | |
| 75 | + auto bars = load_bars(path); MarketCalendar cal; json out = json::object(); | |
| 76 | + est::RealizedOptions ro; ro.plan = plan; ro.bpv_finite_sample = !flag(a, "--bpv-no-fs"); | |
| 77 | + for (auto& [sym, b0] : bars) { auto b = b0; b.normalize(); auto reg = b.filter_session(cal, SessionPolicy::regular); auto ir = data::intraday_returns(reg); auto m = est::realized_measures(ir, ro); | |
| 78 | + json rows = json::array(); for (usize k = 0; k < m.sessions.size(); ++k) rows.push_back({{"session", m.sessions[k].iso()}, {"n", m.n_returns[k]}, {"rv", m.rv[k]}, {"bpv", m.bpv[k]}, {"rsv_neg", m.rsv_neg[k]}, {"rsv_pos", m.rsv_pos[k]}, {"rq", m.rq[k]}, {"jump", m.jump[k]}, {"tripv", m.tripv[k]}, {"ret", m.daily_return[k]}}); | |
| 79 | + out[sym] = {{"measures", rows}, {"backend", std::string(to_string(m.stats.backend))}, {"precision", std::string(to_string(m.stats.precision))}, {"wall_seconds", m.stats.wall_seconds}, {"note", m.stats.note}, {"warnings", m.warnings}}; } | |
| 80 | + std::cout << out.dump(1) << "\n"; return 0; | |
| 81 | + } | |
| 82 | + if (what == "garch") { | |
| 83 | + auto bars = load_bars(path); MarketCalendar cal; json out = json::object(); | |
| 84 | + models::GarchFitOptions go; const std::string mdl = arg(a, "--model", "garch"); go.model = mdl == "gjr" ? models::GarchModel::gjr : (mdl == "egarch" ? models::GarchModel::egarch : models::GarchModel::garch); | |
| 85 | + go.dist = arg(a, "--dist", "gaussian") == "student_t" ? models::GarchDist::student_t : models::GarchDist::gaussian; go.forecast_horizon = std::stoi(arg(a, "--horizon", "0")); go.plan = plan; | |
| 86 | + const bool intraday = arg(a, "--returns", "daily") == "intraday"; | |
| 87 | + for (auto& [sym, b0] : bars) { auto b = b0; b.normalize(); auto reg = b.filter_session(cal, SessionPolicy::regular); auto ir = data::intraday_returns(reg); | |
| 88 | + std::vector<f64> r; if (intraday) r = ir.r; else for (f64 v : ir.close_to_close) if (std::isfinite(v)) r.push_back(v); | |
| 89 | + try { auto e = models::garch_fit(r, go); out[sym] = e.to_json(); } catch (const RaidError& ex) { out[sym] = {{"error", ex.what()}}; } } | |
| 90 | + std::cout << out.dump(1) << "\n"; return 0; | |
| 91 | + } | |
| 92 | + if (what == "ols") { | |
| 93 | + std::ifstream f(path); RAID_REQUIRE(f.good(), io, "cannot open " + path); std::string line; std::getline(f, line); | |
| 94 | + std::vector<std::string> names; { usize s = 0; for (usize i = 0; i <= line.size(); ++i) if (i == line.size() || line[i] == ',') { names.push_back(line.substr(s, i - s)); s = i + 1; } } | |
| 95 | + std::vector<std::vector<f64>> rows; while (std::getline(f, line)) { if (line.empty()) continue; std::vector<f64> r; usize s = 0; for (usize i = 0; i <= line.size(); ++i) if (i == line.size() || line[i] == ',') { r.push_back(std::stod(line.substr(s, i - s))); s = i + 1; } rows.push_back(r); } | |
| 96 | + const usize n = rows.size(), k = names.size() - 1; Eigen::MatrixXd X(n, k + 1); Eigen::VectorXd y(n); | |
| 97 | + for (usize i = 0; i < n; ++i) { y[static_cast<Eigen::Index>(i)] = rows[i][0]; X(static_cast<Eigen::Index>(i), 0) = 1.0; for (usize j = 0; j < k; ++j) X(static_cast<Eigen::Index>(i), static_cast<Eigen::Index>(j + 1)) = rows[i][j + 1]; } | |
| 98 | + est::OlsOptions oo; oo.cov.type = stats::covtype_from_string(arg(a, "--cov", "classical")); oo.cov.hac_lags = std::stoi(arg(a, "--lags", "-1")); oo.names = {"const"}; for (usize j = 1; j < names.size(); ++j) oo.names.push_back(names[j]); | |
| 99 | + std::cout << est::ols(X, y, oo).estimate.to_json().dump(1) << "\n"; return 0; | |
| 100 | + } | |
| 101 | + usage(); return 2; | |
| 102 | +} | |
| 103 | +} // namespace | |
| 104 | + | |
| 105 | +int main(int argc, char** argv) { | |
| 106 | + std::vector<std::string> a(argv + 1, argv + argc); | |
| 107 | + if (a.empty()) { usage(); return 2; } | |
| 108 | + try { | |
| 109 | + const std::string cmd = a[0]; a.erase(a.begin()); | |
| 110 | + if (cmd == "devices") return cmd_devices(); | |
| 111 | + if (cmd == "validate") return cmd_validate(a); | |
| 112 | + if (cmd == "estimate") return cmd_estimate(a); | |
| 113 | +#ifdef RAID_WITH_FINDATA | |
| 114 | + if (cmd == "data") return raid::findata::cli_main(a); | |
| 115 | +#endif | |
| 116 | + if (cmd == "benchmark") { std::fprintf(stderr, "benchmark: use the bench_* executables (Phase 3+)\n"); return 2; } | |
| 117 | + usage(); return 2; | |
| 118 | + } catch (const RaidError& e) { std::fprintf(stderr, "raid error: %s\n", e.what()); return 1; } | |
| 119 | + catch (const std::exception& e) { std::fprintf(stderr, "error: %s\n", e.what()); return 1; } | |
| 120 | +} | |
added
cmake/EmbedMSL.cmake
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +# Concatenates all .metal sources into one C++ translation unit exposing raid::metal::msl_bundle(). | |
| 2 | +set(_out "namespace raid { namespace metal { const char* msl_bundle() { return R\"RAIDMSL(\n") | |
| 3 | +set(_sep "") | |
| 4 | +foreach(s ${SHADERS}) | |
| 5 | + file(READ ${s} _c) | |
| 6 | + string(APPEND _out "// ---- ${s}\n${_c}\n") | |
| 7 | +endforeach() | |
| 8 | +string(APPEND _out ")RAIDMSL\"; } } }\n") | |
| 9 | +file(WRITE ${OUT} "${_out}") | |
added
datasets/fixtures/garch_returns.csv
+3001 −0
Large diff (3,001 lines) is not loaded by default to keep the page fast.Load diff
added
datasets/fixtures/ols_design.csv
+501 −0
Large diff (501 lines) is not loaded by default to keep the page fast.Load diff
added
datasets/fixtures/reference_values.json
+205 −0
Large diff (206 lines) is not loaded by default to keep the page fast.Load diff
added
datasets/fixtures/synthetic_bars_1m.csv
+6818 −0
Large diff (6,818 lines) is not loaded by default to keep the page fast.Load diff
added
datasets/fixtures/synthetic_bars_1m.expected.json
+96 −0
Large diff (97 lines) is not loaded by default to keep the page fast.Load diff
added
docs/DECISIONS.md
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
docs/PROGRESS.md
+56 −0
Large diff (56 lines) is not loaded by default to keep the page fast.Load diff
added
docs/api/cpp.md
+28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added
docs/architecture/overview.md
+44 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added
docs/audit.md
+136 −0
Large diff (136 lines) is not loaded by default to keep the page fast.Load diff
added
docs/capabilities.md
+41 −0
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/findata_openapi_2026-11-02.json
+1 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M1M32.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M2M32.txt
+2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M2M32b.txt
+2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M2M32c.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M2U64.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M3BA16.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M3BA24.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M3U96a.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M3U96b.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M4BP36.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M4BP48.txt
+7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M4M36.txt
+2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M4M64a.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M4M64b.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M4M64c.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/M5M36.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m2m16.txt
+2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m2m8a.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m2m8b.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m4ma.txt
+2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m4mb.txt
+2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m4mc.txt
+2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m4md.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m4ml.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m4mm.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m4mn.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m5ma.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m6ma.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/evidence/nodes/m6mb.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
docs/literature/crossref_resolved.json
+396 −0
Large diff (397 lines) is not loaded by default to keep the page fast.Load diff
added
docs/methods/registry.md
+40 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff
added
docs/novelty_matrix.md
+57 −0
Large diff (57 lines) is not loaded by default to keep the page fast.Load diff
added
docs/risks.md
+22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added
examples/cpp/minimal.cpp
+23 −0
Large diff (23 lines) is not loaded by default to keep the page fast.Load diff
added
examples/python/minimal.py
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/manifests/environment_MacBookPro-de-simon-pierre-2.json
+33 −0
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/backend/backend.hpp
+71 −0
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/core/error.hpp
+33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/core/estimate.hpp
+62 −0
Large diff (62 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/core/hash.hpp
+29 −0
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/core/log.hpp
+28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/core/manifest.hpp
+33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/core/plan.hpp
+45 −0
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/core/rng.hpp
+71 −0
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/core/thread_pool.hpp
+40 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/core/types.hpp
+120 −0
Large diff (120 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/estimators/ols.hpp
+58 −0
Large diff (58 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/estimators/realized.hpp
+54 −0
Large diff (54 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/models/garch.hpp
+64 −0
Large diff (64 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/statistics/covariance.hpp
+32 −0
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/statistics/reductions.hpp
+84 −0
Large diff (84 lines) is not loaded by default to keep the page fast.Load diff
added
include/raid/statistics/rolling.hpp
+30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added
pyproject.toml
+33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added
python/raid/__init__.py
+23 −0
Large diff (23 lines) is not loaded by default to keep the page fast.Load diff
added
python/raid/api.py
+78 −0
Large diff (78 lines) is not loaded by default to keep the page fast.Load diff
added
python/tests/test_api.py
+60 −0
Large diff (60 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/check_no_secrets.sh
+9 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/make_fixtures.py
+90 −0
Large diff (90 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/record_environment.sh
+28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added
src/core/estimate.cpp
+40 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff
added
src/core/hash.cpp
+66 −0
Large diff (66 lines) is not loaded by default to keep the page fast.Load diff
added
src/core/log.cpp
+35 −0
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
added
src/core/manifest.cpp
+76 −0
Large diff (76 lines) is not loaded by default to keep the page fast.Load diff
added
src/core/thread_pool.cpp
+68 −0
Large diff (68 lines) is not loaded by default to keep the page fast.Load diff
added
src/core/types.cpp
+85 −0
Large diff (85 lines) is not loaded by default to keep the page fast.Load diff
added
src/estimators/ols.cpp
+180 −0
Large diff (180 lines) is not loaded by default to keep the page fast.Load diff
added
src/estimators/realized.cpp
+57 −0
Large diff (57 lines) is not loaded by default to keep the page fast.Load diff
added
src/models/garch_fit.cpp
+147 −0
Large diff (147 lines) is not loaded by default to keep the page fast.Load diff
added
src/models/garch_likelihood.cpp
+124 −0
Large diff (124 lines) is not loaded by default to keep the page fast.Load diff
added
src/statistics/covariance.cpp
+75 −0
Large diff (75 lines) is not loaded by default to keep the page fast.Load diff
added
src/statistics/reductions.cpp
+48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added
src/statistics/rolling.cpp
+111 −0
Large diff (111 lines) is not loaded by default to keep the page fast.Load diff
added
tests/reference/test_reference_values.cpp
+75 −0
Large diff (75 lines) is not loaded by default to keep the page fast.Load diff
added
tests/unit/test_calendar.cpp
+49 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
added
tests/unit/test_core.cpp
+48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added
tests/unit/test_ols.cpp
+44 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added
tests/unit/test_realized_garch.cpp
+54 −0
Large diff (54 lines) is not loaded by default to keep the page fast.Load diff
added
tests/unit/test_stats.cpp
+39 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff