Documentation Index
Fetch the complete documentation index at: https://docs.novosky.app/llms.txt
Use this file to discover all available pages before exploring further.
Current state β Phase 17.3 (active)
Signal model
62 features Β· RF+XGB+LGB Β· 3-class Β· M15 Β· Optuna-tuned Β· recency-weighted
- Training cutoff: 2026-05-06 20:04 UTC
- Hugging Face Hub tag:
v20260506
Position model
71 features (62 market + 4 state + 5 M1)
- ATR-aware labels Β· exit_threshold=0.80
- Accuracy: RF=22.4%, XGB=76.1%, LGB=60.2%
- 181,414 training samples
OOS performance
| Window | WR | PF | MaxDD | Sharpe | Return | Score |
|---|---|---|---|---|---|---|
| 37d β Phase 17.3 (weekly optimize, active) | 71.7% | 5.46 | 4.5% | 59.54 | +195.7% | 54.47 |
| 38d β Phase 17.2 (weekly optimize) | 67.6% | 2.83 | 7.3% | 42.30 | +394.5% | 35.13 |
| 38d β Phase 16w (weekly optimize) | 62.8% | 2.26 | 14.5% | 37.88 | +1154.6% | 46.80 |
| 37d β Phase 16 (base retrain) | 65.8% | 3.03 | 7.1% | 49.88 | +390.9% | β |
| 37d β Phase 15 | 78.5% | 2.43 | 1.8% | 32.99 | +50.7% | β |
| 224d (Sep 2025βApr 2026) | 57.4% | 2.21 | 50.2% | 13.60 | β | β |
conf=0.65 Β· prob_diff=0.08 Β· risk=2.0% Β· TP=1.0ΓATR Β· SL=1.0ΓATR Β· CB=7 Β· Profile 3 Balanced
In progress β immediate blockers
- VM cron (15.2): Wire
weekly_optimize.pyon trading VM - Phase 15.5: Broker safety audit documented β see Broker safety audit
Phase 15 β Production transition
15.0 Broker-agnostic refactor
Complete β 2026-04-18
config.jsonno longer requires"broker"key; all scripts load spread/leverage/swap dynamically from MT5 API/accountand/symbolsbacktest.pyβ config-faithful backtester; use--max-lot/--cent-accountfor preset-style comparisonsscripts/sweep.pyβ unified sweep replacingsweep_signal.py+sweep_pos_model.py;--target signal|pos|bothscripts/check_broker_limits.pyβ fixed main guard; added--symbolflag
15.1 Position model validation
Complete β 2026-04-15
- 38d OOS: PF=4.71 vs baseline 4.27 (+10.3%), MaxDD=4.7%, 1 ML_Exit
- Sweep of 13 configs via
scripts/sweep.py --target pos - Thresholds updated: exit=0.80, min_prob_diff=0.25, min_bars_held=4
15.2 Automated retrain pipeline
Complete β 2026-04-17
scripts/weekly_optimize.pyβ 13-phase autonomous pipeline (SHAP β tune β retrain β sweep β evaluate β push β commit β notify)- Score improvement gate: 2% minimum to keep new models
- Incremental warmstart training already enabled by default (
ml/train.py,ml/ensemble_trainer.py)
15.3 Cloud monitoring
Complete β Telegram bot commands cover live status; performance_monitor.py removed
- Degradation detection is handled by the weekly walk-forward OOS gate in
weekly_optimize.py(3 Γ 4-week folds, WR β₯ 55%, PF β₯ 1.8) - Live alerts flow through
trading/telegram_commands.pyandscripts/notify.py
15.4 Telegram bot commands
Complete β 2026-04-15
/help Β· /pause Β· /resume Β· /status Β· /positions Β· /close Β· /closeall Β· /news Β· /pnl Β· /latency
Auth-gated by TELEGRAM_CHAT_ID. Pause flag wired into main trade entry loop.
15.5 Broker safety audit
Documented β 2026-04-25
- Run audit against live RoboForex server:
python scripts/check_broker_limits.pyβ PASS (2026-05-10: all limits confirmed, spread=14.59, stops_level=0, lot 0.01β100, contract_size=1.0) - Run audit against IC Markets when account is provisioned
- Run audit against FundingPips when account is provisioned
15.6 Weekly validation cadence
Complete β 2026-04-17
weekly_optimize.py β runs full OOS backtest every Sunday, auto-rolls back if Score regresses.
Phase 16 β Risk guards & validation
Phase 16 fixes landed 2026-04-24: RF double-weighting removed (val acc 10.5%β49.4%), TP corrected 0.8β1.2ΓATR, labels aligned with live execution, risk model rebuilt with Kelly log-utility label function and sequence augmentation (~5,000 training samples, label std 0.000β0.442, val MAE=0.3084). OOS result: WR=65.8%, PF=3.03, MaxDD=7.1%, Sharpe=49.88, Return=+390.9%, 114 trades over 37 days.16.1 Enable daily loss guard
Complete β 2026-05-10
max_daily_loss_pct is active in trading/bot.py (check_max_loss_profit()). Set to 3.0% of live equity β recomputed each cycle from account.equity.- Confirmed
trading/bot.py:check_max_loss_profit()used fixed USD β upgraded to equity-relative - Added
max_daily_loss_pct: 3.0toconfig.json; removedmax_daily_loss: 99999 - Bot computes
limit = equity * pct / 100dynamically each cycle via_get_account().equity - Dry-run:
python trading.py --dryβ guard logs[DAILY_LOSS_GUARD]on breach - Daily reset uses
LOCAL_TZ_OFFSET(+7 WIB) β resets at WIB midnight β
16.2 Equity curve filter
Complete β 2026-04-25
equity_curve_filterconfig block added (enabled: false,lookback_trades: 10,max_drawdown_pct: 5.0)_recent_trade_pnlspersisted instate.jsonand restored on restart- Entry guard fires
[EC_FILTER]when net loss over last N trades β₯ threshold % of equity - Enable after validating OOS backtest with the filter active
16.3 Extend walk-forward OOS gate
Complete β 2026-04-25
- Phase 7b added to
scripts/weekly_optimize.pybetween phase 7 and phase 8 - 3 non-overlapping 4-week OOS backtest folds run after every retrain
- Gate: all folds WR β₯ 55%, all folds PF β₯ 1.8, median PF β₯ 2.0
- Failure rolls back the snapshot immediately and skips push/commit
- Fold results appended to
logs/wf_gate.log
16.4 Enable Kelly lot sizing
Kelly lot sizing is fully implemented in
trading/bot.py:1193β1257. It is disabled via config.risk_percent achieves. The value of enabling it is that it scales up naturally at high-confidence setups and scales down at marginal ones.
- Enable:
ml_active_management.kelly_lot_sizing.enabled = trueβ already enabled - OOS sweep (3 runs via
--mode kelly): disabled Score=0.19 MaxDD=16.1% | half=0.5 Score=0.30 MaxDD=7.5% | full=1.0 Score=0.31 MaxDD=8.7% - Kelly must stay on β disabled falls back to raw static sizing, MaxDD spikes to 16.1%
- Keeping
max_kelly_fraction: 0.5(half-Kelly): full-Kelly wins Score by 0.01 but costs 1.2% extra MaxDD β not worth it at $500 balance. Re-evaluate at higher equity.
16.5 Broker-Agnostic Multi-Account Architecture
The system is designed to be broker-agnostic, decoupling the ML pipeline and trade logic from any specific broker (like RoboForex, IC Markets, FundingPips, etc.). Training and live execution will seamlessly support multiple brokers simultaneously, and eventually decentralized exchanges (DEX) like Hyperliquid.config/accounts.json schema:
- Create
config/accounts.jsonwith schema above; add RoboForex entry only for now - Add
--account <account_id>flag totrading.pyargument parser; load account config at startup; overriderisk_pct,max_lot,state_file, and API base URL from account config - Update
trading/bot.py: replace hardcodedstate.jsonreference withconfig["state_file"]; replaceAPI_BASE_URLwithterminals[account["terminal_key"]]["url"] - Update
ml/train.py: add--brokers exness,icmarketsflag; when multiple brokers specified, fetch, normalize, and merge their OHLCV datasets; retrain on merged set - Unit test multi-broker merge: assert merged DataFrame has no duplicate timestamps, close prices within 0.5% of each brokerβs midpoint
- Update
ecosystem.config.jswith multi-process template (disabled by default β uncomment when IC Markets account is provisioned) -
config/terminals.jsonport + domain mapping already implemented β RoboForex on port 6542 /terminal-rf1.novosky.app - Draft Hyperliquid adapter spec in
docs/architecture/hyperliquid.mdx: REST endpoints for order placement (POST /exchange), position query (POST /infowithtype: clearinghouseState), and funding rate feed (/infowithtype: fundingHistory)
Phase 21 β Dynamic SL/TP & position model upgrades
These have the second-highest near-term ROI because the core code is already built. Items 21.1 and 21.2 require no retraining.21.1 Enable and validate ml_sltp (confidence-scaled TP/SL at entry)
Complete β 2026-04-22
trading/bot.py. It dynamically predicts the ideal SL/TP ATR multipliers for each trade based on market volatility and regime, overriding the static fallback multipliers. It leverages Walk-Forward validation (TimeSeriesSplit) to ensure the predictions generalize well to out-of-sample regimes.
At order placement, ml_sltp re-scales SL and TP using the signal modelβs entry confidence:
| confidence | SL | TP | RR |
|---|---|---|---|
| 60% (threshold) | 1.00 Γ ATR | 1.50 Γ ATR | 1.50 |
| 80% | 0.85 Γ ATR | 1.75 Γ ATR | 2.06 |
| 100% | 0.70 Γ ATR | 2.25 Γ ATR | 3.21 |
ml_sltp: SL = 1.0 Γ ATR, TP = 0.8 Γ ATR (static 1:0.8 RR, currently live).
Config location: config.json β ml_active_management.ml_sltp
- Enable: set
ml_sltp.enabled = trueinconfig.jsonβ already enabled - OOS backtest result: WR=48.8%, PF=1.72, MaxDD=9.0%, Score=0.26 (37d OOS, post-retrain)
- Score degraded vs disabled baseline (Score=0.30, MaxDD=7.5%) β swept
confidence_sl_adjustΓconfidence_tp_adjust(20 combos via--mode ml_sltp) - Best enabled combo
sladj0.4_tpadj0.5still lost on Score (0.29) and MaxDD (9.1%) β disabled per todo rule. Re-evaluate after more live trades improve confidence signal quality. -
min_tpfloor check already present in theml_sltppath (bot.py:4104β4105) - Removed dead
base_sl_atr_mult/base_tp_atr_multconfig keys β SLTP regression model supplies the base multipliers, not static config
21.2 Trailing stop + lower min_bars_held
Both are fully implemented in the bot. Both are config-only changes. No retrain required.
Trailing stop (bot.py:2928β2960): ATR trail width scales with model confidence and live momentum_decay. High confidence β tight trail; low confidence / adverse momentum β wider trail. Currently disabled.
- Enable: set
ml_trailing_stop.enabled = true,base_trail_atr_mult = 1.2,min_profit_atr = 0.8 - Run OOS backtest and compare Score vs baseline
- OOS result: WR=46.6%, PF=1.45, MaxDD=12.7%, Scoreβ0.19 β worse than baseline (Score=0.30, MaxDD=7.5%). Conflicts with
partial_closebreakeven SL β trailing stop cuts winners after BE move. Disabled. Saved params inconfig.json._note. - Do not enable simultaneously with
ml_sltptesting β change one variable at a time
min_bars_held reduction: Currently 4 bars (60 min minimum hold). At 2 bars the position model can act after 30 minutes instead of 60.
- Set
position_optimization.min_bars_held = 2inconfig.json - Run
python scripts/sweep.py --target pos(pre-retrain): Score 0.30β0.34, best was exit0.60/bars2. Applied temporarily. - Post clean-retrain sweep (2026-04-26): optimal config reverted to
exit_threshold=0.80, min_prob_diff=0.25, min_bars_held=4β Score=0.66, PF=2.69, DD=6.4%. After--no-warmstart, model EXIT signals more reliable at higher threshold. Applied toconfig.json+ml_config.json.
21.3 M1 intra-candle feature augmentation for position model
The feature cache (_latest_features_cache) is M15-derived. All 59 market features stay stale within a 15-minute candle. Adding 5 M1 scalars gives the position model intra-candle microstructure. This requires a position model retrain (breaking scaler change).
Proposed M1 features:
| Feature | Calculation | Signal |
|---|---|---|
m1_price_accel_5 | (close[0] β close[5]) / close[5] on M1 | Intra-candle momentum |
m1_vol_ratio_5 | mean(volume[0:5]) / mean(volume[5:10]) | Volume surge |
m1_rsi_9 | Wilder RSI(9) on M1 close | Fast momentum state |
m1_atr_3 | ATR(3) normalized by close | Intra-candle volatility |
m1_body_pct | abs(close β open) / (high β low + Ξ΅) | Candle conviction |
- Add
compute_m1_fast_features(closes, opens, highs, lows, volumes)toml/feature_engineering.pyβ standalone function, 5 scalars, handles short windows gracefully - Update
ml/position_labeling.py: addM1_FEATURESconstant;generate_position_samples()acceptsdf_m1=None; at each sample timestamp, aligns 25 M1 bars vianp.searchsorted(no lookahead leak β uses bars at or before M15 timestamp) - New constant
M1_FEATURES = ['m1_price_accel_5', 'm1_vol_ratio_5', 'm1_rsi_9', 'm1_atr_3', 'm1_body_pct']β separate fromPOSITION_STATE_FEATURES(keeps 4), total position features 63β68 -
ml/position_predictor.py: detects M1 fromposition_metadata.json(n_features >= 68);get_position_action()gainsm1_features=Noneparam; neutral values substituted if model expects M1 but none provided (backward-compatible) -
ml/position_trainer.py:generate_training_data(df_m1=None)β passes M1 DataFrame to labeling; updatesall_feature_namesand metadata when M1 features included -
ml/train.py: Step 4 fetches M1 bars (same date range as M15, from API); passesdf_m1topos_trainer.generate_training_data(); patchesmodel_compat.jsonwith M1 feature list -
trading/bot.py: incheck_ml_active_management(), fetches 30 M1 bars once per cycle (only whenpos_predictor._has_m1_features=True); passes computed features toget_position_action() - Retrain attempted: M1 API returned empty β MT5 HTTP server must be running during
--refreshto populate M1 cache. Position model retrained on 63 features (same as before). No regression in val accuracy (XGB=0.753, LGB=0.743). - M1 cache solution implemented:
datasets/training_data_btcusd_m1.csv(same pattern as M15 CSV).--refreshwith API running fetches and saves it; subsequent trains load from cache automatically. Path configurable viaml_config.json paths.m1_training_data. - OOS backtest post-retrain (2026-04-26): WR=57.5%, PF=2.86, MaxDD=7.5%, Score=0.60 β doubled baseline score (was 0.30). SL hits: 24 (was 66β151). ML exits: 89/233 (38%). Re-sweep after retrain identified better config (0.80/0.25/bars4 β Score=0.66); applied.
- M1 retrain (when API running):
python train_ml_model.py --ensemble --position --refresh --no-warmstart--refreshfetches live M1 bars from MT5 API and saves todatasets/training_data_btcusd_m1.csv--no-warmstartforces a clean fit (warmstart would load old 63-feature weights into 68-feature model)- Confirm MT5 HTTP server is running before starting:
curl http://localhost:6542/health
- OOS sweep after M1 retrain:
python scripts/sweep.py --target posβ sweepsexit_threshold Γ min_prob_diff Γ min_bars_heldgrid; apply best config toconfig.json - Gate β current best is PF=2.86 (Score=0.60), so PF β₯ 4.0 is aspirational not immediate:
- Accept retrain if: OOS PF β₯ current PF (2.86) AND EXIT precision β₯ 85% AND MaxDD β€ current + 2%
- Aspirational target: PF β₯ 4.0 once M1 cache is fully populated (more M1 bars = better features)
- If retrain regresses (PF
<2.86): revert viaml/hf_hub.py --pulland investigate which M1 feature adds noise
Do this after 21.2 no-retrain items are validated and live. You want a clean baseline before adding features.
21.4 Catastrophic SL + position model as primary exit
Wide hard SL as safety net only. No hard TP. Position model drives all exits. Stays open longer β exits on regime deterioration rather than a fixed ATR multiple. Preconditions (all must be met before enabling):Phase 21.1 (β removed (ml_sltp is disabled; position-as-primary-exit does not depend on confidence-scaled TP/SL)ml_sltp) validated OOS- Position model EXIT precision β₯ 87% on OOS (current: 85%) β this is the primary readiness gate
- β₯ 200 live dry-run trades with
ml_active_management.enabled = trueconfirming EXIT fires at correct rate (target: ML_EXIT β₯ 60% of closes) - Telemetry already in place: each close logged as
SL_HIT,TP_HIT, orML_EXITinml_performance.csvβ (implemented)
-
position_as_primary_exitconfig block added toconfig.json(enabled: false) -
bot.pywired: if enabled, overrideseffective_sl = ATR Γ catastrophic_sl_atr_mult,effective_tp = ATR Γ emergency_tp_atr_mult, logs[PRIMARY_EXIT_MODE] -
SL_HIT/TP_HIT/ML_EXITtelemetry added toml_performance.csvvia_close_type()helper OOS test methodology for wide-SL:
- Run Step 1 OOS (wide-SL alone): log result to
logs/wide_sl_test.log; accept if Score β₯ current β 2.0 (some score loss from wider SL is expected) - Run Step 2 OOS (position-as-primary): accept only if all gate conditions above are met
- Dry-run 100 trades: tail
pm2 logs novosky --lines 200 --nostreamafter each close; countML_EXITvsSL_HITvsTP_HIT; confirm ML_EXIT β₯ 60% - Only enable on live after all preconditions are met; announce in Telegram:
[PRIMARY_EXIT_MODE] Enabled β SL=3.0ΓATR, no TP
21.5 Risk model scope β architectural constraint
The risk model outputs a scalar multiplier[0.10, 1.25] applied to base_risk_pct. Its 7 features are equity-state scalars only. It must not control SL/TP distances.
| Concern | Correct mechanism |
|---|---|
| Lot size based on equity health | Risk model β effective_risk_pct |
| TP/SL based on signal confidence | ml_sltp (Phase 21.1) |
| Intra-trade exit timing | Position model (Phase 21.3/21.4) |
| Protective trailing SL | ml_trailing_stop (Phase 21.2) |
- Add a
Scope:section toml/risk_predictor.pymodule docstring stating the model outputs lot-sizing multipliers only
Phase 17 β Feature engineering
Add new market signals before the next major retrain. Implement inml/feature_engineering.py. Each feature requires a full retrain + OOS validation before going live.
17.1 On-chain & derivatives features
These have direct theoretical backing for BTCUSD direction β funding rate and OI are the primary sentiment signals used by professional crypto traders. Dependency:pip install requests pandas
funding_rate β formula and normalization:
Binance perpetual funding settles every 8 h. Rate represents cost of holding long vs short (positive = longs pay shorts β bearish pressure; negative = shorts pay longs β bullish pressure).
oi_change β rolling delta formula:
fear_greed_index β low-information warning:
96 identical M15 values per day from this feature β SHAP will be near zero unless daily pivots correlate with session opens. Include on a trial basis; drop if SHAP < 0.001 after retrain.
- Create
ml/data_sources/binance.pywithfetch_funding_rate,fetch_open_interest,fetch_fear_greedβ each returns a UTC-indexed DataFrame - Add to
ml/train.pydata loading block: afterdf_m15is built, call all three fetchers and merge via left-join +ffillβ same pattern as M1 features - Add feature names to
model_compat.json["features"]in this order (append at end):funding_rate_norm,oi_change,fear_greed - Run
python scripts/retrain.py --ensemble --no-warmstartwith the 3 new features; runml/shap_analysis.py; removefear_greedfrommodel_compat.jsonif its mean absolute SHAP<0.001 - Cache fetched data to
datasets/funding_rate.csv,datasets/oi_change.csv,datasets/fear_greed.csvβ same pattern astraining_data_btcusd_m1.csv - OOS gate: ensemble with on-chain features must have Score β₯ current + 0.5; if not, revert β on-chain features add API call latency so they must pay for themselves
17.2 Market regime features
These are derived entirely from existing OHLCV data β no external API dependencies.-
volatility_regimeβ Compute ATR(14) percentile rank over a rolling 500-bar window; encode as continuous 0β1 (not bucketed) to avoid artificial boundaries -
w1_ema_biasβ Resample M15 to W1 (504 bars); compute(close β EMA(10)) / close; forward-fill -
w1_rsi_normβ RSI(14) on W1 bars, normalized to 0β1; forward-fill
Weekly features follow the same pattern as existing H4/D1 resampling in
calculate_mtf_features(). Add them to the same function.17.3 OHLCV data redundancy pipeline
Reverted.ml/data_sources.py and the multi-source consensus-averaging pipeline were removed in commit e8fd0da. Root cause: averaging OHLCV across Exness and RoboForex at the same timestamps produced artificial prices that didnβt match the live Exness feed, causing a training-live distribution mismatch that degraded fold_3 WF performance (PF=1.34).
Training now uses a single source (API_URL in .env) with symbol auto-detection. TRAINING_SOURCE_* and TRAINING_YFINANCE env vars are no longer read by any code.
Multi-source redundancy may be revisited in a future phase with a primary-source-wins merge strategy (no averaging) rather than consensus blending.
17.4 Smart Money Concepts (SMC) features
Add institutional order-flow structure as ML features using thesmartmoneyconcepts library. SMC theory models how large players move price toward liquidity, making it a natural complement to the existing momentum and volatility features for BTC/USD.
Dependency: already installed in examples/python/. Add to requirements.txt for the main project.
Features to add
All features are computed from M15 OHLCV data only β no external API dependency. Order Block (OB) features β zones where institutional orders are resting:| Feature | Formula | Rationale |
|---|---|---|
ob_bullish_dist | (close β nearest_bullish_ob_top) / atr_14 | ATR-normalised distance to nearest demand zone below |
ob_bearish_dist | (nearest_bearish_ob_bottom β close) / atr_14 | ATR-normalised distance to nearest supply zone above |
ob_bullish_present | 1 if any unmitigated bullish OB within 3ΓATR else 0 | Binary: price is near a demand zone |
ob_bearish_present | 1 if any unmitigated bearish OB within 3ΓATR else 0 | Binary: price is near a supply zone |
ob_volume_ratio | ob_volume / rolling_mean_volume(50) | Strength of the most recent OB (high volume = stronger zone) |
| Feature | Formula | Rationale |
|---|---|---|
fvg_bull_above | 1 if unmitigated bullish FVG above current close else 0 | Unfilled imbalance pulling price up |
fvg_bear_below | 1 if unmitigated bearish FVG below current close else 0 | Unfilled imbalance pulling price down |
fvg_bull_dist | (nearest_bull_fvg_bottom β close) / atr_14 | Normalised distance to nearest bullish FVG |
fvg_bear_dist | (close β nearest_bear_fvg_top) / atr_14 | Normalised distance to nearest bearish FVG |
| Feature | Formula | Rationale |
|---|---|---|
recent_bos | 1 if a BOS occurred in the last 8 bars else 0 | Trend continuation bias β momentum context |
recent_choch | 1 if a CHoCH occurred in the last 8 bars else 0 | Reversal bias β structural flip context |
structure_bias | +1 BOS, β1 CHoCH, 0 neither (last 16 bars) | Single signed feature combining both signals |
| Feature | Formula | Rationale |
|---|---|---|
liq_above_dist | (nearest_liq_above β close) / atr_14 | Distance to the nearest pool of buy-side stops |
liq_below_dist | (close β nearest_liq_below) / atr_14 | Distance to the nearest pool of sell-side stops |
Implementation
Addadd_smc_features(df) to ml/feature_engineering.py. The function must only use df[:i] at each row β no forward lookahead. Use swing_length=10 as the default (matches existing indicators.py usage).
The row-by-row loop is O(nΒ²) and will be slow on the full training dataset (300k+ bars). Vectorise using
pd.merge_asof or precompute a rolling lookup table once the feature set is validated. Optimise only after SHAP confirms the features are useful.Feature names to add to model_compat.json
Append in this order (after existing features, before any on-chain features from 17.1):
Integration tasks
- Add
smartmoneyconceptstorequirements.txt - Implement
add_smc_features(df)inml/feature_engineering.pyβ noMitigatedIndex/BrokenIndexcolumns may appear in the feature matrix - Add the 14 feature names to
model_compat.json["features"](append at end) - Run
python scripts/retrain.py --ensemble --no-warmstartwith all 14 features - Run
ml/shap_analysis.pyβ inspect beeswarm plot; drop any feature with mean absolute SHAP< 0.001after retrain - If more than 4 features are pruned by SHAP, split into two groups (OB+FVG first, BOS+liquidity second) and add one group at a time
- OOS gate: SMC ensemble Score must be β₯ current Score + 0.5 to ship; if not, revert
model_compat.jsonand remove the features - Performance note: validate that
add_smc_featurescompletes in< 60son the full training dataset; if slower, vectorise before merging to main
Expected impact
- OB / FVG features β highest potential. Price returning to an OB or being drawn toward an FVG is a well-documented BTC pattern. Should show non-zero SHAP especially on
ob_bullish_distandfvg_bull_above. - BOS / CHoCH features β structural regime context.
structure_biasdirectly encodes trend vs reversal, complementing existingh4_ema_biasandd1_trend. - Liquidity features β encodes stop-hunt dynamics. Less certain; BTC liquidity grabs are real but noisier at M15 than on higher timeframes.
Phase 18 β Model architecture
18.1 Stacked meta-learner
Replace majority-vote with a learned combiner. The meta-learner trains on the base modelsβ class probabilities (out-of-fold) and learns optimal weights per class. OOF stacking algorithm (time-series safe):| Option | Model | Overfit risk | Use when |
|---|---|---|---|
| A | LogisticRegression(C=0.1, max_iter=1000) | Very low | β€ 3 base models |
| B | LGBMClassifier(max_depth=3, n_estimators=100) | Low | 4β6 base models |
| C | MLPClassifier(hidden_layer_sizes=(32,), max_iter=500) | Medium | Avoid for now |
- Implement
generate_oof_stacksinml/meta_learner.pyusingTimeSeriesSplit(n_splits=5)β not StratifiedKFold - Start with
LogisticRegression(C=0.1)meta-learner (Option A); switch to LGB after Phase 22 adds neural models - Add
--metaflag toscripts/retrain.pythat triggers OOF generation + meta-learner training after base model training - Apply isotonic calibration on a chronological held-out fold (last 10% of training data); save calibrators to
models/meta_calibrators.pkl - OOS gate β recalibrated targets (current best is WR=67.6%, PF=2.83, Score=35.13):
- Keep if OOS Score β₯ current Score + 1.0
- Keep if OOS WR β₯ 60% (not 75% β that was unreachable; WR > 75% would indicate overfit, not improvement)
- Keep if OOS MaxDD β€ current MaxDD + 2%
- If gate fails: revert to majority-vote, log result to
logs/meta_learner_eval.log, do not re-attempt until base models are retrained
18.2 Calibrated probability outputs
The signal model probabilities are not inherently calibrated (a 70% confidence prediction should be right ~70% of the time). Calibration improves confidence-based downstream decisions likeml_sltp and Kelly sizing.
-
calibrate_models()added toml/ensemble_trainer.pyβ applies_IsotonicCalibratedClassifier(sklearn-version-agnostic wrapper) to each base model after training; saves*_calibrated.pklalongside raw pkl -
_load_model()inml/ensemble_predictor.pyupdated: calibrated pkl takes priority over ONNX (calibration cannot be applied to ONNX sessions), ONNX is second, raw pkl is fallback - Verified calibration on val set via
ml/calibration_check.py: all three models PASS (RF MAE=0.012, XGB MAE=0.018, LGB MAE=0.014, ensemble MAE=0.030 β well within 0.05 threshold) - Completed before enabling
ml_sltp(Phase 21.1) β calibrated probabilities ready
18.3 Regime-switching model
Train two separate signal model variants: one on trending data (ADX > 0.25) and one on ranging data (ADX β€ 0.25). At inference, the active ADX regime routes to the correct model. Prerequisite diagnostic β measure single-model regime performance:< 55% OR ranging WR < 55% AND each segment has β₯ 20,000 training samples.
Hysteresis rule for regime transitions (prevents model-flip thrashing):
ADX fluctuates around the threshold. Without hysteresis, the model can switch dozens of times in a session. Apply a buffer zone:
- Trending-regime Score β₯ 17.0 (better than full-model 35.13 / 2 = 17.6 is the theoretical minimum for half the trades; 17.0 is conservative given trending is the easier regime)
- Ranging-regime Score β₯ 10.0 (ranging is harder; acceptable if at least net-positive)
- Combined (blended) Score β₯ current 35.13 + 2.0 (must beat single model or not worth the complexity)
- Run regime diagnostic above on the latest OOS dataset before doing anything else; log results to
logs/regime_diagnostic.txt - If diagnostic shows no regime gap (both WR > 55%): skip this phase, mark as deferred
- If gap found: segment
datasets/training_data_btcusd.csvbyadx_14; verify β₯ 20k rows per segment - Train two ensembles:
python scripts/retrain.py --ensemble --regime trendingand--regime ranging; each saves tomodels/signal_trending/andmodels/signal_ranging/ - Add
RegimeSwitchPredictortoml/ensemble_predictor.py: maintainsadx_history = deque(maxlen=3); callsget_active_model()before each inference; loads both model sets into memory at startup - OOS backtest: run
backtest.py --regime-switchwhich routes each bar to the correct model; compare blended Score to single-model Score - Gate as above; if blended Score < current + 2.0: abandon regime switching and document
18.4 Multi-instrument expansion
Each instrument gets its own dedicated model stack β never shared with BTCUSD. Architecture per instrument:SL=0.8ΓATR, TP=1.2ΓATR proportions are valid, but the min_atr filter (currently 15 for BTC) needs a per-symbol value:
backtest.py:
- Add
--symbolflag toml/train.py; when set, loadml_config_{symbol.lower()}.jsoninstead of default; save models tomodels/{SYMBOL}/ - Add
--symbolflag tobacktest.py; load correct scaler and model directory; use symbol-specificpip_valuein P&L calculation - Create
ml_config_xauusd.jsonwith XAUUSD-specificmin_atr,labelingparams; keep all other params as BTC defaults initially - Fetch 2 years of XAUUSD M15 from MT5 API:
python ml/train.py --symbol XAUUSD --refreshβ saves todatasets/training_data_xauusd.csv - Train XAUUSD model stack:
python scripts/retrain.py --symbol XAUUSD --ensemble --position --no-warmstart - OOS gate for XAUUSD: PF > 2.0 on 60-day OOS window; MaxDD < 15%
- EURUSD: defer until XAUUSD is live-validated; same pipeline applies
Phase 19 β Infrastructure & reliability
19.1 Live trade dashboard
Superseded by Phase 23 (JARVIS Dashboard) β the Next.js WebSocket dashboard covers all use cases planned here with better UX. This Streamlit version is now a lightweight fallback for quick server-side monitoring without the full frontend stack.
pip install streamlit>=1.35 supabase>=2.5
The Streamlit fallback is useful when SSH-ed into the trading VM and needing a quick equity snapshot without opening the browser dashboard.
- Build
scripts/dashboard_live.py: - Views: equity area chart (500 snapshots), open position P&L, last 20 signals table with confidence color-coding
- Deploy via PM2 port 8501:
streamlit run scripts/dashboard_live.py --server.port 8501 --server.headless true; expose underterminal-rf1.novosky.app/monitorvia Caddy
19.2 API failover
Complete β 2026-04-25
_api_fail_countand_api_pausedglobals track consecutive MT5 API failures intrading/bot.py- After 3 consecutive failures: Telegram alert
[API UNREACHABLE]fired,_api_paused = Trueblocks new entries - Resumes automatically when API responds;
_api_fail_countcleared,_api_paused = False - Guard wraps the
_get_rates()call in the main loop
19.3 Graceful shutdown improvements
Complete β 2026-04-25
_shutdown_requestedflag replacessys.exit(0)in the SIGTERM handler- Main loop top: if flag is set, polls open positions and exits cleanly once count reaches 0
- Entry gate blocks new trades while shutdown is pending
- Logs
[SHUTDOWN]with position count on clean exit
19.4 Config hot-reload
Safe vs unsafe keys β not all config changes can be applied without restart:| Key | Hot-reloadable? | Reason |
|---|---|---|
risk_percent | β Yes | Only affects next lot sizing call |
max_daily_loss_pct | β Yes | Guard checked every cycle |
min_confidence | β Yes | Filter applied at signal gate |
adx_regime_filter.* | β Yes | Checked at signal gate |
circuit_breaker.* | β Yes | State counter reset is safe |
model_paths.* | β No | Requires model reload β restart |
symbol | β No | Would orphan tracked positions |
api_base_url | β No | Active connections would break |
kelly_lot_sizing.* | β οΈ Careful | Only safe if no open position |
- Add
_maybe_reload_config()call at top of main loop (before signal gate) β call every cycle (M15 cadence means 15-min max lag is acceptable; no need for a background thread) - Define
_SAFE_HOT_KEYSset as shown above; never iterate all config keys (would silently apply unsafe changes) - Log changed keys with old/new values (not just key names) β makes audit trail useful
- Telegram notification on reload: send list of changed keys so operator knows the change took effect
- Unit test: write a temp
config.jsonwith modifiedrisk_percent, call_maybe_reload_config(), assertconfig["risk_percent"]updated and_config_mtimeadvanced
Phase 22 β Advanced Ensemble Architecture (XGBoost Β· RF Β· FT-Transformer Β· TFT Β· LSTM)
The current RF + XGB + LGB majority-vote ensemble leaves accuracy on the table because all three base learners are gradient-boosted trees β they share the same inductive bias and make correlated errors. Adding one neural-attention model and one sequence model provides genuine ensemble diversity (target pairwise disagreement rate 0.35β0.50), which lowers the irreducible error floor independent of individual model quality. Ensemble error decomposition:22.1 XGBoost β DART mode + monotonic constraints + Optuna search space
Algorithm β DART tree dropping: At each boosting round, instead of using allt trees built so far, DART randomly drops a subset D β {1β¦t} and trains the new tree to compensate for the removed ones:
rate_drop = probability each tree is included in D.
skip_drop = probability the entire drop is skipped for that round (pure GBM step).
Monotonic constraint math:
For feature j with constraint c_j β {-1, 0, +1}, XGBoost enforces:
[max(left_subtree), min(right_subtree)].
Interaction constraints:
Define which feature groups may share a split path. XGBoost rejects any tree that routes both feature i and feature j on the same root-to-leaf path if they are in different groups:
- Switch booster in
ml_config.json β xgb_params: - In
ml/train.pyaround the XGBClassifier constructor, buildmonotone_constraintstuple frommodel_compat.json["features"]order β map feature names to constraint values: - Build
interaction_constraintslist from 4 feature cluster groups; attach to XGB params before fit - Add DART-specific Optuna search space in
ml/tune.py(new branch underif booster == "dart"): - DART disables
early_stopping_roundsβ use fixedn_estimators=400; remove anyearly_stopping_roundsfrom the DART fit call inml/train.py - Run
python scripts/retrain.py --ensemble --no-warmstartto force a clean DART retrain - OOS gate: keep if Score β₯ current GBTREE baseline β 0.5 (accept slight score trade-off for lower variance)
22.2 Random Forest β ExtraTrees + Quantile intervals for Kelly
Algorithm β ExtraTrees split selection: Standard RF: at each node, evaluatemax_features candidate features and pick the split minimizing Gini. ExtraTrees: pick a random threshold from the featureβs observed range β no exhaustive search:
Ε· = (1/T) Ξ£_t leaf_mean_t(x). Quantile RF instead collects all training labels in each matched leaf across all T trees, forming an empirical distribution, and returns its percentiles:
- Add
ExtraTreesClassifiertoml/ensemble_trainer.pyβ insert after the RF definition: - Export ExtraTrees to ONNX using the same
skl2onnxpipeline inml/onnx_export.py; output shape[1, 3]float32 probabilities - Add
"extra_trees"key tomodel_compat.json["models"]list; updateensemble_predictor.pyload path - Create
ml/quantile_predictor.py:- Class
QuantileRFPredictorwraps a trainedRandomForestClassifier - Method
predict_interval(X_row)β iterates all tree leaves matched byX_row, pools their training labels, returns(p05, p50, p95)for each class - Inference is O(T Γ leaf_size) β keep T β€ 200 for
<50 mslatency at M15 frequency
- Class
- Wire into
trading/bot.pyKelly sizing block (currently around line 1220): fetchinterval_widthfromQuantileRFPredictor; apply adjusted Kelly formula above; log both raw and adjusted Kelly toml_performance.csv - Add
"quantile_rf": {"enabled": false, "p_low": 0.05, "p_high": 0.95}config block toconfig.json - Optuna search space additions in
ml/tune.pyfor ExtraTrees: - OOS sweep: compare 3-model majority-vote vs 4-model (RF+XGB+LGB+ExtraTrees) majority-vote; require Score β₯ current + 0.3
22.3 FT-Transformer (Feature Tokenizer + Transformer)
Architecture β Feature Tokenizer: Each of the 62 features is independently projected from scalar(batch, 1) β embedding vector (batch, d):
[CLS] token is prepended, giving a 63-token sequence. Multi-head self-attention then computes pairwise interaction scores between every pair of feature tokens:
[CLS] token aggregates cross-feature information; its output is fed to the classification head.
Why FT-Transformer > TabTransformer for NOVOSKY:
TabTransformer applies attention only to categorical features (9 out of 59). FT-Transformer applies attention to all 59, making it better suited since 50+ features are numerical time-series derivatives.
Data requirements:
- Create
ml/models/ft_transformer.pyβ puretorch.nn.Module:Forward pass: tokenize each feature β add index embeddings β prepend CLS β transformer β CLS output β head - Create
ml/trainers/ft_transformer_trainer.py:- Convert
X_train(numpy float64) totorch.float32tensor class_weights = compute_sample_weight('balanced', y_train)βtorch.FloatTensorloss = F.cross_entropy(logits, y_batch, weight=class_weights_batch)- Optimizer:
AdamW(lr=5e-4, weight_decay=1e-5, betas=(0.9, 0.999)) - Scheduler:
CosineAnnealingLR(T_max=150, eta_min=1e-6)β cosine decay ensures smooth convergence - Batch size: 256; max epochs: 150; early stop patience: 15 on val cross-entropy
- Save best checkpoint to
models/ft_transformer.pt(state_dict only, not full model)
- Convert
- Add Optuna hyperparameter search for FT-Transformer in
ml/tune.py: - ONNX export β add to
ml/onnx_export.py:Edge case: ifn_headsdoes not divided_modelevenly, ONNX export fails β validated_model % n_heads == 0before export. - Apply isotonic calibration via existing
calibrate_models()inml/ensemble_trainer.pyβ wrap FT-Transformer inference in a sklearn-compatiblepredict_proba(X)adapter class - Add to
ml/ensemble_predictor.pymodel loading: checkmodels/ft_transformer.onnxβ fall back tomodels/ft_transformer_calibrated.pklβ fall back tomodels/ft_transformer.pt - OOS gate: FT-Transformer solo OOS WR β₯ 50%, solo PF β₯ 1.5; ensemble with FT-Transformer Score β₯ baseline + 0.5
22.4 Temporal Fusion Transformer (TFT) β sequence model
Architecture overview: TFT processes a sequence ofT=48 M15 bars. Each bar carries n_dyn=54 time-varying features. Additionally, 5 static features (session flags, day-of-week sin/cos) are processed separately.
v_t are what we expose as βfeature attentionβ in the dashboard.
Data pipeline β SequenceDataset:
[idx β¦ idx+seq_len-1] β label at idx+seq_len-1.
Training config:
- Create
ml/models/tft.pyβ implement GRN, VSN, TFT classes as described above; keepd_model=128,n_heads=4,seq_len=48,n_static=5,n_dynamic=54 - Create
ml/data/sequence_dataset.pyβSequenceDatasetas specified; unit test: assert no label from future bars is in the window (check__getitem__with known data) - Create
ml/trainers/tft_trainer.py:- Build
DataLoader(SequenceDataset(...), batch_size=64, shuffle=False)β do NOT shuffle (time-series ordering) - Training loop: forward β loss β backward β clip_grad β step β scheduler.step(val_loss)
- Save best model (val loss) to
models/tft.pt; also save final-epoch attention weights tensor tomodels/tft_attention_cache.npy(shape(n_val, 48, 54)) for dashboard initialization
- Build
- Add TFT Optuna search space in
ml/tune.py: - ONNX export: TFT has two inputs β export with
input_names=["x_dynamic", "x_static"], shapes[batch, 48, 54]and[batch, 5]; opset 17; verify withonnxruntime.InferenceSession - In
ml/ensemble_predictor.py: maintain a rolling buffer_seq_buffer: deque(maxlen=48)populated after eachbuild_features()call; pass last 48 rows to TFT at inference - Edge case: if
_seq_bufferhas < 48 entries (bot just started), zero-pad the head and still run inference β TFT will output lower-confidence results until buffer fills - OOS gate: TFT solo WR β₯ 50%; ensemble Score β₯ current + 0.5
22.5 LSTM with Bahdanau attention + TCN alternative
Bahdanau (additive) attention β full formula: Given LSTM output sequenceH = [h_1, β¦, h_T] and final hidden state h_T:
W_a β β^{daΓd}, U_a β β^{daΓd}, v β β^{da} are learned. da=64 (attention dim).
TCN receptive field formula (use to choose dilation schedule):
With n_layers dilated causal Conv1d layers, each with kernel_size=k and dilation d_i = 2^i:
n=4 (RF=61) to cover 12β15 h window with lower compute cost.
TCN architecture:
(kβ1) Γ dilation and slice off the right:
- Create
ml/models/lstm_attention.py:- Bidirectional LSTM:
nn.LSTM(input_size=59, hidden_size=128, num_layers=2, bidirectional=True, dropout=0.2, batch_first=True)β output dim = 256 - Attention: implement Bahdanau equations above with
da=64;W_a=Linear(256, 64),U_a=Linear(256, 64),v=Linear(64, 1, bias=False) - Context vector:
c β β^{256}; concat withh_T[-1](last step, bidirectional) βLinear(512, 3) - Inference mode switch: set
self.training_modeflag; whenFalse, run only forward direction of LSTM (unidirectional causal)
- Bidirectional LSTM:
- Create
ml/models/tcn.py:- 4
CausalConv1dlayers with dilations[1, 2, 4, 8],kernel_size=5,out_channels=128 WeightNormon each Conv1d (improves training stability vs BatchNorm on small batches)- Residual connections: if
in_channels β out_channels, addConv1d(in, out, 1)projection - Final layer: last timestep output β
Linear(128, 3)
- 4
- Train both on
SequenceDataset(seq_len=48)using sametft_trainer.pyloop (swap model); record OOS Score for each; keep whichever is higher (TCN likely within 0.5% but 4Γ faster) - Optuna search space for LSTM:
- Optuna search space for TCN:
- ONNX export β LSTM: use
torch.onnx.exportwith opset 17; setdo_constant_folding=True; verify hidden state output is not exported (classification output only); test inference latency withonnxruntimeon a single row β must be < 20 ms on CPU - At inference in
ensemble_predictor.py: feed same_seq_bufferused by TFT; if buffer < 48, pad with zeros (same strategy as TFT) - Attach
alpha_weights(shape(48,)) to the return value ofget_signal()for dashboard streaming
22.6 Stacked meta-learner + regime-adaptive weighting
Algorithm β walk-forward OOF stacking: To prevent data leakage (meta-learner seeing the test set during base-model training), use time-series walk-forward folds:X_meta has shape (N_train, K Γ C) where:
K= number of base models (5: RF, XGB, LGB, ExtraTrees, FT-Transformer/LSTM/TFT)C= number of classes (3: BUY, SELL, HOLD)N_train= training samples with OOF predictions (unavoidably loses first foldβs samples)
max_depth=3, n_estimators=100) to prevent overfitting on KΓC=15 features.
Isotonic calibration of meta output:
After training, apply per-class isotonic regression on a held-out calibration fold:
max(meta_probs) is < 0.55, fall back to regime-weighted averaging:
_latest_features_cache):
- Create
ml/meta_learner.pywith:generate_oof_stacks(base_models, X, y, n_splits=5)β returnsX_meta (N, 15),y_meta (N,)usingTimeSeriesSplit- Algorithm: for each fold, retrain all 5 base models on train split, predict
predict_probaon val split, store inX_meta[val_idx] - Save to
models/oof_stacks.npy train_meta_learner(X_meta, y_meta)β fitsLGBMClassifier(n_estimators=100, max_depth=3, learning_rate=0.1, num_leaves=15, min_child_samples=20), saves tomodels/meta_learner.pklcalibrate_meta(meta_model, X_cal, y_cal)β fits 3IsotonicRegressionobjects, saves tomodels/meta_calibrators.pkl
- Create
ml/regime_router.py:detect_regime(features: dict) β strβ 4-state classification using ADX + ATR percentile as aboveREGIME_WEIGHTSdict with per-regime model weights (initial values: tune viascripts/sweep.py --target regime)route(meta_probs, base_probs_dict, features) β np.ndarrayβ implements the fallback formula above
- Update
ml/ensemble_predictor.py:- Load
meta_learner.pklandmeta_calibrators.pklin__init__(alongside existing model loading) - In
get_signal(): collect all base modelpredict_probaoutputs β stack intoX_meta_row (1, 15)β runmeta_learner.predict_probaβ calibrate β pass toRegimeRouter.route()
- Load
- Update
scripts/weekly_optimize.pyβ add Phase 13b: after base model retraining, regenerate OOF stacks and retrain meta-learner (takes ~5 min extra; acceptable in weekly job) - Optuna search space for meta-learner itself:
- OOS gate: 5-model meta-learner must beat current 3-model majority-vote by β₯ 1.0 Score AND β₯ 2% WR; if not, revert to majority-vote and document result in
logs/meta_learner_eval.log
Phase 23 β JARVIS Live Trading Dashboard
A real-time visualization system inspired by quant firm internal dashboards (Bloomberg DASH, QuantConnect live monitor, Two Sigmaβs internal regime displays) and Jarvis-style AI interface aesthetics. Stack: Next.js + WebSocket + TradingView Lightweight Charts + Framer Motion + Apache ECharts + optional Three.js.This is read-only. The dashboard connects to a new WebSocket endpoint on the bot server and never writes to
config.json, triggers orders, or modifies bot state.package.json):
requirements.txt):
23.1 WebSocket signal stream (backend)
Message contract βSignalEvent (full schema):
- Create
trading/ws_server.pyβ FastAPI app on port 8765: - In
trading/bot.pyafter_get_signal()returns (around line 3200): pushSignalEventto_signal_queueviaasyncio.get_event_loop().call_soon_threadsafe(_signal_queue.put_nowait, event)β bot runs in a thread, ws_server in asyncio event loop - Run
ws_server.pyvia uvicorn in a background thread started at bot startup; addDASHBOARD_WS_SECRETto.env - Add to
ecosystem.config.js: second PM2 processws_serverusinguvicorn trading.ws_server:app --port 8765 - Caddy config: add
reverse_proxy /ws/signal localhost:8765underterminal-rf1.novosky.appblock - SHAP computation: after each
build_features()call, computeshap.TreeExplainer(lgb_model).shap_values(X_current)[signal_class]β takes ~20 ms on CPU; acceptable at M15 frequency; include top 10 by abs value intop_shap
23.2 Core dashboard layout (Next.js)
File structure:useSignalStream.ts β reconnect logic:
- Create
app/dashboard/layout.tsxwithclassName="min-h-screen bg-slate-950 text-slate-100 font-mono"β separate from main site layout; no nav bar -
app/dashboard/page.tsx: CSS Grid layout βgrid-cols-[40%_60%]on desktop, single column on mobile; gap-4; all panels inside<Suspense>boundaries -
stores/signalStore.ts: Zustand store with fieldssignal: SignalEvent | null,history: SignalEvent[](last 200),connected: boolean;setSignalappends to history and updates latest - Throttle store updates: wrap
setSignalwith a 250 ms debounce (4 Hz max re-render rate) - Connection status pill: top-right corner, 8px dot β
animate-pulsegreen when connected; amber when reconnecting; static red when disconnected for > 10 s
23.3 Animated confidence meter
Radial arc implementation using SVG + Framer Motion: The arc is drawn as an SVG<path> using polar-to-Cartesian conversion:
-
ConfidenceMeter.tsx: SVG-based radial arc; background arc (dark stroke) + foreground arc animated withmotion.pathandanimate={{ pathLength: confidence }}(Framer Motion SVG animation); center text shows percentage - Spring config:
transition={{ type: "spring", stiffness: 120, damping: 20 }}on pathLength change β avoids linear snap, feels organic - On new signal: trigger outer ring pulse using
useAnimate: - Color: derive from
signal.predictionβemerald-400(BUY),rose-400(SELL),amber-400(HOLD); use CSS variable for smooth color transition viamotion.divanimate={{ color }}withtransition={{ duration: 0.3 }} -
ModelConfidenceBars.tsx: horizontal progress bars per model usingmotion.div; set theanimatewidth toconfidence * 100percent as a string value,transition={{ duration: 0.25 }}
23.4 Model voting panel
Tasks:-
ModelVotingPanel.tsx: 5-card grid (grid-cols-5 gap-3); each card is amotion.divwithlayoutprop (enables FLIP animation on reorder); background color set viaanimate={{ backgroundColor }}β Framer Motion handles color interpolation - Scale pop on vote change: track
prevVoteinuseRef; ifvote !== prevVote, triggeranimate={{ scale: [1, 1.15, 1] }, { duration: 0.2 }} - Consensus glow: when all 5 models agree β
animate={{ boxShadow: "0 0 24px #10b981" }}(emerald for BUY) withtransition={{ repeat: Infinity, repeatType: "reverse", duration: 1.2 }} - Split signal badge: if
Object.values(votes).filter(v => v === prediction).length <= 2, render amberβ Splitbadge usingAnimatePresencefor enter/exit animation (slide down from top) - Majority fraction badge:
"4 / 5 BUY"string derived from vote counts; update without animation (content only)
23.5 Market regime indicator
Regime transition math: Regime changes should feel deliberate, not flickering. Apply a hysteresis rule: only switch regime if the new regime persists for 3 consecutive signals (45 min at M15 frequency):-
RegimeIndicator.tsx: outerAnimatePresence mode="wait"β exit old card (opacity 0, y -20) then enter new card (opacity 1, y 0);transition={{ duration: 0.4 }} - Glow border:
animate={{ boxShadow: glowColor }}β map regime to glow:STRONG_TRENDβ"0 0 30px #10b981",RANGINGβ"0 0 30px #3b82f6",VOLATILEβ"0 0 30px #ef4444",CHOPPYβ"0 0 30px #eab308" - Stats row: ADX value, ATR percentile (as %), recent WR from last 20 signals in
signalStore.history; formatted asADX 0.31 Β· ATR p72 Β· WR 68% - Mini sparkline:
<AreaChart width={120} height={40} data={adxHistory}>β no axes, no labels, just the shape;<Area dataKey="adx" stroke="#94a3b8" fill="transparent" strokeWidth={1.5} />
23.6 Live feature importance bar chart
SHAP value sign convention: positive SHAP means the feature pushed the model towardprediction class; negative means it pulled away. Color accordingly.
Tasks:
-
FeatureImportance.tsx:<BarChart layout="vertical" width={380} height={300}>β horizontal bars;<Bar dataKey="value" animationDuration={300} animationEasing="ease-out">with<Cell fill={v > 0 ? "#14b8a6" : "#f43f5e"} />per bar - Sort top 10 by
Math.abs(shap)descending; truncate feature name to 18 chars;<Tooltip formatter={(v) => v.toFixed(4)} /> - On update: Recharts re-renders with
animationDuration=300automatically animates bar width changes β no extra work needed - Tabs component (shadcn/ui
<Tabs>): tab 1 = SHAP, tab 2 = Attention (disabled/grayed until Phase 22.4 ships); when tab 2 is unlocked, renderAttentionHeatmapinline
23.7 Trade flow animation
State machine β 6 nodes, transitions triggered by WebSocket events:-
TradeFlowPipeline.tsx: horizontal node list with SVG connecting lines; each node is a 40px circle + label below - Active node animation:
motion.div animate={{ rotate: 360 }} transition={{ repeat: Infinity, duration: 2, ease: "linear" }}on the outer ring; inner circle static - Node-to-node hop: use
useEffectonsignal.open_positionto advance state machine; 200 ms delay between hops via sequentialsetTimeoutcalls (notsleepβ usePromisechain) - Connecting line fills as each node activates:
motion.line animate={{ pathLength: isComplete ? 1 : 0 }} transition={{ duration: 0.2 }} - Close event: subscribe to Supabase
tradestable INSERT; on INSERT, determine close type fromclose_typecolumn β pulseMONITORnode emerald (TP / ML_EXIT) or rose (SL_HIT) with 3Γ scale keyframe, then reset state machine to IDLE after 2 s
23.8 TradingView Lightweight Charts integration
Tasks:-
CandlestickChart.tsx: initialize chart inuseEffectwith cleanup; useuseRef<IChartApi>for the chart instance to survive re-renders - On mount: fetch last 200 M15 bars from
GET /api/bars?symbol=BTCUSD&tf=M15&limit=200(add this Next.js API route that proxies to the MT5 HTTP API) - On each
SignalEvent: appendsignal.ohlcvtocandleSeries.update()β Lightweight Charts handles the scrolling automatically - BUY/SELL markers: accumulate
markersarray fromsignalStore.history; callcandleSeries.setMarkers(markers)on each update β Lightweight Charts re-renders only changed markers - SL/TP lines: use
chart.addLineSeries({ lineStyle: LineStyle.Dashed })for SL (rose) and TP (emerald); updatepriceon position change;series.setData([])when no position open - Confidence histogram panel: add
chart.addHistogramSeries({ priceScaleId: 'confidence', height: 80 })β mapssignal.confidenceto bar height; color by direction - ResizeObserver: watch container width changes β
chart.applyOptions({ width: newWidth })for responsiveness
23.9 Neural attention heatmap
ECharts heatmap config:-
AttentionHeatmap.tsx: useecharts-for-reactwrapper; passoptionas prop;style={{ height: 280 }}; updateoptionviauseStatetriggered bysignal.attention_weights - Transform
attention_weights: number[48]intoflatData: for each of the top 10 features (by SHAP), duplicate the bar-level attention weight β weight is the same per bar regardless of feature (TFT VSN gives per-feature weights separately; show VSN weights on Y axis if available) - Show placeholder
<div className="...">Attention available after Phase 22.4</div>ifsignal.attention_weightsis null
23.10 Equity curve + live P&L panel
Live P&L tick calculation:-
EquityPanel.tsx:<AreaChart>from Recharts with gradient fill (<defs><linearGradient>β emerald above baseline, transparent below); data fromuseEquityHistoryhook (Supabase querySELECT equity, created_at FROM account_snapshots ORDER BY created_at DESC LIMIT 2000) -
useEquityHistory.ts: initial query on mount + Supabase realtime subscriptionsupabase.channel('account_snapshots').on('postgres_changes', { event: 'INSERT' }, handler) - Live P&L counter:
<motion.span animate={{ color: livePnl >= 0 ? '#10b981' : '#f43f5e' }}>withtransition={{ duration: 0.15 }}; value formatted as+$123.45/-$12.30(raw USC with $ prefix per CLAUDE.md) - Todayβs stats row: derive from
signalStore.historyβtrades_today = history.filter(s => sameDay(s.ts)),wr_today = wins/trades_today,gross_pnl_today = sum of closed trade pnl from Supabase - Last 5 trades table: columns
Dir Β· Conf Β· P&L Β· Close Type;ML_EXITshown in emerald,SL_HITin rose,TP_HITin teal; sorted by close time descending
23.11 (Stretch) 3D equity surface β Three.js / R3F
Surface data construction: Runpython scripts/surface_sweep.py (new script) that iterates confidence_threshold β [0.55, 0.60, β¦, 0.85] Γ rolling 90-day windows and records cumulative_return at each point. Output: models/surface_data.json with shape (n_thresholds, n_days).
Geometry construction:
-
scripts/surface_sweep.py: runsbacktest.py --oos-onlyin a subprocess for each confidence threshold (7 values Γ 1 sweep = ~15 min total); outputsmodels/surface_data.json -
components/dashboard/EquitySurface3D.tsxusing@react-three/fiber:<Canvas camera={{ position: [8, 5, 8], fov: 45 }}>+<OrbitControls autoRotate autoRotateSpeed={0.5} />- Mesh:
PlaneGeometrywith vertex colors (jet colormap),MeshPhongMaterial({ vertexColors: true, wireframe: false }) - Wireframe overlay: same geometry with
MeshBasicMaterial({ wireframe: true, color: '#1e293b', opacity: 0.3 }) - Lighting:
<ambientLight intensity={0.4} />+<directionalLight position={[10, 10, 5]} /> - Axis labels as
<Text>sprites from@react-three/drei
- Gate behind
?surface=1URL param; add toggle button in dashboard header; lazy-import component to avoid bundling Three.js by default
Full quarterly roadmap β Roadmap.
Completed phases (1β12)
Phase 1 β Fix the model
- SELL recall improved 3% β 30% by adding 5 directional features:
ema_stack,candle_direction,volume_delta,rsi_slope,consecutive_direction - TP=0.3% / SL=0.25% tuning: WR 60.6%β77.3%, PF 1.48β2.85, MaxDD 12%β6.3%
- Training data extended 365 β 730 days;
lookahead_candles12 β 24 - LightGBM feature-name warning fixed (pass numpy array directly)
Phase 2 β Multi-timeframe features
- H4/D1 features resampled from M15:
h4_ema_bias,h4_rsi_norm(#5 SHAP),h4_macd_dir,d1_trend,price_vs_d1_open(#7 SHAP) - Session flags:
is_london_session,is_ny_session,is_asian_session,session_hour_sin/cos - Volatility/momentum:
atr_percentile(top feature),volume_surge,bb_squeeze,price_acceleration - S/R proximity:
dist_to_round_number(#1 SHAP overall),near_daily_high_low,adx_14,market_quality,momentum_decay,adverse_candle_ratio - ATR-aware labeling groundwork:
create_labels_atr_aware()added (activated in Phase 10)
Phase 3 β Hyperparameter tuning + SHAP
- Optuna 50-trial tuning: WF 44.9%β45.7%, PF 2.82β2.96, MaxDD 7.6%β3.8%, Sharpe 7.23β8.63
- SHAP analysis:
ml/shap_analysis.pywith TreeExplainer; beeswarm plots;models/shap_summary.json - Top SHAP features:
h4_rsi_norm>atr_14>hourly_return>price_vs_ema200>session_hour_sin - Feature pruning tested and rejected β removing low-SHAP features hurt performance (WR 55.9%β50%)
- Sequence model deferred (CPU-only hardware too slow)
Phase 4 β Risk management (superseded)
Items superseded by ML-driven config sweeps in Phases 9/13/14.- EMA trend filter, partial profit taking, trailing stop β all superseded by ML active management
max_weekly_drawdown_pctadded toconfig.json(2026-04-11)
Phase 5 β Backtesting
backtest.pybuilt: config-faithful OOS backtester with ONNX inference; WR, PF, Sharpe, MaxDD, Return- v8 results (1yr OOS, 48 features): Setup A WR=76.4% PF=3.20 MaxDD=2.6% | Setup D Return=+747%
- Walk-forward backtest deferred to Phase 16
Phase 6 β Automation & monitoring
- Signal logging to
models/ml_performance.csv - Performance monitoring via weekly walk-forward OOS gate (
weekly_optimize.py) - Live alerts via
trading/telegram_commands.py+scripts/notify.py
Phase 7 β Live trading integration
- Migrated
trading.pyfrom MetaTrader5 Python package to NOVOSKY HTTP API --dryflag added for safe testing- Paper trade validated via OOS backtest: WR=82.7%, PF=4.27, MaxDD=5.2%
Phase 8 β ML active trade management (2026-04-10)
- Dedicated position model: RF+XGB+LGB on 63 features (59 market + 4 position-state)
- Labels: HOLD / EXIT / ADD β
ml/position_labeling.py+ml/position_trainer.py PositionPredictor.get_position_action()with 2/3 majority vote- Kelly-adjusted lot sizing, ML-based SL/TP scaling, partial close, trailing stop
- Results: Signal WF=43.66%, Position ensemble=73.50% | Setup E: WR=78.8% PF=4.52 MaxDD=1.6%
Phase 9 β Growth config sweep (2026-04-10)
- Goal: maximize monthly return for $10k account, IC Markets RAW
- Best: conf=0.55, risk=20%, max_lot=10, all Phase 8 active management disabled
- Result: WR=57.4%, PF=1.75, MaxDD=15.0%, Sharpe=4.68, Return=+8449%, ~340 trades/yr
Phase 10 β Deep optimization (2026-04-11)
Four critical bugs fixed and retrained:- Label-execution mismatch β activated
atr_awarelabeling inml_config.json - Class imbalance β replaced downsample with
compute_sample_weight('balanced') - Spread underestimation β
backtest.pyspread fallback 14.59 - ADX regime filter β new
adx_regime_filterblock inconfig.json
Phase 11 β M15 scalping + local timezone support (2026-04-11)
- H1 β M15 timeframe; 112 trades/yr β 477 trades/yr (1.31/day)
- ATR-aware labels: SL=0.8ΓATR, TP=1.5ΓATR, lookahead=48 bars
sl_atr_multiplier1.0β0.8;min_atr50β15; ADX filter disabled- Local timezone support via
config.json; Telegram redesign - Backtest: WR=63.7%, PF=3.05, MaxDD=19.6%, Sharpe=7.29, Return=+1,284,866%
Phase 12 β Production hardening (2026-04-12)
15 critical issues resolved:- API retry with exponential backoff (3Γ on network errors)
- Atomic
state.jsonwrites via.tmpβos.replace() tracked_positionspersisted tostate.jsonβ survives PM2 restartsrisk_percent6β2;max_consecutive_losses10β5;max_weekly_drawdown_pct0β20- Full retrain: fresh 2yr M15 data, Optuna 50-trial local tuning, OOS backtest