mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-30 17:07:43 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50c08866f1 | |||
| 50d1fb47e3 | |||
| 68caa7e88c | |||
| 891daf4df9 | |||
| 56346d5ae5 | |||
| 723ba6f004 | |||
| eb6b2dcd1f | |||
| a721605f4b | |||
| a9d181398b | |||
| 4773e95a6c | |||
| 9ce6a4e6ec | |||
| b793a8114b | |||
| 6bce4f2405 | |||
| 2e028ffc1e | |||
| 7d7c267d29 | |||
| 6cd362aa25 | |||
| a373710454 | |||
| ee3d7786c3 | |||
| 4b6dff1710 | |||
| 7c22287793 | |||
| 3874afb8dd | |||
| e168a5df7e | |||
| 8806b12ad6 | |||
| 61e6a09b95 | |||
| 9303b40fb9 | |||
| ab57498ccf | |||
| 6f399c1d96 |
@@ -1 +1 @@
|
||||
{".": "1.5.0"}
|
||||
{".":"0.8.0"}
|
||||
|
||||
+353
@@ -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 <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 >=1.2.2 (CVE symlink overwrite) ([9b87a1f](https://github.com/NicolasBohn/NexQuant/commit/9b87a1f5aec699507a173e10ebc82757fbd5ab69))
|
||||
* **deps:** pin aiohttp>=3.13.4 to patch 4 CVEs ([616590c](https://github.com/NicolasBohn/NexQuant/commit/616590cdc0445a6d9d0ea09a40d5f0a67f4bcaa5))
|
||||
* **deps:** relax aiohttp constraint to >=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 < 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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||

|
||||
|
||||
*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 (2020–2026)
|
||||
- 🔄 **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** (2020–2026) 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 (2020–present) 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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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 × (1−corr))
|
||||
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()
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user