Skip to content
Merged
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
33 changes: 30 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
# ORPflow HFT Paper Trading - Multi-stage Dockerfile
# OCaml + Rust (Jane Street Style - No Python in Hot Path)
# Single unified Rust binary handles market data + strategy + API
# WITH ONNX Runtime for ML inference in hot path

# ============================================================================
# Stage 1: Rust Builder
# Stage 1: Rust Builder with ONNX Runtime
# ============================================================================
FROM rust:1.83-slim-bookworm AS rust-builder

# Install build dependencies + ONNX Runtime
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Download and install ONNX Runtime for building
ENV ORT_VERSION=1.19.2
RUN curl -L https://github.com/microsoft/onnxruntime/releases/download/v${ORT_VERSION}/onnxruntime-linux-x64-${ORT_VERSION}.tgz \
-o /tmp/onnxruntime.tgz \
&& tar -xzf /tmp/onnxruntime.tgz -C /opt \
&& rm /tmp/onnxruntime.tgz

ENV ORT_LIB_LOCATION=/opt/onnxruntime-linux-x64-${ORT_VERSION}
ENV LD_LIBRARY_PATH=/opt/onnxruntime-linux-x64-${ORT_VERSION}/lib:$LD_LIBRARY_PATH

WORKDIR /app/market-data

# Copy Cargo.toml only (Cargo.lock generated during build)
Expand All @@ -23,8 +37,11 @@ COPY market-data/src ./src
# Copy benchmarks (required by Cargo.toml)
COPY market-data/benches ./benches

# Build release binary (generates Cargo.lock automatically)
RUN cargo build --release
# Copy tests for ONNX parity
COPY market-data/tests ./tests

# Build release binary WITH ML feature (ONNX support)
RUN cargo build --release --features ml

