Skip to content

Commit e9364ac

Browse files
committed
Refactor: Split Architecture into Common, Backtester, and Execution Node
This MAJOR architectural refactor decouples the Backtesting Engine from the live Execution Node to support distinct lifecycles and 'Unix Philosophy' workflow. 1. QuanuX-Common Library: - Created 'QuanuX-Common/' to house shared ABI and data structures. - Moved 'StrategyInterface.h' and 'OrderBookL3.h' to 'quanux/common/'. - Namespaced all shared code under 'quanux::common' for safety. 2. QuanuX Backtesting Engine: - Created standalone 'QuanuX-Backtesting-Engine/' (C++20). - Implemented 'BacktestRunner' with Dual-Mode: Fast-Forward and Real-Time Replay (NATS). - Added 'DbnPipeFeeder' for consuming raw Databento Binary Encoding (DBN) from stdin. - Implemented 'PerformanceAnalyzer' with Welford's Algorithm (Single-Pass): - Sharpe, Sortino, Calmar, Omega Ratios. - Max Drawdown, Ulcer Index. - SQN, Expectancy. 3. Python Bindings & Integration: - Implemented 'quanux_metrics' C++ extension using 'pybind11'. - Created 'quanux_backtest' Python package wrapping the C++ engine. - Added Pandas integration for high-performance analysis. 4. Execution Node Updates: - Updated CMakeLists.txt to link 'QuanuX-Common'. - Refactored includes to use <quanux/common/StrategyInterface.h>. - Fixed namespace issues in engine core. 5. Documentation: - Added SKILL.md for new components. - Updated root README.md.
1 parent b10246c commit e9364ac

42 files changed

Lines changed: 1463 additions & 71 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

