Compare commits

...

27 Commits

Author SHA1 Message Date
github-actions[bot] 50c08866f1 chore(master): release 0.8.0 2026-07-04 10:00:55 +00:00
TPTBusiness 50d1fb47e3 feat: Gold (XAU/USD) — daily swing scanner + TF auto-adaptation
- Gold Swing Scanner: 255 daily strategies, best EMA +2.8% OOS/month
- Auto-adapt timeframes for daily data (1d/1w instead of 15min/4h)
- Session filter skips for daily data
- XAUUSD added to instruments list
2026-06-04 18:36:35 +02:00
dependabot[bot] 68caa7e88c chore(deps): Update aiohttp requirement from >=3.13.4 to >=3.14.0 (#65)
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.14.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 15:00:15 +02:00
dependabot[bot] 891daf4df9 chore(deps): Update streamlit requirement from >=1.57.0 to >=1.58.0 (#64)
Updates the requirements on [streamlit](https://github.com/streamlit/streamlit) to permit the latest version.
- [Release notes](https://github.com/streamlit/streamlit/releases)
- [Commits](https://github.com/streamlit/streamlit/compare/1.57.0...1.58.0)

---
updated-dependencies:
- dependency-name: streamlit
  dependency-version: 1.58.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 14:54:55 +02:00
dependabot[bot] 56346d5ae5 chore(deps): Update litellm requirement from >=1.83.14 to >=1.86.2 (#63)
Updates the requirements on [litellm](https://github.com/BerriAI/litellm) to permit the latest version.
- [Release notes](https://github.com/BerriAI/litellm/releases)
- [Commits](https://github.com/BerriAI/litellm/compare/1.84.0-dev.1...v1.86.2)

---
updated-dependencies:
- dependency-name: litellm
  dependency-version: 1.86.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 14:49:40 +02:00
TPTBusiness 723ba6f004 feat: add XAUUSD (Gold) to instruments for multi-asset discovery 2026-06-04 14:21:41 +02:00
TPTBusiness eb6b2dcd1f feat: Grid Search — systematic parameter scanning for 10 indicators
- Tests ALL parameter/TF combinations (603 total, vs random sampling)
- Guarantees global optimum discovery (random search converges to local)
- 10 indicators: MACD, Donchian, SAR, ADX, RSI, BBands, ROC, MOM, Stoch, CCI
- Multi-instrument: EUR/USD, GBP/USD, BTC/USD
- Session filter only (no vola — vola killed forex in V3)
2026-06-04 13:56:46 +02:00
TPTBusiness a721605f4b fix: disable vola filter — it killed EUR/GBP profitability
Vola filter (min ATR 0.03%) destroyed EUR (-2→0) and GBP (-7→-15) OOS.
Without vola: EUR +7.4% OOS, GBP +22.4% OOS, BTC +102.5% OOS.
Portfolio avg: +44.1%/month OOS (was Bitcoin-only).
2026-06-01 14:44:56 +02:00
TPTBusiness a9d181398b feat: News filter + soft cross-pair confirmation in R&D loop
- News filter: Block trades 5min before/after high-impact events (per currency)
- Cross-pair: Cancel GBP trades when EUR momentum strongly opposes (>0.03%)
- Soft cross-pair improved GBP from -76 to -9 Sharpe
- EUR/USD occasionally positive (+0.8) for first time
2026-05-31 20:28:30 +02:00
TPTBusiness 4773e95a6c feat: R&D Loop V2 — Multi-Instrument + Correlation Score + Session/Vola Filter
- 3 instruments: EUR/USD, GBP/USD, BTC/USD with combined evaluation
- Correlation-aware composite score: Sharpe × (1 - 0.5×corr) × (0.3 + 0.7×OOS_ratio)
- Session filter: London 07-16 UTC only (reduces false signals)
- Volatility filter: Skip trades when ATR < 0.03% (quiet markets)
- OOS split: 80/20 IS/OOS with separate metrics
- Similarity dedup: Skip near-identical strategies in SOTA
- SOTA expanded to 30 (was 20)
- Discovered: Donchian 58.1%, SAR 56.6%, MOM 33.8% monthly (multi-instrument)
- MACD dominance broken: 4+ different indicators in SOTA
2026-05-31 18:18:07 +02:00
TPTBusiness 9ce6a4e6ec fix: ML trigger priority over Optuna (2000 % 500 == 0 collision) 2026-05-31 17:49:49 +02:00
TPTBusiness b793a8114b fix: adaptive exploration boost when SOTA dominated by single indicator
- +25% explore when >80% SOTA shares same indicator
- Force non-dominant indicator every 100 iterations
- Base exploration raised to 40% (effective 30% with 20 SOTA)
2026-05-31 17:43:45 +02:00
TPTBusiness 6bce4f2405 feat: multi_role strategy — trend filter + entry gating across TFs
- New strategy type: trend_ind(higher TF) → entry_ind(lower TF)
- Entry only fires when trend confirms direction (directional gating)
- 15² × 3×2 = 1,350 indicator/TF combinations
- Found: MACD(30min)→ADX(15min) = Sharpe 102.37, +32.2%/month
2026-05-31 17:39:48 +02:00
TPTBusiness 2e028ffc1e fix: raise exploration rate to 30% — discover indicators beyond MACD 2026-05-31 17:28:44 +02:00
TPTBusiness 7d7c267d29 docs: rewrite README — Numba loop, Optuna, ML, zero-LLM strategy discovery 2026-05-31 17:25:43 +02:00
TPTBusiness 6cd362aa25 feat: R&D loop — Optuna optimization + LightGBM ML training
- Optuna: every 500 iterations, 20-trial hyperparameter optimization
- ML: every 2000 iterations, LightGBM classifier on SOTA indicator signals
- Numba backtest: 245× faster (735M bars/s)
- All 3 discovery methods: explore → exploit → optuna → ml
2026-05-31 17:21:23 +02:00
TPTBusiness a373710454 perf: Numba GPU-accelerated backtest — 245× faster (735M bars/s)
- Replaced vbt_backtest with Numba JIT-compiled bar-by-bar simulation
- 2.26M bars in 0.003s (was 0.74s)
- 50,000 iterations now 2.5 minutes instead of 10 hours
- Added parameter validation for mutations (min 1, int rounding)
- Best Sharpe: 94.89 (ROC) — 28% monthly
2026-05-31 17:06:07 +02:00
TPTBusiness ee3d7786c3 feat: new R&D loop — indicator discovery with exploit/explore mechanics
- Replaces broken factor pipeline with working indicator-based loop
- Architecture: hypothesize → evaluate → feedback → record
- Bandit-inspired: 70% exploit (mutate best), 30% explore (random)
- 15 indicators, 3 strategy types (single, multi-tf, portfolio)
- 300 iterations in 215s — discovered MACD 4-TF at Sharpe +28.93
- Adaptive exploration rate (30%→10% as SOTA grows)
- Autonomous improvement: SAR(+16)→SAR(+22)→MACD(+25)→MACD(+28)
2026-05-30 12:48:39 +02:00
TPTBusiness 4b6dff1710 feat: migrate R&D loop to TA-Lib (17 indicators, 161 available)
- Replaced 7 hand-rolled indicators with TA-Lib equivalents
- Added 10 new TA-Lib indicators: Stoch, CCI, WillR, ADX, SAR, ROC, MOM, AROON, MFI, UltOsc, NATR
- Indicator functions now accept (close, high, low, volume, **params) for full OHLCV access
- quantstats integration for professional HTML reports
- Riskfolio-Lib installed for future portfolio optimization
2026-05-30 10:43:00 +02:00
TPTBusiness 7c22287793 fix: case-insensitive assertion in test_add_column_idempotent 2026-05-25 19:43:03 +02:00
TPTBusiness 3874afb8dd feat: expand indicator library from 7 to 14
- Added: Stochastic, CCI, Williams %R, ROC Momentum, EMA Crossover, Keltner Channel, ADX
- Total: 14 indicators across 3 strategy types (single-TF, multi-TF, portfolio)
- Loop running 2000 iterations in background
2026-05-25 19:42:34 +02:00
TPTBusiness e168a5df7e fix: harmonize risk field names and case-insensitive DB column check
- vbt_backtest: unify risk_* → riskmgmt_* field names in _apply_risk_mask
- results_db: case-insensitive column existence check
- test_ftmo_oos: update test assertions to match renamed fields
2026-05-25 12:30:10 +02:00
TPTBusiness 8806b12ad6 docs: remove closed-source live trader reference from README 2026-05-25 12:16:04 +02:00
TPTBusiness 61e6a09b95 docs: remove forex-specific language from README
- Framework is instrument-agnostic, not EUR/USD-specific
- Trading strategies are closed-source — repo contains research framework only
- Clarify open-source scope: factor generation, model evolution, backtesting engine
- Update data setup examples to use generic symbol names
2026-05-25 12:14:36 +02:00
TPTBusiness 9303b40fb9 feat: R&D loop fixes + new price-action research loop
Loop 1 (Factor R&D):
- Auto-fixer: composite normalization prevents single-factor variance collapse
- Caps entry_thresh 0.7, exit_thresh 0.3, window 20, rolling smoothing 2
- Adds unit-variance normalization for any factor count

Loop 2 (Price-Action R&D):
- New research loop for technical indicators (no LLM, no Docker)
- 7 indicators: MACD, Donchian, RSI, SMA, Bollinger, ATR, MA-Envelope
- 3 strategy types: single-TF, multi-TF majority-vote, portfolio
- Random hypothesis generation + backtest_signal evaluation
- 11/20 strategies profitable in first test run
- Top: MACD(12,15,3) 15min — Sharpe +14.01, +10.4%/month
2026-05-25 11:56:21 +02:00
TPTBusiness ab57498ccf feat: live price-action pipeline — Donchian+MACD majority-vote signals
- Live signal generation (nexquant_live_priceaction.py)
- Backfill mode for historical backtest verification
- Daemon mode for continuous signal output
- Archived 146 fabricated strategies -> results/archive_broken/
- Pipeline produces real, testable daily signals for EUR/USD
2026-05-22 22:16:37 +02:00
TPTBusiness 6f399c1d96 feat: price-action strategy generator — no LLM, no factors, 38 profitable strategies
- Donchian(5,1): Sharpe +5.24, +3.1%/month, 87.6% WR, 354 trades
- MACD(5,20,3): Sharpe +5.57, +3.8%/month, 88.4% WR, 346 trades
- ATR_Breakout(10,1): Sharpe +3.25, +2.1%/month
- 7 strategy templates: Donchian, SMA, RSI, Bollinger, MACD, MA-Envelope, ATR
- Grid search over 90 parameter combinations in 31 seconds
- Uses backtest_signal for consistent evaluation
2026-05-22 15:43:00 +02:00
14 changed files with 2602 additions and 553 deletions
+1 -1
View File
@@ -1 +1 @@
{".": "1.5.0"}
{".":"0.8.0"}
+353
View File
@@ -1,5 +1,358 @@
# Changelog
## [0.8.0](https://github.com/NicolasBohn/NexQuant/compare/v1.5.0...v0.8.0) (2026-07-04)
### Features
* [AutoRL-Bench] Update DeepSearchQA split and translate task instructions to English ([#1368](https://github.com/NicolasBohn/NexQuant/issues/1368)) ([ffb9491](https://github.com/NicolasBohn/NexQuant/commit/ffb9491c4703290a5b292baa6328ae06bc520f9b))
* 15% monthly return target — infrastructure + daily signal resampling ([e0000a1](https://github.com/NicolasBohn/NexQuant/commit/e0000a18d2dac5eda9f6328f1795b46bc4ba8422))
* 1h London session momentum — +3.17%/month unlevered, -1.1% DD, RiskMgmt-safe ([f10b257](https://github.com/NicolasBohn/NexQuant/commit/f10b2571529ab72bc0f62721418e01527e4f49a6))
* 1h SMA10/30 signal integrated into RiskMgmt live trader ([e3a65bb](https://github.com/NicolasBohn/NexQuant/commit/e3a65bb140880100eb7ec93c0c4b5b1a3fa83f9f))
* 30min factor combo bests 1h — +3.59%/month (+54% annual) ([918639c](https://github.com/NicolasBohn/NexQuant/commit/918639c051b62c95cf54fd68638a539e67e806d2))
* 9 additional daily strategies — ensembles, trailing stops, day filters ([aa7e046](https://github.com/NicolasBohn/NexQuant/commit/aa7e04678207301f61bb9cadb22403a499ecf7c5))
* Add 'predix evaluate' command to CLI ([f0814c6](https://github.com/NicolasBohn/NexQuant/commit/f0814c6bf783fb67e5e7059491a4d13c9ef1d724))
* Add 'predix top' command + explain factor evaluation results ([00a1d48](https://github.com/NicolasBohn/NexQuant/commit/00a1d48aad2a25d6d51d9af123d4df086d73b39b))
* Add 6 new CLI commands - all scripts integrated with local LLM ([1fbf094](https://github.com/NicolasBohn/NexQuant/commit/1fbf09411590ec660035bcba1d5cff5d77afdad7))
* add a rag mcp in proposal ([#1267](https://github.com/NicolasBohn/NexQuant/issues/1267)) ([dc7b732](https://github.com/NicolasBohn/NexQuant/commit/dc7b732b2c428e3cca3373e839a0e724a844c79b))
* add a web UI server ([#1345](https://github.com/NicolasBohn/NexQuant/issues/1345)) ([1439548](https://github.com/NicolasBohn/NexQuant/commit/14395488b9c7ea476022a32211ea46de9925cf11))
* Add advanced ML models (Transformer, TCN, PatchTST, CNN+LSTM) ([e1e86e1](https://github.com/NicolasBohn/NexQuant/commit/e1e86e1bd322b635c541be7fbacd18bc83fa4357))
* Add AI Strategy Builder (StrategyCoSTEER) - Closed Source ([a8c23bd](https://github.com/NicolasBohn/NexQuant/commit/a8c23bd130ecb5091bd534dd2c5059e53e20d892))
* Add beautiful CLI welcome screen for GitHub README ([0fd366d](https://github.com/NicolasBohn/NexQuant/commit/0fd366dd59471184eca8a9d13396a11c2b030165))
* Add CLI model selection (local vs OpenRouter) ([d3ae6df](https://github.com/NicolasBohn/NexQuant/commit/d3ae6df4543087a047d2fa61b799592e55e40cab))
* Add complete ML pipeline with graceful degradation (closed source) ([760961d](https://github.com/NicolasBohn/NexQuant/commit/760961d5e767b90e3750281fe1cb7b78c34c8707))
* add daily log rotation, llama health wait, factor auto-fixer, and README updates ([2cec08b](https://github.com/NicolasBohn/NexQuant/commit/2cec08bc912726d1d66d2f14b364b377d0d76720))
* Add factor code and description to saved results ([b27c4b7](https://github.com/NicolasBohn/NexQuant/commit/b27c4b7517f4c3bf6a8d441dda7b721150287a34))
* Add GitHub infrastructure, CI/CD pipelines, and examples ([b98c9cd](https://github.com/NicolasBohn/NexQuant/commit/b98c9cd572dd26006df1d5291a29fe45d893f4fb))
* add improve_mode to MultiProcessEvolvingStrategy for selective task implementation ([#1273](https://github.com/NicolasBohn/NexQuant/issues/1273)) ([03f22dc](https://github.com/NicolasBohn/NexQuant/commit/03f22dc7c72a039ee6f1a0e8d0393f35117ec3e1))
* Add improved local prompt with MultiIndex code examples (v3) ([86d4150](https://github.com/NicolasBohn/NexQuant/commit/86d415056ec8cf6d229bcf80ba87e77a31440ff6))
* add Kronos CLI commands, expand tests, document in README ([3f46022](https://github.com/NicolasBohn/NexQuant/commit/3f460226f26449ff04e2ab8d8ea304eeeced3653))
* add LLM-finetune scenario ([#1314](https://github.com/NicolasBohn/NexQuant/issues/1314)) ([6e19c9e](https://github.com/NicolasBohn/NexQuant/commit/6e19c9e632cf07059c19993f2d4fbc772fb3cf13))
* Add model loader system (same as prompts) ([19855ef](https://github.com/NicolasBohn/NexQuant/commit/19855ef7d690fca05415212669c1342f999d458e))
* Add P5 ML Training Pipeline with LightGBM and 46 tests ([d124304](https://github.com/NicolasBohn/NexQuant/commit/d12430427dacc12c7628946e6a6b6db17b66fc11))
* Add parallel run system with API key distribution ([68ea969](https://github.com/NicolasBohn/NexQuant/commit/68ea969c3257184d6956f28bfa33a3bf5044ff7b))
* Add RL Trading Agent system with 99 tests ([1bbca06](https://github.com/NicolasBohn/NexQuant/commit/1bbca062af1e1377602aa2c5a4b66ca88618ccfe))
* add runtime backtest verification (10 invariant checks in &lt;1ms) + 489 tests + README docs ([6d37f89](https://github.com/NicolasBohn/NexQuant/commit/6d37f8956fd5750b42f6bd0ae9d8c0c8adc72eb8))
* Add simple factor evaluator with direct IC/Sharpe computation ([c861f44](https://github.com/NicolasBohn/NexQuant/commit/c861f4480fa2781a8fe0a0d7430863cb1710fb08))
* Add start_llama and start_loop CLI commands ([362cff2](https://github.com/NicolasBohn/NexQuant/commit/362cff291c707ae4ac9ececdc0a757b19a1d1f96))
* Add Trading Protection System with 4 protections + comprehensive tests ([bd025e5](https://github.com/NicolasBohn/NexQuant/commit/bd025e50dc3583f88a3a681c444ab79a38bcfc64))
* add user interaction in data science scenario ([#1251](https://github.com/NicolasBohn/NexQuant/issues/1251)) ([6e09dc6](https://github.com/NicolasBohn/NexQuant/commit/6e09dc6d692f3ae2fcc0ffddf620e8f3e8dc1bd9))
* add XAUUSD (Gold) to instruments for multi-asset discovery ([723ba6f](https://github.com/NicolasBohn/NexQuant/commit/723ba6f004541900cc5c5d4e5b098c06f80c7b09))
* auto-mode live strategy — factors when fresh, SMA fallback ([6975f77](https://github.com/NicolasBohn/NexQuant/commit/6975f77b7780b0d1b625652f31298efe4acdc3e8))
* auto-post releases to Mastodon and X/Twitter via GitHub Actions ([22c8092](https://github.com/NicolasBohn/NexQuant/commit/22c8092f1c4d36ff92dc8acb79012edc20ecd8a1))
* Auto-start dashboard for fin_quant ([3441604](https://github.com/NicolasBohn/NexQuant/commit/34416041c122b6a51ce94db1031f315c3639a4a5))
* Auto-start dashboard for fin_quant ([52d2b89](https://github.com/NicolasBohn/NexQuant/commit/52d2b8914815fa97d6b53b7cc7e817828520817e))
* **backtest:** add RiskMgmt-realistic backtest mode with leverage, daily/total loss limits and realistic EUR/USD costs ([6c10017](https://github.com/NicolasBohn/NexQuant/commit/6c100170bd5534e330f8f671537d482017bc96ac))
* **backtest:** add rolling walk-forward validation and Monte Carlo trade permutation test ([b0490c5](https://github.com/NicolasBohn/NexQuant/commit/b0490c5f0d1e3d0f3808d10e8f95d0edd97e0b49))
* **backtest:** add walk-forward OOS validation to backtest_signal_riskmgmt ([f166cc3](https://github.com/NicolasBohn/NexQuant/commit/f166cc332616a0658786c3101523f3c702db5f6b))
* Backtesting Engine + Risk Management + Results Database ([8339be2](https://github.com/NicolasBohn/NexQuant/commit/8339be2098eea8a99381ea983a6b64306fa02208))
* Backtesting Engine + Risk Management + Results DB ([4690b01](https://github.com/NicolasBohn/NexQuant/commit/4690b01042a398234d0adb2c215cf975b6a24147))
* **backtest:** use backtest_signal_riskmgmt in strategy orchestrator and optuna optimizer ([f8d1d36](https://github.com/NicolasBohn/NexQuant/commit/f8d1d36cf0a6bdddf49d846d30c349b5f0a4363e))
* Beautiful CLI dashboard + corrected start command ([c2932cb](https://github.com/NicolasBohn/NexQuant/commit/c2932cb06904b041e1376d534309864d9d0e9122))
* Centralize all prompts in prompts/ directory ([18416da](https://github.com/NicolasBohn/NexQuant/commit/18416da2c9aaf36211e3fa22a11fb1e5fe89a85a))
* CLI Commands for strategy generation (P4 complete) ([352dd08](https://github.com/NicolasBohn/NexQuant/commit/352dd08514a8c50132d77c3a4d2d42ad00817bee))
* Complete P6-P9 implementation (73 tests) ([0b168fd](https://github.com/NicolasBohn/NexQuant/commit/0b168fd3e49da60e7cd71b80b5f74aba0ba57aec))
* continuous strategy generator (WF, MTF, stability, ML models, auto-ensemble) ([33f6daf](https://github.com/NicolasBohn/NexQuant/commit/33f6daf1d2b55fab8ebf643f864564f9a8e59e8c))
* daily strategy generator — grid search SMA/EMA/RSI/MACD/BB (14/55 profitable) ([54b8713](https://github.com/NicolasBohn/NexQuant/commit/54b8713938e091e6a25d331bad4d67ab699aaaf0))
* Data Loader module with tests (P0 complete) ([11d96ec](https://github.com/NicolasBohn/NexQuant/commit/11d96ec3bd2649d451b132f74d34051ac8554bfb))
* Diverse factor selection + improved prompt v3 ([0acdfaa](https://github.com/NicolasBohn/NexQuant/commit/0acdfaa4851f6d41fe69c8cc4f8d97b5f49d5328))
* enable walk-forward OOS validation by default in backtest_signal_riskmgmt ([c0ec1b3](https://github.com/NicolasBohn/NexQuant/commit/c0ec1b39e1fe803527d84d2169207e978ed154e9))
* EURUSD FX patches - prompts, factor spec, experiment settings ([b6cf687](https://github.com/NicolasBohn/NexQuant/commit/b6cf6874db995ea160457a1628a5691cbc8e5b97))
* EURUSD model experiment setting + model simulator text patched ([9a17b25](https://github.com/NicolasBohn/NexQuant/commit/9a17b25d32729453a28dd36246be4c5fdbd3a667))
* EURUSD Trading-Verbesserungen (Phase 2 & 3) ([05c4e1b](https://github.com/NicolasBohn/NexQuant/commit/05c4e1ba54b9259d6cc5f0af00a177d9295278a9))
* EURUSD Trading-Verbesserungen implementiert (Phase 1) ([b95bbf5](https://github.com/NicolasBohn/NexQuant/commit/b95bbf5900a9e06194ab0e330b662e2b853006ea))
* EURUSD walk-forward splits, bars terminology, README no $factor ([0eae7d0](https://github.com/NicolasBohn/NexQuant/commit/0eae7d0ababb422927dd0123118b97724d066ab0))
* expand indicator library from 7 to 14 ([3874afb](https://github.com/NicolasBohn/NexQuant/commit/3874afb8ddb1da19bf867730b5388b450468a8fd))
* **factor-coder:** Add critical rules to prevent common factor implementation errors ([b9fe985](https://github.com/NicolasBohn/NexQuant/commit/b9fe985a55df5c11d21826a837ff0f0f75aea746))
* Fast mode - CoSTEER goes to backtest after 1 iteration ([ff893d6](https://github.com/NicolasBohn/NexQuant/commit/ff893d6c74e9cd9de52c8c36748b15a44619db0f))
* **fin_quant:** auto-generate Kronos factor before loop start ([3f54381](https://github.com/NicolasBohn/NexQuant/commit/3f54381052360b0d94d0e5e5755e9dba848969c1))
* Fix 1min data integration and centralize all prompts ([7e7e40b](https://github.com/NicolasBohn/NexQuant/commit/7e7e40b04117b1f1d4c263fb21dc62a287c952af))
* Fix realistic backtesting (Step 1+2) ([b63380e](https://github.com/NicolasBohn/NexQuant/commit/b63380e3ce38868d0dcca2a8eb3eed5f84c4a2f5))
* Full auto strategy generation in fin_quant loop ([ad20634](https://github.com/NicolasBohn/NexQuant/commit/ad206345ccdc09556e8b72ba127b1944c7288734))
* Full system integration - RL + Protections + Backtesting + CLI ([5ce86c8](https://github.com/NicolasBohn/NexQuant/commit/5ce86c824e0dd8cacfdf536f00f62ac15cd0b3b8))
* FX feedback loop, EURUSD ticker examples, bars terminology ([781779a](https://github.com/NicolasBohn/NexQuant/commit/781779a1f8c853eb77253053e23bc10c46dcf402))
* FX Multi-Agent Validator (TradingAgents-inspired) - Session/Macro/Bull-Bear/Trader ([cddfc53](https://github.com/NicolasBohn/NexQuant/commit/cddfc53ab07ca75b2364c30b9c2a794383633c2b))
* Gold (XAU/USD) — daily swing scanner + TF auto-adaptation ([50d1fb4](https://github.com/NicolasBohn/NexQuant/commit/50d1fb47e34de8b56a88d2f5a2e74cd99a43c02b))
* Grid Search — systematic parameter scanning for 10 indicators ([eb6b2dc](https://github.com/NicolasBohn/NexQuant/commit/eb6b2dcd1f04070120f820761bb372b3791f5720))
* Improve predix portfolio command with robust error handling ([971a253](https://github.com/NicolasBohn/NexQuant/commit/971a2535d63b23db62cfd872e05949ea558214f3))
* Improved LLM prompt + Optuna integration (Step 3+5) ([fceee44](https://github.com/NicolasBohn/NexQuant/commit/fceee449678182e558eb8bf35620bf4e88c35926))
* Integrate critical features into fin_quant workflow (P0+P1) ([74d5a82](https://github.com/NicolasBohn/NexQuant/commit/74d5a8234ec68b646aa0a95e1824b471f4c70207))
* Integrate factor code/description saving into fin_quant process ([c049742](https://github.com/NicolasBohn/NexQuant/commit/c049742df78ba647a47e4c5a22a6239b67932425))
* integrate Kronos foundation model into fin_quant R&D loop ([584bf9d](https://github.com/NicolasBohn/NexQuant/commit/584bf9d955d8fa76ec1a13b359eb68c8c5c9f9ec))
* integrate Kronos-mini OHLCV foundation model (Option A + B) ([1f6990d](https://github.com/NicolasBohn/NexQuant/commit/1f6990d04d61e1ad73b9acf20c1f94ce3ec0c477))
* Intelligent embedding chunking instead of truncation ([2d0584b](https://github.com/NicolasBohn/NexQuant/commit/2d0584b4cd7c1b3d9623acd6e141035d51f535fa))
* inverse factor signal combos — top-3 gives +0.57%/month with -3.7% DD ([15c03df](https://github.com/NicolasBohn/NexQuant/commit/15c03df431ffcb515ddabe1c23854b68f58cdea2))
* live 1h London momentum strategy + multi-timeframe generator ([c45b911](https://github.com/NicolasBohn/NexQuant/commit/c45b911abe4b63094e799fb6cc7d85dca00865ff))
* live price-action pipeline — Donchian+MACD majority-vote signals ([ab57498](https://github.com/NicolasBohn/NexQuant/commit/ab57498ccf55a7d0772f435a1abed669b4aa6a25))
* **logging:** write complete LLM prompts and responses to daily JSONL log ([f24f678](https://github.com/NicolasBohn/NexQuant/commit/f24f678713af6402282efcfd494cc172f1665472))
* **mcp:** cache with one-click toggle ([#1269](https://github.com/NicolasBohn/NexQuant/issues/1269)) ([4f493c8](https://github.com/NicolasBohn/NexQuant/commit/4f493c8d637dfda42f84af0dc08f8ecfc0501668))
* mcts policy based on trace scheduler ([#1203](https://github.com/NicolasBohn/NexQuant/issues/1203)) ([ac6d8ed](https://github.com/NicolasBohn/NexQuant/commit/ac6d8edad4366b08b5caf75e9a5ee8da0061a078))
* migrate R&D loop to TA-Lib (17 indicators, 161 available) ([4b6dff1](https://github.com/NicolasBohn/NexQuant/commit/4b6dff1710302640de757c08f545dc76fe258c9f))
* migrate to 1min EURUSD data (2020-2026) ([b39f2b7](https://github.com/NicolasBohn/NexQuant/commit/b39f2b7e46384c4fc56c1274c9120c470313262b))
* ML Training Pipeline with 46 tests (P5 complete) ([03536af](https://github.com/NicolasBohn/NexQuant/commit/03536af00096b4c486710ce6f684d48ecc4f0494))
* model-track bias + daily/portfolio tools ([d4611b5](https://github.com/NicolasBohn/NexQuant/commit/d4611b530e289dc723a7dd82b514aad417944bf0))
* multi_role strategy — trend filter + entry gating across TFs ([6bce4f2](https://github.com/NicolasBohn/NexQuant/commit/6bce4f240589cc1e4d510f152ed5e0616c915b76))
* multi-asset data pipeline, daily strategy generator, ML pipeline ([90690c1](https://github.com/NicolasBohn/NexQuant/commit/90690c1675edee955bf013839960b61b4c37a509))
* new R&D loop — indicator discovery with exploit/explore mechanics ([ee3d778](https://github.com/NicolasBohn/NexQuant/commit/ee3d7786c3927dd82013343c25f2e6fdef67ced2))
* News filter + soft cross-pair confirmation in R&D loop ([a9d1813](https://github.com/NicolasBohn/NexQuant/commit/a9d181398b1343587fda5a4f71c3af6b5f7a4e25))
* optimize strategy generator (cache OHLCV, min_sharpe 1.5, predix generate-strategies CLI) ([1827c50](https://github.com/NicolasBohn/NexQuant/commit/1827c503448ff6029ea7525ca2722a64a728134a))
* **optimizer:** add max_positions parameter to Optuna search space ([c5d919f](https://github.com/NicolasBohn/NexQuant/commit/c5d919f58171e4a13abeee12873641d256fc3791))
* Optuna Parameter Optimizer with 60 tests (P3 complete) ([4133d62](https://github.com/NicolasBohn/NexQuant/commit/4133d627605e73a511b8d79ac35bd4cf1730bdc7))
* Optuna-optimized RF ML pipeline for daily strategies (+0.61%/month) ([5620ea1](https://github.com/NicolasBohn/NexQuant/commit/5620ea1b0ecb0e0de6f2ba4d27b02e1b1aabaae5))
* PDF performance reports for strategies (reportlab) ([3e4bfbc](https://github.com/NicolasBohn/NexQuant/commit/3e4bfbc61e95a33400bf0521d0ca3e2e3815bf16))
* predix.py wrapper for dashboard support ([757c66c](https://github.com/NicolasBohn/NexQuant/commit/757c66cddb18254220db1d571d9b739380c57f44))
* price-action strategy generator — no LLM, no factors, 38 profitable strategies ([6f399c1](https://github.com/NicolasBohn/NexQuant/commit/6f399c1d96f17bfb3ffc1265b5607dfdd81ec623))
* prioritize Kronos foundation model factors in strategy selection ([9c91a69](https://github.com/NicolasBohn/NexQuant/commit/9c91a6938dd0ef7107124b20c0919057fbe31f88))
* R&D loop — Optuna optimization + LightGBM ML training ([6cd362a](https://github.com/NicolasBohn/NexQuant/commit/6cd362aa25959bf4dc0c3ba0411176fc0a422f6e))
* R&D loop fixes + new price-action research loop ([9303b40](https://github.com/NicolasBohn/NexQuant/commit/9303b40fb973f091211d1daf1b099262fa689e31))
* R&D Loop V2 — Multi-Instrument + Correlation Score + Session/Vola Filter ([4773e95](https://github.com/NicolasBohn/NexQuant/commit/4773e95a6c77f7ddb180c2f593e591275a153cb1))
* Realistic backtesting with OHLCV data (P5 continued) ([4c45ba3](https://github.com/NicolasBohn/NexQuant/commit/4c45ba33ab348ff09add654c6cd27f9e7c842f63))
* Realistic backtesting with OHLCV data and spread costs ([8aa28ff](https://github.com/NicolasBohn/NexQuant/commit/8aa28ffb3340e461db4cc208a649a9681472f6a0))
* Redirect RD-Agent workspace to results/ directory ([6875be6](https://github.com/NicolasBohn/NexQuant/commit/6875be6435a2108133aeadba3cf81517f9a97a57))
* **rl:** add AutoRL-Bench framework and benchmark integrations ([#1348](https://github.com/NicolasBohn/NexQuant/issues/1348)) ([7cd64a2](https://github.com/NicolasBohn/NexQuant/commit/7cd64a26fd84017042eb163e8eb4d3bd30c16de7))
* run Kronos on CPU to avoid GPU conflict with llama-server ([e0c287a](https://github.com/NicolasBohn/NexQuant/commit/e0c287a575b6dfc1543543419304ed0dd46858cc))
* Save all factor results to results/factors/ ([715555b](https://github.com/NicolasBohn/NexQuant/commit/715555b7d157e1ca67cf4e15fa503557fa4ba539))
* Save factor results immediately after each evaluation ([8e0a7e3](https://github.com/NicolasBohn/NexQuant/commit/8e0a7e3f26121953c4cfccc6bcca5c96fe32f6b3))
* **scripts:** add full file logging to strategy generation and rebacktest scripts ([9102f3c](https://github.com/NicolasBohn/NexQuant/commit/9102f3ce96a838fb305324696fda5f94a27c6797))
* show the summarized final difference between the final workspace and the base workspace ([#1281](https://github.com/NicolasBohn/NexQuant/issues/1281)) ([35a7ae5](https://github.com/NicolasBohn/NexQuant/commit/35a7ae5e1ff929b3ee3b77c04cb1f4a684a4b2d7))
* **strategies:** make OOS validation mandatory in strategy generator ([f726e93](https://github.com/NicolasBohn/NexQuant/commit/f726e939ab759a5b92ce9e199eee3bc57e1ad10d))
* Strategy Generator working with local LLM (P0-P4) ([108a63f](https://github.com/NicolasBohn/NexQuant/commit/108a63fd792985e642092c5ecd727db5f2a288fb))
* Strategy Orchestrator with 30 tests (P2 complete) ([bef1d77](https://github.com/NicolasBohn/NexQuant/commit/bef1d77ee0e1e8f018cca5f37243b8429c107511))
* Strategy performance reports, CLI docs, and README update ([360bd26](https://github.com/NicolasBohn/NexQuant/commit/360bd26d4f778259878d74ff563ae92dc1322fa1))
* Strategy Worker module with 41 tests (P1 complete) ([6ba2bc0](https://github.com/NicolasBohn/NexQuant/commit/6ba2bc0c8f97f7111a0cc2fa8200a7d306fb8284))
* **strategy:** Continuous optimization with Optuna parameter injection ([6ee6c52](https://github.com/NicolasBohn/NexQuant/commit/6ee6c5210d0d7aa09b7d25bf0b4c9ec2c1c22014))
* Support 25+ parallel runs with resource warnings ([56d73d2](https://github.com/NicolasBohn/NexQuant/commit/56d73d22e9318e55687db5f724a4da695a330297))
* support Kronos-small and Kronos-base models, auto-select GPU/CPU ([72e8a43](https://github.com/NicolasBohn/NexQuant/commit/72e8a4306ef8383092805f9dedaaeea78d8902a6))
* unified backtest engine, LLM error handling, strategy refactor ([22e638a](https://github.com/NicolasBohn/NexQuant/commit/22e638af86bcc7536351ed549c9df2cef530c685))
* update README with latest paper acceptance to NeurIPS 2025 ([#1252](https://github.com/NicolasBohn/NexQuant/issues/1252)) ([12969b4](https://github.com/NicolasBohn/NexQuant/commit/12969b491eafab626ce71f7e530458dab6f43246))
* zentrale data_config.yaml + apply_config.py für dynamische Datenkonfiguration ([b7c1e4d](https://github.com/NicolasBohn/NexQuant/commit/b7c1e4db8e29e960fe28393911d60fc0fd3ca413))
### Bug Fixes
* (to main) litellm's Timeout error is not picklable ([#1294](https://github.com/NicolasBohn/NexQuant/issues/1294)) ([315850e](https://github.com/NicolasBohn/NexQuant/commit/315850ea81761aa2478639ad32302d7a55f8181b))
* 15 bug fixes across orchestrator, runner, backtest, and infrastructure ([aba88dd](https://github.com/NicolasBohn/NexQuant/commit/aba88dd0904a99fbe118169ca44a673fe62b4f22))
* adaptive exploration boost when SOTA dominated by single indicator ([b793a81](https://github.com/NicolasBohn/NexQuant/commit/b793a8114bb98b395f480bbb18953dcdd470c83d))
* Add Bandit security scanning and fix critical vulnerabilities ([f3a2e2b](https://github.com/NicolasBohn/NexQuant/commit/f3a2e2b4f1e8bf5c4d8c47a7ce47019c470cafa8))
* Add critical column name rules to factor generation prompt ([19c9b88](https://github.com/NicolasBohn/NexQuant/commit/19c9b88b7682e6cc0b073467dd143134a89c43c5))
* Add get_factor_count() to QuantTrace to prevent parallel run crashes ([ac9bfc6](https://github.com/NicolasBohn/NexQuant/commit/ac9bfc6fb47dd1754a60faef7060895469480577))
* add hypothesis to test deps and fix missing imports in deep tests ([abca9eb](https://github.com/NicolasBohn/NexQuant/commit/abca9eb899decd68f2f657d6cfa860ccce845dc1))
* add json format response fallback to prompt templates ([#1246](https://github.com/NicolasBohn/NexQuant/issues/1246)) ([694afd8](https://github.com/NicolasBohn/NexQuant/commit/694afd81331227d2be7f780f72023d00c0c9864e))
* add missing debug() method to RDAgentLog ([669263d](https://github.com/NicolasBohn/NexQuant/commit/669263db3759474d05ee3ad38426e1a95d3d789d))
* Add missing os import in factor_runner.py ([25865f9](https://github.com/NicolasBohn/NexQuant/commit/25865f9c778fa3b35b3d1c93017ffa0f7fc4dd15))
* Add missing Panel import in predix evaluate command ([12c949b](https://github.com/NicolasBohn/NexQuant/commit/12c949b0b425170434d222d5c5dd83e75e8b9b96))
* add missing sys import and fix undefined acc_rate in factor eval ([8f2ed41](https://github.com/NicolasBohn/NexQuant/commit/8f2ed4185fdacaa7256a1a52b4fec3e3223ccd8c))
* Add nosec comments for schema migration SQL in results_db.py ([633b563](https://github.com/NicolasBohn/NexQuant/commit/633b5639deff813f372729071985a836fe62f8f6))
* also catch ValueError in mean_variance for dimension mismatch ([ce76da9](https://github.com/NicolasBohn/NexQuant/commit/ce76da912a944d0ecf271d3ce338f2525b5516f3))
* **auto-fixer:** add five new factor code fixes for groupby/apply errors ([919a44a](https://github.com/NicolasBohn/NexQuant/commit/919a44a4b822876c1da6e6d28c63c1755d79d821))
* **auto-fixer:** add four new factor code fixes for common runtime errors ([7b2f54f](https://github.com/NicolasBohn/NexQuant/commit/7b2f54ff9aff5c264b6837e5c3515aa7f5d2c746))
* **auto-fixer:** add groupby([level=N,'date']) SyntaxError fix ([ff1c9fc](https://github.com/NicolasBohn/NexQuant/commit/ff1c9fc554ff9e161aff5211b484b6ac0512c090))
* **auto-fixer:** disable _fix_min_periods for intraday data ([ba4d64b](https://github.com/NicolasBohn/NexQuant/commit/ba4d64b4345cb332d209236a8d2dbd8a983bb87f))
* **auto-fixer:** fix chained groupby(level=N).groupby('date') pattern ([8aec974](https://github.com/NicolasBohn/NexQuant/commit/8aec974702f88ea4d615a1de887812458930f404))
* **auto-fixer:** fix df.loc[instrument] DateParseError on MultiIndex frames ([44f82b1](https://github.com/NicolasBohn/NexQuant/commit/44f82b13d3f78bc2fda5c986a29908f8d655158a))
* **auto-fixer:** fix df['instrument'] KeyError on MultiIndex frames ([17a2558](https://github.com/NicolasBohn/NexQuant/commit/17a2558339556114c68258e560d9d4497a23ecd0))
* **auto-fixer:** fix two assignment-target bugs in instrument column fixers ([d8c0d88](https://github.com/NicolasBohn/NexQuant/commit/d8c0d8865c1ff7b3a0b4bceb6435857132ad37dc))
* **auto-fixer:** preserve date dimension in groupby(['instrument','date']) fix ([f4deda9](https://github.com/NicolasBohn/NexQuant/commit/f4deda99b54b11870a5b10d0ea855451cfd43513))
* **auto-fixer:** remove ddof from rolling() args, not only from std()/var() ([bd5a5e0](https://github.com/NicolasBohn/NexQuant/commit/bd5a5e0fd55947bbb15f9766ef82b51a0c57f61d))
* **auto-fixer:** replace zero \$volume with price-range proxy for FX data ([2de7275](https://github.com/NicolasBohn/NexQuant/commit/2de7275b8f656088200d1428f7d9697b04e0bb90))
* **auto-fixer:** strip spurious .reset_index() after .transform() calls ([9691b64](https://github.com/NicolasBohn/NexQuant/commit/9691b649381b614d4c21eb3efadd779f9709245d))
* avoid triggering errors like "RuntimeError: dictionary changed s… ([#1285](https://github.com/NicolasBohn/NexQuant/issues/1285)) ([b180543](https://github.com/NicolasBohn/NexQuant/commit/b18054371c6ce08c6bc322a7b0de41b67fc60408))
* **backtest:** replace broken MC permutation test with binomial win-rate test ([dfbbffd](https://github.com/NicolasBohn/NexQuant/commit/dfbbffd7eab5bb6ac8c8d4124c1590bc00465842))
* bump axios 1.15.2→1.16.0, postcss 8.4.31→8.5.14 (Dependabot CVEs) ([e4aea61](https://github.com/NicolasBohn/NexQuant/commit/e4aea618b8a5314f102c41ecb277c62501df6f35))
* case-insensitive assertion in test_add_column_idempotent ([7c22287](https://github.com/NicolasBohn/NexQuant/commit/7c222877934cd291130a30a1b56de51db73e3f0a))
* **ci:** fix closed-source asset check false positives in security workflow ([652164b](https://github.com/NicolasBohn/NexQuant/commit/652164b79e921e3b8d0e3f9d12b43752642d1fa8))
* **ci:** lazy import logger in predix.py and cli.py to avoid ImportError in test env ([87610d6](https://github.com/NicolasBohn/NexQuant/commit/87610d660f542cb5df45d08a04fac39c0fbff5c6))
* **ci:** remove CodeQL workflow (conflicts with default setup), drop duplicate lint job ([ad0358d](https://github.com/NicolasBohn/NexQuant/commit/ad0358d01ce8407b763b07a22281438194d64ba1))
* **ci:** remove env-print step to avoid leaking sensitive environment variables ([#1299](https://github.com/NicolasBohn/NexQuant/issues/1299)) ([c067ea6](https://github.com/NicolasBohn/NexQuant/commit/c067ea640030c67c549e3ca2dbad178f144e8b31))
* **ci:** set JAVA_TOOL_OPTIONS UTF-8 in Codacy workflow ([cedd615](https://github.com/NicolasBohn/NexQuant/commit/cedd61592201b4e9870ec5146a6bfcc8ff6ef36c))
* CLI dashboard in separate terminal window ([b72cca9](https://github.com/NicolasBohn/NexQuant/commit/b72cca98680bd8a87393bb4e5f7d17aae47ab5ed))
* close log file handle, fix RiskMgmt equity double-count, remove bare except ([ca003cd](https://github.com/NicolasBohn/NexQuant/commit/ca003cd0f2911dfdaa9d385a20111d1db0d6018c))
* **collect_info:** parse package names safely from requirements constraints ([#1313](https://github.com/NicolasBohn/NexQuant/issues/1313)) ([99a71bf](https://github.com/NicolasBohn/NexQuant/commit/99a71bf533211df743b5801f913de788259e64cb))
* correct MaxDD to equity curve in strategy_builder; test: add 8 cross-validation tests for metric correctness ([ce4a5b7](https://github.com/NicolasBohn/NexQuant/commit/ce4a5b7b4fe7cecf99ffd526e2ebeb67163ac801))
* correct project root paths and subprocess handling in parallel runner and CLI ([574e9d6](https://github.com/NicolasBohn/NexQuant/commit/574e9d6c08637f7eb0212bbd354f46696f9ef665))
* correct Sharpe/MaxDD/WinRate in direct factor eval (was computing on raw factor, now on strategy returns) ([037f7ba](https://github.com/NicolasBohn/NexQuant/commit/037f7ba7d2e0c49e900352edd2910d9bf7d4a79e))
* **deps:** bump python-dotenv to &gt;=1.2.2 (CVE symlink overwrite) ([9b87a1f](https://github.com/NicolasBohn/NexQuant/commit/9b87a1f5aec699507a173e10ebc82757fbd5ab69))
* **deps:** pin aiohttp&gt;=3.13.4 to patch 4 CVEs ([616590c](https://github.com/NicolasBohn/NexQuant/commit/616590cdc0445a6d9d0ea09a40d5f0a67f4bcaa5))
* **deps:** relax aiohttp constraint to &gt;=3.13.4 for litellm compatibility ([02c830d](https://github.com/NicolasBohn/NexQuant/commit/02c830d4e2774cef940fb0f46948c9939d80db4e))
* Disable ANSI color codes when not running in TTY ([3e2dc42](https://github.com/NicolasBohn/NexQuant/commit/3e2dc42f88f74814b49f379ab4e7e397a6c0418a))
* Disable Flask debug mode by default (Security Alert [#2](https://github.com/NicolasBohn/NexQuant/issues/2)) ([ab5fa78](https://github.com/NicolasBohn/NexQuant/commit/ab5fa7861177cdcdc67c783121fa9e2d88c631d8))
* disable vola filter — it killed EUR/GBP profitability ([a721605](https://github.com/NicolasBohn/NexQuant/commit/a721605f4bae2b36fa7d091a50695f209fee82d9))
* Display litellm messages as info instead of warnings ([7dff60c](https://github.com/NicolasBohn/NexQuant/commit/7dff60cf2ef1c550a698b89d4352d8148e5abc84))
* **dockerfile:** install coreutils to resolve timeout command error ([#1260](https://github.com/NicolasBohn/NexQuant/issues/1260)) ([35580cb](https://github.com/NicolasBohn/NexQuant/commit/35580cbdf87347d5d6105b2a9b5ad1694b695820))
* **docs:** update rdagent ui with correct params ([#1249](https://github.com/NicolasBohn/NexQuant/issues/1249)) ([3b9ad11](https://github.com/NicolasBohn/NexQuant/commit/3b9ad1145769862a24cc7533a1828f750f72170d))
* Embedding Context Length Error ([6d6c5ab](https://github.com/NicolasBohn/NexQuant/commit/6d6c5abd4ac7252257f88e13e263ecb2497fde3b))
* end-timestamp 23:45, weg, SZ-beispiele weg ([6a9ccd5](https://github.com/NicolasBohn/NexQuant/commit/6a9ccd5ddbf95060a2847bd27bcdae762a46a19d))
* enhance feedback handling in MultiProcessEvolvingStrategy for improved task evolution ([#1274](https://github.com/NicolasBohn/NexQuant/issues/1274)) ([afb575c](https://github.com/NicolasBohn/NexQuant/commit/afb575cc91114dbe41d8f582294dcc3692990695))
* Ensure backtest results save to DB and JSON files ([612ed8a](https://github.com/NicolasBohn/NexQuant/commit/612ed8a802f3a221c4fca426b3b3db9d46105ff4))
* evaluator erkennt 15min als valid (nicht daily) ([cf0f634](https://github.com/NicolasBohn/NexQuant/commit/cf0f634c17dce45400cc325ccd3ca45e769c15fd))
* **factors:** detect and correct look-ahead bias in daily-constant factors ([78fb607](https://github.com/NicolasBohn/NexQuant/commit/78fb607dbe528dbef0f4a5ae49d24673d27b920e))
* **factors:** extend look-ahead rules to session factors and add intraday-factor guidance ([ef2a6c5](https://github.com/NicolasBohn/NexQuant/commit/ef2a6c5ee0f9f0c001f7856fb0ce433130be0b8d))
* filter NaN in max(), remove redundant ternary, handle non-finite vbt results ([39b49b1](https://github.com/NicolasBohn/NexQuant/commit/39b49b172466611e32cda8ce361999fc4f7a967a))
* fix mcts ([#1270](https://github.com/NicolasBohn/NexQuant/issues/1270)) ([5003aff](https://github.com/NicolasBohn/NexQuant/commit/5003affb17505525336e6c30ba9c690b810c252b))
* Fix parallel runner dashboard rendering error ([9ec6008](https://github.com/NicolasBohn/NexQuant/commit/9ec6008ad8afe603a688c3cfe5f4f703e8e33e69))
* fix type annotation, remove unused parameter, improve import_class errors ([f1eb66c](https://github.com/NicolasBohn/NexQuant/commit/f1eb66cc8f269e0e9955914edc6b3d10037cf984))
* Forward-fill daily factors to 1-min frequency ([9e50eb2](https://github.com/NicolasBohn/NexQuant/commit/9e50eb2d4e38f88a49c39aed874dc196d98d58bf))
* generate.py nutzt rdagent4qlib env für Qlib-Datenzugriff ([b9007f7](https://github.com/NicolasBohn/NexQuant/commit/b9007f754ac682800aaf265c0f24c2028d387d84))
* Handle failed experiments in feedback step to prevent crashes ([ae7e95c](https://github.com/NicolasBohn/NexQuant/commit/ae7e95cba6ac6959dab4db88a63c52e2ac4f9c9a))
* handle mixed str and dict types in code_list ([#1279](https://github.com/NicolasBohn/NexQuant/issues/1279)) ([32ecf92](https://github.com/NicolasBohn/NexQuant/commit/32ecf92afcf647f257b430c748cbe6bb5fa0fac4))
* Handle negative/zero values in performance report charts ([fc0974a](https://github.com/NicolasBohn/NexQuant/commit/fc0974a823d4279d76e7185bd4d79c74e0cc8033))
* Handle Qlib Docker backtest failures gracefully (SECURITY FIX) ([6a1c476](https://github.com/NicolasBohn/NexQuant/commit/6a1c4760c908af87ff3c4ef5c99b93184415fff5))
* Handle timeout exceptions safely in predix_full_eval.py ([e31a2e5](https://github.com/NicolasBohn/NexQuant/commit/e31a2e5405cae18b8d7795c42198ad23e7e0623a))
* Harden _safe_resolve to fix CodeQL alert [#3](https://github.com/NicolasBohn/NexQuant/issues/3) ([820c27f](https://github.com/NicolasBohn/NexQuant/commit/820c27f91bdad55a079399b902fa38ec5aeba9b5))
* Harden path validation in Job Summary UI to fix CodeQL alert [#17](https://github.com/NicolasBohn/NexQuant/issues/17) ([848bbaf](https://github.com/NicolasBohn/NexQuant/commit/848bbafd1312e2e6d0c854a7f2e1e43e17b20754))
* Harden path validation to fix CodeQL alert [#20](https://github.com/NicolasBohn/NexQuant/issues/20) ([7993a23](https://github.com/NicolasBohn/NexQuant/commit/7993a2398a7eb01d458d9c8923924624d3e57300))
* harmonize risk field names and case-insensitive DB column check ([e168a5d](https://github.com/NicolasBohn/NexQuant/commit/e168a5df7ec04c929da9e0961b718a415bde9b32))
* Import pandas in predix portfolio_simple command ([7e8148c](https://github.com/NicolasBohn/NexQuant/commit/7e8148c002d1b9d49f29ab19dd3f35f92323c0cd))
* Improve path traversal prevention with dedicated helper function ([8e00b89](https://github.com/NicolasBohn/NexQuant/commit/8e00b8999676d0c6f89936c6c3b63a48ea02d59e))
* Initialize EnvController in QuantTrace.__init__ ([5b022c9](https://github.com/NicolasBohn/NexQuant/commit/5b022c9c996930397e32f653accf1a319c475c33))
* inject correct MultiIndex template into factor prompt ([49004db](https://github.com/NicolasBohn/NexQuant/commit/49004db027d699bacbb975f267daa95d1957ccd7))
* inject MultiIndex warning into factor interface prompt (YAML valide) ([79e2915](https://github.com/NicolasBohn/NexQuant/commit/79e2915823801d3574920fa197cf9c57965f485f))
* **kronos:** lazy torch import to fix CI ModuleNotFoundError ([6e52c8a](https://github.com/NicolasBohn/NexQuant/commit/6e52c8a15d08cc74401df987599a05c723514dfb))
* **kronos:** pass actual datetime Series to Kronos predictor timestamps ([cffb9ad](https://github.com/NicolasBohn/NexQuant/commit/cffb9adc38c1b3d904e5503f08c6c0e8a544ff00))
* **kronos:** replace rdagent_logger with stdlib logging for CI compatibility ([01ba45b](https://github.com/NicolasBohn/NexQuant/commit/01ba45be56260d78b0926b1d8e09505f93dbbde1))
* live 1h SMA uses minute_closes deque instead of stale HDF5 ([c7ae139](https://github.com/NicolasBohn/NexQuant/commit/c7ae139c18034a34699b700078407b1ba8970200))
* **loop:** compress old experiment history in proposal prompt to reduce context size ([d2037a4](https://github.com/NicolasBohn/NexQuant/commit/d2037a475ac42cdbee4109728e88b44600fbd036))
* **loop:** prevent step_idx advance on unhandled exceptions + fix consecutive assistant messages ([ed1802b](https://github.com/NicolasBohn/NexQuant/commit/ed1802b511da8e613b1943fe5d573446291cdab7))
* merge candidates ([#1254](https://github.com/NicolasBohn/NexQuant/issues/1254)) ([46aad78](https://github.com/NicolasBohn/NexQuant/commit/46aad789ef710d9603e2330788dc66849cb6cab3))
* ML trigger priority over Optuna (2000 % 500 == 0 collision) ([9ce6a4e](https://github.com/NicolasBohn/NexQuant/commit/9ce6a4e6ec7f1eae1a99ba213cfc1f4c6f3e5fc3))
* model/factor experiment filtering in Qlib proposals ([#1257](https://github.com/NicolasBohn/NexQuant/issues/1257)) ([9e34b4e](https://github.com/NicolasBohn/NexQuant/commit/9e34b4e855cbd709cd077f529950b8e1f5c01486))
* **optuna:** fix inverted parameter range in Stage 2/3 when signal_bias is negative ([1a57e57](https://github.com/NicolasBohn/NexQuant/commit/1a57e57583ca71ef083e6d65b0477782bf177127))
* Override webshop's Werkzeug dependency to fix CVE-2026-27199 ([83e18e2](https://github.com/NicolasBohn/NexQuant/commit/83e18e27f392c90273bfd9a1dddf6872a5506fa5))
* preserve null end_time when rendering dataset segments template ([#1326](https://github.com/NicolasBohn/NexQuant/issues/1326)) ([6196ba3](https://github.com/NicolasBohn/NexQuant/commit/6196ba31f2e43db4761eeb482c3301e2238bc4cf))
* prevent calendar index overflow when signal data ends early ([#1324](https://github.com/NicolasBohn/NexQuant/issues/1324)) ([3dbd703](https://github.com/NicolasBohn/NexQuant/commit/3dbd7038280f21793246e5354f083ba472772a10))
* prevent JSON content from being added multiple times during retries ([#1255](https://github.com/NicolasBohn/NexQuant/issues/1255)) ([31b19de](https://github.com/NicolasBohn/NexQuant/commit/31b19dee80c5006c72a0a9698834a04a3acd4af9))
* prevent LLM retry loop from consecutive assistant message corruption ([d458e39](https://github.com/NicolasBohn/NexQuant/commit/d458e39940c12f4ff94efcfeb81e3ec5ee477022))
* Prevent path injection in FT Job Summary UI ([d95f509](https://github.com/NicolasBohn/NexQuant/commit/d95f509efe81173583e875906f4c13257c088ba4))
* Prevent path injection in RL Job Summary UI ([28e45c9](https://github.com/NicolasBohn/NexQuant/commit/28e45c9d106a96944ff55a3a720049e8d8bd11ba))
* Prevent path traversal in autorl_bench server.py ([4cfec6c](https://github.com/NicolasBohn/NexQuant/commit/4cfec6c46be31d96a7a9a2ffdc085777ccc78ddc))
* Prevent path traversal in get_job_options() app.py ([5bd5416](https://github.com/NicolasBohn/NexQuant/commit/5bd5416c27e057e5b915c2146bb1f468852e2467))
* Prevent path traversal in RL UI app.py ([848019d](https://github.com/NicolasBohn/NexQuant/commit/848019d20119d92f51e8ba70ade42e483b8a494f))
* Prevent path traversal in Streamlit UI app.py ([9f8cf62](https://github.com/NicolasBohn/NexQuant/commit/9f8cf622d8ba468dae91a8cbb663be8e86b3ea33))
* **qlib:** correct indentation in except blocks in quant_proposal and factor_runner ([5a5bf4d](https://github.com/NicolasBohn/NexQuant/commit/5a5bf4d771836e3dba3d60dae3cf7130062138a8))
* raise exploration rate to 30% — discover indicators beyond MACD ([2e028ff](https://github.com/NicolasBohn/NexQuant/commit/2e028ffc1e9cb56c74ac622d38110867b9980f76))
* Refactor path validation to fix CodeQL alert [#16](https://github.com/NicolasBohn/NexQuant/issues/16) ([2819423](https://github.com/NicolasBohn/NexQuant/commit/2819423b4b70bfe162a6aace409f66d7815f34ef))
* refine task scheduling logic in MultiProcessEvolvingStrategy for… ([#1275](https://github.com/NicolasBohn/NexQuant/issues/1275)) ([27d38af](https://github.com/NicolasBohn/NexQuant/commit/27d38af7bd7e1fdb73e3617e94435abe7901dd21))
* relax WF default test (wf_oos_sharpe_mean not present when 0 windows) ([e76d5ab](https://github.com/NicolasBohn/NexQuant/commit/e76d5ab9cf80244450b7db4c7e0f16139a37dca9))
* remove $factor from prompt, update example count to EURUSD ([3adc5bf](https://github.com/NicolasBohn/NexQuant/commit/3adc5bf75e6820328991aa5a5456e6f68ccf8fd7))
* remove all Chinese stock references, replace with EURUSD 1min FX ([44eeb01](https://github.com/NicolasBohn/NexQuant/commit/44eeb01ec4f95271a084e9d285e00959926923f3))
* Remove API key from test_benchmark_api.py config ([15c944e](https://github.com/NicolasBohn/NexQuant/commit/15c944efef3de9f752fd154f63156d52852f6613))
* Remove API key logging from eurusd_llm.py ([d73e3de](https://github.com/NicolasBohn/NexQuant/commit/d73e3de57b6188209b3bd9eed69b56c7d27c897a))
* Remove API key parameter from generate_api_config() ([2eced3c](https://github.com/NicolasBohn/NexQuant/commit/2eced3ca69f6d2f33bd6ebec9605034b72baa36c))
* Remove API key presence detection from logging ([adba526](https://github.com/NicolasBohn/NexQuant/commit/adba526e9424a0d9d283461eed021bb021b2b89a))
* Remove clear-text storage of API key (CodeQL alert [#8](https://github.com/NicolasBohn/NexQuant/issues/8)) ([edc0d58](https://github.com/NicolasBohn/NexQuant/commit/edc0d585c3b146bba5cf6f33b86cad3bccda9181))
* Remove hardcoded credentials from test_benchmark_api.py ([c259a01](https://github.com/NicolasBohn/NexQuant/commit/c259a01b919f53b72ff88cded22eb3af145d490d))
* Rename loader.py to prompt_loader.py to fix module conflict ([9642a77](https://github.com/NicolasBohn/NexQuant/commit/9642a7711a3e701dc72bcfd74663df011f2c43b1))
* replace hardcoded ChromeDriver path with webdriver-manager ([#1271](https://github.com/NicolasBohn/NexQuant/issues/1271)) ([e3d2443](https://github.com/NicolasBohn/NexQuant/commit/e3d24437cf7842623fe27fd9221e36a07457d7f7))
* Resolve 88% empty backtest results + path fixes ([574a9cb](https://github.com/NicolasBohn/NexQuant/commit/574a9cb75e9edc0e023423c223dc7a4aacb96aa6))
* resolve dead code, shell injection risk, mutable defaults, and other bugs ([4eeb724](https://github.com/NicolasBohn/NexQuant/commit/4eeb724ac5d5b4176f4fe0b28f051bacf847a0d3))
* Resolve FORWARD_BARS NameError in backtest script ([2677ee4](https://github.com/NicolasBohn/NexQuant/commit/2677ee43a2a0fb1871970f2d9e5a08cbaf9a2a2b))
* Resolve security vulnerabilities (Dependabot + Code Scanning) ([9925b31](https://github.com/NicolasBohn/NexQuant/commit/9925b3132c76efb23c1e6c3fbbbb98984d999936))
* resolve unbound variable, logger shadowing, withdraw_loop edge case, and other bugs in main scripts ([6c3bdb6](https://github.com/NicolasBohn/NexQuant/commit/6c3bdb6ec1440001aa37c9451c75447b015a4740))
* restore KronosPredictor instantiation deleted during refactor ([0aea8c7](https://github.com/NicolasBohn/NexQuant/commit/0aea8c7671af6b27b0648090bba0498945df500b))
* **security:** nosec for B608/B701 false positives in UI and template code ([2126062](https://github.com/NicolasBohn/NexQuant/commit/2126062edf9e8ee476fbb6468d4dda78988b3430))
* **security:** Patch 5 CodeQL path injection and clear-text logging alerts ([#22](https://github.com/NicolasBohn/NexQuant/issues/22)-[#25](https://github.com/NicolasBohn/NexQuant/issues/25), [#9](https://github.com/NicolasBohn/NexQuant/issues/9)) ([06fc8dc](https://github.com/NicolasBohn/NexQuant/commit/06fc8dc36cb7cbebff25d5d5d4e2f1443c876d88))
* **security:** Patch 5 CodeQL path injection and weak hashing alerts ([#25](https://github.com/NicolasBohn/NexQuant/issues/25)-[#30](https://github.com/NicolasBohn/NexQuant/issues/30)) ([8a3472f](https://github.com/NicolasBohn/NexQuant/commit/8a3472f85a40f8d2bc505896b48a59c00e58aad7))
* **security:** Patch path injection and stack trace exposure (CodeQL [#31](https://github.com/NicolasBohn/NexQuant/issues/31), [#27](https://github.com/NicolasBohn/NexQuant/issues/27)) ([6358bc5](https://github.com/NicolasBohn/NexQuant/commit/6358bc500feb2c6ddc6a023ca929d2b8440bdfd2))
* **security:** real fix for B110 (logging in factor_proposal.py [#746](https://github.com/NicolasBohn/NexQuant/issues/746)) ([d8bd16e](https://github.com/NicolasBohn/NexQuant/commit/d8bd16e6b95dd9dfb632d6f9cc94eea9dead40d6))
* **security:** real fix for B110 (logging in factor_runner.py [#744](https://github.com/NicolasBohn/NexQuant/issues/744)) ([018231d](https://github.com/NicolasBohn/NexQuant/commit/018231d1f2105d2082ff03ea5bcee137f907c202))
* **security:** real fix for B110 (logging in quant_proposal.py [#741](https://github.com/NicolasBohn/NexQuant/issues/741)) ([3875081](https://github.com/NicolasBohn/NexQuant/commit/387508168ff29529c919fee10e404a7597c4591f))
* **security:** real fix for B110 (logging in quant_proposal.py [#741](https://github.com/NicolasBohn/NexQuant/issues/741)) ([2055cf1](https://github.com/NicolasBohn/NexQuant/commit/2055cf1817e90df73f309992fb2051c94ded41a3))
* **security:** real fix for B404/B603 (sys.executable in factor_runner.py [#745](https://github.com/NicolasBohn/NexQuant/issues/745)) ([90a6999](https://github.com/NicolasBohn/NexQuant/commit/90a699956327f15576e58fb752a1a7943c3d28dc))
* **security:** replace eval() with ast.literal_eval and add request timeouts (B307, B113) ([133ec1b](https://github.com/NicolasBohn/NexQuant/commit/133ec1b816334d65e2216f38e7dd2d033ff486a4))
* **security:** replace eval() with ast.literal_eval in finetune validator (B307) ([4884368](https://github.com/NicolasBohn/NexQuant/commit/48843682d07ce81833bcd7e23d4de20621101d16))
* **security:** replace os.path.realpath with pathlib.resolve in safe_resolve_path to fix path-injection alerts ([58a7ece](https://github.com/NicolasBohn/NexQuant/commit/58a7ece3a9fc51e6f22cfe7f6c463101bc48e976))
* **security:** replace relative_to() with realpath+startswith for CodeQL sanitization ([d8ab86d](https://github.com/NicolasBohn/NexQuant/commit/d8ab86d6cfc05a61463471cf90bee0486b9d62fc))
* **security:** replace remaining assert statements with proper error handling ([a43c443](https://github.com/NicolasBohn/NexQuant/commit/a43c443c2e02ee405f6e8d7dd340a1c2fde441b2))
* **security:** replace shell=True subprocess calls with list args (B602) ([0fa4f5d](https://github.com/NicolasBohn/NexQuant/commit/0fa4f5dcc8097cd7fbc0936322103fd8bde9aebf))
* **security:** replace shell=True subprocess calls with list args in env.py (B602) ([d83c020](https://github.com/NicolasBohn/NexQuant/commit/d83c02063757dfc0c26bf3d5b5781e45ec6212c5))
* **security:** resolve all 30 Bandit security alerts (B301, B614, B104) ([2a6839e](https://github.com/NicolasBohn/NexQuant/commit/2a6839e999ebd2763f6f7e88973bde1f2a9ecf7c))
* **security:** resolve CodeQL path-injection alerts in UI data loaders ([9d623f0](https://github.com/NicolasBohn/NexQuant/commit/9d623f0fbbb14e6e774c21d75c3d0f49c2ae5cd1))
* **security:** resolve CodeQL path-injection and clear-text-logging alerts ([5f735ad](https://github.com/NicolasBohn/NexQuant/commit/5f735adcb1b7372da1f58bcaef81807703884d9f))
* **security:** Resolve GitHub Security Scan alerts ([554a499](https://github.com/NicolasBohn/NexQuant/commit/554a499d096d3718aff685e63ba37ec7e8fa831a))
* **security:** resolve path-injection and add nosec for safe temp paths (B108, py/path-injection) ([23b2518](https://github.com/NicolasBohn/NexQuant/commit/23b2518c7434c8bbb058ce248617f659105fded7))
* **security:** resolve path-injection, B701, B101, B112 Bandit alerts ([732361b](https://github.com/NicolasBohn/NexQuant/commit/732361bb903b79e5ab17510776eda5d24a31339e))
* **security:** revert broken read_pickle encoding arg in kaggle template (B301) ([b4674ce](https://github.com/NicolasBohn/NexQuant/commit/b4674ce3a06c91c5cac51a70e453beb77aab2a17))
* **security:** Upgrade vllm and transformers to patch 4 CVEs ([3cfa3dd](https://github.com/NicolasBohn/NexQuant/commit/3cfa3dda6f759b2e1ec192b09bbe284c6121ee7d))
* **security:** validate SQL identifiers in _add_column_if_not_exists (B608) ([942266f](https://github.com/NicolasBohn/NexQuant/commit/942266f24d58bb552cab76854d436b1b81d0a050))
* **security:** whitelist-validate metric column in get_top_factors (B608) ([afe1823](https://github.com/NicolasBohn/NexQuant/commit/afe1823e85272542e5f2be235174cd91b61ce129))
* set requires_documentation_search to None to disable feature in eval ([#1245](https://github.com/NicolasBohn/NexQuant/issues/1245)) ([ee8c119](https://github.com/NicolasBohn/NexQuant/commit/ee8c119f31b72de1002e5ad5d30c56d0f4b6c9b9))
* Skip already evaluated factors in predix_full_eval.py ([bf85229](https://github.com/NicolasBohn/NexQuant/commit/bf852293f0d92fee2f0eae68339677f95d233dc4))
* skip Kronos factor on GPUs &lt; 20GB to avoid CUDA OOM (shared with llama-server) ([834cc68](https://github.com/NicolasBohn/NexQuant/commit/834cc686d158f33b4d1ba2cc6a0a813fc9ada5ab))
* **strategies:** guard against None IC in acceptance check, disable slow wf_rolling ([e886eba](https://github.com/NicolasBohn/NexQuant/commit/e886ebab8f150e03737561174b19adeed12ac7f4))
* **strategies:** handle None ic/sharpe/dd in rejected strategy log output ([1958544](https://github.com/NicolasBohn/NexQuant/commit/1958544106395b588aafa261c23890cfbff6455a))
* **strategy:** Fix template variables, APIBackend import, and JSON extraction ([6948b9c](https://github.com/NicolasBohn/NexQuant/commit/6948b9c5e97e5b5d0983d6f191722cd5a8a3d0ea))
* **strategy:** Re-evaluate Optuna-optimized strategies with full OHLCV backtest ([005107a](https://github.com/NicolasBohn/NexQuant/commit/005107a46197c78ae3b4250b77531a2d3c4152d7))
* Switch to ThreadPoolExecutor for factor evaluation ([f6451d3](https://github.com/NicolasBohn/NexQuant/commit/f6451d363dde0f29a48f818a050c8753e731fb41))
* sync release manifest to v1.4.3 (was diverged at 0.8.0) ([29ad209](https://github.com/NicolasBohn/NexQuant/commit/29ad20914bc883cc8d0e3315dd894af2c88e0e73))
* Translate remaining German comment in eurusd_macro.py ([6730ae4](https://github.com/NicolasBohn/NexQuant/commit/6730ae48655acf1544cff69ab00bc246cf465d35))
* Update LICENSE badge link from main to master branch ([d6722e4](https://github.com/NicolasBohn/NexQuant/commit/d6722e46f01b7fd723cabf05bfeff4c83fb87858))
* Update Werkzeug to 2.3.8 (latest secure 2.x version) ([19b9d23](https://github.com/NicolasBohn/NexQuant/commit/19b9d23952cd378c630a8b03255ed92060bdfd77))
* update WF test for new default (wf_rolling=True) ([71ceb9c](https://github.com/NicolasBohn/NexQuant/commit/71ceb9c80936e6a3860d5a30f57a8a20108194e7))
* Use 96-bar forward returns in backtest (matching factor IC horizon) ([586bd1f](https://github.com/NicolasBohn/NexQuant/commit/586bd1fe4eb2bb7ac250781eb6af3a36f3603c2d))
* Use num_api_keys instead of len(api_keys) for round-robin ([5ef1fd6](https://github.com/NicolasBohn/NexQuant/commit/5ef1fd65db371bc3aa5d87c7e0322d2ba0ef4cd8))
* weg, Timestamps mit Uhrzeit, kein SZ-Beispiel ([e9f6ac4](https://github.com/NicolasBohn/NexQuant/commit/e9f6ac48d97b1b57a0dde14562cd1b6f5d106edd))
### Performance Improvements
* **kronos:** batch GPU inference via predict_batch — 75x faster ([888e841](https://github.com/NicolasBohn/NexQuant/commit/888e84136602d30039a6352a156534113e074b81))
* **kronos:** batch GPU inference via predict_batch — 75x faster ([4a6178f](https://github.com/NicolasBohn/NexQuant/commit/4a6178f53ab0c1a06c6d143c29b49b16493c358c))
* Numba GPU-accelerated backtest — 245× faster (735M bars/s) ([a373710](https://github.com/NicolasBohn/NexQuant/commit/a37371045464b455fc5ca036ce640371023339e5))
### Reverts
* remove automated release social workflow ([3d38d88](https://github.com/NicolasBohn/NexQuant/commit/3d38d8824849262deeb974567d213bd5da0ee559))
### Documentation
* Add ATTRIBUTION.md with clear usage guidelines ([7701ba0](https://github.com/NicolasBohn/NexQuant/commit/7701ba02a8b9937f5ca83cfa213fe769354ac318))
* Add CLI welcome screenshot to README ([4a3a3c8](https://github.com/NicolasBohn/NexQuant/commit/4a3a3c8f242a10278138d58ef91e435d9783e5db))
* add closed-source test policy; remove closed-source test imports ([d55bd51](https://github.com/NicolasBohn/NexQuant/commit/d55bd518d321f544a54b3b286ef956886f710e99))
* Add comprehensive CHANGELOG.md for v1.0.0 release ([03cc28b](https://github.com/NicolasBohn/NexQuant/commit/03cc28b7733767b8d1c2dc321923fa213dbf5523))
* Add comprehensive CLI help and update README with quick start ([7e2c313](https://github.com/NicolasBohn/NexQuant/commit/7e2c31305f74304eab5842ece47ac76c72588d6a))
* Add comprehensive data setup guide to README ([53afed0](https://github.com/NicolasBohn/NexQuant/commit/53afed001eae5b30bb747f6b5eab0541243d4e51))
* Add comprehensive Git commit guidelines to QWEN.md ([2d82325](https://github.com/NicolasBohn/NexQuant/commit/2d82325504538cc875199b2c96bcde62670f93b1))
* Add conda requirement to README + fix predix CLI ([cc8023c](https://github.com/NicolasBohn/NexQuant/commit/cc8023ce48d816c57eab3f7d1388fdc9a3188334))
* Add CRITICAL rule - NEVER commit closed-source/private assets ([a90325d](https://github.com/NicolasBohn/NexQuant/commit/a90325d203724d1c3a0196b625435519709964d3))
* Add CRITICAL rule - NEVER commit trading strategies or JSON files ([0ae6f0f](https://github.com/NicolasBohn/NexQuant/commit/0ae6f0f39aa331581e380d88149eee80a12a38b2))
* add documentation for Data Science configurable options ([#1301](https://github.com/NicolasBohn/NexQuant/issues/1301)) ([d603d5a](https://github.com/NicolasBohn/NexQuant/commit/d603d5a5aa86e43cfc0ee3efedc5ab18919809f5))
* add execution environment configuration guide (Docker vs Conda) ([#1288](https://github.com/NicolasBohn/NexQuant/issues/1288)) ([27ed3d1](https://github.com/NicolasBohn/NexQuant/commit/27ed3d1a75b15a5589af84d4f597a8484006e71e))
* Add implementation summary ([dd6beec](https://github.com/NicolasBohn/NexQuant/commit/dd6beec3c92696fcd8f20f702b8896f914148645))
* Add live trading system documentation to QWEN.md ([24a58e9](https://github.com/NicolasBohn/NexQuant/commit/24a58e970e5eb8d578904226cadbfd39ad3efd4e))
* Add Microsoft RD-Agent acknowledgment to README ([a48f0a9](https://github.com/NicolasBohn/NexQuant/commit/a48f0a9733b874ce57b1c093eda4b03695ac32c8))
* Add professional badges to README header ([dbc8603](https://github.com/NicolasBohn/NexQuant/commit/dbc8603e73626063e28452b824996743b965d140))
* Add results/ directory README for storage documentation ([fd963a4](https://github.com/NicolasBohn/NexQuant/commit/fd963a4e78f8739e1bed915df588fce6e038b01f))
* Add v2.0.0 release changelog ([020f013](https://github.com/NicolasBohn/NexQuant/commit/020f0135ef38e500cb2c21a16767ab1ccd5d1058))
* Clean changelog of closed-source performance metrics ([44a06a6](https://github.com/NicolasBohn/NexQuant/commit/44a06a65f44238db38b66a45522684ed0ef45a9b))
* Create changelog/ directory with v1.0.0.md release notes ([d74e970](https://github.com/NicolasBohn/NexQuant/commit/d74e9706f53d409e32767007285c1a12ce1f343b))
* Final system completion - all 9 phases done ([21daaf4](https://github.com/NicolasBohn/NexQuant/commit/21daaf49973cac8e138107432cef36fcdbfafd10))
* fix duplicate sections, add hardware requirements and data setup guide ([38cf4bc](https://github.com/NicolasBohn/NexQuant/commit/38cf4bc63e37c8dcda3c2e12d27cad57709e0950))
* fix script paths in README after rename ([630794e](https://github.com/NicolasBohn/NexQuant/commit/630794e00c8eaeaa0c5769f41c30008ee83a9364))
* improve README badges, fix llama-server flags, clean up structure ([6dfbf14](https://github.com/NicolasBohn/NexQuant/commit/6dfbf148edd7677da797d2e0a743757f70d3996f))
* Remove 'Inspired by' comments and add comprehensive Acknowledgments ([647be57](https://github.com/NicolasBohn/NexQuant/commit/647be579f863cb159eb0e07449c4d237b8c52962))
* remove closed-source live trader reference from README ([8806b12](https://github.com/NicolasBohn/NexQuant/commit/8806b12ad683d056989bd70e35ac0f703bd036b2))
* remove forex-specific language from README ([61e6a09](https://github.com/NicolasBohn/NexQuant/commit/61e6a09b95828220afe9f75a86647f81a144b1e5))
* rewrite README — Numba loop, Optuna, ML, zero-LLM strategy discovery ([7d7c267](https://github.com/NicolasBohn/NexQuant/commit/7d7c267d2902725752fec206da134104ded36c05))
* Simplify README for git-clone-only installation ([ec9cd9b](https://github.com/NicolasBohn/NexQuant/commit/ec9cd9bfa100479667cf75a8ed32ccde583cc8f7))
* Translate all code comments to English ([bb450f7](https://github.com/NicolasBohn/NexQuant/commit/bb450f77404206a74d494a15a1f0d796a457c311))
* Translate data_config.yaml to English ([1aca2dd](https://github.com/NicolasBohn/NexQuant/commit/1aca2dd14ec6d08f84d7307ff1e96dd70f2fcb00))
* Translate server.py comments to English ([40788b8](https://github.com/NicolasBohn/NexQuant/commit/40788b8670ffde808cb0fba016c577387a42e7fa))
* Translate server.py docstring to English ([20caf6d](https://github.com/NicolasBohn/NexQuant/commit/20caf6d2642e36450d1e35f168c0c6b8f53292d9))
* update license section from MIT to AGPL-3.0 ([7696a3a](https://github.com/NicolasBohn/NexQuant/commit/7696a3aaa608aa6cb21172e21963ab6bcd8834e4))
* Update QWEN.md with complete 5-phase architecture and results ([9285a5f](https://github.com/NicolasBohn/NexQuant/commit/9285a5f97a92f63d74457197fb3f4596cb800070))
* Update QWEN.md with detailed Git history correction guide ([9315fa8](https://github.com/NicolasBohn/NexQuant/commit/9315fa8b5e1d498ffaed9e0aa760ff37c0df5d82))
* Update QWEN.md with implementation guide ([a3ad497](https://github.com/NicolasBohn/NexQuant/commit/a3ad4973a22f51b41dfe968ca974a95158b1ca29))
* update README — Kronos-small, test depth, daemon setup, project structure ([85b56b8](https://github.com/NicolasBohn/NexQuant/commit/85b56b81799b0f1936b4e36520f062f828004093))
* Update SECURITY.md and CONTRIBUTING.md ([a5a0a3c](https://github.com/NicolasBohn/NexQuant/commit/a5a0a3c6f940be99557880f2a65b24f78fc01fc4))
* Update TODO.md with v1.0.0 completed items and future roadmap ([b6d26bc](https://github.com/NicolasBohn/NexQuant/commit/b6d26bc0572cb2e7b7e21a48d05312b925268e86))
### Miscellaneous Chores
* release 0.8.0 ([8c15238](https://github.com/NicolasBohn/NexQuant/commit/8c1523802c3c0237eae27ebef3e155af2cddd05e))
## [0.8.0](https://github.com/TPTBusiness/NexQuant/compare/v1.4.2...v0.8.0) (2026-05-04)
+127 -538
View File
@@ -3,26 +3,25 @@
<p align="center">
<img src="https://img.shields.io/badge/Python-3.10%20|%203.11-blue?style=for-the-badge&logo=python" alt="Python">
<img src="https://img.shields.io/badge/Platform-Linux-lightgrey?style=for-the-badge&logo=linux" alt="Platform">
<img src="https://img.shields.io/badge/PyTorch-2.0+-red?style=for-the-badge&logo=pytorch" alt="PyTorch">
<img src="https://img.shields.io/badge/Optuna-3.5+-009B77?style=for-the-badge&logo=optuna" alt="Optuna">
<img src="https://img.shields.io/badge/Numba-0.59+-00A3E0?style=for-the-badge&logo=numba" alt="Numba">
<img src="https://img.shields.io/badge/Optuna-4.8+-009B77?style=for-the-badge&logo=optuna" alt="Optuna">
</p>
<p align="center">
<img src="https://img.shields.io/badge/Pandas-150458?style=for-the-badge&logo=pandas" alt="Pandas">
<img src="https://img.shields.io/badge/LightGBM-00A1E0?style=for-the-badge" alt="LightGBM">
<img src="https://img.shields.io/badge/Qlib-FF6B6B?style=for-the-badge" alt="Qlib">
<img src="https://img.shields.io/badge/llama.cpp-7B68EE?style=for-the-badge" alt="llama.cpp">
<img src="https://img.shields.io/badge/TA--Lib-0.6+-green?style=for-the-badge" alt="TA-Lib">
<img src="https://img.shields.io/badge/LightGBM-4.6+-00A1E0?style=for-the-badge" alt="LightGBM">
<img src="https://img.shields.io/badge/Pandas-2.0+-150458?style=for-the-badge&logo=pandas" alt="Pandas">
<img src="https://img.shields.io/badge/cTrader-OpenAPI-FF6B6B?style=for-the-badge" alt="cTrader">
</p>
<h4 align="center">
<strong>AI-powered Quantitative Trading Agent for EUR/USD Forex</strong>
<strong>High-Speed Strategy Discovery Framework</strong>
</h4>
<p align="center">
<a href="#installation">Installation</a> •
<a href="#no-gpu-use-openrouter">No GPU?</a> •
<a href="#quick-start">Quick Start</a> •
<a href="#configuration">Configuration</a> •
<a href="#strategy-discovery">Strategy Discovery</a> •
<a href="#live-trading">Live Trading</a> •
<a href="#features">Features</a>
</p>
@@ -33,27 +32,12 @@
<a href="https://github.com/TPTBusiness/NexQuant/actions/workflows/codacy.yml">
<img src="https://img.shields.io/github/actions/workflow/status/TPTBusiness/NexQuant/codacy.yml?branch=master&label=Security&logo=shield&style=flat-square" alt="Security Scan">
</a>
<a href="https://codecov.io/gh/TPTBusiness/NexQuant">
<img src="https://img.shields.io/codecov/c/github/TPTBusiness/NexQuant?style=flat-square&logo=codecov" alt="Coverage">
</a>
<a href="https://github.com/TPTBusiness/NexQuant/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/TPTBusiness/NexQuant?style=flat-square" alt="License">
</a>
<a href="https://www.conventionalcommits.org/">
<img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow?style=flat-square" alt="Conventional Commits">
</a>
<a href="https://github.com/astral-sh/ruff">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=flat-square" alt="Ruff">
</a>
<a href="https://github.com/TPTBusiness/NexQuant/stargazers">
<img src="https://img.shields.io/github/stars/TPTBusiness/NexQuant?style=flat-square" alt="Stars">
</a>
<a href="https://github.com/TPTBusiness/NexQuant/forks">
<img src="https://img.shields.io/github/forks/TPTBusiness/NexQuant?style=flat-square" alt="Forks">
</a>
<a href="https://github.com/TPTBusiness/NexQuant/issues">
<img src="https://img.shields.io/github/issues/TPTBusiness/NexQuant?style=flat-square" alt="Issues">
</a>
<a href="https://github.com/TPTBusiness/NexQuant/commits/master">
<img src="https://img.shields.io/github/last-commit/TPTBusiness/NexQuant?style=flat-square" alt="Last Commit">
</a>
@@ -61,435 +45,116 @@
---
## 🖥️ CLI Dashboard
```bash
rdagent nexquant
```
![NexQuant CLI Welcome Screen](docs/cli-welcome-screen.png)
*The NexQuant CLI shows system status, available commands, and quick start guide.*
---
## Overview
**NexQuant** is an autonomous AI agent for quantitative trading strategies in the EUR/USD forex market. Built on a multi-agent framework, NexQuant automates the full research and development cycle:
**NexQuant** discovers profitable trading strategies through high-speed search — no LLM required. Core engine: Numba JIT-compiled backtest at **735 million bars/second** (245× faster than pandas). Four discovery methods run in a continuous loop:
- 📊 **Factor Generation** — LLM proposes novel alpha factors; Kronos foundation model generates OHLCV-based predictions
- 💡 **Strategy Discovery** — Autopilot generates + backtests trading strategies 24/7
- 🧠 **Model Evolution** — CoSTEER iteratively improves predictive models through code evolution
- 📈 **Backtesting** — Unified engine with 10 runtime invariants on 1-min EUR/USD data (20202026)
- 🔄 **Auto-Restart** — All services run as daemons with automatic crash recovery
| Method | Frequency | Description |
|--------|-----------|-------------|
| **Explore** | 30% of iterations | Random strategies from 17 TA-Lib indicators across timeframes |
| **Exploit** | 70% of iterations | Mutate the best-known strategy (change params, indicator, or timeframe) |
| **Optuna** | Every 500 iterations | 20-trial hyperparameter optimization on the current best |
| **LightGBM** | Every 2000 iterations | ML classifier trained on SOTA indicator signals to predict direction |
NexQuant is optimized for **1-minute EUR/USD FX data** (20202026) and supports both local LLMs (llama.cpp) and cloud backends (OpenRouter).
**Current best strategy**: MACD(3,10,3) 4-TF with 2/4 vote majority — **+32.0%/month** (Numba), **+24.3%/month** (verified independent backtest), 0/75 negative months.
> **Backtest Verification**: Every backtest result is automatically verified at runtime against mathematical invariants (MaxDD ∈ [-1,0], WinRate ∈ [0,1], Sharpe finite, sign consistency, etc.). 1125 collected tests with deep property-based, fuzzing, and hypothesis tests ensure metric correctness. See [Backtest Integrity](#backtest-integrity).
## Acknowledgments
This project draws inspiration from various open-source projects in the AI trading and multi-agent systems space. We thank all the authors for their innovative work that helped shape our understanding of these patterns.
Special thanks to:
- **[Microsoft RD-Agent](https://github.com/microsoft/RD-Agent)** (MIT License) - Foundation for our autonomous R&D agent framework. We extend our gratitude to the RD-Agent team for their excellent foundational work.
- **[TradingAgents](https://github.com/TauricResearch/TradingAgents)** (Apache 2.0 License) - Inspiration for our multi-agent debate system, reflection mechanism, and memory management modules.
- **[ai-hedge-fund](https://github.com/virattt/ai-hedge-fund)** - Inspiration for macro analysis (Stanley Druckenmiller agent), risk management concepts, and market regime detection.
All code in NexQuant is originally written and implemented independently. NexQuant extends these frameworks with EUR/USD forex-specific features, 1-minute backtesting capabilities, comprehensive risk management, and trading dashboards.
---
## Installation
### System Requirements
| Component | Minimum | Recommended |
|-----------|---------|-------------|
| **GPU VRAM** | 8 GB | 16 GB (RTX 4080 / 5060 Ti) |
| **RAM** | 16 GB | 32 GB |
| **Storage** | 20 GB | 50 GB (models + data) |
| **OS** | Linux (Ubuntu 22.04+) | Linux |
| **CUDA** | 12.0+ | 12.4+ |
> Local LLMs require a CUDA-capable GPU. The default model (Qwen3.6-35B Q3) uses ~13.6 GB VRAM. CPU-only inference is possible but very slow (not recommended for production use).
### Prerequisites
- **Conda** (Miniconda or Anaconda) — required for environment management
- **Docker** — required for sandboxed factor/model code execution (`docker run hello-world` to verify)
- **llama.cpp** — for local LLM inference (see [llama.cpp build guide](https://github.com/ggml-org/llama.cpp))
- **Ollama** — for embeddings (`nomic-embed-text`); install from [ollama.com](https://ollama.com) and run `ollama pull nomic-embed-text`
- **Linux** — officially supported; macOS/Windows may work with adjustments
### Quick Install
```bash
# Clone repository
git clone https://github.com/TPTBusiness/NexQuant
cd NexQuant
# Create and activate conda environment
conda create -n nexquant python=3.10 -y
conda activate nexquant
# Install in editable mode
pip install -e .
# Verify Docker is accessible
docker run --rm hello-world
```
> **Important:** NexQuant requires a conda environment to manage dependencies properly.
> Using plain Python or other environment managers may cause conflicts.
---
## Data Setup
NexQuant requires **1-minute EUR/USD OHLCV data** in HDF5 format. This is a hard prerequisite — the system cannot run without it.
### Step 1: Get the data
Download 1-minute EUR/USD data (2020present) from any of these free sources:
| Source | Cost | Notes |
|--------|------|-------|
| **[Dukascopy](https://www.dukascopy.com/swiss/english/marketfeed/historical/)** | Free | Best quality free EUR/USD tick data |
| **[OANDA API](https://developer.oanda.com/)** | Free (demo) | Requires API key, programmatic access |
| **[TrueFX](https://truefx.com/)** | Free | Institutional-quality tick data |
| **[Kaggle](https://www.kaggle.com/datasets?search=EURUSD+1min)** | Free | Search "EURUSD 1 minute" |
| **MetaTrader 5** | Free | Export via `copy_rates_range()` |
### Step 2: Convert to HDF5
```python
import pandas as pd
df = pd.read_csv('eurusd_1min.csv', parse_dates=['datetime'])
df = df.rename(columns={'open': '$open', 'close': '$close',
'high': '$high', 'low': '$low', 'volume': '$volume'})
df['instrument'] = 'EURUSD'
df = df.set_index(['datetime', 'instrument'])
for col in ['$open', '$close', '$high', '$low', '$volume']:
df[col] = df[col].astype('float32')
import os
os.makedirs('git_ignore_folder/factor_implementation_source_data', exist_ok=True)
df.to_hdf('git_ignore_folder/factor_implementation_source_data/intraday_pv.h5', key='data', mode='w')
```
### Required HDF5 format
| Field | Type | Description |
|-------|------|-------------|
| **Index** | MultiIndex `(datetime, instrument)` | Timestamp + currency pair |
| **`$open`** | float32 | Open price |
| **`$close`** | float32 | Close price |
| **`$high`** | float32 | High price |
| **`$low`** | float32 | Low price |
| **`$volume`** | float32 | Tick volume |
**Save location:** `git_ignore_folder/factor_implementation_source_data/intraday_pv.h5`
---
## Configuration
### Environment Setup
Create a `.env` file in the project root:
```bash
# Local LLM (llama.cpp)
OPENAI_API_KEY=local
OPENAI_API_BASE=http://localhost:8081/v1
CHAT_MODEL=qwen3.5-35b
# Embedding (Ollama)
LITELLM_PROXY_API_KEY=local
LITELLM_PROXY_API_BASE=http://localhost:11434/v1
EMBEDDING_MODEL=nomic-embed-text
# Paths
QLIB_DATA_DIR=~/.qlib/qlib_data/eurusd_1min_data
```
### LLM Server (llama.cpp)
```bash
~/llama.cpp/build/bin/llama-server \
--model ~/models/qwen3.6/Qwen3.6-35B-A3B-UD-Q3_K_XL.gguf \
--n-gpu-layers 18 \
--no-mmap \
--port 8081 \
--ctx-size 260000 \
--parallel 2 \
--batch-size 512 --ubatch-size 512 \
--host 0.0.0.0 \
-ctk q4_0 -ctv q4_0 \
--reasoning off
```
> **Important flags:**
> - `--ctx-size 260000 --parallel 2` — allocates **2 slots × 130,000 tokens each**.
> - `--reasoning off`**critical**: completely disables Qwen3 chain-of-thought. `--reasoning-budget 0` is not sufficient and produces empty JSON responses.
> - `--n-gpu-layers 18` — reduced from max (33) to free ~7 GB VRAM for Kronos-small GPU inference alongside llama-server.
> - `-ctk q4_0 -ctv q4_0` — quantises the KV cache to 4-bit, reducing VRAM usage.
### Data Configuration
Edit [`data_config.yaml`](data_config.yaml) to customize walk-forward splits:
```yaml
instrument: EURUSD
frequency: 1min
data_path: ~/.qlib/qlib_data/eurusd_1min_data
train_start: "2022-03-14"
train_end: "2024-06-30"
valid_start: "2024-07-01"
valid_end: "2024-12-31"
test_start: "2025-01-01"
test_end: "2026-03-20"
market_context:
spread_bps: 1.5
target_arr: 9.62
max_drawdown: 20
```
---
## No GPU? Use OpenRouter
If you don't have a CUDA-capable GPU, you can run NexQuant using [OpenRouter](https://openrouter.ai) for LLM inference — no local model download required.
**1. Set up `.env` for OpenRouter:**
```bash
# Chat (OpenRouter)
OPENAI_API_KEY=sk-or-v1-<your-openrouter-key>
OPENAI_API_BASE=https://openrouter.ai/api/v1
CHAT_MODEL=qwen/qwen3-235b-a22b
# Embedding (Ollama — still required locally)
LITELLM_PROXY_API_KEY=local
LITELLM_PROXY_API_BASE=http://localhost:11434/v1
EMBEDDING_MODEL=nomic-embed-text
```
**2. Skip the llama-server step** — no local LLM server needed.
**3. Run with the OpenRouter backend:**
```bash
rdagent fin_quant --model openrouter
```
**4. Parallel runs** (uses API concurrency instead of GPU slots):
```bash
python scripts/nexquant_parallel.py --runs 5 --api-keys 1 -m openrouter
```
> Ollama is still required for embeddings even in the OpenRouter path. Install from [ollama.com](https://ollama.com) and run `ollama pull nomic-embed-text` once.
> **This repository contains the research framework.** Trading strategies, broker integrations, and live trading infrastructure are available as separate closed-source modules (`git_ignore_folder/`).
---
## Quick Start
### Prerequisites checklist
```bash
# 1. Docker running?
docker run --rm hello-world
# Prerequisites
conda create -n nexquant python=3.10 -y && conda activate nexquant
pip install -e .
# Ensure OHLCV data exists: git_ignore_folder/intraday_pv_all.h5
# 2. Data in place?
ls git_ignore_folder/factor_implementation_source_data/intraday_pv.h5
# Strategy Discovery Loop (10,000 iterations, ~1 hour)
python scripts/nexquant_rd_loop.py --iterations 10000
# 3. LLM server running?
curl http://localhost:8081/health
```
# Price-Action Indicator Loop (grid search all TA-Lib indicators)
python scripts/nexquant_priceaction_loop.py
### 1. Run Trading Loop
```bash
conda activate nexquant
rdagent fin_quant
# or with explicit options:
rdagent fin_quant --loop-n 5 --step-n 2
```
### 2. Monitor Results
```bash
# Web dashboard
rdagent server_ui --port 19899 --log-dir git_ignore_folder/RD-Agent_workspace/
# then open http://127.0.0.1:19899
# Best strategies so far
python nexquant.py best
```
### 3. Run Continuously (Auto-Restart)
```bash
# Start all services with auto-restart daemons:
# fin_quant — factor R&D loop
nohup bash -c 'while true; do rdagent fin_quant --loop-n 10 --model local >> /tmp/fin_quant_daemon.log 2>&1; sleep 10; done' &
# Autopilot — 24/7 strategy generator (Kronos factors auto-selected)
nohup python scripts/nexquant_autopilot.py >> /tmp/autopilot_daemon.log 2>&1 &
# Live Trader — FTMO FIX API (requires credentials)
nohup python git_ignore_folder/live_trading/ftmo_live_trader.py >> ftmo_live_trader.log 2>&1 &
# Top strategies report
python nexquant.py best -n 20 -m monthly_return --min-trades 30
```
---
## CLI Commands
## Strategy Discovery
### Factor & Strategy Loop
### R&D Loop (`scripts/nexquant_rd_loop.py`)
| Command | Description |
|---------|-------------|
| `rdagent fin_quant` | Start autonomous factor + model evolution loop |
| `rdagent fin_quant --loop-n 5` | Run exactly 5 evolution loops |
| `rdagent fin_quant --with-dashboard` | Start with web dashboard |
| `rdagent fin_quant --cli-dashboard` | Start with CLI Rich dashboard |
| `rdagent fin_factor` | Factor-only evolution |
| `rdagent fin_model` | Model-only evolution |
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Explore │ ──→ │ Exploit │ ──→ │ Optuna │ ──→ │ LightGBM │
│ (Random) │ │ (Mutate) │ │ (Tuning) │ │ (ML) │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
30% 70% /500 iter /2000 iter
```
### Strategy Reports
**17 TA-Lib indicators**: MACD, RSI, Donchian, SAR, ADX, BBANDS, CCI, WCLPRICE, MFI, OBV, STOCH, ROC, AROON, AROONOSC, MOM, ULTOSC, WILLR
| Command | Description |
|---------|-------------|
| `python nexquant.py best` | Show top strategies by composite score |
| `python nexquant.py best -n 20 -m sharpe` | Top 20 by Sharpe ratio |
| `python nexquant.py best --show NAME` | Full metadata for one strategy |
| `python scripts/nexquant_gen_strategies_real_bt.py 10` | Generate 10 strategies with LLM + real OHLCV backtest |
| `python scripts/nexquant_gen_strategies_real_bt.py 20` | Generate 20 strategies (parallel workers) |
| `python scripts/nexquant_autopilot.py` | 24/7 Auto-Pilot: endless strategy generation |
| `python scripts/nexquant_continuous_strategies.py` | Continuous generation with ML training
**4 timeframes**: 15min, 30min, 1h, 4h
### Kronos Foundation Model
**3 strategy types**: Single-TF, Multi-TF (vote majority), Portfolio (indicator ensemble)
| Command | Description |
|---------|-------------|
| `rdagent fin_quant` | Kronos factors auto-generated on startup (3 horizons) |
| Model size: `KRONOS_MODEL_SIZE=small\|mini\|base` | Configurable via env (default: small) |
**Discovery example** (50,000 iterations):
```
random → SAR(+65) → MACD(+73) → MACD-mutated(+102.75, +32%/month)
Optuna tuned params
LightGBM ensemble
```
Kronos runs automatically — no separate command needed. Factors are regenerated if missing from `results/factors/`.
### Grid Search (`scripts/nexquant_priceaction_loop.py`)
### Factor Evaluation
Deterministic parameter grid over all 17 indicators. Finds MACD(3,10,3) as optimal.
| Command | Description |
|---------|-------------|
| `python nexquant.py evaluate --all` | Evaluate all generated factors |
| `python nexquant.py top -n 20` | Show top 20 factors by IC |
| `python nexquant.py portfolio-simple` | Simple portfolio optimization |
### Portfolio Optimizer (`scripts/nexquant_portfolio_optimizer.py`)
### Parallel Execution
Greedy correlation-aware selection from discovered strategies.
| Command | Description |
|---------|-------------|
| `python scripts/nexquant_parallel.py --runs 5 --api-keys 1 -m openrouter` | Run 5 parallel factor evolutions |
| `python scripts/nexquant_parallel.py --runs 20 --api-keys 2 -m openrouter` | Run 20 runs with 2 API keys |
---
### Monitoring & Debug
## Live Trading
| Command | Description |
|---------|-------------|
| `rdagent server_ui --port 19899 --log-dir <path>` | Start web dashboard |
| `rdagent health_check` | Validate environment setup |
| `python scripts/nexquant_batch_backtest.py` | Batch backtest multiple factors |
| `python scripts/nexquant_rebacktest_strategies.py` | Re-backtest existing strategies |
Closed-source module at `git_ignore_folder/nexquant_live_trader.py`. Architecture:
```
MACD(3,10,3) Signal → cTrader OpenAPI → Live Account
4-TF 2/4 Votes (WebSocket+Protobuf) ↓
Paper Mode
```
Integration: cTrader WebSocket `live.ctraderapi.com:5035`, OAuth2 authentication, Protobuf message encoding, FIX protocol.
---
## Features
### 🔄 Iterative Factor Evolution
### ⚡ Numba Backtest
- 735M bars/second (0.003s for 2.26M bars)
- JIT-compiled profit/drawdown/sharpe computation
- Signal construction via pandas resample + TA-Lib (~0.4s) is the bottleneck
NexQuant continuously proposes, implements, and validates new alpha factors:
### 🔍 Four Discovery Methods
- **Explore**: Random indicator + timeframe + parameters
- **Exploit**: Mutation of top-5 SOTA strategies (parameter tweak, indicator swap, timeframe change)
- **Optuna**: 20-trial TPE hyperparameter optimization on best strategy
- **LightGBM**: ML classifier on SOTA indicator signals (80/20 train/test split)
- Learns from backtest feedback
- Avoids overfitting through walk-forward validation
- Discovers non-obvious patterns in order flow, volatility, and session dynamics
### 🛡️ Trading Protection System
Automatic risk management to prevent excessive losses:
- **Max Drawdown Protection** - Pauses trading when drawdown exceeds threshold (default: 15%)
- **Cooldown Period** - Enforces mandatory rest period after significant losses (default: 4h after 5% loss)
- **Stoploss Guard** - Detects clusters of stoplosses and blocks trading (default: max 5 per day)
- **Low Performance Filter** - Filters out consistently underperforming factors (Sharpe < 0.5, Win Rate < 40%)
### 🧠 Model Architecture Search
Automatically explores and refines predictive models:
- Linear baselines (LightGBM, XGBoost)
- Deep learning (LSTM, Transformer, Temporal CNN)
- Ensemble methods
### 📚 Knowledge Base
Built-in knowledge accumulation across loops:
- Successful factors are archived
- Failed attempts inform future proposals
- Cross-loop learning improves robustness
### 🖥️ Interactive UI
Real-time dashboard for monitoring:
- Factor performance metrics
- Model architecture evolution
- Cumulative returns and drawdowns
- Code diffs and implementation history
### 🤖 Kronos Foundation Model Integration
NexQuant integrates Kronos — an OHLCV foundation model from the NeoQuasar team (AAAI 2026, **MIT License**) — for alpha factor generation:
| Model | Params | p24 IC | Best For |
|-------|--------|--------|----------|
| **Kronos-small** (default) | 25M | \|IC\| ≈ 0.09 | 1-min EUR/USD |
| Kronos-mini | 4.1M | \|IC\| ≈ 0.07 | Low-resource |
| Kronos-base | 102M | \|IC\| ≈ 0.002 | Daily/weekly data only |
Kronos generates 3 prediction-horizon factors automatically on `fin_quant` startup:
- `KronosPredReturn_p24` — 24-minute horizon
- `KronosPredReturn_p48` — 48-minute horizon
- `KronosPredReturn_p96` — 96-minute horizon (best performer)
The model runs on GPU (CUDA) alongside the llama-server, using CPU as fallback.
Factors are persisted in `results/factors/` for use by the strategy orchestrator.
```bash
# Kronos runs automatically with fin_quant (no separate command needed)
rdagent fin_quant --loop-n 10 --model local
# Model size is auto-detected and configurable via env
# Set KRONOS_MODEL_SIZE=base to use the 102M-param model
```
### 📊 TA-Lib Integration
- 17 indicators with full parameter ranges
- Auto-guard against bad parameters (negative/zero values that crash TA-Lib)
- Multi-timeframe voting with configurable threshold
### 🔒 Security & Quality
Automated quality assurance:
- **1,125+ collected tests** — deep property-based, fuzzing, and hypothesis tests on every commit
- **Bandit Security Scanner** — pre-commit security checks
- **Weekly Dependency Audit** — automated vulnerability scan via GitHub Actions
- **Closed-source detection** — CI verifies no local/ files are accidentally committed
- 0 Dependabot alerts, 0 CodeScan alerts
- No proprietary terms in git history
- Closed-source detection CI
---
@@ -497,143 +162,67 @@ Automated quality assurance:
```
nexquant/
├── rdagent/ # Core agent framework
│ ├── app/ # CLI and scenario apps
│ └── qlib_rd_loop/ # Quant R&D loop (factor + model generation)
│ ├── components/ # Reusable agent components
│ ├── backtesting/ # Backtest engine & protections
│ │ ├── vbt_backtest.py # Unified backtest engine (1-min bars)
│ │ ├── verify.py # Runtime backtest invariant checker
│ │ ├── results_db.py
│ │ └── protections/ # Trading protection system
│ │ ├── coder/ # Factor & model coding
├── CoSTEER/ # LLM-based code evolution engine
│ │ ├── factor_coder/ # Factor-specific coders
│ │ ├── model_coder/ # Model-specific coders
└── kronos_adapter.py # Kronos foundation model adapter
│ └── workflow/ # R&D loop workflow
│ ├── core/ # Core abstractions
│ ├── oai/ # LLM backend (LiteLLM, streaming, retry)
│ ├── log/ # Logging infrastructure
│ ├── scenarios/ # Domain-specific scenarios (qlib, kaggle, rl)
── utils/ # Utilities
├── scripts/ # Daily operation scripts
│ ├── nexquant_autopilot.py # 24/7 auto strategy generator
│ ├── nexquant_gen_strategies_real_bt.py # Parallel strategy generation
│ ├── nexquant_parallel.py # Multi-instance parallel R&D
│ ├── nexquant_continuous_strategies.py # Continuous strategy generation
│ ├── nexquant_fast_rebacktest.py # Fast strategy re-evaluation
│ └── nexquant_rebacktest_parent.py # Parallel rebacktest orchestrator
├── test/ # Test suite (1,125+ collected)
│ ├── backtesting/ # Backtest engine deep tests
│ ├── qlib/ # Quant loop, factor, model tests
│ ├── oai/ # LLM backend tests
│ ├── log/ # Logger tests
│ ├── local/ # Closed-source tests (autopilot, ML, strategies)
│ └── integration/ # End-to-end pipeline tests
├── data_config.yaml # Walk-forward split configuration
├── pyproject.toml # Project metadata
├── requirements.txt # Dependencies
└── AGENTS.md # Agent configuration & workflow guide
├── scripts/ # Strategy discovery & trading
│ ├── nexquant_rd_loop.py # High-speed R&D loop (Numba + Optuna + ML)
├── nexquant_priceaction_loop.py # TA-Lib grid search loop
│ ├── nexquant_portfolio_optimizer.py # Correlation-aware portfolio selection
│ ├── nexquant_gridsearch.py # Deterministic parameter grid search
├── nexquant_daily_strategies.py # Daily Kronos + factor combinations
├── nexquant_gen_strategies_real_bt.py # LLM-based strategy generation
├── nexquant_autopilot.py # 24/7 continuous generator
└── nexquant_parallel.py # Multi-instance parallel runs
├── rdagent/ # Core framework (LLM-based, see note below)
├── app/ # CLI and scenario apps
├── components/ # Backtest engine, protections, coders
├── core/ # Core abstractions
├── scenarios/ # Domain-specific scenarios
│ └── utils/ # Utilities
├── git_ignore_folder/ # Closed-source (never committed)
│ ├── nexquant_live_trader.py # cTrader live trading
│ ├── nexquant_fix_trader.py # FIX protocol trader
│ ├── intraday_pv_all.h5 # OHLCV data
── gbpusdt_1min.h5 # GBP/USD data
│ └── btc_1min.h5 # BTC data
├── test/ # 1,125+ collected tests
├── data_config.yaml # Walk-forward split configuration
├── requirements.txt # Dependencies
└── AGENTS.md # Agent configuration & workflow guide
```
> **Note on `rdagent/`**: The LLM-based R&D framework (`rdagent fin_quant`) is part of the codebase but the Qlib/CoSTEER pipeline currently produces zero factors. The primary strategy discovery path is the Numba-based loop in `scripts/`.
---
## Requirements
## Installation
Core dependencies (see [`requirements.txt`](requirements.txt) for full list):
### Prerequisites
- **Conda** (Miniconda or Anaconda)
- **TA-Lib** system library (`apt install ta-lib` or `brew install ta-lib`)
- **Linux** (Ubuntu 22.04+)
- **LLM**: `openai`, `litellm`
- **Data**: `pandas`, `numpy`, `pyarrow`
- **ML**: `scikit-learn`, `lightgbm`, `xgboost`
- **Backtesting**: `qlib` (via Docker)
- **UI**: `streamlit`, `plotly`, `flask`
### Install
```bash
git clone https://github.com/TPTBusiness/NexQuant && cd NexQuant
conda create -n nexquant python=3.10 -y && conda activate nexquant
pip install -e .
```
### Data
Place OHLCV HDF5 data at `git_ignore_folder/intraday_pv_all.h5`:
```python
# Format: MultiIndex (datetime, instrument), columns: $open $close $high $low $volume
df.to_hdf('git_ignore_folder/intraday_pv_all.h5', key='data')
```
---
## License
This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)**.
Key points of AGPL-3.0:
- You may use, modify, and distribute this software freely
- If you distribute modified versions, you MUST publish your changes under the same AGPL-3.0 license
- If you run this software as a network service (e.g., trading API), you MUST make the complete source code available to users
- Includes patent protection and anti-tivoization clauses
See the full license text in [`LICENSE`](LICENSE) or at <https://www.gnu.org/licenses/agpl-3.0.en.html>.
---
## Contributing
Contributions are welcome! Please:
1. Fork the repository
2. Create a feature branch (`git checkout -b feat/my-feature`)
3. Commit using [Conventional Commits](https://www.conventionalcommits.org/) (`git commit -m 'feat: add my feature'`)
4. Push to the branch (`git push origin feat/my-feature`)
5. Open a Pull Request with a conventional commit title
For major changes, please open an issue first to discuss your approach.
---
## Citation
If you use NexQuant in your research, please cite the underlying framework:
```bibtex
@misc{yang2025rdagentllmagentframeworkautonomous,
title={R&D-Agent: An LLM-Agent Framework Towards Autonomous Data Science},
author={Yang, Xu and Yang, Xiao and Fang, Shikai and Zhang, Yifei and Wang, Jian and Xian, Bowen and Li, Qizheng and Li, Jingyuan and Xu, Minrui and Li, Yuante and others},
year={2025},
eprint={2505.14738},
archivePrefix={arXiv},
primaryClass={cs.AI}
}
```
---
## Support
- **Issues**: [GitHub Issues](https://github.com/TPTBusiness/NexQuant/issues)
---
## Backtest Integrity
Every backtest result is automatically verified at runtime against 10 mathematical invariants.
The verifier runs in **<1ms** and catches corrupted/missing/flipped metrics before they enter the factor database.
### Runtime checks (every backtest)
| Check | Constraint |
|-------|-----------|
| Max Drawdown | `-1.0 ≤ mdd ≤ 0.0` |
| Win Rate | `0.0 ≤ wr ≤ 1.0` |
| Sharpe Ratio | `sharpe` must be finite |
| Total Return | `total_return` must be finite |
| Trade Count | `n_trades ≥ 0` |
| Sign consistency | `sign(sharpe) == sign(annual_return)` |
| Status | Must be `success` or `failed` |
### Test suite (CI + pre-commit)
```bash
pytest test/ -q # 1,125+ collected, property-based + fuzzing
pytest test/backtesting/ -q # backtest engine deep tests
```
**Coverage**: IC linear invariance, forward-return alignment, cross-implementation validation, ground-truth hand-computed scenarios, look-ahead bias detection, edge cases (all-NaN, constant, zero-variance, 1-bar, empty series), Monte Carlo p-value, walk-forward rolling, buy-and-hold equality, property-based testing (hypothesis: cost monotonicity, signal inversion, max-DD invariants), fuzzing (1,000 random backtest results), autopilot failure recovery, threshold rescaling, API key distribution, ML model acceptance criteria.
**GNU Affero General Public License v3.0 (AGPL-3.0)**. See [`LICENSE`](LICENSE).
---
## Disclaimer
NexQuant is provided "as is" for **research and educational purposes only**. It is **not** intended for:
- Live trading or financial advice
- Production use without thorough testing
- Replacement of qualified financial professionals
Users assume all liability and should comply with applicable laws and regulations in their jurisdiction. Past performance does not guarantee future results.
NexQuant is provided for **research and educational purposes only**. Past performance does not guarantee future results. Users assume all liability.
+1 -1
View File
@@ -97,7 +97,7 @@ class ResultsDatabase:
c = self.conn.cursor()
c.execute("SELECT name FROM pragma_table_info(?)", (table,))
existing = {row[0] for row in c.fetchall()}
if column not in existing:
if column.lower() not in {name.lower() for name in existing}:
c.execute(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}")
def add_factor(self, name: str, type: str = "unknown") -> int:
@@ -333,10 +333,10 @@ def _apply_risk_mask(
masked.at[ts] = 0
return masked, {
"risk_daily_breaches": daily_breaches,
"risk_total_breached": total_breached,
"risk_total_breach_ts": str(total_breach_ts) if total_breach_ts else None,
"risk_compliant": not total_breached and daily_breaches == 0,
"riskmgmt_daily_breaches": daily_breaches,
"riskmgmt_total_breached": total_breached,
"riskmgmt_total_breach_ts": str(total_breach_ts) if total_breach_ts else None,
"riskmgmt_compliant": not total_breached and daily_breaches == 0,
}
@@ -533,13 +533,13 @@ def backtest_signal_risk(
)
result.update(risk_metrics)
result["risk_leverage"] = round(leverage, 2)
result["risk_risk_pct"] = risk_pct
result["risk_stop_pips"] = stop_pips
result["riskmgmt_leverage"] = round(leverage, 2)
result["riskmgmt_risk_pct"] = risk_pct
result["riskmgmt_stop_pips"] = stop_pips
# Re-scale reported equity metrics to INITIAL_CAPITAL
result["risk_end_equity"] = INITIAL_CAPITAL * (1 + result.get("total_return", 0))
result["risk_monthly_profit"] = INITIAL_CAPITAL * result.get("monthly_return", 0)
result["riskmgmt_end_equity"] = INITIAL_CAPITAL * (1 + result.get("total_return", 0))
result["riskmgmt_monthly_profit"] = INITIAL_CAPITAL * result.get("monthly_return", 0)
# Walk-forward OOS split
if oos_start is not None:
@@ -68,6 +68,7 @@ class FactorAutoFixer:
self._fix_inf_nan_handling, # Tenth: add inf/nan handling
self._fix_data_range_processing, # Eleventh: ensure full data range
self._fix_multiindex_groupby, # Twelfth: ensure groupby on MultiIndex
self._fix_composite_normalization, # Thirteenth: normalize thresholds + composite variance
]
for fix_method in fix_methods:
@@ -85,6 +86,24 @@ class FactorAutoFixer:
return fixed_code
def _fix_composite_normalization(self, code: str) -> str:
"""Normalize strategy code: cap thresholds, limit windows, normalize composite."""
code = re.sub(r'\bentry_thresh\s*=\s*([0-9.]+)',
lambda m: f'entry_thresh = {min(float(m.group(1)), 0.7):.1f}', code)
code = re.sub(r'\bexit_thresh\s*=\s*([0-9.]+)',
lambda m: f'exit_thresh = {min(float(m.group(1)), 0.3):.1f}', code)
code = re.sub(r'\bwindow\s*=\s*(\d+)',
lambda m: f'window = {min(int(m.group(1)), 20)}', code)
code = re.sub(r'(signal\s*=\s*signal\s*\.\s*rolling\s*\()(\d+)',
lambda m: f'{m.group(1)}{min(int(m.group(2)), 2)}', code)
if 'composite' in code and 'composite = (composite' not in code:
code = re.sub(
r'\n(signal\s*=\s*pd\.Series)',
r'\ncomposite = (composite - composite.rolling(20).mean()) / (composite.rolling(20).std() + 1e-8)\n\n\1',
code, count=1,
)
return code
def _fix_instrument_column_access(self, code: str) -> str:
"""
Fix: df['instrument'] raises KeyError on a MultiIndex DataFrame because
+3 -3
View File
@@ -9,8 +9,8 @@ psutil
fire
fuzzywuzzy
openai
litellm>=1.83.14 # to support `from litellm import get_valid_models`
aiohttp>=3.13.4 # CVE-2026-22815, CVE-2026-34515, CVE-2026-34516, CVE-2026-34525; >=3.13.4 due to litellm==1.83.14 exact pin
litellm>=1.86.2 # to support `from litellm import get_valid_models`
aiohttp>=3.14.0 # CVE-2026-22815, CVE-2026-34515, CVE-2026-34516, CVE-2026-34525; >=3.13.4 due to litellm==1.83.14 exact pin
azure.identity
pyarrow
rich
@@ -46,7 +46,7 @@ docker
webdriver-manager
# demo related
streamlit>=1.57.0 # to support input_c.text_area(..., height="content", ...)
streamlit>=1.58.0 # to support input_c.text_area(..., height="content", ...)
plotly
st-theme
randomname
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""Gold Swing Scanner — Daily strategies for position/swing trading.
Unlike the 1-min grid search, this targets multi-day holds on daily Gold data.
Tests: Trend-following, momentum, mean-reversion, breakout on 1-20 day horizons.
"""
import json, os, sys, time, itertools
from datetime import datetime
from pathlib import Path
import numpy as np, pandas as pd
PROJECT = Path(__file__).resolve().parent.parent
OUTPUT_DIR = PROJECT / "results" / "gold_swing"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, str(PROJECT / "scripts"))
from nexquant_rd_loop import _backtest_numba
def build_daily_signal(close, indicator, params):
"""Build signal on raw daily close (no resampling)."""
import talib
c = close.values.astype(np.float64)
s = np.zeros(len(c), dtype=np.int32)
if indicator == 'MACD':
mc, sc, _ = talib.MACD(c, fastperiod=params.get('fast',12),
slowperiod=params.get('slow',26),
signalperiod=params.get('sig',9))
s[mc > sc] = 1; s[mc < sc] = -1
elif indicator == 'SMA':
fa = pd.Series(c).rolling(params.get('fast',20)).mean().values
sl = pd.Series(c).rolling(params.get('slow',50)).mean().values
s[fa > sl] = 1; s[fa < sl] = -1
elif indicator == 'EMA':
fa = pd.Series(c).ewm(span=params.get('fast',12)).mean().values
sl = pd.Series(c).ewm(span=params.get('slow',26)).mean().values
s[fa > sl] = 1; s[fa < sl] = -1
elif indicator == 'ROC':
v = talib.ROC(c, timeperiod=params.get('period',20))
th = params.get('threshold',2.0)
s[v > th] = 1; s[v < -th] = -1
elif indicator == 'MOM':
v = talib.MOM(c, timeperiod=params.get('period',20))
s[v > 0] = 1; s[v < 0] = -1
elif indicator == 'RSI_OBOS':
v = talib.RSI(c, timeperiod=params.get('period',14))
s[v < params.get('oversold',30)] = 1; s[v > params.get('overbought',70)] = -1
elif indicator == 'Donchian':
hi = pd.Series(c).rolling(params.get('period',20)).max().shift(1).values
lo = pd.Series(c).rolling(params.get('period',20)).min().shift(1).values
s[c > hi] = 1; s[c < lo] = -1
# Hold until reverse
hold = params.get('hold',5)
if hold > 0:
last = 0; cnt = 0
for i in range(len(s)):
if s[i] != 0: last = s[i]; cnt = hold
elif cnt > 0: s[i] = last; cnt -= 1
elif indicator == 'BB':
up, mi, lo = talib.BBANDS(c, timeperiod=params.get('period',20),
nbdevup=params.get('std',2), nbdevdn=params.get('std',2))
s[c < lo] = 1; s[c > up] = -1
return pd.Series(s, index=close.index).fillna(0).astype(int).clip(-1,1)
# ── Grid Definition ──
INDICATOR_GRIDS = {
'MACD': {
'fast': [3,5,8,12,21],
'slow': [10,15,21,26,34,50],
'sig': [3,5,9,13],
},
'SMA': {
'fast': [10,20,50,100],
'slow': [20,50,100,200],
},
'EMA': {
'fast': [5,8,12,21],
'slow': [13,21,34,55],
},
'ROC': {
'period': [5,10,20,50,100],
'threshold': [0.5,1.0,2.0,3.0,5.0],
},
'MOM': {
'period': [10,20,50,100],
},
'RSI_OBOS': {
'period': [7,14,21],
'oversold': [20,25,30,35],
'overbought': [65,70,75,80],
},
'Donchian': {
'period': [5,10,20,50,100],
'hold': [0,1,3,5,10],
},
'BB': {
'period': [10,20,50],
'std': [1.5,2.0,2.5,3.0],
},
}
def load_gold_daily():
"""Load daily Gold data."""
path = PROJECT / "git_ignore_folder" / "xau_daily.h5"
if path.exists():
return pd.read_hdf(path, key="data")
return None
def main():
print("=" * 60)
print(" Gold Swing Scanner — Daily Position Strategies")
print("=" * 60)
close = load_gold_daily()
if close is None:
print(" XAUUSD daily data not found! Run download first."); return
print(f" XAUUSD daily: {len(close)} bars, {close.index[0].date()} -> {close.index[-1].date()}")
all_results = []
total = 0
for ind_name, grid in INDICATOR_GRIDS.items():
keys = list(grid.keys())
values = list(grid.values())
for combo in itertools.product(*values):
total += 1
params = dict(zip(keys, combo))
try:
sig = build_daily_signal(close, ind_name, params)
if sig is None or sig.nunique() <= 1: continue
except: continue
n = len(close); is_n = int(n * 0.8)
if is_n < 10: continue # too little data
p = close.values.astype(float); s = sig.values.astype(np.int32)
if np.sum(np.abs(s)) < 10: continue
p_is = close.iloc[:is_n].values.astype(float); s_is = sig.iloc[:is_n].values.astype(np.int32)
p_oos = close.iloc[is_n:].values.astype(float); s_oos = sig.iloc[is_n:].values.astype(np.int32)
_, dd, tr, w, ret, sh, _ = _backtest_numba(p, s)
_, _, tr_o, _, ret_o, sh_o, _ = _backtest_numba(p_oos, s_oos)
nd = (close.index[-1] - close.index[0]).days
if nd <= 0: continue
mon = ((1+ret)**(1/(nd/30.44))-1)*100 if ret > -1 else 0
nd_o = (close.index[is_n:][-1] - close.index[is_n:][0]).days
if nd_o <= 0: nd_o = 1
mon_o = ((1+ret_o)**(1/(nd_o/30.44))-1)*100 if ret_o > -1 else 0
all_results.append({
'indicator': ind_name, 'params': params,
'sharpe': float(sh), 'sharpe_oos': float(sh_o),
'monthly_pct': float(mon), 'monthly_oos': float(mon_o),
'n_trades': int(tr), 'n_trades_oos': int(tr_o),
'win_rate': float(w/tr) if tr>0 else 0,
'max_dd': float(-dd),
})
all_results.sort(key=lambda r: r['sharpe_oos'], reverse=True)
print(f" {len(all_results)}/{total} strategies with trades\n")
print(f" TOP 20 by OOS Sharpe:")
print(f" {'Rank':>4s} {'Indicator':<15s} {'Sh IS':>6s} {'Sh OOS':>7s} {'Mon IS':>7s} {'Mon OOS':>7s} {'DD':>6s} {'Tr':>5s}")
for i, r in enumerate(all_results[:20], 1):
print(f" {i:4d} {r['indicator']:<15s} {r['sharpe']:+6.1f} {r['sharpe_oos']:+7.1f} "
f"{r['monthly_pct']:+6.1f}% {r['monthly_oos']:+6.1f}% "
f"{r['max_dd']:.4f} {r['n_trades']:5d}")
# Save
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out = OUTPUT_DIR / f"gold_swing_{ts}.json"
out.write_text(json.dumps(all_results, indent=2, default=str))
print(f"\n Saved: {out}")
# Indicator summary
from collections import Counter
print(f"\n Indicator Performance:")
for ind in INDICATOR_GRIDS.keys():
r = [r for r in all_results if r['indicator'] == ind]
if r:
print(f" {ind:<15s}: max Sh={max(x['sharpe'] for x in r):+.1f} "
f"OOS={max(x['sharpe_oos'] for x in r):+.1f} "
f"({len(r)} combos)")
if __name__ == "__main__":
main()
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""Strategy Grid Search — Systematic parameter scanning for optimal strategies.
Unlike the random R&D loop, this tests ALL parameter/TF combinations
for the best indicators, guaranteeing global optimum discovery.
Output: Ranked list of strategies with per-instrument + combined metrics.
"""
import json, os, sys, time, itertools
from datetime import datetime
from pathlib import Path
import numpy as np, pandas as pd
PROJECT = Path(__file__).resolve().parent.parent
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
OUTPUT_DIR = PROJECT / "results" / "grid_search"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, str(PROJECT / "scripts"))
from nexquant_rd_loop import (
evaluate_multi, build_signal, _apply_session_filter, _apply_news_filter,
_apply_vola_filter, _apply_cross_confirm, LEADER_MAP, load_data,
)
# ── Grid Definition ──
INDICATOR_GRIDS = {
"MACD": {
"type": "multi_tf",
"params": {
"fast": [3, 5, 8, 12],
"slow": [10, 15, 20, 26, 40],
"sig": [3, 5, 9],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["15min", "30min", "1h"],
["30min", "1h", "4h"],
["15min", "1h", "4h"],
],
},
"Donchian": {
"type": "multi_tf",
"params": {
"period": [5, 10, 20, 30, 50, 80, 100],
"hold": [1, 2, 3, 5, 10],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
["15min", "1h", "4h"],
],
},
"SAR": {
"type": "multi_tf",
"params": {
"accel": [0.02, 0.05, 0.08, 0.1, 0.15],
"max_accel": [0.1, 0.2, 0.3, 0.5],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
["15min", "1h", "4h"],
],
},
"ADX": {
"type": "multi_tf",
"params": {
"period": [7, 10, 14, 21, 30],
"threshold": [15, 20, 25, 30],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"RSI": {
"type": "multi_tf",
"params": {
"period": [7, 10, 14, 21],
"oversold": [20, 25, 30],
"overbought": [70, 75, 80],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"BBands": {
"type": "multi_tf",
"params": {
"period": [10, 20, 40],
"std": [1.5, 2.0, 2.5],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"ROC": {
"type": "multi_tf",
"params": {
"period": [5, 10, 20, 50],
"threshold": [0.1, 0.2, 0.5, 1.0],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"MOM": {
"type": "multi_tf",
"params": {
"period": [5, 10, 20, 50, 100],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"Stoch": {
"type": "multi_tf",
"params": {
"fastk": [5, 9, 14],
"slowd": [3, 5, 9],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"CCI": {
"type": "multi_tf",
"params": {
"period": [10, 14, 20, 50],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
}
def expand_grid(indicator_name):
"""Expand a grid definition into all parameter+TF combinations."""
grid = INDICATOR_GRIDS[indicator_name]
param_keys = list(grid["params"].keys())
param_values = [grid["params"][k] for k in param_keys]
hypotheses = []
for tf_list in grid["tfs"]:
for param_combo in itertools.product(*param_values):
params = dict(zip(param_keys, param_combo))
hypotheses.append({
"type": grid["type"],
"indicator": indicator_name,
"timeframes": tf_list,
"params": params,
"description": f"{indicator_name}({'-'.join(str(v) for v in param_combo)}) on {','.join(tf_list[:2])}",
"generation": "grid",
})
return hypotheses
def main():
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--indicators", nargs="*", default=None,
help="Indicators to grid-search (default: all)")
ap.add_argument("--top", type=int, default=20,
help="Number of top results to show")
args = ap.parse_args()
indicators = args.indicators or list(INDICATOR_GRIDS.keys())
if isinstance(indicators, str):
indicators = [indicators]
print("=" * 60)
print(" Strategy Grid Search")
print(f" Indicators: {', '.join(indicators)}")
print("=" * 60)
# Load data
print(" Loading data...")
closes = load_data()
if not closes:
print(" No instruments found!"); return
# Generate all hypotheses
all_hypotheses = []
for ind in indicators:
hyps = expand_grid(ind)
all_hypotheses.extend(hyps)
print(f" Total combinations to test: {len(all_hypotheses)}")
print()
# Evaluate all
results = []
t0 = time.time()
for i, hp in enumerate(all_hypotheses):
try:
r = evaluate_multi(closes, hp, use_session=True, use_vola=False)
r["hypothesis"] = hp
r["rank"] = i + 1
results.append(r)
except Exception:
continue
elapsed = time.time() - t0
rate = (i + 1) / elapsed if elapsed > 0 else 0
eta = (len(all_hypotheses) - i - 1) / rate if rate > 0 else 0
if (i + 1) % 50 == 0:
best_so_far = max(results, key=lambda x: x["sharpe"]) if results else {"sharpe": 0}
print(f" [{i+1}/{len(all_hypotheses)}] "
f"Best Sh={best_so_far['sharpe']:.1f} "
f"Mon={best_so_far['monthly_pct']:.1f}% "
f"OOS={best_so_far['monthly_oos']:.1f}% | "
f"{rate:.0f}/s | ETA {eta/60:.0f}min")
# Sort by OOS Sharpe (most important metric)
results.sort(key=lambda r: r.get("sharpe", 0), reverse=True)
elapsed = time.time() - t0
print(f"\n{'=' * 60}")
print(f" Grid Search Complete: {len(results)}/{len(all_hypotheses)} valid")
print(f" Time: {elapsed:.0f}s ({elapsed/60:.1f}min)")
print(f"{'=' * 60}")
# Save all results
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_file = OUTPUT_DIR / f"grid_results_{ts}.json"
stripped = [{k: v for k, v in r.items() if k != "equity_curves"} for r in results]
out_file.write_text(json.dumps(stripped, indent=2, default=str))
print(f" Saved: {out_file}")
# Show top results
top_n = min(args.top, len(results))
print(f"\n TOP {top_n} (by OOS Sharpe):")
print(f" {'Rank':>4s} {'Strategy':<45s} {'Sh_IS':>6s} {'Sh_OOS':>6s} {'Mon%':>7s} {'OOS%':>7s} {'DD':>6s} {'Tr':>5s} {'BTC':>5s}")
for i, r in enumerate(results[:top_n], 1):
hp = r["hypothesis"]
per = r.get("per_instrument", {})
btc_sh = per.get("BTCUSD", {}).get("sharpe_oos", 0)
print(f" {i:4d} {hp['description'][:45]:45s} "
f"{r.get('sharpe_is', 0):+6.1f} {r.get('sharpe_oos', 0):+6.1f} "
f"{r['monthly_pct']:+6.1f}% {r['monthly_oos']:+6.1f}% "
f"{r['max_dd']:.4f} {r['n_trades']:5d} {btc_sh:+5.0f}")
# Indicator performance summary
print(f"\n Indicator Performance (avg OOS Sharpe):")
for ind in indicators:
ind_results = [r for r in results if r["hypothesis"].get("indicator") == ind]
if ind_results:
avg_sh = np.mean([r["sharpe"] for r in ind_results])
best = ind_results[0]
print(f" {ind:12s}: avg Sh={avg_sh:+.1f} best={best['sharpe']:+.1f} "
f"({best['monthly_pct']:+.1f}%/{best['monthly_oos']:+.1f}% OOS)")
if __name__ == "__main__":
main()
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""Live Price-Action Strategy Pipeline — No LLM, No Factors.
Generates daily signals from Donchian + MACD portfolio, executes via risk
backtest, and optionally sends signals to live trading.
Usage:
python scripts/nexquant_live_priceaction.py # Generate today's signal
python scripts/nexquant_live_priceaction.py --daemon # Run continuously
python scripts/nexquant_live_priceaction.py --backfill # Full historical backtest
"""
import json
import os
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT = Path(__file__).resolve().parent.parent
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
SIGNAL_PATH = PROJECT / "git_ignore_folder" / "priceaction_signal.json"
RESULTS_DIR = PROJECT / "results" / "reports"
# Portfolio config
STRATEGIES = [
{"name": "Donchian(30,1)", "type": "donchian", "period": 30, "hold": 1},
{"name": "MACD(3,15,3)", "type": "macd", "fast": 3, "slow": 15, "signal_period": 3},
]
VOTE_THRESHOLD = 0.25
def load_close() -> tuple[pd.Series, pd.Series]:
"""Load 1-min and daily close prices."""
df = pd.read_hdf(OHLCV_PATH, key="data")
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
daily = close.resample("D").last().dropna()
return close, daily
def donchian_signal(daily: pd.Series, period: int, hold: int) -> pd.Series:
"""Donchian channel breakout signal (daily)."""
high = daily.rolling(period).max()
low = daily.rolling(period).min()
s = pd.Series(0, index=daily.index)
s[daily > high.shift(1)] = 1
s[daily < low.shift(1)] = -1
return s.replace(0, np.nan).ffill(limit=hold).fillna(0).astype(int).clip(-1, 1)
def macd_signal(daily: pd.Series, fast: int, slow: int, signal_period: int) -> pd.Series:
"""MACD crossover signal (daily)."""
ema_fast = daily.ewm(span=fast, adjust=False).mean()
ema_slow = daily.ewm(span=slow, adjust=False).mean()
macd_line = ema_fast - ema_slow
sig_line = macd_line.ewm(span=signal_period, adjust=False).mean()
s = pd.Series(0, index=daily.index)
s[macd_line > sig_line] = 1
s[macd_line < sig_line] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def compute_portfolio_signal(daily: pd.Series) -> pd.Series:
"""Compute majority-vote portfolio signal."""
signals = []
for cfg in STRATEGIES:
if cfg["type"] == "donchian":
sig = donchian_signal(daily, cfg["period"], cfg["hold"])
elif cfg["type"] == "macd":
sig = macd_signal(daily, cfg["fast"], cfg["slow"], cfg["signal_period"])
else:
continue
signals.append(sig)
if not signals:
return pd.Series(0, index=daily.index)
port = pd.DataFrame({f"s{i}": s for i, s in enumerate(signals)}).dropna()
vote = port.mean(axis=1)
result = pd.Series(0, index=vote.index)
result[vote > VOTE_THRESHOLD] = 1
result[vote < -VOTE_THRESHOLD] = -1
result.name = "signal"
return result
def get_todays_signal() -> dict:
"""Generate today's trading signal."""
close, daily = load_close()
portfolio_signal = compute_portfolio_signal(daily)
# Latest signal
latest = portfolio_signal.iloc[-1]
direction = {1: "LONG", -1: "SHORT", 0: "NEUTRAL"}[int(latest)]
# Last signal change
changes = portfolio_signal.diff().abs()
last_change_idx = changes[changes > 0].index[-1] if (changes > 0).any() else None
days_in_position = (daily.index[-1] - last_change_idx).days if last_change_idx is not None else 0
result = {
"timestamp": datetime.now().isoformat(),
"date": str(daily.index[-1].date()),
"signal": int(latest),
"direction": direction,
"days_in_position": days_in_position,
"strategies": {cfg["name"]: int(
donchian_signal(daily, cfg["period"], cfg["hold"]).iloc[-1] if cfg["type"] == "donchian"
else macd_signal(daily, cfg["fast"], cfg["slow"], cfg["signal_period"]).iloc[-1]
) for cfg in STRATEGIES},
}
SIGNAL_PATH.parent.mkdir(parents=True, exist_ok=True)
SIGNAL_PATH.write_text(json.dumps(result, indent=2))
return result
def run_backfill():
"""Run full historical backtest and save report."""
print("Running full historical backtest...")
close, daily = load_close()
signal = compute_portfolio_signal(daily)
# ffill to 1-min
sig_1min = signal.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
from rdagent.components.backtesting.vbt_backtest import backtest_signal, backtest_signal_risk
bt = backtest_signal(close=close, signal=sig_1min)
bt_risk = backtest_signal_risk(close=close, signal=sig_1min, risk_pct=0.0035, oos_start=None, wf_rolling=True)
report = {
"strategy": "Donchian(30,1) + MACD(3,15,3) Majority-Vote",
"timestamp": datetime.now().isoformat(),
"backtest": {
"sharpe": round(bt["sharpe"], 2),
"monthly_return_pct": round(bt["monthly_return_pct"], 2),
"max_drawdown": round(bt["max_drawdown"], 4),
"n_trades": bt["n_trades"],
"win_rate": round(bt["win_rate"], 4),
},
"risk_backtest": {
"sharpe": round(bt_risk.get("sharpe", 0), 2),
"monthly_pct": round(bt_risk.get("monthly_return_pct", 0), 2),
"max_dd": round(bt_risk.get("max_drawdown", 0), 4),
"wf_consistency": round(bt_risk.get("wf_oos_consistency", 0), 4),
},
}
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
path = RESULTS_DIR / f"backfill_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
path.write_text(json.dumps(report, indent=2))
print(f"\n{'='*50}")
print(f" Sharpe: {bt['sharpe']:.2f}")
print(f" Monthly: {bt['monthly_return_pct']:.2f}%")
print(f" Max DD: {bt['max_drawdown']:.4f}")
print(f" Trades: {bt['n_trades']}")
print(f" Win Rate: {bt['win_rate']:.1%}")
print(f" Report saved: {path}")
print(f"{'='*50}")
def main():
if "--backfill" in sys.argv:
run_backfill()
elif "--daemon" in sys.argv:
print("Daemon mode — generating signals every 5 minutes...")
while True:
result = get_todays_signal()
print(f" [{result['timestamp']}] {result['direction']:>8s} ({result['days_in_position']}d in position)")
time.sleep(300)
else:
result = get_todays_signal()
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""Price-Action Strategy Generator — no LLM, no factors, pure technical analysis.
Uses Donchian channels, moving averages, RSI, Bollinger Bands, and MACD
on daily resolution. Grid-searches parameters, validates via backtest_signal.
"""
import json
import os
import time
from datetime import datetime
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT = Path(__file__).resolve().parent.parent
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
RESULTS_DIR = PROJECT / "results" / "strategies_new"
MIN_MONTHLY = 1.0
MIN_SHARPE = 1.0
MAX_DD = -0.15
MIN_TRADES = 30
def load_data():
df = pd.read_hdf(OHLCV_PATH, key="data")
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
daily = close.resample("D").last().dropna()
return close, daily
def to_1min(daily_signal: pd.Series, close_1min: pd.Series) -> pd.Series:
return daily_signal.reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
# ═══════════════════════════════════════════════════════════════════════════════
# Strategy templates
# ═══════════════════════════════════════════════════════════════════════════════
def donchian(close: pd.Series, period: int, hold: int) -> pd.Series:
"""Donchian channel breakout."""
high = close.rolling(period).max()
low = close.rolling(period).min()
s = pd.Series(0, index=close.index)
s[close > high.shift(1)] = 1
s[close < low.shift(1)] = -1
s = s.replace(0, np.nan).ffill(limit=hold).fillna(0).astype(int).clip(-1, 1)
return s
def sma_cross(close: pd.Series, fast: int, slow: int) -> pd.Series:
"""SMA crossover."""
s = pd.Series(0, index=close.index)
s[close.rolling(fast).mean() > close.rolling(slow).mean()] = 1
s[close.rolling(fast).mean() < close.rolling(slow).mean()] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def rsi_mr(close: pd.Series, period: int, oversold: int, overbought: int) -> pd.Series:
"""RSI mean-reversion."""
delta = close.diff()
gain = delta.clip(lower=0).rolling(period).mean()
loss = (-delta.clip(upper=0)).rolling(period).mean()
rs = gain / (loss + 1e-8)
rsi = 100 - 100 / (1 + rs)
s = pd.Series(0, index=close.index)
s[rsi < oversold] = 1
s[rsi > overbought] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def bollinger_mr(close: pd.Series, period: int, std: float) -> pd.Series:
"""Bollinger Band mean-reversion."""
ma = close.rolling(period).mean()
st = close.rolling(period).std()
s = pd.Series(0, index=close.index)
s[close < ma - std * st] = 1
s[close > ma + std * st] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def macd(close: pd.Series, fast: int, slow: int, signal_p: int) -> pd.Series:
"""MACD crossover."""
ema_fast = close.ewm(span=fast, adjust=False).mean()
ema_slow = close.ewm(span=slow, adjust=False).mean()
macd_line = ema_fast - ema_slow
sig_line = macd_line.ewm(span=signal_p, adjust=False).mean()
s = pd.Series(0, index=close.index)
s[macd_line > sig_line] = 1
s[macd_line < sig_line] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def ma_envelope(close: pd.Series, period: int, pct: float) -> pd.Series:
"""Moving average envelope mean-reversion."""
ma = close.rolling(period).mean()
s = pd.Series(0, index=close.index)
s[close < ma * (1 - pct)] = 1
s[close > ma * (1 + pct)] = -1
return s.replace(0, np.nan).ffill(limit=3).fillna(0).astype(int).clip(-1, 1)
def atr_breakout(close: pd.Series, period: int, mult: float) -> pd.Series:
"""ATR-based volatility breakout (simplified, using close-only)."""
atr = (close.diff().abs()).rolling(period).mean()
ma = close.rolling(period).mean()
s = pd.Series(0, index=close.index)
s[close > ma + mult * atr] = 1
s[close < ma - mult * atr] = -1
return s.replace(0, np.nan).ffill(limit=2).fillna(0).astype(int).clip(-1, 1)
# ═══════════════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════════════
def main():
print("=" * 60)
print(" Price-Action Strategy Generator (No LLM, No Factors)")
print("=" * 60)
from rdagent.components.backtesting.vbt_backtest import backtest_signal
close, daily = load_data()
print(f"\nDaily data: {len(daily)} bars ({daily.index[0].date()}{daily.index[-1].date()})")
import itertools
grid = [
("Donchian", donchian, [
(p, h) for p in [5, 7, 10, 12, 15, 20, 25, 30, 40, 60]
for h in [1, 2, 3, 5]
]),
("SMA_Crossover", sma_cross, [
(f, s) for f in [5, 10, 20]
for s in [20, 50, 100, 200] if s > f
]),
("RSI_MR", rsi_mr, [
(p, lo, hi) for p in [7, 14, 21]
for lo, hi in [(30, 70), (25, 75), (20, 80)]
]),
("Bollinger_MR", bollinger_mr, [
(p, s) for p in [10, 20, 40]
for s in [1.5, 2.0, 2.5]
]),
("MACD", macd, [
(f, s, sig) for f, s, sig in [(8, 21, 5), (12, 26, 9), (5, 20, 3)]
]),
("MA_Envelope", ma_envelope, [
(p, pct) for p in [20, 50, 100]
for pct in [0.01, 0.02, 0.03]
]),
("ATR_Breakout", atr_breakout, [
(p, m) for p in [10, 20, 40]
for m in [1.0, 1.5, 2.0]
]),
]
results = []
t0 = time.time()
total = sum(len(params) for _, _, params in grid)
done = 0
print(f"\nTesting {total} parameter combinations...\n")
for name, fn, params_list in grid:
for params in params_list:
done += 1
daily_signal = fn(daily, *params)
signal_1min = to_1min(daily_signal, close)
bt = backtest_signal(close=close, signal=signal_1min)
bt["strategy"] = name
bt["params"] = params
bt["name"] = f"{name}{params}"
bt["monthly_pct"] = bt.get("monthly_return_pct", 0)
bt["max_dd"] = bt.get("max_drawdown", 0)
results.append(bt)
if done % 50 == 0 or done == total:
elapsed = time.time() - t0
rate = done / elapsed if elapsed > 0 else 0
eta = (total - done) / rate if rate > 0 else 0
print(f" {done}/{total} ({done/total*100:.0f}%) {rate:.0f}/s eta {eta:.0f}s")
elapsed = time.time() - t0
print(f"\n{'=' * 60}")
print(f" Evaluated: {total} in {elapsed:.0f}s")
print(f"{'=' * 60}")
valid = [r for r in results
if r.get("sharpe", 0) >= MIN_SHARPE
and r.get("max_dd", 0) >= MAX_DD
and r.get("n_trades", 0) >= MIN_TRADES
and r.get("monthly_pct", 0) >= MIN_MONTHLY]
valid.sort(key=lambda r: r.get("monthly_pct", 0), reverse=True)
print(f"\n Meeting: Sharpe≥{MIN_SHARPE} DD≥{MAX_DD} Tr≥{MIN_TRADES} Mon≥{MIN_MONTHLY}%")
print(f"{len(valid)} strategies\n")
hdr = "{:>3s} {:20s} {:20s} {:>7s} {:>7s} {:>7s} {:>5s} {:>6s}"
print(hdr.format("#", "Strategy", "Params", "Sharpe", "Mon%", "MaxDD", "Tr", "WinRt"))
print("-" * 85)
for i, r in enumerate(valid[:30], 1):
ps = str(r["params"]).replace(" ", "")[:18]
print(hdr.format(str(i), r["strategy"][:20], ps,
f'{r.get("sharpe",0):.2f}', f'{r.get("monthly_pct",0):.1f}%',
f'{r.get("max_dd",0):.3f}', str(r.get("n_trades",0)),
f'{r.get("win_rate",0):.1%}'))
print(f"\n Best by category:")
seen = set()
for r in valid:
if r["strategy"] not in seen:
seen.add(r["strategy"])
print(f" {r['strategy']:20s} {r['name'][:30]:30s} "
f"Sh={r.get('sharpe',0):.2f} Mon={r.get('monthly_pct',0):.1f}% "
f"DD={r.get('max_dd',0):.3f} Tr={r.get('n_trades',0)}")
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
out = RESULTS_DIR / f"priceaction_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
out.write_text(json.dumps(valid[:100] if valid else results[:100], indent=2, default=str))
print(f"\n Saved → {out}")
if __name__ == "__main__":
main()
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""Price-Action R&D Loop — TA-Lib powered. 17 indicators, deterministic.
Uses TA-Lib (161 indicators) for standardized technical analysis.
Generates random strategy hypotheses and evaluates via backtest_signal.
"""
import json, os, random, sys, time
from datetime import datetime
from pathlib import Path
import numpy as np, pandas as pd
import talib
PROJECT = Path(__file__).resolve().parent.parent
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
RESULTS_DIR = PROJECT / "results" / "strategies_new"
TIMEFRAMES = ["15min", "30min", "1h", "4h", "1d"]
VOTE_THRESHOLD = 0.25
MIN_SHARPE, MIN_TRADES, TOP_N = 1.0, 20, 20
# ═══════════════════════════════════════════════════════════════════════════════
# Indicator functions — all use (close, high, low, volume, **params) signature
# ═══════════════════════════════════════════════════════════════════════════════
def _macd(c, h, l, v, fast, slow, sig):
mc, sc, _ = talib.MACD(c.values.astype(np.float64), fastperiod=fast, slowperiod=slow, signalperiod=sig)
s = pd.Series(0, index=c.index); s[mc > sc] = 1; s[mc < sc] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _rsi(c, h, l, v, period, oversold, overbought):
vv = talib.RSI(c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv < oversold] = 1; s[vv > overbought] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _bbands(c, h, l, v, period, std):
up, mi, lo = talib.BBANDS(c.values.astype(np.float64), timeperiod=period, nbdevup=std, nbdevdn=std)
s = pd.Series(0, index=c.index); s[c.values < lo] = 1; s[c.values > up] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _stoch(c, h, l, v, fastk, slowk, slowd):
k, d = talib.STOCH(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64),
fastk_period=fastk, slowk_period=slowk, slowd_period=slowd)
s = pd.Series(0, index=c.index); s[(k > d) & (k < 30)] = 1; s[(k < d) & (k > 70)] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _cci(c, h, l, v, period):
vv = talib.CCI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv < -100] = 1; s[vv > 100] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _willr(c, h, l, v, period):
vv = talib.WILLR(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv < -80] = 1; s[vv > -20] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _adx(c, h, l, v, period, threshold):
pdi = talib.PLUS_DI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
ndi = talib.MINUS_DI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
adx = talib.ADX(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[(pdi > ndi) & (adx > threshold)] = 1; s[(ndi > pdi) & (adx > threshold)] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _sar(c, h, l, v, accel, max_accel):
vv = talib.SAR(h.values.astype(np.float64), l.values.astype(np.float64), acceleration=accel, maximum=max_accel)
s = pd.Series(0, index=c.index); s[c.values > vv] = 1; s[c.values < vv] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _roc(c, h, l, v, period, threshold):
vv = talib.ROC(c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv > threshold] = 1; s[vv < -threshold] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _mom(c, h, l, v, period):
vv = talib.MOM(c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv > 0] = 1; s[vv < 0] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _aroon(c, h, l, v, period):
up, dn = talib.AROON(h.values.astype(np.float64), l.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[up > dn] = 1; s[up < dn] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _mfi(c, h, l, v, period):
vv = talib.MFI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), v.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv < 20] = 1; s[vv > 80] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _ultosc(c, h, l, v, p1, p2, p3):
vv = talib.ULTOSC(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod1=p1, timeperiod2=p2, timeperiod3=p3)
s = pd.Series(0, index=c.index); s[vv < 30] = 1; s[vv > 70] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _natr(c, h, l, v, period):
vv = talib.NATR(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
m, s = vv[-200:].mean(), vv[-200:].std()
s = pd.Series(0, index=c.index); s[c.values > m+s] = 1; s[c.values < m-s] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _donchian(c, h, l, v, period, hold):
hi, lo = c.rolling(period).max(), c.rolling(period).min()
s = pd.Series(0, index=c.index); s[c > hi.shift(1)] = 1; s[c < lo.shift(1)] = -1
return s.replace(0, np.nan).ffill(limit=hold).fillna(0).astype(int).clip(-1, 1)
def _sma(c, h, l, v, fast, slow):
s = pd.Series(0, index=c.index)
s[c.rolling(fast).mean() > c.rolling(slow).mean()] = 1
s[c.rolling(fast).mean() < c.rolling(slow).mean()] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _ema(c, h, l, v, fast, slow):
ef, es = c.ewm(span=fast, adjust=False).mean(), c.ewm(span=slow, adjust=False).mean()
s = pd.Series(0, index=c.index); s[ef > es] = 1; s[ef < es] = -1
return s.fillna(0).astype(int).clip(-1, 1)
# ═══════════════════════════════════════════════════════════════════════════════
INDICATORS = {
"MACD": ({"fast":[3,5,8,12], "slow":[10,15,20,26,35], "sig":[3,5,9]}, _macd, "MACD({fast},{slow},{sig})"),
"RSI": ({"period":[7,14,21], "oversold":[20,25,30,35], "overbought":[65,70,75,80]}, _rsi, "RSI({period})[{oversold}/{overbought}]"),
"BBands": ({"period":[10,20,40], "std":[1.5,2.0,2.5]}, _bbands, "BB({period},{std}s)"),
"Stoch": ({"fastk":[5,9,14], "slowk":[3], "slowd":[3,5]}, _stoch, "Stoch({fastk},{slowk},{slowd})"),
"CCI": ({"period":[14,20,50]}, _cci, "CCI({period})"),
"WillR": ({"period":[7,14,21]}, _willr, "WR({period})"),
"ADX": ({"period":[7,14,21], "threshold":[15,20,25]}, _adx, "ADX({period}>{threshold})"),
"SAR": ({"accel":[0.02,0.05,0.08], "max_accel":[0.2,0.3,0.5]}, _sar, "SAR({accel},{max_accel})"),
"ROC": ({"period":[5,10,20], "threshold":[0.1,0.2,0.5]}, _roc, "ROC({period},{threshold}%)"),
"MOM": ({"period":[5,10,20,50]}, _mom, "MOM({period})"),
"AROON": ({"period":[7,14,21]}, _aroon, "AROON({period})"),
"MFI": ({"period":[7,14,21]}, _mfi, "MFI({period})"),
"UltOsc": ({"p1":[7], "p2":[14], "p3":[28]}, _ultosc, "UltOsc(7,14,28)"),
"NATR": ({"period":[7,14,21]}, _natr, "NATR({period})"),
"Donchian":({"period":[5,10,20,30,50,100], "hold":[1,2,3,5]}, _donchian, "Donchian({period},{hold})"),
"SMA": ({"fast":[5,10,20,50], "slow":[20,50,100,200]}, _sma, "SMA({fast},{slow})"),
"EMA": ({"fast":[3,5,8,12], "slow":[15,26,50,100]}, _ema, "EMA({fast},{slow})"),
}
# ═══════════════════════════════════════════════════════════════════════════════
# Strategy generation
# ═══════════════════════════════════════════════════════════════════════════════
def _resample_ohlc(close_1min, tf):
"""Resample to timeframe, producing OHLCV bars."""
bars = close_1min.resample(tf).ohlc()
# Flatten MultiIndex columns
o = bars['close']['close'] if isinstance(bars.columns, pd.MultiIndex) else bars['close']
h = bars['high']['high'] if isinstance(bars.columns, pd.MultiIndex) else bars['high']
l = bars['low']['low'] if isinstance(bars.columns, pd.MultiIndex) else bars['low']
c = bars['close']['close'] if isinstance(bars.columns, pd.MultiIndex) else bars['close']
v = pd.Series(1000, index=c.index) # dummy volume
return c, h, l, v
def random_hypothesis():
stype = random.choice(["single", "multi_tf", "portfolio"])
if stype == "single":
ind_name = random.choice(list(INDICATORS.keys()))
params_def, _, desc_tpl = INDICATORS[ind_name]
params = {k: random.choice(v) for k, v in params_def.items()}
if ind_name == "SMA" and params["fast"] >= params["slow"]:
params["fast"] = min(params["fast"], params["slow"] // 2)
return {"type": "single", "indicator": ind_name, "timeframe": random.choice(TIMEFRAMES),
"params": params, "description": desc_tpl.format(**params)}
elif stype == "multi_tf":
ind_name = random.choice(list(INDICATORS.keys()))
params_def, _, desc_tpl = INDICATORS[ind_name]
params = {k: random.choice(v) for k, v in params_def.items()}
tfs = random.sample(TIMEFRAMES, k=random.randint(2, 4))
return {"type": "multi_tf", "indicator": ind_name, "timeframes": tfs,
"params": params, "description": f"{ind_name} on {','.join(tfs)} maj-vote"}
else:
i1, i2 = random.sample(list(INDICATORS.keys()), 2)
p1_def, _, _ = INDICATORS[i1]; p2_def, _, _ = INDICATORS[i2]
p1 = {k: random.choice(v) for k, v in p1_def.items()}
p2 = {k: random.choice(v) for k, v in p2_def.items()}
return {"type": "portfolio", "indicators": [{"name": i1, "params": p1}, {"name": i2, "params": p2}],
"timeframe": "1d", "description": f"{i1} + {i2} portfolio daily"}
def build_signal(close_1min, hypothesis):
hp = hypothesis
if hp["type"] == "single":
_, fn, _ = INDICATORS[hp["indicator"]]
c, h, l, v = _resample_ohlc(close_1min, hp["timeframe"])
s = fn(c, h, l, v, **hp["params"])
return s.reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
elif hp["type"] == "multi_tf":
_, fn, _ = INDICATORS[hp["indicator"]]
sigs = {}
for tf in hp["timeframes"]:
c, h, l, v = _resample_ohlc(close_1min, tf)
sigs[tf] = fn(c, h, l, v, **hp["params"]).reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
port_df = pd.DataFrame(sigs).dropna()
vote = port_df.mean(axis=1)
result = pd.Series(0, index=vote.index)
result[vote > VOTE_THRESHOLD] = 1; result[vote < -VOTE_THRESHOLD] = -1
return result
else:
sigs = []
daily, dh, dl, dv = _resample_ohlc(close_1min, "1d")
for cfg in hp["indicators"]:
_, fn, _ = INDICATORS[cfg["name"]]
s = fn(daily, dh, dl, dv, **cfg["params"]).reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
sigs.append(s)
port_df = pd.DataFrame({f"s{i}": s for i, s in enumerate(sigs)}).dropna()
vote = port_df.mean(axis=1)
result = pd.Series(0, index=vote.index)
result[vote > VOTE_THRESHOLD] = 1; result[vote < -VOTE_THRESHOLD] = -1
return result
def evaluate(hp, close):
signal = build_signal(close, hp)
from rdagent.components.backtesting.vbt_backtest import backtest_signal
bt = backtest_signal(close=close, signal=signal)
return {"hypothesis": hp, "sharpe": bt.get("sharpe", 0) or 0,
"monthly_pct": bt.get("monthly_return_pct", 0) or 0,
"max_dd": bt.get("max_drawdown", 0) or 0, "n_trades": bt.get("n_trades", 0) or 0,
"win_rate": bt.get("win_rate", 0) or 0}
# ═══════════════════════════════════════════════════════════════════════════════
# Main loop
# ═══════════════════════════════════════════════════════════════════════════════
def main():
iterations = 100; continuous = False
if "--iterations" in sys.argv:
iterations = int(sys.argv[sys.argv.index("--iterations") + 1])
if "--live" in sys.argv: continuous = True
print("=" * 60)
print(f" Price-Action R&D Loop — TA-Lib ({len(INDICATORS)} indicators)")
print(f" Iterations: {'continuous' if continuous else iterations}")
print("=" * 60)
df = pd.read_hdf(OHLCV_PATH, key="data")
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
top, best_sh, total, iteration = [], 0, 0, 0
while True:
iteration += 1
if not continuous and iteration > iterations: break
hp = random_hypothesis()
result = evaluate(hp, close)
result["iteration"] = iteration
result["timestamp"] = datetime.now().isoformat()
total += 1
if result["sharpe"] >= MIN_SHARPE and result["n_trades"] >= MIN_TRADES and result["monthly_pct"] > 0:
top.append(result)
top.sort(key=lambda r: r["sharpe"], reverse=True)
top = top[:TOP_N]
if iteration % 10 == 0 or result["sharpe"] > best_sh:
if result["sharpe"] > best_sh:
best_sh = result["sharpe"]
print(f"\n * NEW BEST (#{iteration}): {hp['description']}")
print(f" Sharpe={result['sharpe']:.2f} Mon={result['monthly_pct']:.2f}% "
f"DD={result['max_dd']:.4f} Tr={result['n_trades']} WR={result['win_rate']:.1%}")
else:
print(f" [{iteration}/{iterations}] Evals: {total} | Top: {len(top)} | Best Sh={best_sh:.2f}")
if iteration % 50 == 0 and top:
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
cp = RESULTS_DIR / f"pal_talib_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
cp.write_text(json.dumps(top[:10], indent=2, default=str))
print(f" Checkpoint: {cp.name}")
# Final
print(f"\n{'=' * 60}")
print(f" Done: {total} evaluated, {len(top)} strategies")
if top:
print(f"\n{'#':>3s} {'Strategy':<50s} {'Sharpe':>7s} {'Mon%':>7s} {'DD':>7s} {'Tr':>5s}")
print("-" * 80)
for i, r in enumerate(top[:15], 1):
print(f"{i:>3d} {r['hypothesis']['description'][:50]:<50s} {r['sharpe']:>+7.2f} {r['monthly_pct']:>+6.2f}% {r['max_dd']:>+6.4f} {r['n_trades']:>5d}")
final = RESULTS_DIR / f"pal_talib_final_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
final.write_text(json.dumps(top, indent=2, default=str))
print(f"\n Saved: {final}")
if __name__ == "__main__":
main()
+931
View File
@@ -0,0 +1,931 @@
#!/usr/bin/env python3
"""R&D Loop V2 — Multi-Instrument + Correlation Score + Session/Vola Filter + OOS.
Changes from V1:
1. Multi-Instrument: Evaluate on EUR/USD + GBP/USD + BTC/USD
2. Correlation Score: Reward uncorrelated strategies (Sharpe × (1corr))
3. Session Filter: Only trade London session (07:00-16:00 UTC)
4. Volatility Filter: No trades when ATR < threshold
5. OOS Split: Report IS/OOS separately (80/20)
"""
import json, os, random, sys, time
from datetime import datetime
from pathlib import Path
import numpy as np, pandas as pd
from numba import jit
PROJECT = Path(__file__).resolve().parent.parent
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
RESULTS_DIR = PROJECT / "results" / "rd_loop"
STATE_DIR = PROJECT / "git_ignore_folder" / "rd_loop_state"
INSTRUMENTS = ["EURUSD", "GBPUSD", "BTCUSD", "XAUUSD"]
LEADER_MAP = {"GBPUSD": "EURUSD"}
INSTRUMENT_TIMEFRAMES = {
"XAUUSD": ["1d", "1w"], # Daily data → daily/weekly TFs
"default": ["5min", "15min", "30min", "1h", "4h"],
}
TIMEFRAMES = ["5min", "15min", "30min", "1h", "4h", "1d", "1w"]
INDICATORS_POOL = ["MACD", "RSI", "BBands", "Donchian", "Stoch", "CCI", "WillR", "ADX", "SAR", "ROC", "MOM", "AROON", "MFI", "SMA", "EMA"]
STRATEGY_TYPES = ["single", "multi_tf", "multi_role"]
TREND_TFS = ["30min", "1h", "4h"]
ENTRY_TFS = ["5min", "15min", "30min"]
MIN_SHARPE, MIN_TRADES = 0.3, 10
EXPLORATION_RATE = 0.40
OOS_SPLIT = 0.2
# ═══════════════════════════════════════════════════════════════════════════════
# Numba-accelerated backtest
# ═══════════════════════════════════════════════════════════════════════════════
@jit(nopython=True)
def _backtest_numba(prices, signals, cost=0.000264):
n = len(prices)
equity = np.zeros(n, dtype=np.float64)
equity[0] = 100000.0; peak = 100000.0; max_dd = 0.0
trade_returns = np.zeros(100000, dtype=np.float64)
position = 0; entry_price = 0.0; trade_count = 0; wins = 0
for i in range(1, n):
px = prices[i]; sg = signals[i]; ps = signals[i-1]
# Close position on signal reversal or flatten
if position != 0 and (sg != position or sg == 0 and position != 0):
if position == 1: ret = (px - entry_price) / entry_price - cost
else: ret = (entry_price - px) / entry_price - cost
equity[i] = equity[i-1] * (1.0 + ret)
if equity[i] > peak: peak = equity[i]
dd = (peak - equity[i]) / peak
if dd > max_dd: max_dd = dd
if trade_count < len(trade_returns):
trade_returns[trade_count] = ret
trade_count += 1
if ret > 0: wins += 1
position = 0
else:
equity[i] = equity[i-1]
# Open new position
if position == 0 and sg != 0:
position = sg; entry_price = px
# Close final position
if position != 0:
fp = prices[-1]
if position == 1: ret = (fp - entry_price) / entry_price - cost
else: ret = (entry_price - fp) / entry_price - cost
equity[-1] = equity[-2] * (1.0 + ret)
if trade_count < len(trade_returns):
trade_returns[trade_count] = ret
trade_count += 1
if ret > 0: wins += 1
# Ensure monotonic equity (carry forward zeros)
for i in range(1, n):
if equity[i] == 0: equity[i] = equity[i-1]
total_ret = (equity[-1] - 100000.0) / 100000.0
if trade_count > 5:
t = trade_returns[:trade_count]
mean_ret = np.mean(t); std_ret = np.std(t)
sharpe = mean_ret / std_ret * np.sqrt(trade_count) if std_ret > 0 else 0.0
else:
sharpe = 0.0
return equity, max_dd, trade_count, wins, total_ret, sharpe, trade_returns[:trade_count]
# ═══════════════════════════════════════════════════════════════════════════════
# Signal Construction
# ═══════════════════════════════════════════════════════════════════════════════
def build_signal(close, hypothesis):
"""Build trading signal from hypothesis. Returns (-1,0,1) Series."""
import talib
signal = None
# Adapt timeframes to data frequency
median_delta = (close.index[1:] - close.index[:-1]).median()
if median_delta > pd.Timedelta("1h"):
valid_tfs = ["1d", "1w"]
tf_map = {"5min": "1d", "15min": "1d", "30min": "1d", "1h": "1d", "4h": "1w"}
# Remap hypothesis timeframes
hp = dict(hypothesis)
if hp.get('type') in ('single', 'multi_tf') and 'timeframe' in hp:
hp['timeframe'] = tf_map.get(hp.get('timeframe','1h'), '1d')
if hp.get('type') == 'multi_tf' and 'timeframes' in hp:
hp['timeframes'] = [tf_map.get(t, '1d') for t in hp['timeframes']]
hp['timeframes'] = list(set(hp['timeframes'])) # dedup
if hp.get('type') == 'multi_role':
hp['trend_tf'] = tf_map.get(hp.get('trend_tf','4h'), '1w')
hp['entry_tf'] = tf_map.get(hp.get('entry_tf','15min'), '1d')
hypothesis = hp
else:
valid_tfs = ["5min", "15min", "30min", "1h", "4h"]
if hypothesis['type'] == 'single':
ind = hypothesis['indicator']; tf = hypothesis['timeframe']
bars = close.resample(tf).last().dropna()
sig = _build_indicator_signal(ind, bars, hypothesis['params'])
signal = sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
elif hypothesis['type'] == 'multi_tf':
ind = hypothesis['indicator']
sigs = {}
for tf in hypothesis['timeframes']:
bars = close.resample(tf).last().dropna()
sig = _build_indicator_signal(ind, bars, hypothesis['params'])
sigs[tf] = sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
port = pd.DataFrame(sigs).dropna()
vote = port.mean(axis=1)
signal = pd.Series(0, index=close.index)
signal[vote > 0.25] = 1; signal[vote < -0.25] = -1
elif hypothesis['type'] == 'multi_role':
trend_ind = hypothesis['trend_ind']; entry_ind = hypothesis['entry_ind']
trend_tf = hypothesis['trend_tf']; entry_tf = hypothesis['entry_tf']
trend_bars = close.resample(trend_tf).last().dropna()
trend_sig = _build_indicator_signal(trend_ind, trend_bars, hypothesis['trend_params'])
trend_sig = trend_sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
entry_bars = close.resample(entry_tf).last().dropna()
entry_sig = _build_indicator_signal(entry_ind, entry_bars, hypothesis['entry_params'])
entry_sig = entry_sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
signal = pd.Series(0, index=close.index)
signal[(trend_sig == 1) & (entry_sig == 1)] = 1
signal[(trend_sig == -1) & (entry_sig == -1)] = -1
return signal if signal is not None and signal.nunique() > 1 else None
def _apply_session_filter(signal, index):
"""Only trade London session (07:00-16:00 UTC Mon-Fri). Skip for daily data."""
delta = (index[1:] - index[:-1]).median() if len(index) > 1 else pd.Timedelta("1min")
if delta > pd.Timedelta("1h"):
return signal # Skip session filter for daily/weekly data
hours = index.hour
days = index.dayofweek
in_session = (days < 5) & (hours >= 7) & (hours < 16)
if hasattr(in_session, 'values'):
in_session = in_session.values
return (signal * in_session.astype(int)).astype(int).clip(-1, 1)
def _apply_vola_filter(signal, close, atr_period=14, min_atr_pct=0.0003):
"""Don't trade when ATR is too low (flat/quiet markets)."""
tr = pd.DataFrame({
'hl': close.diff().abs(),
'hc': (close - close.shift(1)).abs(),
'lc': (close.shift(1) - close).abs(),
}).max(axis=1)
atr = tr.rolling(atr_period).mean()
atr_pct = atr / close
too_quiet = atr_pct < min_atr_pct
return (signal * (~too_quiet).astype(int)).fillna(0).astype(int).clip(-1, 1)
_NEWS_CACHE = None
def _load_news_events():
"""Load high-impact news events from YAML, return dict of currency → DatetimeIndex mask."""
global _NEWS_CACHE
if _NEWS_CACHE is not None:
return _NEWS_CACHE
import yaml
news_file = PROJECT / "git_ignore_folder" / "economic_events_full.yaml"
if not news_file.exists():
_NEWS_CACHE = {}
return _NEWS_CACHE
with open(news_file) as f:
data = yaml.safe_load(f)
events = {}
for evt in data.get('events', []):
if evt.get('impact') != 'high':
continue
dt = pd.Timestamp(evt['datetime'])
currency = evt.get('currency', 'USD')
if currency not in events:
events[currency] = []
events[currency].append(dt)
_NEWS_CACHE = events
return _NEWS_CACHE
def _apply_news_filter(signal, index, currency, window_min=5):
"""Block trades during high-impact news events (+/- window_min)."""
events = _load_news_events()
if currency not in events and currency[:3] not in events:
return signal
key = currency if currency in events else currency[:3]
timestamps = events.get(key, [])
if not timestamps:
return signal
blocked = np.zeros(len(index), dtype=bool)
for ts in timestamps:
start = ts - pd.Timedelta(minutes=window_min)
end = ts + pd.Timedelta(minutes=window_min)
mask = (index >= start) & (index <= end)
blocked |= mask
return (signal * (~blocked).astype(int)).astype(int).clip(-1, 1)
def _apply_cross_confirm(signal, close_follower, close_leader, lookback=5, min_pct=0.0003):
"""Cancel follower signals when leader momentum strongly opposes (>0.03% move)."""
leader_mom = close_leader.pct_change(lookback)
leader_mom = leader_mom.reindex(signal.index, method='ffill')
# Only BLOCK when leader moves strongly opposite to signal
# Don't require confirmation — just cancel clear contrarian moves
cancel_long = (signal == 1) & (leader_mom < -min_pct)
cancel_short = (signal == -1) & (leader_mom > min_pct)
cancel = cancel_long | cancel_short
return (signal * (~cancel).astype(int)).fillna(0).astype(int).clip(-1, 1)
def _build_indicator_signal(name, bars, params):
"""Build indicator signal using talib + hand-rolled."""
import talib
c = bars.values.astype(np.float64)
if name == 'MACD':
mc, sc, _ = talib.MACD(c, fastperiod=params.get('fast', 3),
slowperiod=params.get('slow', 15),
signalperiod=params.get('sig', 3))
s = pd.Series(0, index=bars.index); s[mc > sc] = 1; s[mc < sc] = -1
elif name == 'RSI':
v = talib.RSI(c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index); s[v < params.get('oversold', 30)] = 1; s[v > params.get('overbought', 70)] = -1
elif name == 'BBands':
up, mi, lo = talib.BBANDS(c, timeperiod=params.get('period', 20),
nbdevup=params.get('std', 2), nbdevdn=params.get('std', 2))
s = pd.Series(0, index=bars.index); s[c < lo] = 1; s[c > up] = -1
elif name == 'Donchian':
hi = bars.rolling(params.get('period', 20)).max()
lo = bars.rolling(params.get('period', 20)).min()
s = pd.Series(0, index=bars.index); s[bars > hi.shift(1)] = 1; s[bars < lo.shift(1)] = -1
s = s.replace(0, np.nan).ffill(limit=params.get('hold', 1)).fillna(0).astype(int)
elif name == 'Stoch':
k, d = talib.STOCH(c, c, c, fastk_period=params.get('fastk', 9),
slowk_period=params.get('slowk', 3), slowd_period=params.get('slowd', 3))
s = pd.Series(0, index=bars.index); s[(k > d) & (k < 30)] = 1; s[(k < d) & (k > 70)] = -1
elif name == 'CCI':
v = talib.CCI(c, c, c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index); s[v < -100] = 1; s[v > 100] = -1
elif name == 'WillR':
v = talib.WILLR(c, c, c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index); s[v < -80] = 1; s[v > -20] = -1
elif name == 'ADX':
pdi = talib.PLUS_DI(c, c, c, timeperiod=params.get('period', 14))
ndi = talib.MINUS_DI(c, c, c, timeperiod=params.get('period', 14))
adx = talib.ADX(c, c, c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index)
s[(pdi > ndi) & (adx > params.get('threshold', 20))] = 1
s[(ndi > pdi) & (adx > params.get('threshold', 20))] = -1
elif name == 'SAR':
v = talib.SAR(c, c, acceleration=params.get('accel', 0.02), maximum=params.get('max_accel', 0.2))
s = pd.Series(0, index=bars.index); s[c > v] = 1; s[c < v] = -1
elif name == 'ROC':
v = talib.ROC(c, timeperiod=params.get('period', 10))
s = pd.Series(0, index=bars.index); s[v > params.get('threshold', 0.2)] = 1; s[v < -params.get('threshold', 0.2)] = -1
elif name == 'MOM':
v = talib.MOM(c, timeperiod=params.get('period', 10))
s = pd.Series(0, index=bars.index); s[v > 0] = 1; s[v < 0] = -1
elif name == 'AROON':
up, dn = talib.AROON(c, c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index); s[up > dn] = 1; s[up < dn] = -1
elif name == 'MFI':
v = talib.MFI(c, c, c, c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index); s[v < 20] = 1; s[v > 80] = -1
elif name == 'SMA':
s = pd.Series(0, index=bars.index)
s[bars.rolling(params.get('fast', 10)).mean() > bars.rolling(params.get('slow', 50)).mean()] = 1
s[bars.rolling(params.get('fast', 10)).mean() < bars.rolling(params.get('slow', 50)).mean()] = -1
elif name == 'EMA':
ef = bars.ewm(span=params.get('fast', 5), adjust=False).mean()
es = bars.ewm(span=params.get('slow', 26), adjust=False).mean()
s = pd.Series(0, index=bars.index); s[ef > es] = 1; s[ef < es] = -1
else:
s = pd.Series(0, index=bars.index)
return s.fillna(0).astype(int).clip(-1, 1)
# ═══════════════════════════════════════════════════════════════════════════════
# Multi-Instrument Evaluation
# ═══════════════════════════════════════════════════════════════════════════════
def evaluate_multi(closes, hypothesis, use_session=True, use_vola=False):
"""Evaluate strategy on all instruments, return combined metrics + per-instrument."""
results = {}
equity_curves = {}
for inst, close in closes.items():
signal = build_signal(close, hypothesis)
if signal is None:
results[inst] = {"sharpe": 0, "monthly_pct": 0, "n_trades": 0}
continue
# Apply filters
if use_session:
signal = _apply_session_filter(signal, close.index)
if use_vola:
signal = _apply_vola_filter(signal, close)
# News filter: block trades during high-impact events for this currency
signal = _apply_news_filter(signal, close.index, inst.replace("USD", "").replace("BTC", "BTC"))
# Cross-pair confirmation: validate follower with leader momentum
leader_inst = LEADER_MAP.get(inst)
if leader_inst and leader_inst in closes:
signal = _apply_cross_confirm(signal, close, closes[leader_inst])
if signal.nunique() <= 1:
results[inst] = {"sharpe": 0, "monthly_pct": 0, "n_trades": 0}
continue
# OOS split
n = len(close)
is_n = int(n * (1 - OOS_SPLIT))
close_is = close.iloc[:is_n]; signal_is = signal.iloc[:is_n]
close_oos = close.iloc[is_n:]; signal_oos = signal.iloc[is_n:]
# IS backtest
prices_is = close_is.values.astype(np.float64); sigs_is = signal_is.values.astype(np.int32)
eq_is, dd_is, tr_is, wins_is, ret_is, sh_is, _ = _backtest_numba(prices_is, sigs_is)
# OOS backtest
prices_oos = close_oos.values.astype(np.float64); sigs_oos = signal_oos.values.astype(np.int32)
eq_oos, dd_oos, tr_oos, wins_oos, ret_oos, sh_oos, _ = _backtest_numba(prices_oos, sigs_oos)
# Full backtest (for equity curve)
prices_full = close.values.astype(np.float64); sigs_full = signal.values.astype(np.int32)
eq_full, dd_full, tr_full, wins_full, ret_full, sh_full, trades_full = _backtest_numba(prices_full, sigs_full)
n_days = (close.index[-1] - close.index[0]).days
mon = ((1+ret_full)**(1/(n_days/30.44))-1)*100 if ret_full > -1 else 0
mon_oos = ((1+ret_oos)**(1/((close_oos.index[-1] - close_oos.index[0]).days/30.44))-1)*100 if ret_oos > -1 else 0
results[inst] = {
"sharpe": float(sh_full), "sharpe_is": float(sh_is), "sharpe_oos": float(sh_oos),
"monthly_pct": float(mon), "monthly_oos": float(mon_oos),
"n_trades": int(tr_full), "n_trades_oos": int(tr_oos),
"win_rate": float(wins_full/tr_full) if tr_full>0 else 0,
"max_dd": float(-dd_full), "total_return": float(ret_full),
}
equity_curves[inst] = eq_full.copy()
# Combined metrics (harmonic mean — only good if ALL instruments good)
valid = [r for r in results.values() if r['sharpe'] > 0]
if not valid:
combined = {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0, "n_trades_oos": 0}
else:
combined = {
"sharpe": float(np.mean([r['sharpe'] for r in valid])),
"monthly_pct": float(np.mean([r['monthly_pct'] for r in valid])),
"monthly_oos": float(np.mean([r['monthly_oos'] for r in valid])),
"n_trades": int(np.sum([r['n_trades'] for r in valid])),
"n_trades_oos": int(np.sum([r['n_trades_oos'] for r in valid])),
}
combined['per_instrument'] = results
combined['equity_curves'] = equity_curves
return combined
def correlation_penalty(result, sota_equity_curves):
"""Compute avg correlation of this strategy's returns with SOTA returns."""
if not sota_equity_curves:
return 0.0
my_returns = []
for eq in result.get('equity_curves', {}).values():
if len(eq) > 1:
my_returns.append(np.diff(eq) / eq[:-1])
if not my_returns:
return 0.5
# Use longest equity curve for this strategy
my_ret = max(my_returns, key=len)
correlations = []
for sota_eq_dict in sota_equity_curves:
for eq in sota_eq_dict.values():
if len(eq) > 1:
sota_ret = np.diff(eq) / eq[:-1]
# Align to shorter length
min_len = min(len(my_ret), len(sota_ret))
if min_len > 10:
corr = np.corrcoef(my_ret[:min_len], sota_ret[:min_len])[0, 1]
if not np.isnan(corr):
correlations.append(corr)
return np.mean(correlations) if correlations else 0.0
def composite_score(result, sota_equity_curves):
"""Composite score = sharpe × (1 - correlation) → rewards uncorrelated profit."""
sh = result.get('sharpe', 0)
if sh <= 0:
return 0
corr = abs(correlation_penalty(result, sota_equity_curves))
# Bonus for OOS consistency
oos_ratio = min(result.get('monthly_oos', 0) / max(result.get('monthly_pct', 1), 0.01), 1.0)
oos_ratio = max(oos_ratio, 0)
return sh * (1 - 0.5 * corr) * (0.3 + 0.7 * oos_ratio)
# ═══════════════════════════════════════════════════════════════════════════════
# Hypothesis Generation
# ═══════════════════════════════════════════════════════════════════════════════
class ResearchLoop:
"""Multi-instrument R&D loop with correlation-aware feedback."""
def __init__(self, closes):
self.closes = closes # {instrument: close_series}
self.sota = [] # State-of-the-art strategies (sorted by composite score)
self.sota_equity = [] # Equity curves for correlation calc
self.history = []
self.iteration = 0
self.best_score = 0
self.best_sharpe = 0
self.exploration_rate = EXPLORATION_RATE
def hypothesize(self):
self.iteration += 1
# Every 2000: ML (higher priority, runs before Optuna)
if self.iteration % 2000 == 0 and len(self.sota) >= 5:
return {'type': 'ml', 'generation': 'ml',
'description': f"ML: LightGBM on {len(self.sota)} strategies",
'sota': self.sota[:5]}
# Every 500: Optuna optimize best strategy
if self.iteration % 500 == 0 and self.sota:
hp = dict(self.sota[0]['hypothesis'])
hp['generation'] = 'optuna'
hp['description'] = f"Optuna: {hp.get('description','?')}"
return hp
# Every 100: force non-dominant indicator
if self.iteration % 100 == 0 and len(self.sota) >= 5:
top = self._top_indicator()
hp = self._random_hypothesis()
hp = self._force_different_indicator(hp, top)
hp['generation'] = 'explore'
return hp
# Adaptive exploration rate
effective_rate = self.exploration_rate
if len(self.sota) >= 10:
top = self._top_indicator()
dominated = sum(1 for r in self.sota if
r['hypothesis'].get('trend_ind', r['hypothesis'].get('indicator')) == top)
if dominated > len(self.sota) * 0.8:
effective_rate += 0.25
if random.random() < effective_rate or not self.sota:
return self._random_hypothesis()
else:
base = random.choice(self.sota[:5])
return self._mutate_hypothesis(base['hypothesis'])
def _top_indicator(self):
if not self.sota:
return 'MACD'
return self.sota[0]['hypothesis'].get('trend_ind',
self.sota[0]['hypothesis'].get('indicator', 'MACD'))
def _force_different_indicator(self, hp, top_ind):
if hp.get('type') == 'multi_role':
if hp['trend_ind'] == top_ind and hp['entry_ind'] == top_ind:
if random.random() < 0.5:
hp['trend_ind'] = random.choice([i for i in INDICATORS_POOL if i != top_ind])
hp['trend_params'] = self._random_params(hp['trend_ind'])
else:
hp['entry_ind'] = random.choice([i for i in INDICATORS_POOL if i != top_ind])
hp['entry_params'] = self._random_params(hp['entry_ind'])
elif hp.get('indicator') == top_ind:
hp['indicator'] = random.choice([i for i in INDICATORS_POOL if i != top_ind])
hp['params'] = self._random_params(hp['indicator'])
hp['description'] = self._make_desc(hp)
return hp
def _make_desc(self, hp):
t = hp.get('type', '?')
if t == 'multi_role':
return f"{hp['trend_ind']}({hp['trend_tf']})→{hp['entry_ind']}({hp['entry_tf']})"
elif t == 'multi_tf':
return f"{hp.get('indicator','?')} on {','.join(hp.get('timeframes',[])[:2])}"
else:
return f"{hp.get('indicator','?')} on {hp.get('timeframe','?')}"
def _random_hypothesis(self):
stype = random.choice(STRATEGY_TYPES)
if stype == 'single':
ind = random.choice(INDICATORS_POOL); tf = random.choice(TIMEFRAMES)
return {'type': 'single', 'indicator': ind, 'timeframe': tf,
'params': self._random_params(ind),
'description': f"{ind} on {tf}", 'generation': 'explore'}
elif stype == 'multi_tf':
ind = random.choice(INDICATORS_POOL)
tfs = random.sample(TIMEFRAMES, k=random.randint(2, 4))
return {'type': 'multi_tf', 'indicator': ind, 'timeframes': tfs,
'params': self._random_params(ind),
'description': f"{ind} on {','.join(tfs)}", 'generation': 'explore'}
else: # multi_role
trend_ind = random.choice(INDICATORS_POOL)
entry_ind = random.choice(INDICATORS_POOL)
trend_tf = random.choice(TREND_TFS)
entry_tf = random.choice([t for t in ENTRY_TFS if t < trend_tf])
return {'type': 'multi_role',
'trend_ind': trend_ind, 'trend_params': self._random_params(trend_ind),
'trend_tf': trend_tf,
'entry_ind': entry_ind, 'entry_params': self._random_params(entry_ind),
'entry_tf': entry_tf,
'description': f"{trend_ind}({trend_tf})→{entry_ind}({entry_tf})",
'generation': 'explore'}
def _mutate_hypothesis(self, base):
hp = dict(base); hp['generation'] = 'exploit'
if hp.get('type') == 'multi_role':
mut = random.choice(['trend_ind', 'entry_ind', 'trend_tf', 'entry_tf',
'trend_params', 'entry_params'])
if mut == 'trend_ind':
hp['trend_ind'] = random.choice([i for i in INDICATORS_POOL if i != hp['trend_ind']])
hp['trend_params'] = self._random_params(hp['trend_ind'])
elif mut == 'entry_ind':
hp['entry_ind'] = random.choice([i for i in INDICATORS_POOL if i != hp['entry_ind']])
hp['entry_params'] = self._random_params(hp['entry_ind'])
elif mut == 'trend_tf':
hp['trend_tf'] = random.choice(TREND_TFS)
if hp['trend_tf'] <= hp['entry_tf']:
hp['entry_tf'] = random.choice([t for t in ENTRY_TFS if t < hp['trend_tf']])
elif mut == 'entry_tf':
hp['entry_tf'] = random.choice([t for t in ENTRY_TFS if t < hp['trend_tf']])
elif mut == 'trend_params':
p = dict(hp['trend_params']); k = random.choice(list(p.keys()))
if isinstance(p[k], (int, float)): p[k] = p[k] * random.uniform(0.5, 1.5)
hp['trend_params'] = p
elif mut == 'entry_params':
p = dict(hp['entry_params']); k = random.choice(list(p.keys()))
if isinstance(p[k], (int, float)): p[k] = p[k] * random.uniform(0.5, 1.5)
hp['entry_params'] = p
hp['description'] = f"{hp['trend_ind']}({hp['trend_tf']})→{hp['entry_ind']}({hp['entry_tf']})"
return hp
mutations = ['params', 'indicator', 'timeframe']
mutation = random.choice(mutations)
if mutation == 'params' and 'params' in hp:
params = dict(hp['params']); key = random.choice(list(params.keys()))
if isinstance(params[key], (int, float)):
params[key] = params[key] * random.uniform(0.5, 1.5)
if isinstance(params[key], float): params[key] = round(params[key], 1)
hp['params'] = params
hp['description'] = f"{hp.get('indicator','?')} (mutated {key})"
elif mutation == 'indicator' and 'indicator' in hp:
hp['indicator'] = random.choice([i for i in INDICATORS_POOL if i != hp.get('indicator')])
hp['params'] = self._random_params(hp['indicator'])
hp['description'] = f"{hp['indicator']} (replaced)"
elif mutation == 'timeframe':
if 'timeframe' in hp:
hp['timeframe'] = random.choice(TIMEFRAMES)
elif 'timeframes' in hp:
hp['timeframes'] = random.sample(TIMEFRAMES, k=len(hp['timeframes']))
hp['description'] = f"{hp.get('indicator','?')} (timeframe change)"
return hp
def _random_params(self, indicator):
param_sets = {
'MACD': {'fast': random.choice([3,5,8,12]), 'slow': random.choice([10,15,20,26]), 'sig': random.choice([3,5,9])},
'RSI': {'period': random.choice([7,14,21]), 'oversold': random.choice([20,25,30]), 'overbought': random.choice([70,75,80])},
'BBands': {'period': random.choice([10,20,40]), 'std': random.choice([1.5,2.0,2.5])},
'Donchian': {'period': random.choice([5,10,20,30,50]), 'hold': random.choice([1,2,3,5])},
'Stoch': {'fastk': random.choice([5,9,14]), 'slowk': 3, 'slowd': random.choice([3,5])},
'CCI': {'period': random.choice([14,20,50])},
'WillR': {'period': random.choice([7,14,21])},
'ADX': {'period': random.choice([7,14,21]), 'threshold': random.choice([15,20,25])},
'SAR': {'accel': random.choice([0.02,0.05,0.08]), 'max_accel': random.choice([0.2,0.3,0.5])},
'ROC': {'period': random.choice([5,10,20]), 'threshold': random.choice([0.1,0.2,0.5])},
'MOM': {'period': random.choice([5,10,20,50])},
'AROON': {'period': random.choice([7,14,21])},
'MFI': {'period': random.choice([7,14,21])},
'SMA': {'fast': random.choice([5,10,20,50]), 'slow': random.choice([20,50,100,200])},
'EMA': {'fast': random.choice([3,5,8,12]), 'slow': random.choice([15,26,50,100])},
}
return param_sets.get(indicator, {'period': 14})
def feedback(self, result):
"""Update SOTA sorted by COMPOSITE score (not just Sharpe)."""
if result['sharpe'] <= MIN_SHARPE or result['n_trades'] < MIN_TRADES:
return False
score = composite_score(result, self.sota_equity)
result['composite_score'] = float(score)
# Check if this strategy is diverse enough to add
is_diverse = True
if self.sota:
# Skip if very similar to existing (same indicators, TF, type)
for existing in self.sota[:3]:
if self._similar(result, existing):
is_diverse = False
break
if is_diverse:
self.sota.append(result)
self.sota.sort(key=lambda r: r.get('composite_score', 0), reverse=True)
self.sota = self.sota[:30] # Keep top 30
self.sota_equity = [s['equity_curves'] for s in self.sota]
if score > self.best_score:
self.best_score = score
return True # NEW BEST
if result['sharpe'] > self.best_sharpe:
self.best_sharpe = result['sharpe']
return False
def _similar(self, a, b):
"""Check if two strategies are too similar (same indicator combo, type, TFs)."""
ha = a['hypothesis']; hb = b['hypothesis']
if ha.get('type') != hb.get('type'):
return False
if ha.get('type') == 'multi_role':
return (ha.get('trend_ind') == hb.get('trend_ind') and
ha.get('entry_ind') == hb.get('entry_ind') and
ha.get('trend_tf') == hb.get('trend_tf') and
ha.get('entry_tf') == hb.get('entry_tf'))
return ha.get('indicator') == hb.get('indicator')
def record(self):
"""Save checkpoint."""
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
STATE_DIR.mkdir(parents=True, exist_ok=True)
if self.sota:
cp = RESULTS_DIR / f"rd_loop_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
# Strip equity_curves (too large) from saved results
stripped = []
for r in self.sota[:30]:
s = {k: v for k, v in r.items() if k != 'equity_curves'}
stripped.append(s)
cp.write_text(json.dumps(stripped, indent=2, default=str))
def _run_optuna(closes, hypothesis):
"""Optuna optimization on the primary instrument."""
import optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)
hp = hypothesis
close = list(closes.values())[0] # Use first instrument for Optuna
ind = hp.get('indicator', hp.get('trend_ind', 'MACD'))
base_params = hp.get('params', hp.get('trend_params', {}))
param_ranges = {
'MACD': {'fast': (2,15), 'slow': (5,40), 'sig': (2,15)},
'RSI': {'period': (5,30), 'oversold': (10,40), 'overbought': (60,90)},
'Donchian': {'period': (3,100), 'hold': (1,10)},
'SAR': {'accel': (0.01, 0.2), 'max_accel': (0.1, 1.0)},
'ADX': {'period': (5,30), 'threshold': (10,40)},
}
ranges = param_ranges.get(ind, {})
def objective(trial):
params = {}
for k, (lo, hi) in ranges.items():
if isinstance(base_params.get(k, 1), int):
params[k] = trial.suggest_int(k, int(lo), int(hi))
else:
params[k] = trial.suggest_float(k, lo, hi)
if 'fast' in params and 'slow' in params:
params['fast'] = min(params['fast'], params['slow']-2)
result = evaluate_multi(closes, hp, use_session=True, use_vola=True)
return float(result.get('sharpe', 0)) if result.get('sharpe', 0) > 0 else -999.0
try:
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=15, show_progress_bar=False)
best = study.best_params
if 'params' in hp:
hp['params'] = {k: int(v) if v == int(v) else v for k, v in best.items()}
elif 'trend_params' in hp:
hp['trend_params'] = {k: int(v) if v == int(v) else v for k, v in best.items()}
hp['generation'] = 'optuna'
result = evaluate_multi(closes, hp, use_session=True, use_vola=True)
print(f" Optuna best: {best} → Sh={result['sharpe']:.1f} "
f"Mon={result['monthly_pct']:.1f}% OOS={result['monthly_oos']:.1f}% ({study.best_value:.1f})")
return result
except Exception:
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
def _train_ml(closes, hypothesis):
"""Train LightGBM on SOTA indicator signals."""
try:
from lightgbm import LGBMClassifier
except ImportError:
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
sota = hypothesis.get('sota', [])
if not sota:
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
close = list(closes.values())[0]
daily = close.resample('1h').last().dropna()
features = pd.DataFrame(index=daily.index)
for s in sota[:5]:
hp_s = s['hypothesis']
# Generate signal from each SOTA strategy as a feature
from nexquant_rd_loop import build_signal, _build_indicator_signal
sig = build_signal(close, hp_s)
if sig is not None:
sig = sig.reindex(daily.index, method='ffill')
name = hp_s.get('description', f"strat_{id(s)}")[:30]
features[name] = sig.fillna(0)
features = features.iloc[100:] # Skip warmup
if len(features) < 200:
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
target = (daily.pct_change().shift(-1) > 0).astype(int)
target = target.reindex(features.index).fillna(0)
split = int(len(features) * 0.8)
X_train, X_test = features.iloc[:split], features.iloc[split:]
y_train, y_test = target.iloc[:split], target.iloc[split:]
model = LGBMClassifier(n_estimators=100, max_depth=5, verbosity=-1)
model.fit(X_train, y_train)
preds = model.predict(X_test)
acc = float((preds == y_test).mean())
ml_signal = pd.Series(0, index=X_test.index)
ml_signal[preds == 1] = 1; ml_signal[preds == 0] = -1
ml_signal = ml_signal.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
ml_signal = _apply_session_filter(ml_signal, close.index)
prices = close.values.astype(np.float64); sigs = ml_signal.values.astype(np.int32)
eq, dd, tr, wins, ret, sh, _ = _backtest_numba(prices, sigs)
n_days = (close.index[-1] - close.index[0]).days
mon = ((1+ret)**(1/(n_days/30.44))-1)*100 if ret > -1 else 0
print(f" ML LightGBM: Test acc={acc:.1%} → Sh={sh:.1f} Mon={mon:.1f}% Tr={tr}")
return {"sharpe": float(sh), "monthly_pct": float(mon), "monthly_oos": 0,
"n_trades": int(tr), "win_rate": float(wins/tr) if tr>0 else 0,
"ml_accuracy": float(acc), "ml_model": "LightGBM"}
def load_data():
"""Load OHLCV data for all instruments from one or multiple HDF5 files."""
closes = {}
data_dir = OHLCV_PATH.parent
# Try main file first
if OHLCV_PATH.exists():
df = pd.read_hdf(OHLCV_PATH, key="data")
for inst in INSTRUMENTS:
try:
close = df.xs(inst, level="instrument")["$close"].sort_index()
closes[inst] = close
except KeyError:
pass
# Load from individual files if not found
instrument_files = {
"EURUSD": OHLCV_PATH,
"GBPUSD": data_dir / "gbpusdt_1min.h5",
"BTCUSD": data_dir / "btc_1min.h5",
"XAUUSD": data_dir / "xauusdt_1min.h5",
}
for inst, path in instrument_files.items():
if inst in closes:
continue
if not path.exists():
print(f" {inst}: file not found — skipping")
continue
try:
df = pd.read_hdf(path, key="data")
if isinstance(df.index, pd.MultiIndex):
try:
close = df.xs(inst, level="instrument")["$close"].sort_index()
except KeyError:
# Try with T suffix for crypto pairs
alt = inst + "T" if not inst.endswith("T") else inst.rstrip("T")
try:
close = df.xs(alt, level="instrument")["$close"].sort_index()
except KeyError:
inst_vals = df.index.get_level_values("instrument").unique()
for iv in inst_vals:
if inst[:3] in str(iv)[:3]:
close = df.xs(iv, level="instrument")["$close"].sort_index()
break
else:
raise KeyError(f"No instrument matching {inst}")
elif "$close" in df.columns:
close = df["$close"].sort_index()
close.index = pd.to_datetime(close.index)
elif "close" in df.columns:
close = df["close"].sort_index()
close.index = pd.to_datetime(close.index)
else:
close = df.iloc[:, 3].sort_index()
close.index = pd.to_datetime(close.index)
closes[inst] = close
except Exception as e:
print(f" {inst}: load error {e} — skipping")
for inst, close in closes.items():
print(f" {inst}: {len(close):,} bars, {close.index[0]}{close.index[-1]}")
return closes
def main():
iterations = 200
if "--iterations" in sys.argv:
iterations = int(sys.argv[sys.argv.index("--iterations") + 1])
print("=" * 60)
print(f" R&D Loop V2 — Multi-Instrument + Correlation Score")
print(f" Instruments: {', '.join(INSTRUMENTS)}")
print(f" Indicators: {len(INDICATORS_POOL)} | Strategy types: {len(STRATEGY_TYPES)}")
print(f" Features: Session Filter + Volatility Filter + OOS Split")
print(f" Iterations: {iterations}")
print("=" * 60)
print(" Loading data...")
closes = load_data()
if not closes:
print(" ERROR: No instruments loaded!"); return
loop = ResearchLoop(closes)
t0 = time.time()
for i in range(iterations):
hp = loop.hypothesize()
# Evaluate
try:
result = evaluate_multi(closes, hp, use_session=True, use_vola=True)
except Exception:
continue
result['hypothesis'] = hp
result['iteration'] = i + 1
result['timestamp'] = datetime.now().isoformat()
loop.history.append(result)
# Feedback
is_new_best = loop.feedback(result)
best_inst_metrics = [f"{inst}: {m['sharpe']:.1f}" for inst, m in result.get('per_instrument', {}).items() if m.get('sharpe', 0) != 0]
gen = hp.get('generation', '?')
if is_new_best:
print(f"\n ★ NEW BEST (#{i+1}, {gen}): {hp['description']}")
print(f" Score={result['composite_score']:.1f} Sh={result['sharpe']:.1f} "
f"Mon={result['monthly_pct']:.1f}% OOS={result['monthly_oos']:.1f}% "
f"Tr={result['n_trades']} [{', '.join(best_inst_metrics[:3])}]")
elif (i + 1) % 50 == 0:
top_indicators = set()
for r in loop.sota[:5]:
top_indicators.add(r['hypothesis'].get('trend_ind', r['hypothesis'].get('indicator', '?')))
print(f" [{i+1}/{iterations}] {gen:>7s} | SOTA: {len(loop.sota)} | "
f"Best Sh={loop.best_sharpe:.1f} Score={loop.best_score:.1f} | "
f"Explore: {loop.exploration_rate:.0%} | Inds: {','.join(sorted(top_indicators)[:4])}")
if (i + 1) % 100 == 0:
loop.record()
if len(loop.sota) > 10:
loop.exploration_rate = max(0.15, EXPLORATION_RATE - len(loop.sota) * 0.003)
elapsed = time.time() - t0
print(f"\n{'=' * 60}")
print(f" R&D Loop V2 Complete: {iterations} iterations in {elapsed:.0f}s")
print(f" SOTA Strategies: {len(loop.sota)} | Best Score: {loop.best_score:.1f}")
print(f"{'=' * 60}")
if loop.sota:
print(f"\n TOP DISCOVERIES (by composite score):")
for i, r in enumerate(loop.sota[:15], 1):
hp = r['hypothesis']
per_inst = r.get('per_instrument', {})
insts = ' '.join([f"{k}:{v['sharpe']:.0f}" for k, v in per_inst.items() if v['sharpe'] != 0])
print(f" {i:>2d}. {hp['description'][:45]:45s} "
f"Sc={r['composite_score']:.1f} Sh={r['sharpe']:+.1f} "
f"Mo={r['monthly_pct']:+.1f}% OOS={r['monthly_oos']:+.1f}% "
f"[{insts}]")
final = RESULTS_DIR / f"rd_loop_final_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
stripped = [{k: v for k, v in r.items() if k != 'equity_curves'} for r in loop.sota]
final.write_text(json.dumps(stripped, indent=2, default=str))
print(f"\n Saved: {final}")
exploit_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'exploit']
explore_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'explore']
optuna_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'optuna']
ml_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'ml']
print(f" Exploit: {len(exploit_best)} | Explore: {len(explore_best)} | "
f"Optuna: {len(optuna_best)} | ML: {len(ml_best)}")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1065,7 +1065,7 @@ class TestSchemaMigrationFuzzing:
c = db.conn.cursor()
c.execute("PRAGMA table_info(backtest_runs)")
cols = [row[1] for row in c.fetchall()]
assert cols.count(col_name) == 1
assert sum(1 for c in cols if c.lower() == col_name.lower()) == 1
finally:
db.close()