From alpha to production: dataset, model, inference node¶
A model trained on flox features runs in production against the same features, computed by the same code. This page walks the whole loop: tape to dataset, dataset to model, model to a scoring node in C++.
The C++ inference part needs a build with -DFLOX_ENABLE_ONNX=ON
(ONNX Runtime).
1. Export a dataset¶
import flox_py
from flox_py.dataset import build_dataset, to_arrow
ds = build_dataset(
data_dir="/data/tapes/bybit", # binary-log tape directory
interval_seconds=60,
features=["sma_20", "rsi_14", "parkinson_vol_30"],
label_horizon_bars=5, # y = log(close[t+5] / close[t])
)
X, y = ds["X"], ds["y"]
Row t is computed strictly from bars 0..t. This is tested
(python/tests/test_dataset_export.py), so a model trained here cannot
accidentally learn from the future. to_arrow(ds) gives a pyarrow table
if the training stack wants parquet files.
2. Train and export ONNX¶
Any framework that exports ONNX works; that is the reason the runtime is ONNX rather than a binding to one training library. sklearn example:
from sklearn.linear_model import Ridge
from skl2onnx import to_onnx
model = Ridge().fit(X, y)
onnx_model = to_onnx(model, X[:1].astype("float32"))
with open("alpha.onnx", "wb") as f:
f.write(onnx_model.SerializeToString())
3. Score in C++¶
Three execution modes share one loaded model class. Pick by how tightly the score is coupled to the hot path:
| Mode | Coupling | Use when |
|---|---|---|
| Graph node | none (batch) | backtest, replay, research |
BudgetedInference |
in-path, budgeted | the score gates the order |
OnnxSidecar |
decoupled | the score enriches state, staleness is acceptable |
As a graph node, for backtest and replay:
#include "flox/ml/onnx_inference.h"
auto model = std::make_shared<flox::ml::OnnxModel>("alpha.onnx");
flox::ml::registerOnnxNode(graph, "alpha", {"sma_20", "rsi_14", "pvol_30"}, model);
const auto& alphaColumn = graph.require(symbol, "alpha");
Backtest and live execute the same node, so their scores agree by construction rather than by a separate verification step.
In-path with a budget:
flox::ml::OnnxModel hotModel("alpha.onnx"); // intra-op = 1, no thread pool
flox::ml::BudgetedInference inference(hotModel, /*budgetNs=*/20'000);
const auto r = inference.run(featureRow);
r.fresh is false when the DROP policy served the previous score after a
budget overrun. inference.histogram() plugs into the latency contour.
As a sidecar, for heavier models:
flox::ml::OnnxSidecar sidecar(std::make_unique<flox::ml::OnnxModel>("alpha.onnx"));
sidecar.submit(featureRow, nowNs); // returns immediately
const auto p = sidecar.latest(); // values + featureTsNs + seq
nowNs - p.featureTsNs is the staleness of the score; the strategy
decides how much it tolerates.
What still needs your attention¶
Feature order. Train and serve must agree on the order of inputs, and the
node takes its dependency list explicitly for that reason. Everything else
on the parity list is covered by tests: feature values
(python/tests/test_dataset_export.py) and node scoring
(tests/test_onnx_inference.cpp).