QuanuX-Backtesting-Engine/SKILL.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
description: Definitive guide to the QuanuX High-Performance Backtesting Engine (C++20/Python).
3+
---
4+
5+
# QuanuX Backtesting Engine
6+
7+
## Overview
8+
The **QuanuX Backtesting Engine** is a high-performance, C++20 simulation framework designed for:
9+
1. **Speed**: Millions of events per second (fast-forward mode).
10+
2. **Accuracy**: Event-driven architecture with precise market replay.
11+
3. **Flexibility**: Supports "Unix Philosophy" via pipe streaming and NATS-based real-time replay.
12+
4. **Hybrid Workflow**: Core logic in C++, analysis/scripting in Python.
13+
14+
## Architecture
15+
16+
### 1. Dual-Mode Simulation
17+
* **Fast Forward**: Runs as fast as CPU allows. Best for parameter optimization and statistical validation.
18+
* **Real-Time Replay (NATS)**: Replays historical data at wall-clock speed (or accelerated factor) and publishes to NATS.
19+
* **Purpose**: Allows you to connect a frontend or live trading bot to the "past" as if it were happening now.
20+
* **Flag**: `--nats-replay`
21+
22+
### 2. Data Feeders
23+
* **DuckDB Feeder**: Queries `*.parquet` or `*.csv` files efficiently using DuckDB.
24+
* **DBN Pipe Feeder**: Reads raw **Databento Binary Encoding (DBN)** from `stdin`.
25+
* **Usage**: `dbn-cli get range ... | ./quanux_backtest --stdin`
26+
* **Benefit**: No intermediate disk storage; streams terabytes of data directly from Databento servers to the engine.
27+
28+
### 3. Common Library
29+
* **Location**: `QuanuX-Common/`
30+
* **Shared Components**: `OrderBookL3`, `StrategyInterface`.
31+
* **Consistency**: Ensures the `OrderBook` logic used in backtesting is *identical* to the live `execution-node`.
32+
33+
## Metrics Engine (`PerformanceAnalyzer`)
34+
The engine features a single-pass, O(n) metrics calculator using Welford's Algorithm regarding variance/standard deviation, ensuring numerical stability.
35+
36+
### Available Metrics
37+
#### Profitability
38+
* **Net Profit**: Total PnL.
39+
* **Profit Factor**: Gross Profit / Gross Loss.
40+
* **Expectancy**: Average dollar amount per trade.
41+
* **CAGR**: Compound Annual Growth Rate.
42+
43+
#### Risk & Drawdown
44+
* **Max Drawdown (MDD)**: Largest peak-to-valley decline (%).
45+
* **Ulcer Index**: Measure of downside volatility (root mean square of drawdowns).
46+
* **Serenity Index**: Risk-adjusted return metric (TBD).
47+
48+
#### Ratios
49+
* **Sharpe Ratio**: Excess return per unit of total volatility.
50+
* **Sortino Ratio**: Excess return per unit of *downside* volatility.
51+
* **Omega Ratio**: Probability weighted ratio of gains vs losses.
52+
* **Calmar Ratio**: CAGR / Max Drawdown.
53+
* **SQN (System Quality Number)**: Van Tharp's metric for system expectancy stability.
54+
55+
### Python Integration (`quanux_backtest`)
56+
We provide a high-level Python package that wraps the C++ engine and integrates with Pandas.
57+
58+
* **Package**: `quanux_backtest` (located in `python/`)
59+
* **Usage**:
60+
```python
61+
import pandas as pd
62+
from quanux_backtest import BacktestAnalyzer
63+
64+
# 1. Load Data
65+
equity_curve = pd.Series([...])
66+
67+
# 2. Analyze (C++ Backend)
68+
analyzer = BacktestAnalyzer(start_equity=10000.0)
69+
metrics = analyzer.process_equity(equity_curve)
70+
71+
print(f"Sharpe: {metrics['sharpe_ratio']}")
72+
print(f"Ulcer Index: {metrics['ulcer_index']}")
73+
```
74+
75+
## Extending the Engine
76+
77+
### How to Add a New Metric
78+
1. **Update Struct**: Add the field to `struct Metrics` in `generated/include/engine/metrics/PerformanceAnalyzer.h`.
79+
2. **Update Logic**: Add the calculation code in `PerformanceAnalyzer::calculateMetrics()`.
80+
* *Tip*: If it requires time-series variance, use the `WelfordAccumulator`.
81+
3. **Update Python Bindings**: Add `.def_readonly(...)` to `src/bindings/bindings.cpp` so Python can see it.
82+
4. **Recompile**: Run `./build.sh` or `cmake --build build --target quanux_metrics`.
83+
84+
## Agent Instructions
85+
* **To Run Backtest**:
86+
* Look for `quanux_backtest` executable in `QuanuX-Backtesting-Engine/cpp/build/`.
87+
* Command: `./quanux_backtest --symbols "ESZ4" --strategy "PingPong"`
88+
* **To Use Pipe Feeder**:
89+
* Ensure `dbn-cli` is installed.
90+
* Command: `cat my_data.dbn | ./quanux_backtest --stdin`
91+
* **To Verify Bindings**:
92+
* Run `python verify_bindings.py` in the root of the backtester directory.
93+
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
cmake_minimum_required(VERSION 3.20)
2+
project(quanux_backtest_engine CXX)
3+
4+
set(CMAKE_CXX_STANDARD 20)
5+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
6+
7+
# Includes
8+
include_directories(include)
9+
include_directories(../../QuanuX-Common/cpp/include)
10+
11+
# Dependencies
12+
include(FetchContent)
13+
14+
# DuckDB
15+
FetchContent_Declare(
16+
duckdb
17+
GIT_REPOSITORY https://github.com/duckdb/duckdb.git
18+
GIT_TAG v1.1.3
19+
GIT_SHALLOW TRUE
20+
)
21+
set(BUILD_UNITTESTS OFF CACHE BOOL "" FORCE)
22+
set(BUILD_SHELL OFF CACHE BOOL "" FORCE)
23+
set(BUILD_EXTENSIONS "parquet;json" CACHE STRING "" FORCE)
24+
FetchContent_MakeAvailable(duckdb)
25+
26+
# NATS (For Market Replay)
27+
set(NATS_BUILD_STREAMING OFF CACHE BOOL "" FORCE)
28+
FetchContent_Declare(
29+
cnats
30+
GIT_REPOSITORY https://github.com/nats-io/nats.c.git
31+
GIT_TAG v3.9.0
32+
)
33+
FetchContent_MakeAvailable(cnats)
34+
35+
# JSON (Dependency of Databento, explicit override for CMake 3.20+ compact)
36+
FetchContent_Declare(
37+
json
38+
GIT_REPOSITORY https://github.com/nlohmann/json.git
39+
GIT_TAG v3.11.3
40+
)
41+
FetchContent_MakeAvailable(json)
42+
43+
# Databento CPP
44+
FetchContent_Declare(
45+
databento
46+
GIT_REPOSITORY https://github.com/databento/databento-cpp.git
47+
GIT_TAG v0.9.1
48+
)
49+
FetchContent_MakeAvailable(databento)
50+
51+
# Core Backtest Engine Library
52+
add_library(backtest_engine STATIC
53+
src/engine/BacktestRunner.cpp
54+
src/engine/NatsReplayer.cpp
55+
src/engine/DbnPipeFeeder.cpp
56+
)
57+
target_link_libraries(backtest_engine PUBLIC duckdb_static nats_static databento::databento)
58+
59+
# Main Executable
60+
add_executable(quanux_backtest src/main.cpp)
61+
target_link_libraries(quanux_backtest PRIVATE backtest_engine duckdb_static nats_static pthread dl)
62+
63+
64+
# Metrics Test
65+
add_executable(test_metrics tests/test_metrics.cpp)
66+
target_link_libraries(test_metrics PRIVATE backtest_engine)
67+
68+
# Pybind11 (Python Bindings)
69+
FetchContent_Declare(
70+
pybind11
71+
GIT_REPOSITORY https://github.com/pybind/pybind11
72+
GIT_TAG v2.12.0
73+
)
74+
FetchContent_MakeAvailable(pybind11)
75+
76+
pybind11_add_module(quanux_metrics src/bindings/bindings.cpp)
77+
# PerformanceAnalyzer is header-only, so no need to link backtest_engine lib if not using other parts.
78+
# But we include the directories implicitly via target linkage or include_directories above.
79+
target_link_libraries(quanux_metrics PRIVATE pybind11::module)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#pragma once
2+
#include "engine/DuckDBFeeder.h"
3+
#include "engine/SimulatedExchange.h"
4+
5+
namespace quanux::engine {
6+
7+
class BacktestRunner {
8+
public:
9+
void run();
10+
};
11+
12+
} // namespace quanux::engine
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#pragma once
2+
#include "engine/SimulatedExchange.h"
3+
#include "quanux/common/StrategyInterface.h"
4+
#include <memory>
5+
#include <string>
6+
7+
// Forward declare databento type to avoid leaking heavy include
8+
namespace databento {
9+
class DbnDecoder;
10+
}
11+
12+
namespace quanux::engine {
13+
14+
class DbnPipeFeeder {
15+
SimulatedExchange *exchange_;
16+
std::unique_ptr<databento::DbnDecoder> decoder_;
17+
// Buffer for reading from stdin
18+
std::vector<uint8_t> read_buffer_;
19+
20+
public:
21+
explicit DbnPipeFeeder(SimulatedExchange *exchange);
22+
~DbnPipeFeeder();
23+
24+
// Run the feeder loop (blocking)
25+
void run();
26+
};
27+
28+
} // namespace quanux::engine

