Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
CMakeLists.txt -filter -diff -merge
cpp/CMakeLists.txt -filter -diff -merge
python_ml/requirements.txt -filter -diff -merge

# Demo receipts are small, reviewable provenance artifacts rather than datasets.
docs/demo/*.csv -filter -diff -merge text
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,23 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python -m pip install -r python_ml/requirements.txt
- run: make all test PYTHON=python
- run: make accounting-test PYTHON=python
- run: python scripts/backtester-e2e.py
- name: Install ffmpeg on Ubuntu
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y ffmpeg
- name: Install ffmpeg on macOS
if: runner.os == 'macOS'
run: brew install ffmpeg
- run: python scripts/verify-accounting-demo.py
- name: Verify tests leave checkout clean
run: |
git diff --exit-code
test -z "$(git status --porcelain)"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ build/
trading_system # Your specific executable name
.DS_Store # macOS specific file
.vscode/

# Python runtime caches are generated locally and are never source artifacts.
__pycache__/
*.py[cod]
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ add_executable(trading_system
# Add other source groups here if needed (e.g., ${INDICATOR_SOURCES})
)

add_executable(accounting_tests tests/accounting_test.cpp)
target_include_directories(accounting_tests PRIVATE src)
enable_testing()
add_test(NAME accounting_transitions COMMAND accounting_tests)

# --- Include Directories ---
# Tell the 'trading_system' target where to find header files during compilation.
target_include_directories(trading_system PRIVATE
Expand Down Expand Up @@ -123,4 +128,4 @@ endif()
# option(ENABLE_DEBUG_SYMBOLS_IN_RELEASE "Enable debug symbols in release builds" OFF)
# if(ENABLE_DEBUG_SYMBOLS_IN_RELEASE AND CMAKE_BUILD_TYPE STREQUAL "Release")
# target_compile_options(trading_system PRIVATE -g) # Add debug symbols flag (-g for GCC/Clang)
# endif()
# endif()
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
CXX ?= c++
CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra
PYTHON ?= python3
.PHONY: all test backtester
.PHONY: all test accounting-test backtester
all: build/ml_bridge_test
build:
mkdir -p build
build/ml_bridge_test: ml_bridge.cpp ml_bridge.h | build
$(CXX) $(CXXFLAGS) ml_bridge.cpp -o $@
test:
$(PYTHON) scripts/bridge-e2e.py
accounting-test:
$(PYTHON) scripts/accounting-e2e.py
backtester:
cmake -S . -B build/backtester
cmake --build build/backtester
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
## Reproducible native validation

![Signed-delta accounting replay](docs/demo/accounting-workflow.gif)

The demo above is generated from an actual native C++ replay: it opens and adds
to a long position, partially and fully closes it, opens a short, then crosses
through flat into a new long. The receipt, source manifest, MP4, GIF, and verifier
live in [`docs/demo`](docs/demo). Reproduce the accounting gates with
`make accounting-test`, the 68-configuration executable smoke with
`python3 scripts/backtester-e2e.py`, and the published-media proof with
`python3 scripts/verify-accounting-demo.py`.

This is deterministic simulated accounting over fixture fills. It does not place
live orders or establish profitability, execution quality, or latency.

`make all test` builds the portable C++ bridge and executes a real persisted
BayesianRidge model through the Python predictor. The gate trains a bounded
synthetic fixture, checks concurrent C++ requests against direct model predictions,
Expand Down
Binary file added docs/demo/accounting-poster.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/demo/accounting-receipt.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
label,direction,fill_quantity,fill_price,position,average_price,realized_pnl,cash,commission
open_long,BUY,10.000000,100.000000,10.000000,100.000000,0.000000,98999.000000,1.000000
increase_long,BUY,5.000000,120.000000,15.000000,106.666667,0.000000,98398.000000,2.000000
partial_close,SELL,4.000000,130.000000,11.000000,106.666667,93.333333,98917.000000,3.000000
full_close,SELL,11.000000,90.000000,0.000000,0.000000,-90.000000,99906.000000,4.000000
open_short,SELL,5.000000,100.000000,-5.000000,100.000000,-90.000000,100405.000000,5.000000
reverse_long,BUY,8.000000,90.000000,3.000000,90.000000,-40.000000,99684.000000,6.000000
Binary file added docs/demo/accounting-workflow.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/demo/accounting-workflow.mp4
Binary file not shown.
66 changes: 66 additions & 0 deletions docs/demo/capture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"source_commit": "d4b0acc6cde5475f532c76a7f310e9f2a357d7cb",
"source_tree": "f73563a87d30a16632156985ad8730073632c59f",
"runtime_inputs": [
{
"path": "examples/accounting_demo.cpp",
"git_blob": "b5ec93312652d071559ba7ede61d40372acd7c96",
"sha256": "1c7c4ff6e5140d515758ab31d154af1f7ab16047a309198fd4afcd3049bced32"
},
{
"path": "src/core/Portfolio.h",
"git_blob": "dd68a3b4e23920c0abe29f8040a5af349d720c4a",
"sha256": "6e8f0b75382190624b800232bb13ebe1c2d49bec5021fe835091201562adc466"
},
{
"path": "src/core/Event.h",
"git_blob": "f734bd7c9056a012effcac89c648f023d8e2a06f",
"sha256": "a624bcda8cf268cff969601eda85ca4feff879d291934c95f9302dd057430f14"
},
{
"path": "src/core/Utils.h",
"git_blob": "6c5605fc01a0274b248135ff804af66562381d65",
"sha256": "ca70ca0c0aac6d85526aeaef385703bd14c815bf3aa470ab061d57a6ba3b30fb"
},
{
"path": "src/data/PriceBar.h",
"git_blob": "8dc7c79449ca176fbad25bdd740db647c87977d3",
"sha256": "8d72e50638c950ca26080356a63907b647f396c3196b6620a88cbed260a7946a"
}
],
"scope": "deterministic simulated fills; no brokerage, exchange, live market, profit, or latency claim",
"recording": {
"frames": 72,
"fps": 6,
"duration_seconds": 12.0,
"width": 1280,
"height": 720
},
"observations": {
"transitions": 6,
"partial_close_position": 11.0,
"partial_close_basis": 106.666667,
"partial_close_realized_pnl": 93.333333,
"final_position": 3.0,
"final_average_price": 90.0,
"final_realized_pnl": -40.0
},
"artifacts": {
"accounting-receipt.csv": {
"bytes": 628,
"sha256": "379b52c7e5781a50a37585ea58448978eb24b0f70fe843ca110e2e852b0fe87d"
},
"accounting-poster.png": {
"bytes": 48383,
"sha256": "774e0022d66fed56fa73d1dc00b03b706e86d8a8511fa9c1aea97e437a2681cc"
},
"accounting-workflow.mp4": {
"bytes": 84103,
"sha256": "cda6a3044aec91f8c636fab57c579262b1e54a9a278d9dcf46b57fc68ed0a7f4"
},
"accounting-workflow.gif": {
"bytes": 121109,
"sha256": "06d606fa11fb74064dc40cf9d4bbe106fb0eda3053b85a80f018eae4a90f3995"
}
}
}
31 changes: 31 additions & 0 deletions docs/demo/source-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"source_commit": "d4b0acc6cde5475f532c76a7f310e9f2a357d7cb",
"source_tree": "f73563a87d30a16632156985ad8730073632c59f",
"runtime_inputs": [
{
"path": "examples/accounting_demo.cpp",
"git_blob": "b5ec93312652d071559ba7ede61d40372acd7c96",
"sha256": "1c7c4ff6e5140d515758ab31d154af1f7ab16047a309198fd4afcd3049bced32"
},
{
"path": "src/core/Portfolio.h",
"git_blob": "dd68a3b4e23920c0abe29f8040a5af349d720c4a",
"sha256": "6e8f0b75382190624b800232bb13ebe1c2d49bec5021fe835091201562adc466"
},
{
"path": "src/core/Event.h",
"git_blob": "f734bd7c9056a012effcac89c648f023d8e2a06f",
"sha256": "a624bcda8cf268cff969601eda85ca4feff879d291934c95f9302dd057430f14"
},
{
"path": "src/core/Utils.h",
"git_blob": "6c5605fc01a0274b248135ff804af66562381d65",
"sha256": "ca70ca0c0aac6d85526aeaef385703bd14c815bf3aa470ab061d57a6ba3b30fb"
},
{
"path": "src/data/PriceBar.h",
"git_blob": "8dc7c79449ca176fbad25bdd740db647c87977d3",
"sha256": "8d72e50638c950ca26080356a63907b647f396c3196b6620a88cbed260a7946a"
}
]
}
35 changes: 35 additions & 0 deletions examples/accounting_demo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include "core/Portfolio.h"

#include <iomanip>
#include <iostream>
#include <string>

namespace {

void apply_and_report(Portfolio& portfolio, const std::string& label,
OrderDirection direction, double quantity, double price,
double commission = 1.0) {
portfolio.handle_fill_event(FillEvent({}, "BTC", direction, quantity, price, commission));
const auto& position = portfolio.get_positions().at("BTC");
const auto results = portfolio.get_results_summary();
std::cout << "RECEIPT," << label << ','
<< (direction == OrderDirection::BUY ? "BUY" : "SELL") << ','
<< quantity << ',' << price << ',' << position.quantity << ','
<< position.average_price << ',' << results.realized_pnl << ','
<< portfolio.get_current_cash() << ',' << results.total_commission << '\n';
}

} // namespace

int main() {
std::cout << std::fixed << std::setprecision(6);
std::cout << "label,direction,fill_quantity,fill_price,position,average_price,realized_pnl,cash,commission\n";

Portfolio portfolio(100000.0);
apply_and_report(portfolio, "open_long", OrderDirection::BUY, 10.0, 100.0);
apply_and_report(portfolio, "increase_long", OrderDirection::BUY, 5.0, 120.0);
apply_and_report(portfolio, "partial_close", OrderDirection::SELL, 4.0, 130.0);
apply_and_report(portfolio, "full_close", OrderDirection::SELL, 11.0, 90.0);
apply_and_report(portfolio, "open_short", OrderDirection::SELL, 5.0, 100.0);
apply_and_report(portfolio, "reverse_long", OrderDirection::BUY, 8.0, 90.0);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed python_ml/__pycache__/ml_predictor.cpython-312.pyc
Binary file not shown.
Binary file removed python_ml/__pycache__/mock_model.cpython-311.pyc
Binary file not shown.
Binary file removed python_ml/__pycache__/mock_model.cpython-312.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions python_ml/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ numpy>=1.26,<3
pandas>=2.2,<3
scikit-learn>=1.4,<2
joblib>=1.3,<2
Pillow>=11,<13
49 changes: 49 additions & 0 deletions scripts/accounting-e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Compile and run direct accounting tests with strict warnings and UBSan."""

import os
from pathlib import Path
import subprocess
import tempfile

ROOT = Path(__file__).resolve().parent.parent


def compile_and_run(compiler: str, output: Path, extra_flags: list[str]) -> None:
subprocess.run(
[
compiler,
"-std=c++17",
"-O1",
"-g",
"-Wall",
"-Wextra",
"-Wpedantic",
"-Werror",
*extra_flags,
"-I",
str(ROOT / "src"),
str(ROOT / "tests/accounting_test.cpp"),
"-o",
str(output),
],
check=True,
)
subprocess.run([str(output)], check=True, timeout=30)


def main() -> None:
compiler = os.environ.get("CXX", "c++")
with tempfile.TemporaryDirectory(prefix="financial-accounting-e2e-") as directory:
work = Path(directory)
compile_and_run(compiler, work / "accounting-tests", [])
compile_and_run(
compiler,
work / "accounting-tests-ubsan",
["-fsanitize=undefined", "-fno-sanitize-recover=undefined"],
)
print("PASS: strict and UBSan accounting transition executables")


if __name__ == "__main__":
main()
30 changes: 25 additions & 5 deletions scripts/backtester-e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,31 @@


def main():
cmake = os.environ.get("CMAKE", "cmake")
compiler = os.environ.get("CXX", "c++")
with tempfile.TemporaryDirectory(prefix="financial-backtester-e2e-") as directory:
root = Path(directory)
subprocess.run([cmake, "-S", str(ROOT), "-B", str(root / "build")], check=True)
subprocess.run([cmake, "--build", str(root / "build"), "-j2"], check=True)
build = root / "build"
build.mkdir()
subprocess.run(
[
compiler,
"-std=c++17",
"-O2",
"-Wall",
"-Wextra",
"-Wpedantic",
"-Wno-unused-parameter",
"-I",
str(ROOT / "src"),
"-I",
str(ROOT / "external/csv2/include"),
str(ROOT / "src/main.cpp"),
str(ROOT / "src/data/DataManager.cpp"),
"-o",
str(build / "trading_system"),
],
check=True,
)
datasets = {
"stocks_april": ["quant_seconds_data_MSFT", "quant_seconds_data_NVDA", "quant_seconds_data_google"],
"2024_only": ["btc_2024_data", "eth_2024_data", "sol_2024_data", "ada_2024_data"],
Expand All @@ -38,9 +58,9 @@ def main():
assert "No strategy results" not in result.stdout
assert "ERROR:" not in result.stdout + result.stderr
completed = result.stdout.count("===== Finished Strategy:")
assert completed > 0
assert completed == 68, completed
assert "Comprehensive Run Invocation Complete" in result.stdout
print(f"PASS: real backtester completed {completed} strategy/dataset configurations on 11 persisted 64-row OHLCV files")
print(f"PASS: real backtester completed exactly {completed} strategy/dataset configurations on 11 persisted 64-row OHLCV files")


if __name__ == "__main__":
Expand Down
6 changes: 5 additions & 1 deletion scripts/bridge-e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ def main():
subprocess.run([os.environ.get("CXX", "c++"), "-std=c++17", "-O2", "-Wall", "-Wextra", str(ROOT / "tests/bridge_driver.cpp"), "-o", str(driver)], check=True)

def predict(close):
result = subprocess.run([str(driver), sys.executable, str(ROOT / "python_ml/ml_predictor.py"), str(asset.parent), str(close)], capture_output=True, text=True, timeout=60, check=True)
result = subprocess.run([str(driver), sys.executable, str(ROOT / "python_ml/ml_predictor.py"), str(asset.parent), str(close)], capture_output=True, text=True, timeout=60)
if result.returncode != 0:
raise RuntimeError(
f"bridge failed for close={close}: stdout={result.stdout!r}, stderr={result.stderr!r}"
)
line = next(line for line in result.stdout.splitlines() if line.startswith("BRIDGE_RESULT "))
observed, uncertainty = map(float, line.removeprefix("BRIDGE_RESULT ").split(","))
expected = model.predict([[close, close + 100, close - 100, close, 1000]])[0]
Expand Down
Loading
Loading