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