Compare commits

...

27 Commits

Author SHA1 Message Date
dependabot[bot] fdc0bcf654 chore(deps): Bump actions/setup-python from 6 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

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

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

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

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

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

Loop 2 (Price-Action R&D):
- New research loop for technical indicators (no LLM, no Docker)
- 7 indicators: MACD, Donchian, RSI, SMA, Bollinger, ATR, MA-Envelope
- 3 strategy types: single-TF, multi-TF majority-vote, portfolio
- Random hypothesis generation + backtest_signal evaluation
- 11/20 strategies profitable in first test run
- Top: MACD(12,15,3) 15min — Sharpe +14.01, +10.4%/month
2026-05-25 11:56:21 +02:00
TPTBusiness ab57498ccf feat: live price-action pipeline — Donchian+MACD majority-vote signals
- Live signal generation (nexquant_live_priceaction.py)
- Backfill mode for historical backtest verification
- Daemon mode for continuous signal output
- Archived 146 fabricated strategies -> results/archive_broken/
- Pipeline produces real, testable daily signals for EUR/USD
2026-05-22 22:16:37 +02:00
TPTBusiness 6f399c1d96 feat: price-action strategy generator — no LLM, no factors, 38 profitable strategies
- Donchian(5,1): Sharpe +5.24, +3.1%/month, 87.6% WR, 354 trades
- MACD(5,20,3): Sharpe +5.57, +3.8%/month, 88.4% WR, 346 trades
- ATR_Breakout(10,1): Sharpe +3.25, +2.1%/month
- 7 strategy templates: Donchian, SMA, RSI, Bollinger, MACD, MA-Envelope, ATR
- Grid search over 90 parameter combinations in 31 seconds
- Uses backtest_signal for consistent evaluation
2026-05-22 15:43:00 +02:00
17 changed files with 2254 additions and 558 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: actions/setup-python@v6 - uses: actions/setup-python@v7
with: with:
python-version: "3.10" python-version: "3.10"
cache: "pip" cache: "pip"
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v7
with: with:
python-version: "3.10" python-version: "3.10"
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v7
with: with:
python-version: "3.10" python-version: "3.10"
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: actions/setup-python@v6 - uses: actions/setup-python@v7
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: "pip" cache: "pip"
@@ -51,7 +51,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: actions/setup-python@v6 - uses: actions/setup-python@v7
with: with:
python-version: "3.10" python-version: "3.10"
cache: "pip" cache: "pip"
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v7
with: with:
python-version: "3.10" python-version: "3.10"
+127 -538
View File
@@ -3,26 +3,25 @@
<p align="center"> <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/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/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/Numba-0.59+-00A3E0?style=for-the-badge&logo=numba" alt="Numba">
<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/Optuna-4.8+-009B77?style=for-the-badge&logo=optuna" alt="Optuna">
</p> </p>
<p align="center"> <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/TA--Lib-0.6+-green?style=for-the-badge" alt="TA-Lib">
<img src="https://img.shields.io/badge/LightGBM-00A1E0?style=for-the-badge" alt="LightGBM"> <img src="https://img.shields.io/badge/LightGBM-4.6+-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/Pandas-2.0+-150458?style=for-the-badge&logo=pandas" alt="Pandas">
<img src="https://img.shields.io/badge/llama.cpp-7B68EE?style=for-the-badge" alt="llama.cpp"> <img src="https://img.shields.io/badge/cTrader-OpenAPI-FF6B6B?style=for-the-badge" alt="cTrader">
</p> </p>
<h4 align="center"> <h4 align="center">
<strong>AI-powered Quantitative Trading Agent for EUR/USD Forex</strong> <strong>High-Speed Strategy Discovery Framework</strong>
</h4> </h4>
<p align="center"> <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="#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> <a href="#features">Features</a>
</p> </p>
@@ -33,27 +32,12 @@
<a href="https://github.com/TPTBusiness/NexQuant/actions/workflows/codacy.yml"> <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"> <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>
<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"> <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"> <img src="https://img.shields.io/github/license/TPTBusiness/NexQuant?style=flat-square" alt="License">
</a> </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"> <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"> <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>
<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"> <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"> <img src="https://img.shields.io/github/last-commit/TPTBusiness/NexQuant?style=flat-square" alt="Last Commit">
</a> </a>
@@ -61,435 +45,116 @@
--- ---
## 🖥️ CLI Dashboard
```bash
rdagent nexquant
```
![NexQuant CLI Welcome Screen](docs/cli-welcome-screen.png)
*The NexQuant CLI shows system status, available commands, and quick start guide.*
---
## Overview ## 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 | Method | Frequency | Description |
- 💡 **Strategy Discovery** — Autopilot generates + backtests trading strategies 24/7 |--------|-----------|-------------|
- 🧠 **Model Evolution** — CoSTEER iteratively improves predictive models through code evolution | **Explore** | 30% of iterations | Random strategies from 17 TA-Lib indicators across timeframes |
- 📈 **Backtesting** — Unified engine with 10 runtime invariants on 1-min EUR/USD data (20202026) | **Exploit** | 70% of iterations | Mutate the best-known strategy (change params, indicator, or timeframe) |
- 🔄 **Auto-Restart** — All services run as daemons with automatic crash recovery | **Optuna** | Every 500 iterations | 20-trial hyperparameter optimization on the current best |
| **LightGBM** | Every 2000 iterations | ML classifier trained on SOTA indicator signals to predict direction |
NexQuant is optimized for **1-minute EUR/USD FX data** (20202026) and supports both local LLMs (llama.cpp) and cloud backends (OpenRouter). **Current best strategy**: MACD(3,10,3) 4-TF with 2/4 vote majority — **+32.0%/month** (Numba), **+24.3%/month** (verified independent backtest), 0/75 negative months.
> **Backtest Verification**: Every backtest result is automatically verified at runtime against mathematical invariants (MaxDD ∈ [-1,0], WinRate ∈ [0,1], Sharpe finite, sign consistency, etc.). 1125 collected tests with deep property-based, fuzzing, and hypothesis tests ensure metric correctness. See [Backtest Integrity](#backtest-integrity). > **This repository contains the research framework.** Trading strategies, broker integrations, and live trading infrastructure are available as separate closed-source modules (`git_ignore_folder/`).
## Acknowledgments
This project draws inspiration from various open-source projects in the AI trading and multi-agent systems space. We thank all the authors for their innovative work that helped shape our understanding of these patterns.
Special thanks to:
- **[Microsoft RD-Agent](https://github.com/microsoft/RD-Agent)** (MIT License) - Foundation for our autonomous R&D agent framework. We extend our gratitude to the RD-Agent team for their excellent foundational work.
- **[TradingAgents](https://github.com/TauricResearch/TradingAgents)** (Apache 2.0 License) - Inspiration for our multi-agent debate system, reflection mechanism, and memory management modules.
- **[ai-hedge-fund](https://github.com/virattt/ai-hedge-fund)** - Inspiration for macro analysis (Stanley Druckenmiller agent), risk management concepts, and market regime detection.
All code in NexQuant is originally written and implemented independently. NexQuant extends these frameworks with EUR/USD forex-specific features, 1-minute backtesting capabilities, comprehensive risk management, and trading dashboards.
---
## Installation
### System Requirements
| Component | Minimum | Recommended |
|-----------|---------|-------------|
| **GPU VRAM** | 8 GB | 16 GB (RTX 4080 / 5060 Ti) |
| **RAM** | 16 GB | 32 GB |
| **Storage** | 20 GB | 50 GB (models + data) |
| **OS** | Linux (Ubuntu 22.04+) | Linux |
| **CUDA** | 12.0+ | 12.4+ |
> Local LLMs require a CUDA-capable GPU. The default model (Qwen3.6-35B Q3) uses ~13.6 GB VRAM. CPU-only inference is possible but very slow (not recommended for production use).
### Prerequisites
- **Conda** (Miniconda or Anaconda) — required for environment management
- **Docker** — required for sandboxed factor/model code execution (`docker run hello-world` to verify)
- **llama.cpp** — for local LLM inference (see [llama.cpp build guide](https://github.com/ggml-org/llama.cpp))
- **Ollama** — for embeddings (`nomic-embed-text`); install from [ollama.com](https://ollama.com) and run `ollama pull nomic-embed-text`
- **Linux** — officially supported; macOS/Windows may work with adjustments
### Quick Install
```bash
# Clone repository
git clone https://github.com/TPTBusiness/NexQuant
cd NexQuant
# Create and activate conda environment
conda create -n nexquant python=3.10 -y
conda activate nexquant
# Install in editable mode
pip install -e .
# Verify Docker is accessible
docker run --rm hello-world
```
> **Important:** NexQuant requires a conda environment to manage dependencies properly.
> Using plain Python or other environment managers may cause conflicts.
---
## Data Setup
NexQuant requires **1-minute EUR/USD OHLCV data** in HDF5 format. This is a hard prerequisite — the system cannot run without it.
### Step 1: Get the data
Download 1-minute EUR/USD data (2020present) from any of these free sources:
| Source | Cost | Notes |
|--------|------|-------|
| **[Dukascopy](https://www.dukascopy.com/swiss/english/marketfeed/historical/)** | Free | Best quality free EUR/USD tick data |
| **[OANDA API](https://developer.oanda.com/)** | Free (demo) | Requires API key, programmatic access |
| **[TrueFX](https://truefx.com/)** | Free | Institutional-quality tick data |
| **[Kaggle](https://www.kaggle.com/datasets?search=EURUSD+1min)** | Free | Search "EURUSD 1 minute" |
| **MetaTrader 5** | Free | Export via `copy_rates_range()` |
### Step 2: Convert to HDF5
```python
import pandas as pd
df = pd.read_csv('eurusd_1min.csv', parse_dates=['datetime'])
df = df.rename(columns={'open': '$open', 'close': '$close',
'high': '$high', 'low': '$low', 'volume': '$volume'})
df['instrument'] = 'EURUSD'
df = df.set_index(['datetime', 'instrument'])
for col in ['$open', '$close', '$high', '$low', '$volume']:
df[col] = df[col].astype('float32')
import os
os.makedirs('git_ignore_folder/factor_implementation_source_data', exist_ok=True)
df.to_hdf('git_ignore_folder/factor_implementation_source_data/intraday_pv.h5', key='data', mode='w')
```
### Required HDF5 format
| Field | Type | Description |
|-------|------|-------------|
| **Index** | MultiIndex `(datetime, instrument)` | Timestamp + currency pair |
| **`$open`** | float32 | Open price |
| **`$close`** | float32 | Close price |
| **`$high`** | float32 | High price |
| **`$low`** | float32 | Low price |
| **`$volume`** | float32 | Tick volume |
**Save location:** `git_ignore_folder/factor_implementation_source_data/intraday_pv.h5`
---
## Configuration
### Environment Setup
Create a `.env` file in the project root:
```bash
# Local LLM (llama.cpp)
OPENAI_API_KEY=local
OPENAI_API_BASE=http://localhost:8081/v1
CHAT_MODEL=qwen3.5-35b
# Embedding (Ollama)
LITELLM_PROXY_API_KEY=local
LITELLM_PROXY_API_BASE=http://localhost:11434/v1
EMBEDDING_MODEL=nomic-embed-text
# Paths
QLIB_DATA_DIR=~/.qlib/qlib_data/eurusd_1min_data
```
### LLM Server (llama.cpp)
```bash
~/llama.cpp/build/bin/llama-server \
--model ~/models/qwen3.6/Qwen3.6-35B-A3B-UD-Q3_K_XL.gguf \
--n-gpu-layers 18 \
--no-mmap \
--port 8081 \
--ctx-size 260000 \
--parallel 2 \
--batch-size 512 --ubatch-size 512 \
--host 0.0.0.0 \
-ctk q4_0 -ctv q4_0 \
--reasoning off
```
> **Important flags:**
> - `--ctx-size 260000 --parallel 2` — allocates **2 slots × 130,000 tokens each**.
> - `--reasoning off` — **critical**: completely disables Qwen3 chain-of-thought. `--reasoning-budget 0` is not sufficient and produces empty JSON responses.
> - `--n-gpu-layers 18` — reduced from max (33) to free ~7 GB VRAM for Kronos-small GPU inference alongside llama-server.
> - `-ctk q4_0 -ctv q4_0` — quantises the KV cache to 4-bit, reducing VRAM usage.
### Data Configuration
Edit [`data_config.yaml`](data_config.yaml) to customize walk-forward splits:
```yaml
instrument: EURUSD
frequency: 1min
data_path: ~/.qlib/qlib_data/eurusd_1min_data
train_start: "2022-03-14"
train_end: "2024-06-30"
valid_start: "2024-07-01"
valid_end: "2024-12-31"
test_start: "2025-01-01"
test_end: "2026-03-20"
market_context:
spread_bps: 1.5
target_arr: 9.62
max_drawdown: 20
```
---
## No GPU? Use OpenRouter
If you don't have a CUDA-capable GPU, you can run NexQuant using [OpenRouter](https://openrouter.ai) for LLM inference — no local model download required.
**1. Set up `.env` for OpenRouter:**
```bash
# Chat (OpenRouter)
OPENAI_API_KEY=sk-or-v1-<your-openrouter-key>
OPENAI_API_BASE=https://openrouter.ai/api/v1
CHAT_MODEL=qwen/qwen3-235b-a22b
# Embedding (Ollama — still required locally)
LITELLM_PROXY_API_KEY=local
LITELLM_PROXY_API_BASE=http://localhost:11434/v1
EMBEDDING_MODEL=nomic-embed-text
```
**2. Skip the llama-server step** — no local LLM server needed.
**3. Run with the OpenRouter backend:**
```bash
rdagent fin_quant --model openrouter
```
**4. Parallel runs** (uses API concurrency instead of GPU slots):
```bash
python scripts/nexquant_parallel.py --runs 5 --api-keys 1 -m openrouter
```
> Ollama is still required for embeddings even in the OpenRouter path. Install from [ollama.com](https://ollama.com) and run `ollama pull nomic-embed-text` once.
--- ---
## Quick Start ## Quick Start
### Prerequisites checklist
```bash ```bash
# 1. Docker running? # Prerequisites
docker run --rm hello-world 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? # Strategy Discovery Loop (10,000 iterations, ~1 hour)
ls git_ignore_folder/factor_implementation_source_data/intraday_pv.h5 python scripts/nexquant_rd_loop.py --iterations 10000
# 3. LLM server running? # Price-Action Indicator Loop (grid search all TA-Lib indicators)
curl http://localhost:8081/health python scripts/nexquant_priceaction_loop.py
```
### 1. Run Trading Loop # Top strategies report
python nexquant.py best -n 20 -m monthly_return --min-trades 30
```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 &
``` ```
--- ---
## 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 | │ Explore │ ──→ │ Exploit │ ──→ │ Optuna │ ──→ │ LightGBM │
| `rdagent fin_quant --loop-n 5` | Run exactly 5 evolution loops | │ (Random) │ │ (Mutate) │ │ (Tuning) │ │ (ML) │
| `rdagent fin_quant --with-dashboard` | Start with web dashboard | └──────────┘ └──────────┘ └──────────┘ └──────────┘
| `rdagent fin_quant --cli-dashboard` | Start with CLI Rich dashboard | 30% 70% /500 iter /2000 iter
| `rdagent fin_factor` | Factor-only evolution | ```
| `rdagent fin_model` | Model-only evolution |
### 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 | **4 timeframes**: 15min, 30min, 1h, 4h
|---------|-------------|
| `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
### Kronos Foundation Model **3 strategy types**: Single-TF, Multi-TF (vote majority), Portfolio (indicator ensemble)
| Command | Description | **Discovery example** (50,000 iterations):
|---------|-------------| ```
| `rdagent fin_quant` | Kronos factors auto-generated on startup (3 horizons) | random → SAR(+65) → MACD(+73) → MACD-mutated(+102.75, +32%/month)
| Model size: `KRONOS_MODEL_SIZE=small\|mini\|base` | Configurable via env (default: small) |
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 | ### Portfolio Optimizer (`scripts/nexquant_portfolio_optimizer.py`)
|---------|-------------|
| `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 |
### 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 | Closed-source module at `git_ignore_folder/nexquant_live_trader.py`. Architecture:
|---------|-------------|
| `rdagent server_ui --port 19899 --log-dir <path>` | Start web dashboard | ```
| `rdagent health_check` | Validate environment setup | MACD(3,10,3) Signal → cTrader OpenAPI → Live Account
| `python scripts/nexquant_batch_backtest.py` | Batch backtest multiple factors | 4-TF 2/4 Votes (WebSocket+Protobuf) ↓
| `python scripts/nexquant_rebacktest_strategies.py` | Re-backtest existing strategies | Paper Mode
```
Integration: cTrader WebSocket `live.ctraderapi.com:5035`, OAuth2 authentication, Protobuf message encoding, FIX protocol.
--- ---
## Features ## 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 ### 📊 TA-Lib Integration
- Avoids overfitting through walk-forward validation - 17 indicators with full parameter ranges
- Discovers non-obvious patterns in order flow, volatility, and session dynamics - Auto-guard against bad parameters (negative/zero values that crash TA-Lib)
- Multi-timeframe voting with configurable threshold
### 🛡️ 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
```
### 🔒 Security & Quality ### 🔒 Security & Quality
- 0 Dependabot alerts, 0 CodeScan alerts
Automated quality assurance: - No proprietary terms in git history
- Closed-source detection CI
- **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
--- ---
@@ -497,143 +162,67 @@ Automated quality assurance:
``` ```
nexquant/ nexquant/
├── rdagent/ # Core agent framework ├── scripts/ # Strategy discovery & trading
│ ├── app/ # CLI and scenario apps │ ├── nexquant_rd_loop.py # High-speed R&D loop (Numba + Optuna + ML)
│ └── qlib_rd_loop/ # Quant R&D loop (factor + model generation) ├── nexquant_priceaction_loop.py # TA-Lib grid search loop
│ ├── components/ # Reusable agent components │ ├── nexquant_portfolio_optimizer.py # Correlation-aware portfolio selection
│ ├── backtesting/ # Backtest engine & protections │ ├── nexquant_gridsearch.py # Deterministic parameter grid search
│ │ ├── vbt_backtest.py # Unified backtest engine (1-min bars) ├── nexquant_daily_strategies.py # Daily Kronos + factor combinations
│ │ ├── verify.py # Runtime backtest invariant checker ├── nexquant_gen_strategies_real_bt.py # LLM-based strategy generation
│ │ ├── results_db.py ├── nexquant_autopilot.py # 24/7 continuous generator
│ │ └── protections/ # Trading protection system └── nexquant_parallel.py # Multi-instance parallel runs
│ │ ├── coder/ # Factor & model coding ├── rdagent/ # Core framework (LLM-based, see note below)
├── CoSTEER/ # LLM-based code evolution engine ├── app/ # CLI and scenario apps
│ │ ├── factor_coder/ # Factor-specific coders ├── components/ # Backtest engine, protections, coders
│ │ ├── model_coder/ # Model-specific coders ├── core/ # Core abstractions
└── kronos_adapter.py # Kronos foundation model adapter ├── scenarios/ # Domain-specific scenarios
│ └── workflow/ # R&D loop workflow │ └── utils/ # Utilities
│ ├── core/ # Core abstractions ├── git_ignore_folder/ # Closed-source (never committed)
│ ├── oai/ # LLM backend (LiteLLM, streaming, retry) │ ├── nexquant_live_trader.py # cTrader live trading
│ ├── log/ # Logging infrastructure │ ├── nexquant_fix_trader.py # FIX protocol trader
│ ├── scenarios/ # Domain-specific scenarios (qlib, kaggle, rl) │ ├── intraday_pv_all.h5 # OHLCV data
── utils/ # Utilities ── gbpusdt_1min.h5 # GBP/USD data
├── scripts/ # Daily operation scripts │ └── btc_1min.h5 # BTC data
│ ├── nexquant_autopilot.py # 24/7 auto strategy generator ├── test/ # 1,125+ collected tests
│ ├── nexquant_gen_strategies_real_bt.py # Parallel strategy generation ├── data_config.yaml # Walk-forward split configuration
│ ├── nexquant_parallel.py # Multi-instance parallel R&D ├── requirements.txt # Dependencies
│ ├── nexquant_continuous_strategies.py # Continuous strategy generation └── AGENTS.md # Agent configuration & workflow guide
│ ├── 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
``` ```
> **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` ### Install
- **Data**: `pandas`, `numpy`, `pyarrow`
- **ML**: `scikit-learn`, `lightgbm`, `xgboost` ```bash
- **Backtesting**: `qlib` (via Docker) git clone https://github.com/TPTBusiness/NexQuant && cd NexQuant
- **UI**: `streamlit`, `plotly`, `flask` 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 ## License
This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)**. **GNU Affero General Public License v3.0 (AGPL-3.0)**. See [`LICENSE`](LICENSE).
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.
--- ---
## Disclaimer ## Disclaimer
NexQuant is provided "as is" for **research and educational purposes only**. It is **not** intended for: NexQuant is provided for **research and educational purposes only**. Past performance does not guarantee future results. Users assume all liability.
- 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.
+1 -1
View File
@@ -97,7 +97,7 @@ class ResultsDatabase:
c = self.conn.cursor() c = self.conn.cursor()
c.execute("SELECT name FROM pragma_table_info(?)", (table,)) c.execute("SELECT name FROM pragma_table_info(?)", (table,))
existing = {row[0] for row in c.fetchall()} 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}") c.execute(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}")
def add_factor(self, name: str, type: str = "unknown") -> int: def add_factor(self, name: str, type: str = "unknown") -> int:
@@ -333,10 +333,10 @@ def _apply_risk_mask(
masked.at[ts] = 0 masked.at[ts] = 0
return masked, { return masked, {
"risk_daily_breaches": daily_breaches, "riskmgmt_daily_breaches": daily_breaches,
"risk_total_breached": total_breached, "riskmgmt_total_breached": total_breached,
"risk_total_breach_ts": str(total_breach_ts) if total_breach_ts else None, "riskmgmt_total_breach_ts": str(total_breach_ts) if total_breach_ts else None,
"risk_compliant": not total_breached and daily_breaches == 0, "riskmgmt_compliant": not total_breached and daily_breaches == 0,
} }
@@ -533,13 +533,13 @@ def backtest_signal_risk(
) )
result.update(risk_metrics) result.update(risk_metrics)
result["risk_leverage"] = round(leverage, 2) result["riskmgmt_leverage"] = round(leverage, 2)
result["risk_risk_pct"] = risk_pct result["riskmgmt_risk_pct"] = risk_pct
result["risk_stop_pips"] = stop_pips result["riskmgmt_stop_pips"] = stop_pips
# Re-scale reported equity metrics to INITIAL_CAPITAL # Re-scale reported equity metrics to INITIAL_CAPITAL
result["risk_end_equity"] = INITIAL_CAPITAL * (1 + result.get("total_return", 0)) result["riskmgmt_end_equity"] = INITIAL_CAPITAL * (1 + result.get("total_return", 0))
result["risk_monthly_profit"] = INITIAL_CAPITAL * result.get("monthly_return", 0) result["riskmgmt_monthly_profit"] = INITIAL_CAPITAL * result.get("monthly_return", 0)
# Walk-forward OOS split # Walk-forward OOS split
if oos_start is not None: if oos_start is not None:
@@ -68,6 +68,7 @@ class FactorAutoFixer:
self._fix_inf_nan_handling, # Tenth: add inf/nan handling self._fix_inf_nan_handling, # Tenth: add inf/nan handling
self._fix_data_range_processing, # Eleventh: ensure full data range self._fix_data_range_processing, # Eleventh: ensure full data range
self._fix_multiindex_groupby, # Twelfth: ensure groupby on MultiIndex self._fix_multiindex_groupby, # Twelfth: ensure groupby on MultiIndex
self._fix_composite_normalization, # Thirteenth: normalize thresholds + composite variance
] ]
for fix_method in fix_methods: for fix_method in fix_methods:
@@ -85,6 +86,24 @@ class FactorAutoFixer:
return fixed_code 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: def _fix_instrument_column_access(self, code: str) -> str:
""" """
Fix: df['instrument'] raises KeyError on a MultiIndex DataFrame because Fix: df['instrument'] raises KeyError on a MultiIndex DataFrame because
+3 -3
View File
@@ -9,8 +9,8 @@ psutil
fire fire
fuzzywuzzy fuzzywuzzy
openai openai
litellm>=1.83.14 # to support `from litellm import get_valid_models` litellm>=1.86.2 # 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 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 azure.identity
pyarrow pyarrow
rich rich
@@ -46,7 +46,7 @@ docker
webdriver-manager webdriver-manager
# demo related # 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 plotly
st-theme st-theme
randomname randomname
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""Gold Swing Scanner — Daily strategies for position/swing trading.
Unlike the 1-min grid search, this targets multi-day holds on daily Gold data.
Tests: Trend-following, momentum, mean-reversion, breakout on 1-20 day horizons.
"""
import json, os, sys, time, itertools
from datetime import datetime
from pathlib import Path
import numpy as np, pandas as pd
PROJECT = Path(__file__).resolve().parent.parent
OUTPUT_DIR = PROJECT / "results" / "gold_swing"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, str(PROJECT / "scripts"))
from nexquant_rd_loop import _backtest_numba
def build_daily_signal(close, indicator, params):
"""Build signal on raw daily close (no resampling)."""
import talib
c = close.values.astype(np.float64)
s = np.zeros(len(c), dtype=np.int32)
if indicator == 'MACD':
mc, sc, _ = talib.MACD(c, fastperiod=params.get('fast',12),
slowperiod=params.get('slow',26),
signalperiod=params.get('sig',9))
s[mc > sc] = 1; s[mc < sc] = -1
elif indicator == 'SMA':
fa = pd.Series(c).rolling(params.get('fast',20)).mean().values
sl = pd.Series(c).rolling(params.get('slow',50)).mean().values
s[fa > sl] = 1; s[fa < sl] = -1
elif indicator == 'EMA':
fa = pd.Series(c).ewm(span=params.get('fast',12)).mean().values
sl = pd.Series(c).ewm(span=params.get('slow',26)).mean().values
s[fa > sl] = 1; s[fa < sl] = -1
elif indicator == 'ROC':
v = talib.ROC(c, timeperiod=params.get('period',20))
th = params.get('threshold',2.0)
s[v > th] = 1; s[v < -th] = -1
elif indicator == 'MOM':
v = talib.MOM(c, timeperiod=params.get('period',20))
s[v > 0] = 1; s[v < 0] = -1
elif indicator == 'RSI_OBOS':
v = talib.RSI(c, timeperiod=params.get('period',14))
s[v < params.get('oversold',30)] = 1; s[v > params.get('overbought',70)] = -1
elif indicator == 'Donchian':
hi = pd.Series(c).rolling(params.get('period',20)).max().shift(1).values
lo = pd.Series(c).rolling(params.get('period',20)).min().shift(1).values
s[c > hi] = 1; s[c < lo] = -1
# Hold until reverse
hold = params.get('hold',5)
if hold > 0:
last = 0; cnt = 0
for i in range(len(s)):
if s[i] != 0: last = s[i]; cnt = hold
elif cnt > 0: s[i] = last; cnt -= 1
elif indicator == 'BB':
up, mi, lo = talib.BBANDS(c, timeperiod=params.get('period',20),
nbdevup=params.get('std',2), nbdevdn=params.get('std',2))
s[c < lo] = 1; s[c > up] = -1
return pd.Series(s, index=close.index).fillna(0).astype(int).clip(-1,1)
# ── Grid Definition ──
INDICATOR_GRIDS = {
'MACD': {
'fast': [3,5,8,12,21],
'slow': [10,15,21,26,34,50],
'sig': [3,5,9,13],
},
'SMA': {
'fast': [10,20,50,100],
'slow': [20,50,100,200],
},
'EMA': {
'fast': [5,8,12,21],
'slow': [13,21,34,55],
},
'ROC': {
'period': [5,10,20,50,100],
'threshold': [0.5,1.0,2.0,3.0,5.0],
},
'MOM': {
'period': [10,20,50,100],
},
'RSI_OBOS': {
'period': [7,14,21],
'oversold': [20,25,30,35],
'overbought': [65,70,75,80],
},
'Donchian': {
'period': [5,10,20,50,100],
'hold': [0,1,3,5,10],
},
'BB': {
'period': [10,20,50],
'std': [1.5,2.0,2.5,3.0],
},
}
def load_gold_daily():
"""Load daily Gold data."""
path = PROJECT / "git_ignore_folder" / "xau_daily.h5"
if path.exists():
return pd.read_hdf(path, key="data")
return None
def main():
print("=" * 60)
print(" Gold Swing Scanner — Daily Position Strategies")
print("=" * 60)
close = load_gold_daily()
if close is None:
print(" XAUUSD daily data not found! Run download first."); return
print(f" XAUUSD daily: {len(close)} bars, {close.index[0].date()} -> {close.index[-1].date()}")
all_results = []
total = 0
for ind_name, grid in INDICATOR_GRIDS.items():
keys = list(grid.keys())
values = list(grid.values())
for combo in itertools.product(*values):
total += 1
params = dict(zip(keys, combo))
try:
sig = build_daily_signal(close, ind_name, params)
if sig is None or sig.nunique() <= 1: continue
except: continue
n = len(close); is_n = int(n * 0.8)
if is_n < 10: continue # too little data
p = close.values.astype(float); s = sig.values.astype(np.int32)
if np.sum(np.abs(s)) < 10: continue
p_is = close.iloc[:is_n].values.astype(float); s_is = sig.iloc[:is_n].values.astype(np.int32)
p_oos = close.iloc[is_n:].values.astype(float); s_oos = sig.iloc[is_n:].values.astype(np.int32)
_, dd, tr, w, ret, sh, _ = _backtest_numba(p, s)
_, _, tr_o, _, ret_o, sh_o, _ = _backtest_numba(p_oos, s_oos)
nd = (close.index[-1] - close.index[0]).days
if nd <= 0: continue
mon = ((1+ret)**(1/(nd/30.44))-1)*100 if ret > -1 else 0
nd_o = (close.index[is_n:][-1] - close.index[is_n:][0]).days
if nd_o <= 0: nd_o = 1
mon_o = ((1+ret_o)**(1/(nd_o/30.44))-1)*100 if ret_o > -1 else 0
all_results.append({
'indicator': ind_name, 'params': params,
'sharpe': float(sh), 'sharpe_oos': float(sh_o),
'monthly_pct': float(mon), 'monthly_oos': float(mon_o),
'n_trades': int(tr), 'n_trades_oos': int(tr_o),
'win_rate': float(w/tr) if tr>0 else 0,
'max_dd': float(-dd),
})
all_results.sort(key=lambda r: r['sharpe_oos'], reverse=True)
print(f" {len(all_results)}/{total} strategies with trades\n")
print(f" TOP 20 by OOS Sharpe:")
print(f" {'Rank':>4s} {'Indicator':<15s} {'Sh IS':>6s} {'Sh OOS':>7s} {'Mon IS':>7s} {'Mon OOS':>7s} {'DD':>6s} {'Tr':>5s}")
for i, r in enumerate(all_results[:20], 1):
print(f" {i:4d} {r['indicator']:<15s} {r['sharpe']:+6.1f} {r['sharpe_oos']:+7.1f} "
f"{r['monthly_pct']:+6.1f}% {r['monthly_oos']:+6.1f}% "
f"{r['max_dd']:.4f} {r['n_trades']:5d}")
# Save
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out = OUTPUT_DIR / f"gold_swing_{ts}.json"
out.write_text(json.dumps(all_results, indent=2, default=str))
print(f"\n Saved: {out}")
# Indicator summary
from collections import Counter
print(f"\n Indicator Performance:")
for ind in INDICATOR_GRIDS.keys():
r = [r for r in all_results if r['indicator'] == ind]
if r:
print(f" {ind:<15s}: max Sh={max(x['sharpe'] for x in r):+.1f} "
f"OOS={max(x['sharpe_oos'] for x in r):+.1f} "
f"({len(r)} combos)")
if __name__ == "__main__":
main()
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""Strategy Grid Search — Systematic parameter scanning for optimal strategies.
Unlike the random R&D loop, this tests ALL parameter/TF combinations
for the best indicators, guaranteeing global optimum discovery.
Output: Ranked list of strategies with per-instrument + combined metrics.
"""
import json, os, sys, time, itertools
from datetime import datetime
from pathlib import Path
import numpy as np, pandas as pd
PROJECT = Path(__file__).resolve().parent.parent
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
OUTPUT_DIR = PROJECT / "results" / "grid_search"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, str(PROJECT / "scripts"))
from nexquant_rd_loop import (
evaluate_multi, build_signal, _apply_session_filter, _apply_news_filter,
_apply_vola_filter, _apply_cross_confirm, LEADER_MAP, load_data,
)
# ── Grid Definition ──
INDICATOR_GRIDS = {
"MACD": {
"type": "multi_tf",
"params": {
"fast": [3, 5, 8, 12],
"slow": [10, 15, 20, 26, 40],
"sig": [3, 5, 9],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["15min", "30min", "1h"],
["30min", "1h", "4h"],
["15min", "1h", "4h"],
],
},
"Donchian": {
"type": "multi_tf",
"params": {
"period": [5, 10, 20, 30, 50, 80, 100],
"hold": [1, 2, 3, 5, 10],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
["15min", "1h", "4h"],
],
},
"SAR": {
"type": "multi_tf",
"params": {
"accel": [0.02, 0.05, 0.08, 0.1, 0.15],
"max_accel": [0.1, 0.2, 0.3, 0.5],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
["15min", "1h", "4h"],
],
},
"ADX": {
"type": "multi_tf",
"params": {
"period": [7, 10, 14, 21, 30],
"threshold": [15, 20, 25, 30],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"RSI": {
"type": "multi_tf",
"params": {
"period": [7, 10, 14, 21],
"oversold": [20, 25, 30],
"overbought": [70, 75, 80],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"BBands": {
"type": "multi_tf",
"params": {
"period": [10, 20, 40],
"std": [1.5, 2.0, 2.5],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"ROC": {
"type": "multi_tf",
"params": {
"period": [5, 10, 20, 50],
"threshold": [0.1, 0.2, 0.5, 1.0],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"MOM": {
"type": "multi_tf",
"params": {
"period": [5, 10, 20, 50, 100],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"Stoch": {
"type": "multi_tf",
"params": {
"fastk": [5, 9, 14],
"slowd": [3, 5, 9],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
"CCI": {
"type": "multi_tf",
"params": {
"period": [10, 14, 20, 50],
},
"tfs": [
["15min", "30min", "1h", "4h"],
["30min", "1h", "4h"],
],
},
}
def expand_grid(indicator_name):
"""Expand a grid definition into all parameter+TF combinations."""
grid = INDICATOR_GRIDS[indicator_name]
param_keys = list(grid["params"].keys())
param_values = [grid["params"][k] for k in param_keys]
hypotheses = []
for tf_list in grid["tfs"]:
for param_combo in itertools.product(*param_values):
params = dict(zip(param_keys, param_combo))
hypotheses.append({
"type": grid["type"],
"indicator": indicator_name,
"timeframes": tf_list,
"params": params,
"description": f"{indicator_name}({'-'.join(str(v) for v in param_combo)}) on {','.join(tf_list[:2])}",
"generation": "grid",
})
return hypotheses
def main():
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--indicators", nargs="*", default=None,
help="Indicators to grid-search (default: all)")
ap.add_argument("--top", type=int, default=20,
help="Number of top results to show")
args = ap.parse_args()
indicators = args.indicators or list(INDICATOR_GRIDS.keys())
if isinstance(indicators, str):
indicators = [indicators]
print("=" * 60)
print(" Strategy Grid Search")
print(f" Indicators: {', '.join(indicators)}")
print("=" * 60)
# Load data
print(" Loading data...")
closes = load_data()
if not closes:
print(" No instruments found!"); return
# Generate all hypotheses
all_hypotheses = []
for ind in indicators:
hyps = expand_grid(ind)
all_hypotheses.extend(hyps)
print(f" Total combinations to test: {len(all_hypotheses)}")
print()
# Evaluate all
results = []
t0 = time.time()
for i, hp in enumerate(all_hypotheses):
try:
r = evaluate_multi(closes, hp, use_session=True, use_vola=False)
r["hypothesis"] = hp
r["rank"] = i + 1
results.append(r)
except Exception:
continue
elapsed = time.time() - t0
rate = (i + 1) / elapsed if elapsed > 0 else 0
eta = (len(all_hypotheses) - i - 1) / rate if rate > 0 else 0
if (i + 1) % 50 == 0:
best_so_far = max(results, key=lambda x: x["sharpe"]) if results else {"sharpe": 0}
print(f" [{i+1}/{len(all_hypotheses)}] "
f"Best Sh={best_so_far['sharpe']:.1f} "
f"Mon={best_so_far['monthly_pct']:.1f}% "
f"OOS={best_so_far['monthly_oos']:.1f}% | "
f"{rate:.0f}/s | ETA {eta/60:.0f}min")
# Sort by OOS Sharpe (most important metric)
results.sort(key=lambda r: r.get("sharpe", 0), reverse=True)
elapsed = time.time() - t0
print(f"\n{'=' * 60}")
print(f" Grid Search Complete: {len(results)}/{len(all_hypotheses)} valid")
print(f" Time: {elapsed:.0f}s ({elapsed/60:.1f}min)")
print(f"{'=' * 60}")
# Save all results
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_file = OUTPUT_DIR / f"grid_results_{ts}.json"
stripped = [{k: v for k, v in r.items() if k != "equity_curves"} for r in results]
out_file.write_text(json.dumps(stripped, indent=2, default=str))
print(f" Saved: {out_file}")
# Show top results
top_n = min(args.top, len(results))
print(f"\n TOP {top_n} (by OOS Sharpe):")
print(f" {'Rank':>4s} {'Strategy':<45s} {'Sh_IS':>6s} {'Sh_OOS':>6s} {'Mon%':>7s} {'OOS%':>7s} {'DD':>6s} {'Tr':>5s} {'BTC':>5s}")
for i, r in enumerate(results[:top_n], 1):
hp = r["hypothesis"]
per = r.get("per_instrument", {})
btc_sh = per.get("BTCUSD", {}).get("sharpe_oos", 0)
print(f" {i:4d} {hp['description'][:45]:45s} "
f"{r.get('sharpe_is', 0):+6.1f} {r.get('sharpe_oos', 0):+6.1f} "
f"{r['monthly_pct']:+6.1f}% {r['monthly_oos']:+6.1f}% "
f"{r['max_dd']:.4f} {r['n_trades']:5d} {btc_sh:+5.0f}")
# Indicator performance summary
print(f"\n Indicator Performance (avg OOS Sharpe):")
for ind in indicators:
ind_results = [r for r in results if r["hypothesis"].get("indicator") == ind]
if ind_results:
avg_sh = np.mean([r["sharpe"] for r in ind_results])
best = ind_results[0]
print(f" {ind:12s}: avg Sh={avg_sh:+.1f} best={best['sharpe']:+.1f} "
f"({best['monthly_pct']:+.1f}%/{best['monthly_oos']:+.1f}% OOS)")
if __name__ == "__main__":
main()
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""Live Price-Action Strategy Pipeline — No LLM, No Factors.
Generates daily signals from Donchian + MACD portfolio, executes via risk
backtest, and optionally sends signals to live trading.
Usage:
python scripts/nexquant_live_priceaction.py # Generate today's signal
python scripts/nexquant_live_priceaction.py --daemon # Run continuously
python scripts/nexquant_live_priceaction.py --backfill # Full historical backtest
"""
import json
import os
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT = Path(__file__).resolve().parent.parent
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
SIGNAL_PATH = PROJECT / "git_ignore_folder" / "priceaction_signal.json"
RESULTS_DIR = PROJECT / "results" / "reports"
# Portfolio config
STRATEGIES = [
{"name": "Donchian(30,1)", "type": "donchian", "period": 30, "hold": 1},
{"name": "MACD(3,15,3)", "type": "macd", "fast": 3, "slow": 15, "signal_period": 3},
]
VOTE_THRESHOLD = 0.25
def load_close() -> tuple[pd.Series, pd.Series]:
"""Load 1-min and daily close prices."""
df = pd.read_hdf(OHLCV_PATH, key="data")
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
daily = close.resample("D").last().dropna()
return close, daily
def donchian_signal(daily: pd.Series, period: int, hold: int) -> pd.Series:
"""Donchian channel breakout signal (daily)."""
high = daily.rolling(period).max()
low = daily.rolling(period).min()
s = pd.Series(0, index=daily.index)
s[daily > high.shift(1)] = 1
s[daily < low.shift(1)] = -1
return s.replace(0, np.nan).ffill(limit=hold).fillna(0).astype(int).clip(-1, 1)
def macd_signal(daily: pd.Series, fast: int, slow: int, signal_period: int) -> pd.Series:
"""MACD crossover signal (daily)."""
ema_fast = daily.ewm(span=fast, adjust=False).mean()
ema_slow = daily.ewm(span=slow, adjust=False).mean()
macd_line = ema_fast - ema_slow
sig_line = macd_line.ewm(span=signal_period, adjust=False).mean()
s = pd.Series(0, index=daily.index)
s[macd_line > sig_line] = 1
s[macd_line < sig_line] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def compute_portfolio_signal(daily: pd.Series) -> pd.Series:
"""Compute majority-vote portfolio signal."""
signals = []
for cfg in STRATEGIES:
if cfg["type"] == "donchian":
sig = donchian_signal(daily, cfg["period"], cfg["hold"])
elif cfg["type"] == "macd":
sig = macd_signal(daily, cfg["fast"], cfg["slow"], cfg["signal_period"])
else:
continue
signals.append(sig)
if not signals:
return pd.Series(0, index=daily.index)
port = pd.DataFrame({f"s{i}": s for i, s in enumerate(signals)}).dropna()
vote = port.mean(axis=1)
result = pd.Series(0, index=vote.index)
result[vote > VOTE_THRESHOLD] = 1
result[vote < -VOTE_THRESHOLD] = -1
result.name = "signal"
return result
def get_todays_signal() -> dict:
"""Generate today's trading signal."""
close, daily = load_close()
portfolio_signal = compute_portfolio_signal(daily)
# Latest signal
latest = portfolio_signal.iloc[-1]
direction = {1: "LONG", -1: "SHORT", 0: "NEUTRAL"}[int(latest)]
# Last signal change
changes = portfolio_signal.diff().abs()
last_change_idx = changes[changes > 0].index[-1] if (changes > 0).any() else None
days_in_position = (daily.index[-1] - last_change_idx).days if last_change_idx is not None else 0
result = {
"timestamp": datetime.now().isoformat(),
"date": str(daily.index[-1].date()),
"signal": int(latest),
"direction": direction,
"days_in_position": days_in_position,
"strategies": {cfg["name"]: int(
donchian_signal(daily, cfg["period"], cfg["hold"]).iloc[-1] if cfg["type"] == "donchian"
else macd_signal(daily, cfg["fast"], cfg["slow"], cfg["signal_period"]).iloc[-1]
) for cfg in STRATEGIES},
}
SIGNAL_PATH.parent.mkdir(parents=True, exist_ok=True)
SIGNAL_PATH.write_text(json.dumps(result, indent=2))
return result
def run_backfill():
"""Run full historical backtest and save report."""
print("Running full historical backtest...")
close, daily = load_close()
signal = compute_portfolio_signal(daily)
# ffill to 1-min
sig_1min = signal.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
from rdagent.components.backtesting.vbt_backtest import backtest_signal, backtest_signal_risk
bt = backtest_signal(close=close, signal=sig_1min)
bt_risk = backtest_signal_risk(close=close, signal=sig_1min, risk_pct=0.0035, oos_start=None, wf_rolling=True)
report = {
"strategy": "Donchian(30,1) + MACD(3,15,3) Majority-Vote",
"timestamp": datetime.now().isoformat(),
"backtest": {
"sharpe": round(bt["sharpe"], 2),
"monthly_return_pct": round(bt["monthly_return_pct"], 2),
"max_drawdown": round(bt["max_drawdown"], 4),
"n_trades": bt["n_trades"],
"win_rate": round(bt["win_rate"], 4),
},
"risk_backtest": {
"sharpe": round(bt_risk.get("sharpe", 0), 2),
"monthly_pct": round(bt_risk.get("monthly_return_pct", 0), 2),
"max_dd": round(bt_risk.get("max_drawdown", 0), 4),
"wf_consistency": round(bt_risk.get("wf_oos_consistency", 0), 4),
},
}
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
path = RESULTS_DIR / f"backfill_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
path.write_text(json.dumps(report, indent=2))
print(f"\n{'='*50}")
print(f" Sharpe: {bt['sharpe']:.2f}")
print(f" Monthly: {bt['monthly_return_pct']:.2f}%")
print(f" Max DD: {bt['max_drawdown']:.4f}")
print(f" Trades: {bt['n_trades']}")
print(f" Win Rate: {bt['win_rate']:.1%}")
print(f" Report saved: {path}")
print(f"{'='*50}")
def main():
if "--backfill" in sys.argv:
run_backfill()
elif "--daemon" in sys.argv:
print("Daemon mode — generating signals every 5 minutes...")
while True:
result = get_todays_signal()
print(f" [{result['timestamp']}] {result['direction']:>8s} ({result['days_in_position']}d in position)")
time.sleep(300)
else:
result = get_todays_signal()
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""Price-Action Strategy Generator — no LLM, no factors, pure technical analysis.
Uses Donchian channels, moving averages, RSI, Bollinger Bands, and MACD
on daily resolution. Grid-searches parameters, validates via backtest_signal.
"""
import json
import os
import time
from datetime import datetime
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT = Path(__file__).resolve().parent.parent
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
RESULTS_DIR = PROJECT / "results" / "strategies_new"
MIN_MONTHLY = 1.0
MIN_SHARPE = 1.0
MAX_DD = -0.15
MIN_TRADES = 30
def load_data():
df = pd.read_hdf(OHLCV_PATH, key="data")
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
daily = close.resample("D").last().dropna()
return close, daily
def to_1min(daily_signal: pd.Series, close_1min: pd.Series) -> pd.Series:
return daily_signal.reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
# ═══════════════════════════════════════════════════════════════════════════════
# Strategy templates
# ═══════════════════════════════════════════════════════════════════════════════
def donchian(close: pd.Series, period: int, hold: int) -> pd.Series:
"""Donchian channel breakout."""
high = close.rolling(period).max()
low = close.rolling(period).min()
s = pd.Series(0, index=close.index)
s[close > high.shift(1)] = 1
s[close < low.shift(1)] = -1
s = s.replace(0, np.nan).ffill(limit=hold).fillna(0).astype(int).clip(-1, 1)
return s
def sma_cross(close: pd.Series, fast: int, slow: int) -> pd.Series:
"""SMA crossover."""
s = pd.Series(0, index=close.index)
s[close.rolling(fast).mean() > close.rolling(slow).mean()] = 1
s[close.rolling(fast).mean() < close.rolling(slow).mean()] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def rsi_mr(close: pd.Series, period: int, oversold: int, overbought: int) -> pd.Series:
"""RSI mean-reversion."""
delta = close.diff()
gain = delta.clip(lower=0).rolling(period).mean()
loss = (-delta.clip(upper=0)).rolling(period).mean()
rs = gain / (loss + 1e-8)
rsi = 100 - 100 / (1 + rs)
s = pd.Series(0, index=close.index)
s[rsi < oversold] = 1
s[rsi > overbought] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def bollinger_mr(close: pd.Series, period: int, std: float) -> pd.Series:
"""Bollinger Band mean-reversion."""
ma = close.rolling(period).mean()
st = close.rolling(period).std()
s = pd.Series(0, index=close.index)
s[close < ma - std * st] = 1
s[close > ma + std * st] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def macd(close: pd.Series, fast: int, slow: int, signal_p: int) -> pd.Series:
"""MACD crossover."""
ema_fast = close.ewm(span=fast, adjust=False).mean()
ema_slow = close.ewm(span=slow, adjust=False).mean()
macd_line = ema_fast - ema_slow
sig_line = macd_line.ewm(span=signal_p, adjust=False).mean()
s = pd.Series(0, index=close.index)
s[macd_line > sig_line] = 1
s[macd_line < sig_line] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def ma_envelope(close: pd.Series, period: int, pct: float) -> pd.Series:
"""Moving average envelope mean-reversion."""
ma = close.rolling(period).mean()
s = pd.Series(0, index=close.index)
s[close < ma * (1 - pct)] = 1
s[close > ma * (1 + pct)] = -1
return s.replace(0, np.nan).ffill(limit=3).fillna(0).astype(int).clip(-1, 1)
def atr_breakout(close: pd.Series, period: int, mult: float) -> pd.Series:
"""ATR-based volatility breakout (simplified, using close-only)."""
atr = (close.diff().abs()).rolling(period).mean()
ma = close.rolling(period).mean()
s = pd.Series(0, index=close.index)
s[close > ma + mult * atr] = 1
s[close < ma - mult * atr] = -1
return s.replace(0, np.nan).ffill(limit=2).fillna(0).astype(int).clip(-1, 1)
# ═══════════════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════════════
def main():
print("=" * 60)
print(" Price-Action Strategy Generator (No LLM, No Factors)")
print("=" * 60)
from rdagent.components.backtesting.vbt_backtest import backtest_signal
close, daily = load_data()
print(f"\nDaily data: {len(daily)} bars ({daily.index[0].date()}{daily.index[-1].date()})")
import itertools
grid = [
("Donchian", donchian, [
(p, h) for p in [5, 7, 10, 12, 15, 20, 25, 30, 40, 60]
for h in [1, 2, 3, 5]
]),
("SMA_Crossover", sma_cross, [
(f, s) for f in [5, 10, 20]
for s in [20, 50, 100, 200] if s > f
]),
("RSI_MR", rsi_mr, [
(p, lo, hi) for p in [7, 14, 21]
for lo, hi in [(30, 70), (25, 75), (20, 80)]
]),
("Bollinger_MR", bollinger_mr, [
(p, s) for p in [10, 20, 40]
for s in [1.5, 2.0, 2.5]
]),
("MACD", macd, [
(f, s, sig) for f, s, sig in [(8, 21, 5), (12, 26, 9), (5, 20, 3)]
]),
("MA_Envelope", ma_envelope, [
(p, pct) for p in [20, 50, 100]
for pct in [0.01, 0.02, 0.03]
]),
("ATR_Breakout", atr_breakout, [
(p, m) for p in [10, 20, 40]
for m in [1.0, 1.5, 2.0]
]),
]
results = []
t0 = time.time()
total = sum(len(params) for _, _, params in grid)
done = 0
print(f"\nTesting {total} parameter combinations...\n")
for name, fn, params_list in grid:
for params in params_list:
done += 1
daily_signal = fn(daily, *params)
signal_1min = to_1min(daily_signal, close)
bt = backtest_signal(close=close, signal=signal_1min)
bt["strategy"] = name
bt["params"] = params
bt["name"] = f"{name}{params}"
bt["monthly_pct"] = bt.get("monthly_return_pct", 0)
bt["max_dd"] = bt.get("max_drawdown", 0)
results.append(bt)
if done % 50 == 0 or done == total:
elapsed = time.time() - t0
rate = done / elapsed if elapsed > 0 else 0
eta = (total - done) / rate if rate > 0 else 0
print(f" {done}/{total} ({done/total*100:.0f}%) {rate:.0f}/s eta {eta:.0f}s")
elapsed = time.time() - t0
print(f"\n{'=' * 60}")
print(f" Evaluated: {total} in {elapsed:.0f}s")
print(f"{'=' * 60}")
valid = [r for r in results
if r.get("sharpe", 0) >= MIN_SHARPE
and r.get("max_dd", 0) >= MAX_DD
and r.get("n_trades", 0) >= MIN_TRADES
and r.get("monthly_pct", 0) >= MIN_MONTHLY]
valid.sort(key=lambda r: r.get("monthly_pct", 0), reverse=True)
print(f"\n Meeting: Sharpe≥{MIN_SHARPE} DD≥{MAX_DD} Tr≥{MIN_TRADES} Mon≥{MIN_MONTHLY}%")
print(f"{len(valid)} strategies\n")
hdr = "{:>3s} {:20s} {:20s} {:>7s} {:>7s} {:>7s} {:>5s} {:>6s}"
print(hdr.format("#", "Strategy", "Params", "Sharpe", "Mon%", "MaxDD", "Tr", "WinRt"))
print("-" * 85)
for i, r in enumerate(valid[:30], 1):
ps = str(r["params"]).replace(" ", "")[:18]
print(hdr.format(str(i), r["strategy"][:20], ps,
f'{r.get("sharpe",0):.2f}', f'{r.get("monthly_pct",0):.1f}%',
f'{r.get("max_dd",0):.3f}', str(r.get("n_trades",0)),
f'{r.get("win_rate",0):.1%}'))
print(f"\n Best by category:")
seen = set()
for r in valid:
if r["strategy"] not in seen:
seen.add(r["strategy"])
print(f" {r['strategy']:20s} {r['name'][:30]:30s} "
f"Sh={r.get('sharpe',0):.2f} Mon={r.get('monthly_pct',0):.1f}% "
f"DD={r.get('max_dd',0):.3f} Tr={r.get('n_trades',0)}")
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
out = RESULTS_DIR / f"priceaction_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
out.write_text(json.dumps(valid[:100] if valid else results[:100], indent=2, default=str))
print(f"\n Saved → {out}")
if __name__ == "__main__":
main()
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""Price-Action R&D Loop — TA-Lib powered. 17 indicators, deterministic.
Uses TA-Lib (161 indicators) for standardized technical analysis.
Generates random strategy hypotheses and evaluates via backtest_signal.
"""
import json, os, random, sys, time
from datetime import datetime
from pathlib import Path
import numpy as np, pandas as pd
import talib
PROJECT = Path(__file__).resolve().parent.parent
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
RESULTS_DIR = PROJECT / "results" / "strategies_new"
TIMEFRAMES = ["15min", "30min", "1h", "4h", "1d"]
VOTE_THRESHOLD = 0.25
MIN_SHARPE, MIN_TRADES, TOP_N = 1.0, 20, 20
# ═══════════════════════════════════════════════════════════════════════════════
# Indicator functions — all use (close, high, low, volume, **params) signature
# ═══════════════════════════════════════════════════════════════════════════════
def _macd(c, h, l, v, fast, slow, sig):
mc, sc, _ = talib.MACD(c.values.astype(np.float64), fastperiod=fast, slowperiod=slow, signalperiod=sig)
s = pd.Series(0, index=c.index); s[mc > sc] = 1; s[mc < sc] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _rsi(c, h, l, v, period, oversold, overbought):
vv = talib.RSI(c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv < oversold] = 1; s[vv > overbought] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _bbands(c, h, l, v, period, std):
up, mi, lo = talib.BBANDS(c.values.astype(np.float64), timeperiod=period, nbdevup=std, nbdevdn=std)
s = pd.Series(0, index=c.index); s[c.values < lo] = 1; s[c.values > up] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _stoch(c, h, l, v, fastk, slowk, slowd):
k, d = talib.STOCH(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64),
fastk_period=fastk, slowk_period=slowk, slowd_period=slowd)
s = pd.Series(0, index=c.index); s[(k > d) & (k < 30)] = 1; s[(k < d) & (k > 70)] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _cci(c, h, l, v, period):
vv = talib.CCI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv < -100] = 1; s[vv > 100] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _willr(c, h, l, v, period):
vv = talib.WILLR(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv < -80] = 1; s[vv > -20] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _adx(c, h, l, v, period, threshold):
pdi = talib.PLUS_DI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
ndi = talib.MINUS_DI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
adx = talib.ADX(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[(pdi > ndi) & (adx > threshold)] = 1; s[(ndi > pdi) & (adx > threshold)] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _sar(c, h, l, v, accel, max_accel):
vv = talib.SAR(h.values.astype(np.float64), l.values.astype(np.float64), acceleration=accel, maximum=max_accel)
s = pd.Series(0, index=c.index); s[c.values > vv] = 1; s[c.values < vv] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _roc(c, h, l, v, period, threshold):
vv = talib.ROC(c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv > threshold] = 1; s[vv < -threshold] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _mom(c, h, l, v, period):
vv = talib.MOM(c.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv > 0] = 1; s[vv < 0] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _aroon(c, h, l, v, period):
up, dn = talib.AROON(h.values.astype(np.float64), l.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[up > dn] = 1; s[up < dn] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _mfi(c, h, l, v, period):
vv = talib.MFI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), v.values.astype(np.float64), timeperiod=period)
s = pd.Series(0, index=c.index); s[vv < 20] = 1; s[vv > 80] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _ultosc(c, h, l, v, p1, p2, p3):
vv = talib.ULTOSC(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod1=p1, timeperiod2=p2, timeperiod3=p3)
s = pd.Series(0, index=c.index); s[vv < 30] = 1; s[vv > 70] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _natr(c, h, l, v, period):
vv = talib.NATR(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
m, s = vv[-200:].mean(), vv[-200:].std()
s = pd.Series(0, index=c.index); s[c.values > m+s] = 1; s[c.values < m-s] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _donchian(c, h, l, v, period, hold):
hi, lo = c.rolling(period).max(), c.rolling(period).min()
s = pd.Series(0, index=c.index); s[c > hi.shift(1)] = 1; s[c < lo.shift(1)] = -1
return s.replace(0, np.nan).ffill(limit=hold).fillna(0).astype(int).clip(-1, 1)
def _sma(c, h, l, v, fast, slow):
s = pd.Series(0, index=c.index)
s[c.rolling(fast).mean() > c.rolling(slow).mean()] = 1
s[c.rolling(fast).mean() < c.rolling(slow).mean()] = -1
return s.fillna(0).astype(int).clip(-1, 1)
def _ema(c, h, l, v, fast, slow):
ef, es = c.ewm(span=fast, adjust=False).mean(), c.ewm(span=slow, adjust=False).mean()
s = pd.Series(0, index=c.index); s[ef > es] = 1; s[ef < es] = -1
return s.fillna(0).astype(int).clip(-1, 1)
# ═══════════════════════════════════════════════════════════════════════════════
INDICATORS = {
"MACD": ({"fast":[3,5,8,12], "slow":[10,15,20,26,35], "sig":[3,5,9]}, _macd, "MACD({fast},{slow},{sig})"),
"RSI": ({"period":[7,14,21], "oversold":[20,25,30,35], "overbought":[65,70,75,80]}, _rsi, "RSI({period})[{oversold}/{overbought}]"),
"BBands": ({"period":[10,20,40], "std":[1.5,2.0,2.5]}, _bbands, "BB({period},{std}s)"),
"Stoch": ({"fastk":[5,9,14], "slowk":[3], "slowd":[3,5]}, _stoch, "Stoch({fastk},{slowk},{slowd})"),
"CCI": ({"period":[14,20,50]}, _cci, "CCI({period})"),
"WillR": ({"period":[7,14,21]}, _willr, "WR({period})"),
"ADX": ({"period":[7,14,21], "threshold":[15,20,25]}, _adx, "ADX({period}>{threshold})"),
"SAR": ({"accel":[0.02,0.05,0.08], "max_accel":[0.2,0.3,0.5]}, _sar, "SAR({accel},{max_accel})"),
"ROC": ({"period":[5,10,20], "threshold":[0.1,0.2,0.5]}, _roc, "ROC({period},{threshold}%)"),
"MOM": ({"period":[5,10,20,50]}, _mom, "MOM({period})"),
"AROON": ({"period":[7,14,21]}, _aroon, "AROON({period})"),
"MFI": ({"period":[7,14,21]}, _mfi, "MFI({period})"),
"UltOsc": ({"p1":[7], "p2":[14], "p3":[28]}, _ultosc, "UltOsc(7,14,28)"),
"NATR": ({"period":[7,14,21]}, _natr, "NATR({period})"),
"Donchian":({"period":[5,10,20,30,50,100], "hold":[1,2,3,5]}, _donchian, "Donchian({period},{hold})"),
"SMA": ({"fast":[5,10,20,50], "slow":[20,50,100,200]}, _sma, "SMA({fast},{slow})"),
"EMA": ({"fast":[3,5,8,12], "slow":[15,26,50,100]}, _ema, "EMA({fast},{slow})"),
}
# ═══════════════════════════════════════════════════════════════════════════════
# Strategy generation
# ═══════════════════════════════════════════════════════════════════════════════
def _resample_ohlc(close_1min, tf):
"""Resample to timeframe, producing OHLCV bars."""
bars = close_1min.resample(tf).ohlc()
# Flatten MultiIndex columns
o = bars['close']['close'] if isinstance(bars.columns, pd.MultiIndex) else bars['close']
h = bars['high']['high'] if isinstance(bars.columns, pd.MultiIndex) else bars['high']
l = bars['low']['low'] if isinstance(bars.columns, pd.MultiIndex) else bars['low']
c = bars['close']['close'] if isinstance(bars.columns, pd.MultiIndex) else bars['close']
v = pd.Series(1000, index=c.index) # dummy volume
return c, h, l, v
def random_hypothesis():
stype = random.choice(["single", "multi_tf", "portfolio"])
if stype == "single":
ind_name = random.choice(list(INDICATORS.keys()))
params_def, _, desc_tpl = INDICATORS[ind_name]
params = {k: random.choice(v) for k, v in params_def.items()}
if ind_name == "SMA" and params["fast"] >= params["slow"]:
params["fast"] = min(params["fast"], params["slow"] // 2)
return {"type": "single", "indicator": ind_name, "timeframe": random.choice(TIMEFRAMES),
"params": params, "description": desc_tpl.format(**params)}
elif stype == "multi_tf":
ind_name = random.choice(list(INDICATORS.keys()))
params_def, _, desc_tpl = INDICATORS[ind_name]
params = {k: random.choice(v) for k, v in params_def.items()}
tfs = random.sample(TIMEFRAMES, k=random.randint(2, 4))
return {"type": "multi_tf", "indicator": ind_name, "timeframes": tfs,
"params": params, "description": f"{ind_name} on {','.join(tfs)} maj-vote"}
else:
i1, i2 = random.sample(list(INDICATORS.keys()), 2)
p1_def, _, _ = INDICATORS[i1]; p2_def, _, _ = INDICATORS[i2]
p1 = {k: random.choice(v) for k, v in p1_def.items()}
p2 = {k: random.choice(v) for k, v in p2_def.items()}
return {"type": "portfolio", "indicators": [{"name": i1, "params": p1}, {"name": i2, "params": p2}],
"timeframe": "1d", "description": f"{i1} + {i2} portfolio daily"}
def build_signal(close_1min, hypothesis):
hp = hypothesis
if hp["type"] == "single":
_, fn, _ = INDICATORS[hp["indicator"]]
c, h, l, v = _resample_ohlc(close_1min, hp["timeframe"])
s = fn(c, h, l, v, **hp["params"])
return s.reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
elif hp["type"] == "multi_tf":
_, fn, _ = INDICATORS[hp["indicator"]]
sigs = {}
for tf in hp["timeframes"]:
c, h, l, v = _resample_ohlc(close_1min, tf)
sigs[tf] = fn(c, h, l, v, **hp["params"]).reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
port_df = pd.DataFrame(sigs).dropna()
vote = port_df.mean(axis=1)
result = pd.Series(0, index=vote.index)
result[vote > VOTE_THRESHOLD] = 1; result[vote < -VOTE_THRESHOLD] = -1
return result
else:
sigs = []
daily, dh, dl, dv = _resample_ohlc(close_1min, "1d")
for cfg in hp["indicators"]:
_, fn, _ = INDICATORS[cfg["name"]]
s = fn(daily, dh, dl, dv, **cfg["params"]).reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
sigs.append(s)
port_df = pd.DataFrame({f"s{i}": s for i, s in enumerate(sigs)}).dropna()
vote = port_df.mean(axis=1)
result = pd.Series(0, index=vote.index)
result[vote > VOTE_THRESHOLD] = 1; result[vote < -VOTE_THRESHOLD] = -1
return result
def evaluate(hp, close):
signal = build_signal(close, hp)
from rdagent.components.backtesting.vbt_backtest import backtest_signal
bt = backtest_signal(close=close, signal=signal)
return {"hypothesis": hp, "sharpe": bt.get("sharpe", 0) or 0,
"monthly_pct": bt.get("monthly_return_pct", 0) or 0,
"max_dd": bt.get("max_drawdown", 0) or 0, "n_trades": bt.get("n_trades", 0) or 0,
"win_rate": bt.get("win_rate", 0) or 0}
# ═══════════════════════════════════════════════════════════════════════════════
# Main loop
# ═══════════════════════════════════════════════════════════════════════════════
def main():
iterations = 100; continuous = False
if "--iterations" in sys.argv:
iterations = int(sys.argv[sys.argv.index("--iterations") + 1])
if "--live" in sys.argv: continuous = True
print("=" * 60)
print(f" Price-Action R&D Loop — TA-Lib ({len(INDICATORS)} indicators)")
print(f" Iterations: {'continuous' if continuous else iterations}")
print("=" * 60)
df = pd.read_hdf(OHLCV_PATH, key="data")
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
top, best_sh, total, iteration = [], 0, 0, 0
while True:
iteration += 1
if not continuous and iteration > iterations: break
hp = random_hypothesis()
result = evaluate(hp, close)
result["iteration"] = iteration
result["timestamp"] = datetime.now().isoformat()
total += 1
if result["sharpe"] >= MIN_SHARPE and result["n_trades"] >= MIN_TRADES and result["monthly_pct"] > 0:
top.append(result)
top.sort(key=lambda r: r["sharpe"], reverse=True)
top = top[:TOP_N]
if iteration % 10 == 0 or result["sharpe"] > best_sh:
if result["sharpe"] > best_sh:
best_sh = result["sharpe"]
print(f"\n * NEW BEST (#{iteration}): {hp['description']}")
print(f" Sharpe={result['sharpe']:.2f} Mon={result['monthly_pct']:.2f}% "
f"DD={result['max_dd']:.4f} Tr={result['n_trades']} WR={result['win_rate']:.1%}")
else:
print(f" [{iteration}/{iterations}] Evals: {total} | Top: {len(top)} | Best Sh={best_sh:.2f}")
if iteration % 50 == 0 and top:
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
cp = RESULTS_DIR / f"pal_talib_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
cp.write_text(json.dumps(top[:10], indent=2, default=str))
print(f" Checkpoint: {cp.name}")
# Final
print(f"\n{'=' * 60}")
print(f" Done: {total} evaluated, {len(top)} strategies")
if top:
print(f"\n{'#':>3s} {'Strategy':<50s} {'Sharpe':>7s} {'Mon%':>7s} {'DD':>7s} {'Tr':>5s}")
print("-" * 80)
for i, r in enumerate(top[:15], 1):
print(f"{i:>3d} {r['hypothesis']['description'][:50]:<50s} {r['sharpe']:>+7.2f} {r['monthly_pct']:>+6.2f}% {r['max_dd']:>+6.4f} {r['n_trades']:>5d}")
final = RESULTS_DIR / f"pal_talib_final_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
final.write_text(json.dumps(top, indent=2, default=str))
print(f"\n Saved: {final}")
if __name__ == "__main__":
main()
+931
View File
@@ -0,0 +1,931 @@
#!/usr/bin/env python3
"""R&D Loop V2 — Multi-Instrument + Correlation Score + Session/Vola Filter + OOS.
Changes from V1:
1. Multi-Instrument: Evaluate on EUR/USD + GBP/USD + BTC/USD
2. Correlation Score: Reward uncorrelated strategies (Sharpe × (1corr))
3. Session Filter: Only trade London session (07:00-16:00 UTC)
4. Volatility Filter: No trades when ATR < threshold
5. OOS Split: Report IS/OOS separately (80/20)
"""
import json, os, random, sys, time
from datetime import datetime
from pathlib import Path
import numpy as np, pandas as pd
from numba import jit
PROJECT = Path(__file__).resolve().parent.parent
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
RESULTS_DIR = PROJECT / "results" / "rd_loop"
STATE_DIR = PROJECT / "git_ignore_folder" / "rd_loop_state"
INSTRUMENTS = ["EURUSD", "GBPUSD", "BTCUSD", "XAUUSD"]
LEADER_MAP = {"GBPUSD": "EURUSD"}
INSTRUMENT_TIMEFRAMES = {
"XAUUSD": ["1d", "1w"], # Daily data → daily/weekly TFs
"default": ["5min", "15min", "30min", "1h", "4h"],
}
TIMEFRAMES = ["5min", "15min", "30min", "1h", "4h", "1d", "1w"]
INDICATORS_POOL = ["MACD", "RSI", "BBands", "Donchian", "Stoch", "CCI", "WillR", "ADX", "SAR", "ROC", "MOM", "AROON", "MFI", "SMA", "EMA"]
STRATEGY_TYPES = ["single", "multi_tf", "multi_role"]
TREND_TFS = ["30min", "1h", "4h"]
ENTRY_TFS = ["5min", "15min", "30min"]
MIN_SHARPE, MIN_TRADES = 0.3, 10
EXPLORATION_RATE = 0.40
OOS_SPLIT = 0.2
# ═══════════════════════════════════════════════════════════════════════════════
# Numba-accelerated backtest
# ═══════════════════════════════════════════════════════════════════════════════
@jit(nopython=True)
def _backtest_numba(prices, signals, cost=0.000264):
n = len(prices)
equity = np.zeros(n, dtype=np.float64)
equity[0] = 100000.0; peak = 100000.0; max_dd = 0.0
trade_returns = np.zeros(100000, dtype=np.float64)
position = 0; entry_price = 0.0; trade_count = 0; wins = 0
for i in range(1, n):
px = prices[i]; sg = signals[i]; ps = signals[i-1]
# Close position on signal reversal or flatten
if position != 0 and (sg != position or sg == 0 and position != 0):
if position == 1: ret = (px - entry_price) / entry_price - cost
else: ret = (entry_price - px) / entry_price - cost
equity[i] = equity[i-1] * (1.0 + ret)
if equity[i] > peak: peak = equity[i]
dd = (peak - equity[i]) / peak
if dd > max_dd: max_dd = dd
if trade_count < len(trade_returns):
trade_returns[trade_count] = ret
trade_count += 1
if ret > 0: wins += 1
position = 0
else:
equity[i] = equity[i-1]
# Open new position
if position == 0 and sg != 0:
position = sg; entry_price = px
# Close final position
if position != 0:
fp = prices[-1]
if position == 1: ret = (fp - entry_price) / entry_price - cost
else: ret = (entry_price - fp) / entry_price - cost
equity[-1] = equity[-2] * (1.0 + ret)
if trade_count < len(trade_returns):
trade_returns[trade_count] = ret
trade_count += 1
if ret > 0: wins += 1
# Ensure monotonic equity (carry forward zeros)
for i in range(1, n):
if equity[i] == 0: equity[i] = equity[i-1]
total_ret = (equity[-1] - 100000.0) / 100000.0
if trade_count > 5:
t = trade_returns[:trade_count]
mean_ret = np.mean(t); std_ret = np.std(t)
sharpe = mean_ret / std_ret * np.sqrt(trade_count) if std_ret > 0 else 0.0
else:
sharpe = 0.0
return equity, max_dd, trade_count, wins, total_ret, sharpe, trade_returns[:trade_count]
# ═══════════════════════════════════════════════════════════════════════════════
# Signal Construction
# ═══════════════════════════════════════════════════════════════════════════════
def build_signal(close, hypothesis):
"""Build trading signal from hypothesis. Returns (-1,0,1) Series."""
import talib
signal = None
# Adapt timeframes to data frequency
median_delta = (close.index[1:] - close.index[:-1]).median()
if median_delta > pd.Timedelta("1h"):
valid_tfs = ["1d", "1w"]
tf_map = {"5min": "1d", "15min": "1d", "30min": "1d", "1h": "1d", "4h": "1w"}
# Remap hypothesis timeframes
hp = dict(hypothesis)
if hp.get('type') in ('single', 'multi_tf') and 'timeframe' in hp:
hp['timeframe'] = tf_map.get(hp.get('timeframe','1h'), '1d')
if hp.get('type') == 'multi_tf' and 'timeframes' in hp:
hp['timeframes'] = [tf_map.get(t, '1d') for t in hp['timeframes']]
hp['timeframes'] = list(set(hp['timeframes'])) # dedup
if hp.get('type') == 'multi_role':
hp['trend_tf'] = tf_map.get(hp.get('trend_tf','4h'), '1w')
hp['entry_tf'] = tf_map.get(hp.get('entry_tf','15min'), '1d')
hypothesis = hp
else:
valid_tfs = ["5min", "15min", "30min", "1h", "4h"]
if hypothesis['type'] == 'single':
ind = hypothesis['indicator']; tf = hypothesis['timeframe']
bars = close.resample(tf).last().dropna()
sig = _build_indicator_signal(ind, bars, hypothesis['params'])
signal = sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
elif hypothesis['type'] == 'multi_tf':
ind = hypothesis['indicator']
sigs = {}
for tf in hypothesis['timeframes']:
bars = close.resample(tf).last().dropna()
sig = _build_indicator_signal(ind, bars, hypothesis['params'])
sigs[tf] = sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
port = pd.DataFrame(sigs).dropna()
vote = port.mean(axis=1)
signal = pd.Series(0, index=close.index)
signal[vote > 0.25] = 1; signal[vote < -0.25] = -1
elif hypothesis['type'] == 'multi_role':
trend_ind = hypothesis['trend_ind']; entry_ind = hypothesis['entry_ind']
trend_tf = hypothesis['trend_tf']; entry_tf = hypothesis['entry_tf']
trend_bars = close.resample(trend_tf).last().dropna()
trend_sig = _build_indicator_signal(trend_ind, trend_bars, hypothesis['trend_params'])
trend_sig = trend_sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
entry_bars = close.resample(entry_tf).last().dropna()
entry_sig = _build_indicator_signal(entry_ind, entry_bars, hypothesis['entry_params'])
entry_sig = entry_sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
signal = pd.Series(0, index=close.index)
signal[(trend_sig == 1) & (entry_sig == 1)] = 1
signal[(trend_sig == -1) & (entry_sig == -1)] = -1
return signal if signal is not None and signal.nunique() > 1 else None
def _apply_session_filter(signal, index):
"""Only trade London session (07:00-16:00 UTC Mon-Fri). Skip for daily data."""
delta = (index[1:] - index[:-1]).median() if len(index) > 1 else pd.Timedelta("1min")
if delta > pd.Timedelta("1h"):
return signal # Skip session filter for daily/weekly data
hours = index.hour
days = index.dayofweek
in_session = (days < 5) & (hours >= 7) & (hours < 16)
if hasattr(in_session, 'values'):
in_session = in_session.values
return (signal * in_session.astype(int)).astype(int).clip(-1, 1)
def _apply_vola_filter(signal, close, atr_period=14, min_atr_pct=0.0003):
"""Don't trade when ATR is too low (flat/quiet markets)."""
tr = pd.DataFrame({
'hl': close.diff().abs(),
'hc': (close - close.shift(1)).abs(),
'lc': (close.shift(1) - close).abs(),
}).max(axis=1)
atr = tr.rolling(atr_period).mean()
atr_pct = atr / close
too_quiet = atr_pct < min_atr_pct
return (signal * (~too_quiet).astype(int)).fillna(0).astype(int).clip(-1, 1)
_NEWS_CACHE = None
def _load_news_events():
"""Load high-impact news events from YAML, return dict of currency → DatetimeIndex mask."""
global _NEWS_CACHE
if _NEWS_CACHE is not None:
return _NEWS_CACHE
import yaml
news_file = PROJECT / "git_ignore_folder" / "economic_events_full.yaml"
if not news_file.exists():
_NEWS_CACHE = {}
return _NEWS_CACHE
with open(news_file) as f:
data = yaml.safe_load(f)
events = {}
for evt in data.get('events', []):
if evt.get('impact') != 'high':
continue
dt = pd.Timestamp(evt['datetime'])
currency = evt.get('currency', 'USD')
if currency not in events:
events[currency] = []
events[currency].append(dt)
_NEWS_CACHE = events
return _NEWS_CACHE
def _apply_news_filter(signal, index, currency, window_min=5):
"""Block trades during high-impact news events (+/- window_min)."""
events = _load_news_events()
if currency not in events and currency[:3] not in events:
return signal
key = currency if currency in events else currency[:3]
timestamps = events.get(key, [])
if not timestamps:
return signal
blocked = np.zeros(len(index), dtype=bool)
for ts in timestamps:
start = ts - pd.Timedelta(minutes=window_min)
end = ts + pd.Timedelta(minutes=window_min)
mask = (index >= start) & (index <= end)
blocked |= mask
return (signal * (~blocked).astype(int)).astype(int).clip(-1, 1)
def _apply_cross_confirm(signal, close_follower, close_leader, lookback=5, min_pct=0.0003):
"""Cancel follower signals when leader momentum strongly opposes (>0.03% move)."""
leader_mom = close_leader.pct_change(lookback)
leader_mom = leader_mom.reindex(signal.index, method='ffill')
# Only BLOCK when leader moves strongly opposite to signal
# Don't require confirmation — just cancel clear contrarian moves
cancel_long = (signal == 1) & (leader_mom < -min_pct)
cancel_short = (signal == -1) & (leader_mom > min_pct)
cancel = cancel_long | cancel_short
return (signal * (~cancel).astype(int)).fillna(0).astype(int).clip(-1, 1)
def _build_indicator_signal(name, bars, params):
"""Build indicator signal using talib + hand-rolled."""
import talib
c = bars.values.astype(np.float64)
if name == 'MACD':
mc, sc, _ = talib.MACD(c, fastperiod=params.get('fast', 3),
slowperiod=params.get('slow', 15),
signalperiod=params.get('sig', 3))
s = pd.Series(0, index=bars.index); s[mc > sc] = 1; s[mc < sc] = -1
elif name == 'RSI':
v = talib.RSI(c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index); s[v < params.get('oversold', 30)] = 1; s[v > params.get('overbought', 70)] = -1
elif name == 'BBands':
up, mi, lo = talib.BBANDS(c, timeperiod=params.get('period', 20),
nbdevup=params.get('std', 2), nbdevdn=params.get('std', 2))
s = pd.Series(0, index=bars.index); s[c < lo] = 1; s[c > up] = -1
elif name == 'Donchian':
hi = bars.rolling(params.get('period', 20)).max()
lo = bars.rolling(params.get('period', 20)).min()
s = pd.Series(0, index=bars.index); s[bars > hi.shift(1)] = 1; s[bars < lo.shift(1)] = -1
s = s.replace(0, np.nan).ffill(limit=params.get('hold', 1)).fillna(0).astype(int)
elif name == 'Stoch':
k, d = talib.STOCH(c, c, c, fastk_period=params.get('fastk', 9),
slowk_period=params.get('slowk', 3), slowd_period=params.get('slowd', 3))
s = pd.Series(0, index=bars.index); s[(k > d) & (k < 30)] = 1; s[(k < d) & (k > 70)] = -1
elif name == 'CCI':
v = talib.CCI(c, c, c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index); s[v < -100] = 1; s[v > 100] = -1
elif name == 'WillR':
v = talib.WILLR(c, c, c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index); s[v < -80] = 1; s[v > -20] = -1
elif name == 'ADX':
pdi = talib.PLUS_DI(c, c, c, timeperiod=params.get('period', 14))
ndi = talib.MINUS_DI(c, c, c, timeperiod=params.get('period', 14))
adx = talib.ADX(c, c, c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index)
s[(pdi > ndi) & (adx > params.get('threshold', 20))] = 1
s[(ndi > pdi) & (adx > params.get('threshold', 20))] = -1
elif name == 'SAR':
v = talib.SAR(c, c, acceleration=params.get('accel', 0.02), maximum=params.get('max_accel', 0.2))
s = pd.Series(0, index=bars.index); s[c > v] = 1; s[c < v] = -1
elif name == 'ROC':
v = talib.ROC(c, timeperiod=params.get('period', 10))
s = pd.Series(0, index=bars.index); s[v > params.get('threshold', 0.2)] = 1; s[v < -params.get('threshold', 0.2)] = -1
elif name == 'MOM':
v = talib.MOM(c, timeperiod=params.get('period', 10))
s = pd.Series(0, index=bars.index); s[v > 0] = 1; s[v < 0] = -1
elif name == 'AROON':
up, dn = talib.AROON(c, c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index); s[up > dn] = 1; s[up < dn] = -1
elif name == 'MFI':
v = talib.MFI(c, c, c, c, timeperiod=params.get('period', 14))
s = pd.Series(0, index=bars.index); s[v < 20] = 1; s[v > 80] = -1
elif name == 'SMA':
s = pd.Series(0, index=bars.index)
s[bars.rolling(params.get('fast', 10)).mean() > bars.rolling(params.get('slow', 50)).mean()] = 1
s[bars.rolling(params.get('fast', 10)).mean() < bars.rolling(params.get('slow', 50)).mean()] = -1
elif name == 'EMA':
ef = bars.ewm(span=params.get('fast', 5), adjust=False).mean()
es = bars.ewm(span=params.get('slow', 26), adjust=False).mean()
s = pd.Series(0, index=bars.index); s[ef > es] = 1; s[ef < es] = -1
else:
s = pd.Series(0, index=bars.index)
return s.fillna(0).astype(int).clip(-1, 1)
# ═══════════════════════════════════════════════════════════════════════════════
# Multi-Instrument Evaluation
# ═══════════════════════════════════════════════════════════════════════════════
def evaluate_multi(closes, hypothesis, use_session=True, use_vola=False):
"""Evaluate strategy on all instruments, return combined metrics + per-instrument."""
results = {}
equity_curves = {}
for inst, close in closes.items():
signal = build_signal(close, hypothesis)
if signal is None:
results[inst] = {"sharpe": 0, "monthly_pct": 0, "n_trades": 0}
continue
# Apply filters
if use_session:
signal = _apply_session_filter(signal, close.index)
if use_vola:
signal = _apply_vola_filter(signal, close)
# News filter: block trades during high-impact events for this currency
signal = _apply_news_filter(signal, close.index, inst.replace("USD", "").replace("BTC", "BTC"))
# Cross-pair confirmation: validate follower with leader momentum
leader_inst = LEADER_MAP.get(inst)
if leader_inst and leader_inst in closes:
signal = _apply_cross_confirm(signal, close, closes[leader_inst])
if signal.nunique() <= 1:
results[inst] = {"sharpe": 0, "monthly_pct": 0, "n_trades": 0}
continue
# OOS split
n = len(close)
is_n = int(n * (1 - OOS_SPLIT))
close_is = close.iloc[:is_n]; signal_is = signal.iloc[:is_n]
close_oos = close.iloc[is_n:]; signal_oos = signal.iloc[is_n:]
# IS backtest
prices_is = close_is.values.astype(np.float64); sigs_is = signal_is.values.astype(np.int32)
eq_is, dd_is, tr_is, wins_is, ret_is, sh_is, _ = _backtest_numba(prices_is, sigs_is)
# OOS backtest
prices_oos = close_oos.values.astype(np.float64); sigs_oos = signal_oos.values.astype(np.int32)
eq_oos, dd_oos, tr_oos, wins_oos, ret_oos, sh_oos, _ = _backtest_numba(prices_oos, sigs_oos)
# Full backtest (for equity curve)
prices_full = close.values.astype(np.float64); sigs_full = signal.values.astype(np.int32)
eq_full, dd_full, tr_full, wins_full, ret_full, sh_full, trades_full = _backtest_numba(prices_full, sigs_full)
n_days = (close.index[-1] - close.index[0]).days
mon = ((1+ret_full)**(1/(n_days/30.44))-1)*100 if ret_full > -1 else 0
mon_oos = ((1+ret_oos)**(1/((close_oos.index[-1] - close_oos.index[0]).days/30.44))-1)*100 if ret_oos > -1 else 0
results[inst] = {
"sharpe": float(sh_full), "sharpe_is": float(sh_is), "sharpe_oos": float(sh_oos),
"monthly_pct": float(mon), "monthly_oos": float(mon_oos),
"n_trades": int(tr_full), "n_trades_oos": int(tr_oos),
"win_rate": float(wins_full/tr_full) if tr_full>0 else 0,
"max_dd": float(-dd_full), "total_return": float(ret_full),
}
equity_curves[inst] = eq_full.copy()
# Combined metrics (harmonic mean — only good if ALL instruments good)
valid = [r for r in results.values() if r['sharpe'] > 0]
if not valid:
combined = {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0, "n_trades_oos": 0}
else:
combined = {
"sharpe": float(np.mean([r['sharpe'] for r in valid])),
"monthly_pct": float(np.mean([r['monthly_pct'] for r in valid])),
"monthly_oos": float(np.mean([r['monthly_oos'] for r in valid])),
"n_trades": int(np.sum([r['n_trades'] for r in valid])),
"n_trades_oos": int(np.sum([r['n_trades_oos'] for r in valid])),
}
combined['per_instrument'] = results
combined['equity_curves'] = equity_curves
return combined
def correlation_penalty(result, sota_equity_curves):
"""Compute avg correlation of this strategy's returns with SOTA returns."""
if not sota_equity_curves:
return 0.0
my_returns = []
for eq in result.get('equity_curves', {}).values():
if len(eq) > 1:
my_returns.append(np.diff(eq) / eq[:-1])
if not my_returns:
return 0.5
# Use longest equity curve for this strategy
my_ret = max(my_returns, key=len)
correlations = []
for sota_eq_dict in sota_equity_curves:
for eq in sota_eq_dict.values():
if len(eq) > 1:
sota_ret = np.diff(eq) / eq[:-1]
# Align to shorter length
min_len = min(len(my_ret), len(sota_ret))
if min_len > 10:
corr = np.corrcoef(my_ret[:min_len], sota_ret[:min_len])[0, 1]
if not np.isnan(corr):
correlations.append(corr)
return np.mean(correlations) if correlations else 0.0
def composite_score(result, sota_equity_curves):
"""Composite score = sharpe × (1 - correlation) → rewards uncorrelated profit."""
sh = result.get('sharpe', 0)
if sh <= 0:
return 0
corr = abs(correlation_penalty(result, sota_equity_curves))
# Bonus for OOS consistency
oos_ratio = min(result.get('monthly_oos', 0) / max(result.get('monthly_pct', 1), 0.01), 1.0)
oos_ratio = max(oos_ratio, 0)
return sh * (1 - 0.5 * corr) * (0.3 + 0.7 * oos_ratio)
# ═══════════════════════════════════════════════════════════════════════════════
# Hypothesis Generation
# ═══════════════════════════════════════════════════════════════════════════════
class ResearchLoop:
"""Multi-instrument R&D loop with correlation-aware feedback."""
def __init__(self, closes):
self.closes = closes # {instrument: close_series}
self.sota = [] # State-of-the-art strategies (sorted by composite score)
self.sota_equity = [] # Equity curves for correlation calc
self.history = []
self.iteration = 0
self.best_score = 0
self.best_sharpe = 0
self.exploration_rate = EXPLORATION_RATE
def hypothesize(self):
self.iteration += 1
# Every 2000: ML (higher priority, runs before Optuna)
if self.iteration % 2000 == 0 and len(self.sota) >= 5:
return {'type': 'ml', 'generation': 'ml',
'description': f"ML: LightGBM on {len(self.sota)} strategies",
'sota': self.sota[:5]}
# Every 500: Optuna optimize best strategy
if self.iteration % 500 == 0 and self.sota:
hp = dict(self.sota[0]['hypothesis'])
hp['generation'] = 'optuna'
hp['description'] = f"Optuna: {hp.get('description','?')}"
return hp
# Every 100: force non-dominant indicator
if self.iteration % 100 == 0 and len(self.sota) >= 5:
top = self._top_indicator()
hp = self._random_hypothesis()
hp = self._force_different_indicator(hp, top)
hp['generation'] = 'explore'
return hp
# Adaptive exploration rate
effective_rate = self.exploration_rate
if len(self.sota) >= 10:
top = self._top_indicator()
dominated = sum(1 for r in self.sota if
r['hypothesis'].get('trend_ind', r['hypothesis'].get('indicator')) == top)
if dominated > len(self.sota) * 0.8:
effective_rate += 0.25
if random.random() < effective_rate or not self.sota:
return self._random_hypothesis()
else:
base = random.choice(self.sota[:5])
return self._mutate_hypothesis(base['hypothesis'])
def _top_indicator(self):
if not self.sota:
return 'MACD'
return self.sota[0]['hypothesis'].get('trend_ind',
self.sota[0]['hypothesis'].get('indicator', 'MACD'))
def _force_different_indicator(self, hp, top_ind):
if hp.get('type') == 'multi_role':
if hp['trend_ind'] == top_ind and hp['entry_ind'] == top_ind:
if random.random() < 0.5:
hp['trend_ind'] = random.choice([i for i in INDICATORS_POOL if i != top_ind])
hp['trend_params'] = self._random_params(hp['trend_ind'])
else:
hp['entry_ind'] = random.choice([i for i in INDICATORS_POOL if i != top_ind])
hp['entry_params'] = self._random_params(hp['entry_ind'])
elif hp.get('indicator') == top_ind:
hp['indicator'] = random.choice([i for i in INDICATORS_POOL if i != top_ind])
hp['params'] = self._random_params(hp['indicator'])
hp['description'] = self._make_desc(hp)
return hp
def _make_desc(self, hp):
t = hp.get('type', '?')
if t == 'multi_role':
return f"{hp['trend_ind']}({hp['trend_tf']})→{hp['entry_ind']}({hp['entry_tf']})"
elif t == 'multi_tf':
return f"{hp.get('indicator','?')} on {','.join(hp.get('timeframes',[])[:2])}"
else:
return f"{hp.get('indicator','?')} on {hp.get('timeframe','?')}"
def _random_hypothesis(self):
stype = random.choice(STRATEGY_TYPES)
if stype == 'single':
ind = random.choice(INDICATORS_POOL); tf = random.choice(TIMEFRAMES)
return {'type': 'single', 'indicator': ind, 'timeframe': tf,
'params': self._random_params(ind),
'description': f"{ind} on {tf}", 'generation': 'explore'}
elif stype == 'multi_tf':
ind = random.choice(INDICATORS_POOL)
tfs = random.sample(TIMEFRAMES, k=random.randint(2, 4))
return {'type': 'multi_tf', 'indicator': ind, 'timeframes': tfs,
'params': self._random_params(ind),
'description': f"{ind} on {','.join(tfs)}", 'generation': 'explore'}
else: # multi_role
trend_ind = random.choice(INDICATORS_POOL)
entry_ind = random.choice(INDICATORS_POOL)
trend_tf = random.choice(TREND_TFS)
entry_tf = random.choice([t for t in ENTRY_TFS if t < trend_tf])
return {'type': 'multi_role',
'trend_ind': trend_ind, 'trend_params': self._random_params(trend_ind),
'trend_tf': trend_tf,
'entry_ind': entry_ind, 'entry_params': self._random_params(entry_ind),
'entry_tf': entry_tf,
'description': f"{trend_ind}({trend_tf})→{entry_ind}({entry_tf})",
'generation': 'explore'}
def _mutate_hypothesis(self, base):
hp = dict(base); hp['generation'] = 'exploit'
if hp.get('type') == 'multi_role':
mut = random.choice(['trend_ind', 'entry_ind', 'trend_tf', 'entry_tf',
'trend_params', 'entry_params'])
if mut == 'trend_ind':
hp['trend_ind'] = random.choice([i for i in INDICATORS_POOL if i != hp['trend_ind']])
hp['trend_params'] = self._random_params(hp['trend_ind'])
elif mut == 'entry_ind':
hp['entry_ind'] = random.choice([i for i in INDICATORS_POOL if i != hp['entry_ind']])
hp['entry_params'] = self._random_params(hp['entry_ind'])
elif mut == 'trend_tf':
hp['trend_tf'] = random.choice(TREND_TFS)
if hp['trend_tf'] <= hp['entry_tf']:
hp['entry_tf'] = random.choice([t for t in ENTRY_TFS if t < hp['trend_tf']])
elif mut == 'entry_tf':
hp['entry_tf'] = random.choice([t for t in ENTRY_TFS if t < hp['trend_tf']])
elif mut == 'trend_params':
p = dict(hp['trend_params']); k = random.choice(list(p.keys()))
if isinstance(p[k], (int, float)): p[k] = p[k] * random.uniform(0.5, 1.5)
hp['trend_params'] = p
elif mut == 'entry_params':
p = dict(hp['entry_params']); k = random.choice(list(p.keys()))
if isinstance(p[k], (int, float)): p[k] = p[k] * random.uniform(0.5, 1.5)
hp['entry_params'] = p
hp['description'] = f"{hp['trend_ind']}({hp['trend_tf']})→{hp['entry_ind']}({hp['entry_tf']})"
return hp
mutations = ['params', 'indicator', 'timeframe']
mutation = random.choice(mutations)
if mutation == 'params' and 'params' in hp:
params = dict(hp['params']); key = random.choice(list(params.keys()))
if isinstance(params[key], (int, float)):
params[key] = params[key] * random.uniform(0.5, 1.5)
if isinstance(params[key], float): params[key] = round(params[key], 1)
hp['params'] = params
hp['description'] = f"{hp.get('indicator','?')} (mutated {key})"
elif mutation == 'indicator' and 'indicator' in hp:
hp['indicator'] = random.choice([i for i in INDICATORS_POOL if i != hp.get('indicator')])
hp['params'] = self._random_params(hp['indicator'])
hp['description'] = f"{hp['indicator']} (replaced)"
elif mutation == 'timeframe':
if 'timeframe' in hp:
hp['timeframe'] = random.choice(TIMEFRAMES)
elif 'timeframes' in hp:
hp['timeframes'] = random.sample(TIMEFRAMES, k=len(hp['timeframes']))
hp['description'] = f"{hp.get('indicator','?')} (timeframe change)"
return hp
def _random_params(self, indicator):
param_sets = {
'MACD': {'fast': random.choice([3,5,8,12]), 'slow': random.choice([10,15,20,26]), 'sig': random.choice([3,5,9])},
'RSI': {'period': random.choice([7,14,21]), 'oversold': random.choice([20,25,30]), 'overbought': random.choice([70,75,80])},
'BBands': {'period': random.choice([10,20,40]), 'std': random.choice([1.5,2.0,2.5])},
'Donchian': {'period': random.choice([5,10,20,30,50]), 'hold': random.choice([1,2,3,5])},
'Stoch': {'fastk': random.choice([5,9,14]), 'slowk': 3, 'slowd': random.choice([3,5])},
'CCI': {'period': random.choice([14,20,50])},
'WillR': {'period': random.choice([7,14,21])},
'ADX': {'period': random.choice([7,14,21]), 'threshold': random.choice([15,20,25])},
'SAR': {'accel': random.choice([0.02,0.05,0.08]), 'max_accel': random.choice([0.2,0.3,0.5])},
'ROC': {'period': random.choice([5,10,20]), 'threshold': random.choice([0.1,0.2,0.5])},
'MOM': {'period': random.choice([5,10,20,50])},
'AROON': {'period': random.choice([7,14,21])},
'MFI': {'period': random.choice([7,14,21])},
'SMA': {'fast': random.choice([5,10,20,50]), 'slow': random.choice([20,50,100,200])},
'EMA': {'fast': random.choice([3,5,8,12]), 'slow': random.choice([15,26,50,100])},
}
return param_sets.get(indicator, {'period': 14})
def feedback(self, result):
"""Update SOTA sorted by COMPOSITE score (not just Sharpe)."""
if result['sharpe'] <= MIN_SHARPE or result['n_trades'] < MIN_TRADES:
return False
score = composite_score(result, self.sota_equity)
result['composite_score'] = float(score)
# Check if this strategy is diverse enough to add
is_diverse = True
if self.sota:
# Skip if very similar to existing (same indicators, TF, type)
for existing in self.sota[:3]:
if self._similar(result, existing):
is_diverse = False
break
if is_diverse:
self.sota.append(result)
self.sota.sort(key=lambda r: r.get('composite_score', 0), reverse=True)
self.sota = self.sota[:30] # Keep top 30
self.sota_equity = [s['equity_curves'] for s in self.sota]
if score > self.best_score:
self.best_score = score
return True # NEW BEST
if result['sharpe'] > self.best_sharpe:
self.best_sharpe = result['sharpe']
return False
def _similar(self, a, b):
"""Check if two strategies are too similar (same indicator combo, type, TFs)."""
ha = a['hypothesis']; hb = b['hypothesis']
if ha.get('type') != hb.get('type'):
return False
if ha.get('type') == 'multi_role':
return (ha.get('trend_ind') == hb.get('trend_ind') and
ha.get('entry_ind') == hb.get('entry_ind') and
ha.get('trend_tf') == hb.get('trend_tf') and
ha.get('entry_tf') == hb.get('entry_tf'))
return ha.get('indicator') == hb.get('indicator')
def record(self):
"""Save checkpoint."""
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
STATE_DIR.mkdir(parents=True, exist_ok=True)
if self.sota:
cp = RESULTS_DIR / f"rd_loop_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
# Strip equity_curves (too large) from saved results
stripped = []
for r in self.sota[:30]:
s = {k: v for k, v in r.items() if k != 'equity_curves'}
stripped.append(s)
cp.write_text(json.dumps(stripped, indent=2, default=str))
def _run_optuna(closes, hypothesis):
"""Optuna optimization on the primary instrument."""
import optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)
hp = hypothesis
close = list(closes.values())[0] # Use first instrument for Optuna
ind = hp.get('indicator', hp.get('trend_ind', 'MACD'))
base_params = hp.get('params', hp.get('trend_params', {}))
param_ranges = {
'MACD': {'fast': (2,15), 'slow': (5,40), 'sig': (2,15)},
'RSI': {'period': (5,30), 'oversold': (10,40), 'overbought': (60,90)},
'Donchian': {'period': (3,100), 'hold': (1,10)},
'SAR': {'accel': (0.01, 0.2), 'max_accel': (0.1, 1.0)},
'ADX': {'period': (5,30), 'threshold': (10,40)},
}
ranges = param_ranges.get(ind, {})
def objective(trial):
params = {}
for k, (lo, hi) in ranges.items():
if isinstance(base_params.get(k, 1), int):
params[k] = trial.suggest_int(k, int(lo), int(hi))
else:
params[k] = trial.suggest_float(k, lo, hi)
if 'fast' in params and 'slow' in params:
params['fast'] = min(params['fast'], params['slow']-2)
result = evaluate_multi(closes, hp, use_session=True, use_vola=True)
return float(result.get('sharpe', 0)) if result.get('sharpe', 0) > 0 else -999.0
try:
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=15, show_progress_bar=False)
best = study.best_params
if 'params' in hp:
hp['params'] = {k: int(v) if v == int(v) else v for k, v in best.items()}
elif 'trend_params' in hp:
hp['trend_params'] = {k: int(v) if v == int(v) else v for k, v in best.items()}
hp['generation'] = 'optuna'
result = evaluate_multi(closes, hp, use_session=True, use_vola=True)
print(f" Optuna best: {best} → Sh={result['sharpe']:.1f} "
f"Mon={result['monthly_pct']:.1f}% OOS={result['monthly_oos']:.1f}% ({study.best_value:.1f})")
return result
except Exception:
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
def _train_ml(closes, hypothesis):
"""Train LightGBM on SOTA indicator signals."""
try:
from lightgbm import LGBMClassifier
except ImportError:
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
sota = hypothesis.get('sota', [])
if not sota:
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
close = list(closes.values())[0]
daily = close.resample('1h').last().dropna()
features = pd.DataFrame(index=daily.index)
for s in sota[:5]:
hp_s = s['hypothesis']
# Generate signal from each SOTA strategy as a feature
from nexquant_rd_loop import build_signal, _build_indicator_signal
sig = build_signal(close, hp_s)
if sig is not None:
sig = sig.reindex(daily.index, method='ffill')
name = hp_s.get('description', f"strat_{id(s)}")[:30]
features[name] = sig.fillna(0)
features = features.iloc[100:] # Skip warmup
if len(features) < 200:
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
target = (daily.pct_change().shift(-1) > 0).astype(int)
target = target.reindex(features.index).fillna(0)
split = int(len(features) * 0.8)
X_train, X_test = features.iloc[:split], features.iloc[split:]
y_train, y_test = target.iloc[:split], target.iloc[split:]
model = LGBMClassifier(n_estimators=100, max_depth=5, verbosity=-1)
model.fit(X_train, y_train)
preds = model.predict(X_test)
acc = float((preds == y_test).mean())
ml_signal = pd.Series(0, index=X_test.index)
ml_signal[preds == 1] = 1; ml_signal[preds == 0] = -1
ml_signal = ml_signal.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
ml_signal = _apply_session_filter(ml_signal, close.index)
prices = close.values.astype(np.float64); sigs = ml_signal.values.astype(np.int32)
eq, dd, tr, wins, ret, sh, _ = _backtest_numba(prices, sigs)
n_days = (close.index[-1] - close.index[0]).days
mon = ((1+ret)**(1/(n_days/30.44))-1)*100 if ret > -1 else 0
print(f" ML LightGBM: Test acc={acc:.1%} → Sh={sh:.1f} Mon={mon:.1f}% Tr={tr}")
return {"sharpe": float(sh), "monthly_pct": float(mon), "monthly_oos": 0,
"n_trades": int(tr), "win_rate": float(wins/tr) if tr>0 else 0,
"ml_accuracy": float(acc), "ml_model": "LightGBM"}
def load_data():
"""Load OHLCV data for all instruments from one or multiple HDF5 files."""
closes = {}
data_dir = OHLCV_PATH.parent
# Try main file first
if OHLCV_PATH.exists():
df = pd.read_hdf(OHLCV_PATH, key="data")
for inst in INSTRUMENTS:
try:
close = df.xs(inst, level="instrument")["$close"].sort_index()
closes[inst] = close
except KeyError:
pass
# Load from individual files if not found
instrument_files = {
"EURUSD": OHLCV_PATH,
"GBPUSD": data_dir / "gbpusdt_1min.h5",
"BTCUSD": data_dir / "btc_1min.h5",
"XAUUSD": data_dir / "xauusdt_1min.h5",
}
for inst, path in instrument_files.items():
if inst in closes:
continue
if not path.exists():
print(f" {inst}: file not found — skipping")
continue
try:
df = pd.read_hdf(path, key="data")
if isinstance(df.index, pd.MultiIndex):
try:
close = df.xs(inst, level="instrument")["$close"].sort_index()
except KeyError:
# Try with T suffix for crypto pairs
alt = inst + "T" if not inst.endswith("T") else inst.rstrip("T")
try:
close = df.xs(alt, level="instrument")["$close"].sort_index()
except KeyError:
inst_vals = df.index.get_level_values("instrument").unique()
for iv in inst_vals:
if inst[:3] in str(iv)[:3]:
close = df.xs(iv, level="instrument")["$close"].sort_index()
break
else:
raise KeyError(f"No instrument matching {inst}")
elif "$close" in df.columns:
close = df["$close"].sort_index()
close.index = pd.to_datetime(close.index)
elif "close" in df.columns:
close = df["close"].sort_index()
close.index = pd.to_datetime(close.index)
else:
close = df.iloc[:, 3].sort_index()
close.index = pd.to_datetime(close.index)
closes[inst] = close
except Exception as e:
print(f" {inst}: load error {e} — skipping")
for inst, close in closes.items():
print(f" {inst}: {len(close):,} bars, {close.index[0]}{close.index[-1]}")
return closes
def main():
iterations = 200
if "--iterations" in sys.argv:
iterations = int(sys.argv[sys.argv.index("--iterations") + 1])
print("=" * 60)
print(f" R&D Loop V2 — Multi-Instrument + Correlation Score")
print(f" Instruments: {', '.join(INSTRUMENTS)}")
print(f" Indicators: {len(INDICATORS_POOL)} | Strategy types: {len(STRATEGY_TYPES)}")
print(f" Features: Session Filter + Volatility Filter + OOS Split")
print(f" Iterations: {iterations}")
print("=" * 60)
print(" Loading data...")
closes = load_data()
if not closes:
print(" ERROR: No instruments loaded!"); return
loop = ResearchLoop(closes)
t0 = time.time()
for i in range(iterations):
hp = loop.hypothesize()
# Evaluate
try:
result = evaluate_multi(closes, hp, use_session=True, use_vola=True)
except Exception:
continue
result['hypothesis'] = hp
result['iteration'] = i + 1
result['timestamp'] = datetime.now().isoformat()
loop.history.append(result)
# Feedback
is_new_best = loop.feedback(result)
best_inst_metrics = [f"{inst}: {m['sharpe']:.1f}" for inst, m in result.get('per_instrument', {}).items() if m.get('sharpe', 0) != 0]
gen = hp.get('generation', '?')
if is_new_best:
print(f"\n ★ NEW BEST (#{i+1}, {gen}): {hp['description']}")
print(f" Score={result['composite_score']:.1f} Sh={result['sharpe']:.1f} "
f"Mon={result['monthly_pct']:.1f}% OOS={result['monthly_oos']:.1f}% "
f"Tr={result['n_trades']} [{', '.join(best_inst_metrics[:3])}]")
elif (i + 1) % 50 == 0:
top_indicators = set()
for r in loop.sota[:5]:
top_indicators.add(r['hypothesis'].get('trend_ind', r['hypothesis'].get('indicator', '?')))
print(f" [{i+1}/{iterations}] {gen:>7s} | SOTA: {len(loop.sota)} | "
f"Best Sh={loop.best_sharpe:.1f} Score={loop.best_score:.1f} | "
f"Explore: {loop.exploration_rate:.0%} | Inds: {','.join(sorted(top_indicators)[:4])}")
if (i + 1) % 100 == 0:
loop.record()
if len(loop.sota) > 10:
loop.exploration_rate = max(0.15, EXPLORATION_RATE - len(loop.sota) * 0.003)
elapsed = time.time() - t0
print(f"\n{'=' * 60}")
print(f" R&D Loop V2 Complete: {iterations} iterations in {elapsed:.0f}s")
print(f" SOTA Strategies: {len(loop.sota)} | Best Score: {loop.best_score:.1f}")
print(f"{'=' * 60}")
if loop.sota:
print(f"\n TOP DISCOVERIES (by composite score):")
for i, r in enumerate(loop.sota[:15], 1):
hp = r['hypothesis']
per_inst = r.get('per_instrument', {})
insts = ' '.join([f"{k}:{v['sharpe']:.0f}" for k, v in per_inst.items() if v['sharpe'] != 0])
print(f" {i:>2d}. {hp['description'][:45]:45s} "
f"Sc={r['composite_score']:.1f} Sh={r['sharpe']:+.1f} "
f"Mo={r['monthly_pct']:+.1f}% OOS={r['monthly_oos']:+.1f}% "
f"[{insts}]")
final = RESULTS_DIR / f"rd_loop_final_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
stripped = [{k: v for k, v in r.items() if k != 'equity_curves'} for r in loop.sota]
final.write_text(json.dumps(stripped, indent=2, default=str))
print(f"\n Saved: {final}")
exploit_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'exploit']
explore_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'explore']
optuna_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'optuna']
ml_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'ml']
print(f" Exploit: {len(exploit_best)} | Explore: {len(explore_best)} | "
f"Optuna: {len(optuna_best)} | ML: {len(ml_best)}")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1065,7 +1065,7 @@ class TestSchemaMigrationFuzzing:
c = db.conn.cursor() c = db.conn.cursor()
c.execute("PRAGMA table_info(backtest_runs)") c.execute("PRAGMA table_info(backtest_runs)")
cols = [row[1] for row in c.fetchall()] 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: finally:
db.close() db.close()