execution-node/cpp/include/simulator/DuckDBFeeder.h renamed to QuanuX-Backtesting-Engine/cpp/include/engine/DuckDBFeeder.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
#pragma once
22

33
#include "duckdb.hpp"
4-
#include "simulator/MatchingModel.h"
4+
#include <functional>
55
#include <iostream>
66
#include <vector>
77

8-
namespace quanux::simulator {
8+
namespace quanux::engine {
99

1010
class DuckDBFeeder {
1111
public:
@@ -49,4 +49,4 @@ class DuckDBFeeder {
4949
<< std::endl;
5050
}
5151
};
52-
} // namespace quanux::simulator
52+
} // namespace quanux::engine

execution-node/cpp/include/simulator/FifoMatcher.h renamed to QuanuX-Backtesting-Engine/cpp/include/engine/FifoMatcher.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#pragma once
22

3-
#include "simulator/MatchingModel.h"
3+
#include "engine/MatchingModel.h"
44
#include <vector>
55

6-
namespace quanux::simulator {
6+
namespace quanux::engine {
7+
8+
using namespace quanux::common;
79

810
class FifoMatcher : public MatchingModel {
911
public:
@@ -21,4 +23,4 @@ class FifoMatcher : public MatchingModel {
2123
}
2224
};
2325

24-
} // namespace quanux::simulator
26+
} // namespace quanux::engine

execution-node/cpp/include/simulator/MatchingModel.h renamed to QuanuX-Backtesting-Engine/cpp/include/engine/MatchingModel.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#pragma once
22

3-
#include "simulator/OrderBookL3.h"
3+
#include "quanux/common/OrderBookL3.h"
44
#include <vector>
55