# ============================================================================
# Stage 2: OCaml Builder
Expand Down Expand Up @@ -74,6 +91,10 @@ RUN apt-get update && apt-get install -y \
supervisor \
&& rm -rf /var/lib/apt/lists/*

# Copy ONNX Runtime libraries for inference
COPY --from=rust-builder /opt/onnxruntime-linux-x64-1.19.2/lib /opt/onnxruntime/lib
ENV LD_LIBRARY_PATH=/opt/onnxruntime/lib:$LD_LIBRARY_PATH

WORKDIR /app

# Copy Rust binary (unified: market-data + strategy + API)
Expand All @@ -82,6 +103,9 @@ COPY --from=rust-builder /app/market-data/target/release/orp-flow-market-data /a
# Copy OCaml binary (risk gateway)
COPY --from=ocaml-builder /home/opam/app/_build/default/bin/risk_gateway.exe /app/bin/risk_gateway

# Copy ONNX models for ML inference
COPY trained/onnx /app/models/onnx

# Copy supervisor configuration
COPY deploy/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

Expand All @@ -92,6 +116,9 @@ RUN chmod +x /app/entrypoint.sh
# Create data directory for SQLite
RUN mkdir -p /data

# Set ONNX model path environment variable
ENV ONNX_MODEL_DIR=/app/models/onnx

# Expose ports:
# 8000 - Main API (health, status, trades, positions)
# 9090 - Metrics/Health checks
Expand Down
11 changes: 7 additions & 4 deletions market-data/src/strategy/inference_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
//! - Thread-safe for async runtime integration
//! - Profile-friendly (array-based lookups, not HashMap)

// Allow dead_code - module is compiled with ml feature but not all functions are used yet
#![allow(dead_code)]

use std::collections::HashMap;
use std::path::Path;

Expand Down Expand Up @@ -596,16 +599,16 @@ impl InferencePipeline {
self.cached_weights[1] = weights.get(&ModelType::XGBoost)
.map(|w| w * (base_factor * 0.9 + 0.1))
.unwrap_or(0.0);
self.cached_weights[2] = weights.get(&ModelType::LSTM)
self.cached_weights[2] = weights.get(&ModelType::Lstm)
.map(|w| w * base_factor)
.unwrap_or(0.0);
self.cached_weights[3] = weights.get(&ModelType::CNN)
self.cached_weights[3] = weights.get(&ModelType::Cnn)
.map(|w| w * base_factor)
.unwrap_or(0.0);
self.cached_weights[4] = weights.get(&ModelType::D4PG)
self.cached_weights[4] = weights.get(&ModelType::D4pg)
.map(|w| w * (base_factor * 0.8 + 0.2 * regime_weight))
.unwrap_or(0.0);
self.cached_weights[5] = weights.get(&ModelType::MARL)
self.cached_weights[5] = weights.get(&ModelType::Marl)
.map(|w| w * (base_factor * 0.8 + 0.2 * regime_weight))
.unwrap_or(0.0);

Expand Down
40 changes: 22 additions & 18 deletions market-data/src/strategy/ml_inference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
//! - Feature augmentation with NSMI-derived features
//! - Zero-allocation hot path via pre-allocated buffers

// Allow dead_code - module has comprehensive API, not all functions used yet
#![allow(dead_code)]

use std::collections::HashMap;
use std::path::Path;

Expand All @@ -21,24 +24,25 @@ use super::nsmi::{NSMIConfig, NSMIFeatures, NSMIResult, NSMIState};

/// Model type enumeration
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[allow(clippy::upper_case_acronyms)]
pub enum ModelType {
LightGBM,
XGBoost,
LSTM,
CNN,
D4PG,
MARL,
Lstm,
Cnn,
D4pg,
Marl,
}

impl std::fmt::Display for ModelType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModelType::LightGBM => write!(f, "lightgbm"),
ModelType::XGBoost => write!(f, "xgboost"),
ModelType::LSTM => write!(f, "lstm"),
ModelType::CNN => write!(f, "cnn"),
ModelType::D4PG => write!(f, "d4pg"),
ModelType::MARL => write!(f, "marl"),
ModelType::Lstm => write!(f, "lstm"),
ModelType::Cnn => write!(f, "cnn"),
ModelType::D4pg => write!(f, "d4pg"),
ModelType::Marl => write!(f, "marl"),
}
}
}
Expand Down Expand Up @@ -250,10 +254,10 @@ impl ModelEnsemble {
let model_type = match name_str {
"lightgbm" | "lightgbm_model" => Some(ModelType::LightGBM),
"xgboost" | "xgboost_model" => Some(ModelType::XGBoost),
"lstm" | "lstm_model" => Some(ModelType::LSTM),
"cnn" | "cnn_model" => Some(ModelType::CNN),
"d4pg" | "d4pg_actor" => Some(ModelType::D4PG),
n if n.starts_with("marl") => Some(ModelType::MARL),
"lstm" | "lstm_model" => Some(ModelType::Lstm),
"cnn" | "cnn_model" => Some(ModelType::Cnn),
"d4pg" | "d4pg_actor" => Some(ModelType::D4pg),
n if n.starts_with("marl") => Some(ModelType::Marl),
_ => None,
};

Expand Down Expand Up @@ -391,11 +395,11 @@ impl ModelEnsemble {
base_weight * (base_factor * 0.9 + 0.1)
}
// DL models may overfit to recent regime
ModelType::LSTM | ModelType::CNN => {
ModelType::Lstm | ModelType::Cnn => {
base_weight * base_factor
}
// RL models need stable regimes
ModelType::D4PG | ModelType::MARL => {
ModelType::D4pg | ModelType::Marl => {
base_weight * (base_factor * 0.8 + 0.2 * regime_factor)
}
};
Expand Down Expand Up @@ -456,7 +460,7 @@ impl ModelEnsemble {
let mut weighted_sum = 0.0f32;
let mut total_weight = 0.0f32;

for model_type in [ModelType::LSTM, ModelType::CNN] {
for model_type in [ModelType::Lstm, ModelType::Cnn] {
if let Some(&weight) = weights.get(&model_type) {
if let Some(model) = models.get_mut(&model_type) {
match model.predict_sequence(sequence) {
Expand Down Expand Up @@ -484,7 +488,7 @@ impl ModelEnsemble {
pub fn get_rl_action(&self, state: &[f32]) -> Result<Vec<f32>> {
let mut models = self.models.write();

if let Some(model) = models.get_mut(&ModelType::D4PG) {
if let Some(model) = models.get_mut(&ModelType::D4pg) {
model.predict_action(state)
} else {
Err(anyhow::anyhow!("D4PG model not available"))
Expand Down Expand Up @@ -569,7 +573,7 @@ impl ModelEnsemble {
let mut weighted_sum = 0.0f32;
let mut total_weight = 0.0f32;

for model_type in [ModelType::LSTM, ModelType::CNN] {
for model_type in [ModelType::Lstm, ModelType::Cnn] {
if let Some(&weight) = adjusted.adjusted_weights.get(&model_type) {
if let Some(model) = models.get_mut(&model_type) {
match model.predict_sequence(&effective_sequence) {
Expand Down Expand Up @@ -751,7 +755,7 @@ mod tests {
#[test]
fn test_model_type_display() {
assert_eq!(ModelType::LightGBM.to_string(), "lightgbm");
assert_eq!(ModelType::D4PG.to_string(), "d4pg");
assert_eq!(ModelType::D4pg.to_string(), "d4pg");
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions market-data/src/strategy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,20 @@ pub use broker::PaperBroker;
pub use config::StrategyConfig;
pub use features::MicrostructureFeatures;
pub use models::{Account, Position, Trade};
// NSMI exports - used by ml_inference when ml feature is enabled
#[allow(unused_imports)]
pub use nsmi::{NSMIConfig, NSMIFeatures, NSMIResult, NSMIState};
pub use signals::ImbalanceStrategy;
pub use storage::TradeStorage;

#[cfg(feature = "ml")]
#[allow(unused_imports)]
pub use ml_inference::{
FeatureBuffer, ModelEnsemble, ModelType, NSMIAdjustedWeights, NSMIAugmentBuffer, OnnxModel,
};

#[cfg(feature = "ml")]
#[allow(unused_imports)]
pub use inference_pipeline::{InferenceConfig, InferencePipeline, InferenceResult, TradingSignal};

use std::sync::Arc;
Expand Down
13 changes: 8 additions & 5 deletions market-data/src/strategy/nsmi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
//! - Zero allocations in hot path (pre-allocated buffers)
//! - Thread-safe for async runtimes

// Allow dead_code when compiled without ml feature (NSMI is used by ml_inference)
#![allow(dead_code)]

use std::sync::atomic::{AtomicU64, Ordering};

/// Configuration for NSMI state tracking
Expand Down Expand Up @@ -52,7 +55,7 @@ impl NSMIConfig {
Self {
dimension,
half_life,
tracked_eigenvalues: (dimension / 3).max(2).min(5),
tracked_eigenvalues: (dimension / 3).clamp(2, 5),
..Default::default()
}
}
Expand Down Expand Up @@ -280,13 +283,13 @@ impl NSMIState {
}

// Update running mean: mean = (1-alpha) * mean + alpha * x
for i in 0..dim {
self.mean[i] = (1.0 - alpha) * self.mean[i] + alpha * observation[i];
for (mean_i, &obs_i) in self.mean.iter_mut().zip(observation.iter()) {
*mean_i = (1.0 - alpha) * *mean_i + alpha * obs_i;
}

// Compute centered observation: x_centered = x - mean
for i in 0..dim {
self.buffers.centered[i] = observation[i] - self.mean[i];
for ((centered_i, &obs_i), &mean_i) in self.buffers.centered.iter_mut().zip(observation.iter()).zip(self.mean.iter()) {
*centered_i = obs_i - mean_i;
}

// Update covariance matrix using rank-1 update:
Expand Down
36 changes: 27 additions & 9 deletions models/export/onnx_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,27 @@ def export_lightgbm(
model_name: str = "lightgbm_model",
) -> str:
"""Export LightGBM model to ONNX"""
import lightgbm as lgb

output_path = self.output_dir / f"{model_name}.onnx"

# Handle both wrapped model (has .model) and direct Booster
if hasattr(model, 'model'):
booster = model.model
elif isinstance(model, lgb.Booster):
booster = model
else:
raise ValueError(f"Unknown model type: {type(model)}")

try:
from onnxmltools import convert_lightgbm
from onnxmltools.convert.common.data_types import FloatTensorType

initial_types = [("input", FloatTensorType([None, len(feature_names)]))]
onnx_model = convert_lightgbm(
model.model,
booster,
initial_types=initial_types,
target_opset=17,
target_opset=15,
)

onnx.save_model(onnx_model, str(output_path))
Expand All @@ -58,7 +67,7 @@ def export_lightgbm(
except ImportError:
logger.warning("onnxmltools not installed, saving native format")
native_path = self.output_dir / f"{model_name}.lgb"
model.model.save_model(str(native_path))
booster.save_model(str(native_path))
return str(native_path)

def export_xgboost(
Expand All @@ -68,18 +77,27 @@ def export_xgboost(
model_name: str = "xgboost_model",
) -> str:
"""Export XGBoost model to ONNX"""
import xgboost as xgb

output_path = self.output_dir / f"{model_name}.onnx"

# Handle both wrapped model (has .model) and direct Booster
if hasattr(model, 'model'):
booster = model.model
elif isinstance(model, xgb.Booster):
booster = model
else:
raise ValueError(f"Unknown model type: {type(model)}")

try:
from onnxmltools import convert_xgboost
from onnxmltools.convert.common.data_types import FloatTensorType

initial_types = [("input", FloatTensorType([None, len(feature_names)]))]
onnx_model = convert_xgboost(
model.model,
booster,
initial_types=initial_types,
target_opset=17,
target_opset=15,
)

onnx.save_model(onnx_model, str(output_path))
Expand All @@ -93,7 +111,7 @@ def export_xgboost(
except ImportError:
logger.warning("onnxmltools not installed, saving native format")
native_path = self.output_dir / f"{model_name}.xgb"
model.model.save_model(str(native_path))
booster.save_model(str(native_path))
return str(native_path)

def export_pytorch(
Expand Down Expand Up @@ -123,7 +141,7 @@ def export_pytorch(
dummy_input,
str(output_path),
export_params=True,
opset_version=17,
opset_version=15,
do_constant_folding=True,
input_names=input_names,
output_names=output_names,
Expand Down Expand Up @@ -195,7 +213,7 @@ def export_d4pg_actor(
dummy_input,
str(output_path),
export_params=True,
opset_version=17,
opset_version=15,
do_constant_folding=True,
input_names=["state"],
output_names=["action"],
Expand Down Expand Up @@ -256,7 +274,7 @@ def forward(self, state, messages):
(dummy_state, dummy_messages),
str(output_path),
export_params=True,
opset_version=17,
opset_version=15,
do_constant_folding=True,
input_names=["state", "messages"],
output_names=["action"],
Expand Down
7 changes: 6 additions & 1 deletion render.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Render Blueprint - ORPflow HFT Paper Trading
# OCaml + Rust + Python Flow
# Rust + OCaml with ONNX ML Inference (Jane Street Style)
# https://render.com/docs/blueprint-spec

services:
Expand Down Expand Up @@ -30,6 +30,11 @@ services:
value: "0.05"
- key: PAPER_TRADING
value: "true"
# ONNX ML Inference Configuration
- key: ONNX_MODEL_DIR
value: /app/models/onnx
- key: ML_ENABLED
value: "true"
# Telegram notifications (optional - set in dashboard)
- key: TELEGRAM_BOT_TOKEN
sync: false # Must be set manually in dashboard
Expand Down
Loading
Loading