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 | −# | 1 | +# Build | ||
| 2 | −Build | ||||
| 3 | build/ | 2 | build/ | ||
| 4 | build-*/ | 3 | build-*/ | ||
| 5 | cmake-build-*/ | 4 | cmake-build-*/ | ||
@@ -13,38 +12,27 @@ wheelhouse/ | |||||
| 13 | *.so | 12 | *.so | ||
| 14 | *.air | 13 | *.air | ||
| 15 | *.metallib | 14 | *.metallib | ||
| 16 | −# | 15 | +# Python | ||
| 17 | −Python | ||||
| 18 | __pycache__/ | 16 | __pycache__/ | ||
| 19 | *.pyc | 17 | *.pyc | ||
| 20 | .pytest_cache/ | 18 | .pytest_cache/ | ||
| 21 | .hypothesis/ | 19 | .hypothesis/ | ||
| 22 | .venv/ | 20 | .venv/ | ||
| 23 | .ruff_cache/ | 21 | .ruff_cache/ | ||
| 24 | −# | 22 | +# Data — FirstRate private license: never commit real bars | ||
| 25 | −Data | ||||
| 26 | −— | ||||
| 27 | −FirstRate | ||||
| 28 | −private | ||||
| 29 | −license: | ||||
| 30 | −never | ||||
| 31 | −commit | ||||
| 32 | −real | ||||
| 33 | −bars | ||||
| 34 | datasets/cache/ | 23 | datasets/cache/ | ||
| 35 | datasets/real/ | 24 | datasets/real/ | ||
| 36 | data/ | 25 | data/ | ||
| 26 | +experiments/results/raw/ | ||||
| 37 | *.parquet | 27 | *.parquet | ||
| 28 | +*.arrow | ||||
| 38 | !datasets/fixtures/*.parquet | 29 | !datasets/fixtures/*.parquet | ||
| 39 | !datasets/synthetic/*.parquet | 30 | !datasets/synthetic/*.parquet | ||
| 40 | −experiments/results/raw/*.arrow | 31 | +# Secrets | ||
| 41 | −# | ||||
| 42 | −Secrets | ||||
| 43 | .env | 32 | .env | ||
| 44 | *.key | 33 | *.key | ||
| 45 | api-key*.txt | 34 | api-key*.txt | ||
| 46 | −# | 35 | +# LaTeX | ||
| 47 | −LaTeX | ||||
| 48 | paper/*.aux | 36 | paper/*.aux | ||
| 49 | paper/*.log | 37 | paper/*.log | ||
| 50 | paper/*.out | 38 | paper/*.out | ||
@@ -58,8 +46,7 @@ paper/*.blg | |||||
| 58 | paper/*.synctex.gz | 46 | paper/*.synctex.gz | ||
| 59 | paper/*.run.xml | 47 | paper/*.run.xml | ||
| 60 | paper/*.bcf | 48 | paper/*.bcf | ||
| 61 | −# | 49 | +# macOS | ||
| 62 | −macOS | ||||
| 63 | .DS_Store | 50 | .DS_Store | ||
| 64 | Icon? | 51 | Icon? | ||
| 65 | logs/ | 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