Methodology
How the data is produced
Every step from raw venue data to a published signal — the sources, the indicator parameters, how labels are assigned, how models are validated, and where the pipeline is currently weak. Written from the implementation, not from an idealised description of it.
1. Sources
Two ingestion jobs write into one feature store. They are separate because their rate limits and cadences have nothing in common.
| Source | Covers | Timeframes | Cadence |
|---|---|---|---|
| Alpha Vantage | EUR/USD, GBP/USD, USD/JPY | 1d | Daily, 22:15 UTC on trading days |
| Kraken | BTC/USD, ETH/USD | 5m, 1h | Every 5 minutes |
Crypto comes from Kraken, not Binance: Binance returns HTTP 451 from the hosting region, so those rows would never exist. Both venues are registered in the execution topology but disabled — see the infrastructure page.
2. Bar construction
Only closed bars are stored. The most recent candle a venue returns is dropped before any computation, because an incomplete bar trained on as though it were final leaks information that did not exist at decision time.
Forex bars carry volume: null rather than a fabricated number — forex has no single centralised tape, so there is no volume figure to report.
3. Indicators
Computed with the technicalindicators library at ingestion, over a 300-bar warmup window, and stored alongside the bar. A bar inside an indicator's warmup period stores null for it — never a back-filled estimate.
| Field | Indicator | Period |
|---|---|---|
| rsi_14 | Relative Strength Index | 14 |
| ema_20 | Exponential moving average | 20 |
| ema_50 | Exponential moving average | 50 |
| sma_200 | Simple moving average | 200 |
| atr_14 | Average True Range | 14 |
| macd | MACD line | 12 / 26 |
| macd_signal | MACD signal line | 9 |
| macd_hist | MACD histogram | — |
| bb_upper | Bollinger band, upper | 20, 2σ |
| bb_lower | Bollinger band, lower | 20, 2σ |
4. Derived features
Raw price level is deliberately not a feature. Close price is non-stationary — a model trained on 2008 EUR/USD levels learns nothing transferable to 2026. Everything below is a return or a ratio.
oc_return_pct
(close − open) / open × 100
Intrabar direction, scale-free.
hl_range_pct
(high − low) / close × 100
Realised volatility for the bar.
close_change_pct
pct_change(close) × 100
Bar-over-bar return.
volume_zscore
(volume − μ₅₀) / (σ₅₀ + 1e−9)
Unusual participation. Null for forex — see the limitations below.
5. Labelling
Each bar is labelled by the realised move over a configured horizon, against a configured threshold, into three classes:
| Class | Condition |
|---|---|
| 2 — up | realised move ≥ +threshold_pct |
| 0 — down | realised move ≤ −threshold_pct |
| 1 — flat | everything between |
The storedlabel column is direction-agnostic — it records only whether abs(move) ≥ threshold. Training on it directly would teach a model to predict volatility while presenting the output as a directional call. The training target is therefore rebuilt from the sign of the realised move, not from that column.
6. Validation
Expanding-window walk-forward, 5 folds, starting from the first 50% of the series. Each fold trains on everything before its test window and is evaluated only on data after it.
Random train/test splits are never used. Adjacent bars' indicators are autocorrelated, so a random split puts near-duplicate rows on both sides and reports a score the model cannot reproduce on unseen time.
Model: gradient-boosted trees (XGBoost), 300 estimators, max depth 4, learning rate 0.05, with balanced class weights — the flat class otherwise dominates and a model can score well by never making a call.
7. Promotion
A trained model enters the registry as candidate and is inert. It must be promoted explicitly through each stage; nothing self-promotes on a metric threshold.
| Status | Behaviour |
|---|---|
| candidate | Registered, never evaluated live. |
| shadow | Predictions computed and logged; nothing published. |
| live | Read by the inference service; may publish signals. |
| paused | Withdrawn from live. Reversible. |
A live model is polled every 60 seconds. A prediction is published only if the class is directional and confidence clears 0.65; stop and target default to a symmetric 1:1 around the same threshold the label was built on.
Known limitations
Stated because a methodology page that lists only strengths is not a methodology page.
No gold in the feature store.
Alpha Vantage's FX_DAILY endpoint rejects XAU — verified directly, it is not a documented currency there despite appearances. Gold has live pricing on the site but no daily OHLC history, so no gold model can be trained today.
volume_zscore is dead for forex.
It is computed, but forex bars have no volume, so the feature is null for every forex row. It contributes nothing to those models and is retained only because crypto rows do populate it.
Crypto history is shallow.
The Kraken job only stores a rolling recent window, so crypto has days of history against forex's 19 years. Crypto models are not currently trainable to the same standard.
Symmetric stops are a default, not a result.
Stop and target are placed at ±threshold. ATR-scaled levels would very likely do better; this has not been tuned.
No model is live.
The registry is functional and the promotion path works, but zero models are in the live state, so the pipeline is unproven in production.
Current coverage is on the infrastructure page; this data is queryable via the API.