6-
namespace quanux::simulator {
6+
namespace quanux::engine {
7+
8+
using namespace quanux::common;
79

810
struct MatchResult {
911
bool filled;
@@ -28,4 +30,4 @@ class MatchingModel {
2830
uint64_t current_time) = 0;
2931
};
3032

31-
} // namespace quanux::simulator
33+
} // namespace quanux::engine
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#pragma once
2+
#include <iostream>
3+
#include <nats.h>
4+
#include <string>
5+
6+
namespace quanux::engine {
7+
8+
class NatsReplayer {
9+
natsConnection *conn = nullptr;
10+
natsOptions *opts = nullptr;
11+
bool enabled_ = false;
12+
13+
public:
14+
NatsReplayer(const std::string &url = "nats://localhost:4222");
15+
~NatsReplayer();
16+
17+
void publish_tick(const std::string &symbol, uint64_t ts, double price,
18+
uint32_t size, bool is_bid);
19+
};
20+
21+
} // namespace quanux::engine
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#pragma once
2+
3+
#include "engine/FifoMatcher.h"
4+
#include "engine/MatchingModel.h"
5+
#include "quanux/common/OrderBookL3.h"
6+
#include <memory>
7+
#include <queue>
8+
#include <string>
9+
#include <vector>
10+
11+
namespace quanux::engine {
12+
using namespace quanux::common;
13+
14+
struct LatencyConfig {
15+
uint64_t wire_delay_ns = 5000; // 5us default
16+
uint64_t matching_engine_ns = 2000; // 2us
17+
};
18+
19+
class SimulatedExchange {
20+
public:
21+
OrderBookL3 book_;
22+
LatencyConfig config_;
23+
std::unique_ptr<MatchingModel> matcher_;
24+
uint64_t current_time_ns_ = 0;
25+
26+
SimulatedExchange() {
27+
// Default to FIFO
28+
matcher_ = std::make_unique<FifoMatcher>();
29+
}
30+
31+
struct PendingOrder {
32+
uint64_t effective_time;
33+
// Order details would go here, simplified:
34+
uint64_t id;
35+
int64_t price;
36+
uint32_t size;
37+
bool is_bid;
38+
39+
bool operator>(const PendingOrder &other) const {
40+
return effective_time > other.effective_time;
41+
}
42+
};
43+
44+
std::priority_queue<PendingOrder, std::vector<PendingOrder>,
45+
std::greater<PendingOrder>>
46+
pending_orders_;
47+
std::vector<std::string> fills_; // Hacky fill log for now
48+
49+
void process_pending_orders(uint64_t until_time_ns) {
50+
while (!pending_orders_.empty() &&
51+
pending_orders_.top().effective_time <= until_time_ns) {
52+
auto po = pending_orders_.top();
53+
pending_orders_.pop();
54+
55+
// "Router" to "Exchange" latency done. Now inject into simulated matcher.
56+
// In real MBO, we would just "Place" it in the book at the end of the
57+
// queue. For our simulated strategy, we track it separately or just
58+
// pretend it's a market order if aggressive. Simplified: Add to book as a
59+
// limit order.
60+
Side side = po.is_bid ? Side::Bid : Side::Ask;
61+
book_.add(po.id, po.price, po.size, side, po.effective_time);
62+
63+
// Try match immediately upon arrival
64+
auto matches = matcher_->check_matches(book_, po.effective_time);
65+
for (auto &m : matches) {
66+
fills_.push_back("Fill: " + std::to_string(m.fill_qty) + " @ " +
67+
std::to_string(m.fill_price));
68+
}
69+
}
70+
current_time_ns_ = until_time_ns;
71+
}
72+
73+
void on_market_data(uint64_t id, int64_t price, uint32_t size, bool is_bid,
74+
bool is_add) {
75+
// Advance time to this event? Or does the runner do that?
76+
// Let's assume the runner calls process_pending_orders BEFORE calling
77+
// on_market_data with the event time.
78+
79+
if (is_add) {
80+
book_.add(id, price, size, is_bid ? Side::Bid : Side::Ask,
81+
current_time_ns_);
82+
} else {
83+
book_.remove(id);
84+
}
85+
auto matches = matcher_->check_matches(book_, current_time_ns_);
86+
for (auto &m : matches) {
87+
// Our order might have been matched by this trade!
88+
fills_.push_back("Fill (Passive): " + std::to_string(m.fill_qty) + " @ " +
89+
std::to_string(m.fill_price));
90+
}
91+
}
92+
93+
void send_order(uint64_t id, int64_t price, uint32_t size, bool is_bid) {
94+
uint64_t arrival_time = current_time_ns_ + config_.wire_delay_ns;
95+
pending_orders_.push({arrival_time, id, price, size, is_bid});
96+
}
97+
};
98+
99+
} // namespace quanux::engine

0 commit comments

Comments
 (0)