mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-30 00:47:43 +00:00
Compare commits
300 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24357152df | |||
| 50d1fb47e3 | |||
| 68caa7e88c | |||
| 891daf4df9 | |||
| 56346d5ae5 | |||
| 723ba6f004 | |||
| eb6b2dcd1f | |||
| a721605f4b | |||
| a9d181398b | |||
| 4773e95a6c | |||
| 9ce6a4e6ec | |||
| b793a8114b | |||
| 6bce4f2405 | |||
| 2e028ffc1e | |||
| 7d7c267d29 | |||
| 6cd362aa25 | |||
| a373710454 | |||
| ee3d7786c3 | |||
| 4b6dff1710 | |||
| 7c22287793 | |||
| 3874afb8dd | |||
| e168a5df7e | |||
| 8806b12ad6 | |||
| 61e6a09b95 | |||
| 9303b40fb9 | |||
| ab57498ccf | |||
| 6f399c1d96 | |||
| 4758de0eee | |||
| d4611b530e | |||
| e0000a18d2 | |||
| 847a30a787 | |||
| c7ae139c18 | |||
| 774a581184 | |||
| 6975f77b77 | |||
| 918639c051 | |||
| e3a65bb140 | |||
| c45b911abe | |||
| f10b257152 | |||
| 15c03df431 | |||
| 0d9b0916f2 | |||
| 827f80ce2e | |||
| e4aea618b8 | |||
| a469692141 | |||
| 90690c1675 | |||
| 5620ea1b0e | |||
| 5b2b2ca9cc | |||
| aa7e046782 | |||
| 54b8713938 | |||
| 630794e00c | |||
| 69c148f1f3 | |||
| a781d003ba | |||
| efd77b434c | |||
| abca9eb899 | |||
| 3d38d88248 | |||
| 22c8092f1c | |||
| 21afe878e1 | |||
| cbe1c52e00 | |||
| 85b56b8179 | |||
| ba7458fba5 | |||
| 92ddcc47c7 | |||
| 160ac96130 | |||
| e029120090 | |||
| 4d9459a9f9 | |||
| f0ac999dbe | |||
| 9c91a6938d | |||
| 0aea8c7671 | |||
| 72e8a4306e | |||
| 669263db37 | |||
| 584bf9d955 | |||
| d458e39940 | |||
| e20f3b0ea4 | |||
| 3584aee089 | |||
| e0c287a575 | |||
| 8a8fb6688a | |||
| fddc472e16 | |||
| 29ad20914b | |||
| e76d5ab9cf | |||
| 4b0898b377 | |||
| f98ef71693 | |||
| 54718b1663 | |||
| ce86eb8a8a | |||
| 8cce616434 | |||
| d55bd518d3 | |||
| f99acca6e0 | |||
| 49a2c34c5c | |||
| 71ceb9c809 | |||
| 7f5acccfd9 | |||
| a459518dfa | |||
| 41c231e418 | |||
| 5c93c786e7 | |||
| 834cc686d1 | |||
| c0ec1b39e1 | |||
| f5e55d2dad | |||
| b78b9dea8c | |||
| 58a7ece3a9 | |||
| 02083409e0 | |||
| 33f6daf1d2 | |||
| 1827c50344 | |||
| 06a5d5d92d | |||
| 7696a3aaa6 | |||
| 6d37f8956f | |||
| 020bc11742 | |||
| 0f7eb908b4 | |||
| 4d8b389b47 | |||
| 72b9a735c9 | |||
| ce4a5b7b4f | |||
| 037f7ba7d2 | |||
| e31963713b | |||
| dc2a1a41f4 | |||
| c3ff9d5e63 | |||
| 1a530440c1 | |||
| b803d9fab5 | |||
| 13dbf27457 | |||
| 49ebc19fd0 | |||
| 58eb551898 | |||
| 2e18b7c104 | |||
| 28743eb043 | |||
| f6f5531427 | |||
| bd9362e2f3 | |||
| a4d4266295 | |||
| 8f2ed4185f | |||
| 7f24af6f05 | |||
| aba88dd090 | |||
| 28766c932e | |||
| 574e9d6c08 | |||
| ce76da912a | |||
| 02ac3f7aae | |||
| 39b49b1724 | |||
| f1eb66cc8f | |||
| ca003cd0f2 | |||
| 4eeb724ac5 | |||
| 6c3bdb6ec1 | |||
| 82633d328a | |||
| c5d919f581 | |||
| 44ed82283d | |||
| 87610d660f | |||
| c0516d60f9 | |||
| a43c443c2e | |||
| 9e58c64805 | |||
| 732361bb90 | |||
| dff6262871 | |||
| 23b2518c74 | |||
| d83c020637 | |||
| 48843682d0 | |||
| 5a5bf4d771 | |||
| 02c830d4e2 | |||
| 1328a8f0c4 | |||
| 2126062edf | |||
| 133ec1b816 | |||
| 0fa4f5dcc8 | |||
| a076b58304 | |||
| afe1823e85 | |||
| b4674ce3a0 | |||
| 942266f24d | |||
| 95b14fcfc8 | |||
| e9422be6e2 | |||
| 161c4043d7 | |||
| 6b3669c3be | |||
| a7fc33b9a8 | |||
| 1448106230 | |||
| 5eeaf1f496 | |||
| 90a6999563 | |||
| 387508168f | |||
| 2055cf1817 | |||
| 018231d1f2 | |||
| d8bd16e6b9 | |||
| 940859a85b | |||
| 2de7275b8f | |||
| 9691b64938 | |||
| d8c0d8865c | |||
| bf5dfd1d12 | |||
| 919a44a4b8 | |||
| 44f82b13d3 | |||
| 17a2558339 | |||
| ed1802b511 | |||
| ff1c9fc554 | |||
| ba4d64b434 | |||
| 8aec974702 | |||
| f4deda99b5 | |||
| bd5a5e0fd5 | |||
| 7b2f54ff9a | |||
| a50987687d | |||
| d2037a475a | |||
| ef2a6c5ee0 | |||
| dfbbffd7ea | |||
| 78fb607dbe | |||
| b1db0b1f7d | |||
| 1958544106 | |||
| e886ebab8f | |||
| ed80fe8160 | |||
| 9b87a1f5ae | |||
| 6c8a1257c8 | |||
| 2a6839e999 | |||
| d74c5a8f84 | |||
| 837f1f6a0e | |||
| 5fc9aa026c | |||
| f0bf7e2d61 | |||
| b0490c5f0d | |||
| 6d514d3ed5 | |||
| d021f77242 | |||
| 23c528917c | |||
| 02bb7724c6 | |||
| f726e939ab | |||
| f166cc3326 | |||
| 9102f3ce96 | |||
| f8d1d36cf0 | |||
| 6c100170bd | |||
| 64e96bd350 | |||
| 01ba45be56 | |||
| 3f54381052 | |||
| 888e841366 | |||
| 4a6178f53a | |||
| cffb9adc38 | |||
| 6e52c8a15d | |||
| 3f460226f2 | |||
| 1f6990d04d | |||
| ce232733d9 | |||
| 1de5bbe1c6 | |||
| 38cf4bc63e | |||
| 96e4f49740 | |||
| f4747eb907 | |||
| 958560e78f | |||
| e4d400e3f6 | |||
| 69d8b5c871 | |||
| 743ff45976 | |||
| fef5a0610d | |||
| c468e9f00e | |||
| 632d11ae53 | |||
| 6dfbf148ed | |||
| f1c2b7d3a9 | |||
| b8d9564fae | |||
| 1a57e57583 | |||
| 22e638af86 | |||
| ad0358d01c | |||
| cedd615922 | |||
| 5860b8487d | |||
| 64dc7ba425 | |||
| 616590cdc0 | |||
| d8ab86d6cf | |||
| 5f735adcb1 | |||
| 9d623f0fbb | |||
| f24f678713 | |||
| a49f63cfec | |||
| 652164b79e | |||
| 2cec08bc91 | |||
| 5a6446a287 | |||
| 005107a461 | |||
| 554a499d09 | |||
| 0a275528ed | |||
| b9fe985a55 | |||
| 6ee6c5210d | |||
| 6948b9c5e9 | |||
| 06fc8dc36c | |||
| 8a3472f85a | |||
| 6358bc500f | |||
| 3cfa3dda6f | |||
| b98c9cd572 | |||
| 19c9b88b76 | |||
| 53afed001e | |||
| cc8023ce48 | |||
| 4a3a3c8f24 | |||
| 35a1a62df5 | |||
| 44a06a65f4 | |||
| 0fd366dd59 | |||
| 009f18eb46 | |||
| 020f0135ef | |||
| 0acdfaa485 | |||
| 1fbf094115 | |||
| dbc8603e73 | |||
| d6722e46f0 | |||
| 9925b3132c | |||
| a5a0a3c6f9 | |||
| 7a0a95163b | |||
| d35764d44c | |||
| 7e2c31305f | |||
| 362cff291c | |||
| a523e62d94 | |||
| e0a5e6d86c | |||
| a90325d203 | |||
| 0ae6f0f39a | |||
| 087af5b297 | |||
| dd6beec3c9 | |||
| ad206345cc | |||
| fceee44967 | |||
| 9e50eb2d4e | |||
| b63380e3ce | |||
| 4c45ba33ab | |||
| 8aa28ffb33 | |||
| 108a63fd79 | |||
| 21daaf4997 | |||
| 0b168fd3e4 | |||
| 03536af000 | |||
| d12430427d | |||
| 352dd08514 | |||
| 4133d62760 | |||
| bef1d77ee0 | |||
| 6ba2bc0c8f | |||
| 11d96ec3bd | |||
| 2677ee43a2 | |||
| 24a58e970e |
@@ -14,7 +14,7 @@ jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Run Bandit (Security Scan)
|
||||
uses: PyCQA/bandit-action@v1
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
steps:
|
||||
# Checkout the repository to the GitHub Actions runner
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
|
||||
- name: Run Codacy Analysis CLI
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
name: Validate Commit Messages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
python-version: ["3.10", "3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
name: Dependency Audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -65,6 +65,7 @@ QWEN.md
|
||||
CLAUDE.md
|
||||
docs/COMPLETE_WORKFLOW.md
|
||||
docs/SMART_STRATEGY_GEN.md
|
||||
STARRED_REPOS_ANALYSIS.md
|
||||
|
||||
# OpenACP workspace (secrets)
|
||||
.openacp
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Pre-commit hooks configuration for Predix
|
||||
# Pre-commit hooks configuration for NexQuant
|
||||
# See https://pre-commit.com for more information
|
||||
|
||||
repos:
|
||||
|
||||
@@ -1 +1 @@
|
||||
{".": "1.4.3"}
|
||||
{".": "1.5.0"}
|
||||
|
||||
+453
-453
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -52,7 +52,7 @@ an individual is officially representing the community in public spaces.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
nico@predix.io.
|
||||
nico@nexquant.io.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
## Attribution
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
# Contributing to Predix
|
||||
# Contributing to NexQuant
|
||||
|
||||
We welcome contributions and suggestions to improve Predix. Whether it's solving an issue, addressing a bug, enhancing documentation, or even correcting a typo, every contribution is valuable and helps improve the project.
|
||||
We welcome contributions and suggestions to improve NexQuant. Whether it's solving an issue, addressing a bug, enhancing documentation, or even correcting a typo, every contribution is valuable and helps improve the project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -15,11 +15,11 @@ grep -r "TODO:"
|
||||
|
||||
```bash
|
||||
# Fork the repository on GitHub, then clone your fork
|
||||
git clone https://github.com/YOUR-USERNAME/Predix.git
|
||||
cd Predix
|
||||
git clone https://github.com/YOUR-USERNAME/NexQuant.git
|
||||
cd NexQuant
|
||||
|
||||
# Add upstream remote
|
||||
git remote add upstream https://github.com/TPTBusiness/Predix.git
|
||||
git remote add upstream https://github.com/TPTBusiness/NexQuant.git
|
||||
```
|
||||
|
||||
### 2. Create a Branch
|
||||
@@ -141,7 +141,7 @@ All PRs are reviewed by maintainers. Expect:
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Predix/
|
||||
NexQuant/
|
||||
├── rdagent/ # Core framework (open source)
|
||||
│ ├── app/ # CLI and scenario apps
|
||||
│ ├── components/ # Reusable agent components
|
||||
@@ -157,8 +157,8 @@ Predix/
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/TPTBusiness/Predix/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/TPTBusiness/Predix/discussions)
|
||||
- **Issues**: [GitHub Issues](https://github.com/TPTBusiness/NexQuant/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/TPTBusiness/NexQuant/discussions)
|
||||
- **Documentation**: See `docs/` folder
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,603 +1,228 @@
|
||||
# Predix
|
||||
# NexQuant
|
||||
|
||||
<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>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TPTBusiness/Predix/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/TPTBusiness/Predix/ci.yml?branch=master&label=CI&logo=github&style=flat-square" alt="CI Status">
|
||||
<a href="https://github.com/TPTBusiness/NexQuant/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/TPTBusiness/NexQuant/ci.yml?branch=master&label=CI&logo=github&style=flat-square" alt="CI Status">
|
||||
</a>
|
||||
<a href="https://github.com/TPTBusiness/Predix/actions/workflows/codacy.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/TPTBusiness/Predix/codacy.yml?branch=master&label=Security&logo=shield&style=flat-square" alt="Security Scan">
|
||||
<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/Predix">
|
||||
<img src="https://img.shields.io/codecov/c/github/TPTBusiness/Predix?style=flat-square&logo=codecov" alt="Coverage">
|
||||
</a>
|
||||
<a href="https://github.com/TPTBusiness/Predix/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/TPTBusiness/Predix?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 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://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/Predix/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/TPTBusiness/Predix?style=flat-square" alt="Stars">
|
||||
</a>
|
||||
<a href="https://github.com/TPTBusiness/Predix/forks">
|
||||
<img src="https://img.shields.io/github/forks/TPTBusiness/Predix?style=flat-square" alt="Forks">
|
||||
</a>
|
||||
<a href="https://github.com/TPTBusiness/Predix/issues">
|
||||
<img src="https://img.shields.io/github/issues/TPTBusiness/Predix?style=flat-square" alt="Issues">
|
||||
</a>
|
||||
<a href="https://github.com/TPTBusiness/Predix/commits/master">
|
||||
<img src="https://img.shields.io/github/last-commit/TPTBusiness/Predix?style=flat-square" alt="Last Commit">
|
||||
<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>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ CLI Dashboard
|
||||
|
||||
```bash
|
||||
rdagent predix
|
||||
```
|
||||
|
||||

|
||||
|
||||
*The Predix CLI shows system status, available commands, and quick start guide.*
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**Predix** is an autonomous AI agent for quantitative trading strategies in the EUR/USD forex market. Built on a multi-agent framework, Predix 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:
|
||||
|
||||
- 📊 **Data Analysis** – Automatically analyzes market patterns and microstructure
|
||||
- 💡 **Strategy Discovery** – Proposes novel trading factors and signals
|
||||
- 🧠 **Model Evolution** – Iteratively improves predictive models
|
||||
- 📈 **Backtesting** – Validates strategies on historical 1-minute data
|
||||
| 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 |
|
||||
|
||||
Predix is optimized for **1-minute EUR/USD FX data** (2020–2026) and uses Qlib as the underlying backtesting engine.
|
||||
**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.). 479 unit tests + 10 ground-truth validation tests ensure ~99% 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 Predix is originally written and implemented independently. Predix 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/Predix
|
||||
cd Predix
|
||||
|
||||
# Create and activate conda environment
|
||||
conda create -n predix python=3.10 -y
|
||||
conda activate predix
|
||||
|
||||
# Install in editable mode
|
||||
pip install -e .
|
||||
|
||||
# Verify Docker is accessible
|
||||
docker run --rm hello-world
|
||||
```
|
||||
|
||||
> **Important:** Predix requires a conda environment to manage dependencies properly.
|
||||
> Using plain Python or other environment managers may cause conflicts.
|
||||
|
||||
---
|
||||
|
||||
## Data Setup
|
||||
|
||||
Predix requires **1-minute EUR/USD OHLCV data** in HDF5 format. This is a hard prerequisite — the system cannot run without it.
|
||||
|
||||
### Step 1: Get the data
|
||||
|
||||
Download 1-minute EUR/USD data (2020–present) from any of these free sources:
|
||||
|
||||
| Source | Cost | Notes |
|
||||
|--------|------|-------|
|
||||
| **[Dukascopy](https://www.dukascopy.com/swiss/english/marketfeed/historical/)** | Free | Best quality free EUR/USD tick data |
|
||||
| **[OANDA API](https://developer.oanda.com/)** | Free (demo) | Requires API key, programmatic access |
|
||||
| **[TrueFX](https://truefx.com/)** | Free | Institutional-quality tick data |
|
||||
| **[Kaggle](https://www.kaggle.com/datasets?search=EURUSD+1min)** | Free | Search "EURUSD 1 minute" |
|
||||
| **MetaTrader 5** | Free | Export via `copy_rates_range()` |
|
||||
|
||||
### Step 2: Convert to HDF5
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
df = pd.read_csv('eurusd_1min.csv', parse_dates=['datetime'])
|
||||
df = df.rename(columns={'open': '$open', 'close': '$close',
|
||||
'high': '$high', 'low': '$low', 'volume': '$volume'})
|
||||
df['instrument'] = 'EURUSD'
|
||||
df = df.set_index(['datetime', 'instrument'])
|
||||
for col in ['$open', '$close', '$high', '$low', '$volume']:
|
||||
df[col] = df[col].astype('float32')
|
||||
|
||||
import os
|
||||
os.makedirs('git_ignore_folder/factor_implementation_source_data', exist_ok=True)
|
||||
df.to_hdf('git_ignore_folder/factor_implementation_source_data/intraday_pv.h5', key='data', mode='w')
|
||||
```
|
||||
|
||||
### Required HDF5 format
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| **Index** | MultiIndex `(datetime, instrument)` | Timestamp + currency pair |
|
||||
| **`$open`** | float32 | Open price |
|
||||
| **`$close`** | float32 | Close price |
|
||||
| **`$high`** | float32 | High price |
|
||||
| **`$low`** | float32 | Low price |
|
||||
| **`$volume`** | float32 | Tick volume |
|
||||
|
||||
**Save location:** `git_ignore_folder/factor_implementation_source_data/intraday_pv.h5`
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Setup
|
||||
|
||||
Create a `.env` file in the project root:
|
||||
|
||||
```bash
|
||||
# Local LLM (llama.cpp)
|
||||
OPENAI_API_KEY=local
|
||||
OPENAI_API_BASE=http://localhost:8081/v1
|
||||
CHAT_MODEL=qwen3.5-35b
|
||||
|
||||
# Embedding (Ollama)
|
||||
LITELLM_PROXY_API_KEY=local
|
||||
LITELLM_PROXY_API_BASE=http://localhost:11434/v1
|
||||
EMBEDDING_MODEL=nomic-embed-text
|
||||
|
||||
# Paths
|
||||
QLIB_DATA_DIR=~/.qlib/qlib_data/eurusd_1min_data
|
||||
```
|
||||
|
||||
### LLM Server (llama.cpp)
|
||||
|
||||
```bash
|
||||
~/llama.cpp/build/bin/llama-server \
|
||||
--model ~/models/qwen3.6/Qwen3.6-35B-A3B-UD-Q3_K_XL.gguf \
|
||||
--n-gpu-layers 24 \
|
||||
--no-mmap \
|
||||
--port 8081 \
|
||||
--ctx-size 240000 \
|
||||
--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 240000 --parallel 2` — allocates **2 slots × 120,000 tokens each**. `fin_quant` prompts can reach 80k+ tokens with full factor history; a smaller slot causes silent overflow and empty responses.
|
||||
> - `--reasoning off` — **critical**: completely disables Qwen3 chain-of-thought. `--reasoning-budget 0` is not sufficient and produces empty JSON responses.
|
||||
> - `--n-gpu-layers 24` — 4 fewer than maximum on RTX 5060 Ti (16 GB), freeing ~500 MB VRAM for the larger KV cache.
|
||||
> - `-ctk q4_0 -ctv q4_0` — quantises the KV cache to 4-bit, reducing VRAM from ~5 GB to ~1.3 GB at 240k context.
|
||||
|
||||
### 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 Predix 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 predix_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 predix
|
||||
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 predix.py best
|
||||
```
|
||||
|
||||
### 3. Run Continuously
|
||||
|
||||
```bash
|
||||
while true; do
|
||||
rdagent fin_quant
|
||||
sleep 5
|
||||
done
|
||||
# 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 predix.py best` | Show top strategies by composite score |
|
||||
| `python predix.py best -n 20 -m sharpe` | Top 20 by Sharpe ratio |
|
||||
| `python predix.py best --show NAME` | Full metadata for one strategy |
|
||||
| `python predix_gen_strategies_real_bt.py` | Generate 10 strategies with LLM + real backtest |
|
||||
| `python predix_gen_strategies_real_bt.py 20` | Generate 20 strategies |
|
||||
**4 timeframes**: 15min, 30min, 1h, 4h
|
||||
|
||||
### Kronos Foundation Model
|
||||
**3 strategy types**: Single-TF, Multi-TF (vote majority), Portfolio (indicator ensemble)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `python predix.py kronos-factor` | Generate Kronos predicted-return factor (daily stride, ~15 min GPU) |
|
||||
| `python predix.py kronos-factor --pred 30` | 30-bar prediction horizon |
|
||||
| `python predix.py kronos-factor --device cpu` | CPU inference (slower) |
|
||||
| `python predix.py kronos-eval` | Evaluate Kronos IC / hit rate vs LightGBM baseline |
|
||||
| `python predix.py kronos-eval --pred 96` | Daily horizon evaluation |
|
||||
**Discovery example** (50,000 iterations):
|
||||
```
|
||||
random → SAR(+65) → MACD(+73) → MACD-mutated(+102.75, +32%/month)
|
||||
↓
|
||||
Optuna tuned params
|
||||
↓
|
||||
LightGBM ensemble
|
||||
```
|
||||
|
||||
### Factor Evaluation
|
||||
### Grid Search (`scripts/nexquant_priceaction_loop.py`)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `python predix.py evaluate --all` | Evaluate all generated factors |
|
||||
| `python predix.py top -n 20` | Show top 20 factors by IC |
|
||||
| `python predix.py portfolio-simple` | Simple portfolio optimization |
|
||||
Deterministic parameter grid over all 17 indicators. Finds MACD(3,10,3) as optimal.
|
||||
|
||||
### Parallel Execution
|
||||
### Portfolio Optimizer (`scripts/nexquant_portfolio_optimizer.py`)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `python predix_parallel.py --runs 5 --api-keys 1 -m openrouter` | Run 5 parallel factor evolutions |
|
||||
| `python predix_parallel.py --runs 20 --api-keys 2 -m openrouter` | Run 20 runs with 2 API keys |
|
||||
Greedy correlation-aware selection from discovered strategies.
|
||||
|
||||
### Monitoring & Debug
|
||||
---
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `rdagent server_ui --port 19899 --log-dir <path>` | Start web dashboard |
|
||||
| `rdagent health_check` | Validate environment setup |
|
||||
| `python predix_batch_backtest.py` | Batch backtest multiple factors |
|
||||
| `python predix_rebacktest_strategies.py` | Re-backtest existing strategies |
|
||||
## Live Trading
|
||||
|
||||
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
|
||||
|
||||
Predix 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
|
||||
|
||||
Predix integrates [Kronos-mini](https://github.com/shiyu-coder/Kronos) — a 4.1M parameter OHLCV foundation model pretrained on 12+ billion K-lines from 45 global exchanges (AAAI 2026, MIT):
|
||||
|
||||
- **Option A — Alpha Factor**: Rolling daily inference generates a `KronosPredReturn` factor. Every 96 bars (one trading day), Kronos predicts the next day's return from the previous 512 bars of EUR/USD OHLCV data. The factor is forward-filled to 1-min frequency and plugs directly into Predix's factor evaluation pipeline.
|
||||
|
||||
- **Option B — Model Evaluation**: Kronos runs alongside LightGBM as a standalone predictor. IC (Information Coefficient), IC IR, and directional hit rate are computed over the full dataset for direct comparison with LightGBM-generated models.
|
||||
|
||||
```bash
|
||||
# One-time setup
|
||||
git clone https://github.com/shiyu-coder/Kronos ~/Kronos
|
||||
|
||||
# Generate factor (Option A) — saves to results/factors/
|
||||
python predix.py kronos-factor
|
||||
|
||||
# Evaluate as model (Option B) — prints IC vs LightGBM reference
|
||||
python predix.py kronos-eval
|
||||
```
|
||||
### 📊 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:
|
||||
|
||||
- **134+ Tests** — all features tested automatically on every commit
|
||||
- **Bandit Security Scanner** — pre-commit security checks
|
||||
- **Weekly Dependency Audit** — automated vulnerability scan via GitHub Actions
|
||||
- 0 Dependabot alerts, 0 CodeScan alerts
|
||||
- No proprietary terms in git history
|
||||
- Closed-source detection CI
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
predix/
|
||||
├── rdagent/ # Core agent framework
|
||||
│ ├── app/ # CLI and scenario apps
|
||||
│ ├── components/ # Reusable agent components
|
||||
│ │ ├── backtesting/ # Backtest engine & protections
|
||||
│ │ │ ├── backtest_engine.py
|
||||
│ │ │ ├── vbt_backtest.py # Unified backtest engine
|
||||
│ │ │ ├── results_db.py
|
||||
│ │ │ └── protections/ # Trading protection system
|
||||
│ │ └── coder/ # Factor & model coding (CoSTEER + Optuna)
|
||||
│ ├── core/ # Core abstractions
|
||||
│ ├── scenarios/ # Domain-specific scenarios
|
||||
│ └── utils/ # Utilities
|
||||
├── test/ # Test suite (134 tests)
|
||||
│ └── backtesting/ # Backtest unit tests
|
||||
├── web/ # Web UI frontend
|
||||
├── data_config.yaml # Walk-forward split configuration
|
||||
├── pyproject.toml # Project metadata
|
||||
└── requirements.txt # Dependencies
|
||||
nexquant/
|
||||
├── 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 Predix 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/Predix/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/qlib/ -q # 479 tests, 0 failures
|
||||
pytest test/backtesting/ -q # backtest engine 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), Monte Carlo p-value, walk-forward rolling, buy-and-hold equality.
|
||||
**GNU Affero General Public License v3.0 (AGPL-3.0)**. See [`LICENSE`](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Predix 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.
|
||||
|
||||
+2
-2
@@ -2,13 +2,13 @@
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We take the security of Predix seriously. If you believe you have found a security vulnerability, please report it responsibly.
|
||||
We take the security of NexQuant seriously. If you believe you have found a security vulnerability, please report it responsibly.
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
### How to Report
|
||||
|
||||
1. **Open a private security advisory** on GitHub: https://github.com/TPTBusiness/Predix/security/advisories
|
||||
1. **Open a private security advisory** on GitHub: https://github.com/TPTBusiness/NexQuant/security/advisories
|
||||
2. Provide a detailed description of the vulnerability
|
||||
3. Include steps to reproduce if possible
|
||||
4. We will respond within 48 hours
|
||||
|
||||
+3
-3
@@ -6,12 +6,12 @@ This project uses GitHub Issues to track bugs and feature requests. Please searc
|
||||
issues before filing new issues to avoid duplicates. For new issues, file your bug or
|
||||
feature request as a new Issue.
|
||||
|
||||
- **Issues**: [https://github.com/PredixAI/predix/issues](https://github.com/PredixAI/predix/issues)
|
||||
- **Issues**: [https://github.com/NexQuantAI/nexquant/issues](https://github.com/NexQuantAI/nexquant/issues)
|
||||
|
||||
For help and questions about using this project, please reach out via:
|
||||
|
||||
- **Email**: nico@predix.io
|
||||
- **GitHub Discussions**: [https://github.com/PredixAI/predix/discussions](https://github.com/PredixAI/predix/discussions)
|
||||
- **Email**: nico@nexquant.io
|
||||
- **GitHub Discussions**: [https://github.com/NexQuantAI/nexquant/discussions](https://github.com/NexQuantAI/nexquant/discussions)
|
||||
|
||||
## Community Support
|
||||
|
||||
|
||||
+8
-8
@@ -1,4 +1,4 @@
|
||||
# Predix v1.0.0 Release Notes
|
||||
# NexQuant v1.0.0 Release Notes
|
||||
|
||||
**Release Date:** 2026-04-02
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 🎉 Overview
|
||||
|
||||
Initial release of Predix - an autonomous AI-powered quantitative trading agent for EUR/USD forex markets.
|
||||
Initial release of NexQuant - an autonomous AI-powered quantitative trading agent for EUR/USD forex markets.
|
||||
|
||||
---
|
||||
|
||||
@@ -75,8 +75,8 @@ Initial release of Predix - an autonomous AI-powered quantitative trading agent
|
||||
|
||||
## 🔧 Changed
|
||||
|
||||
- Rebranded from RD-Agent to Predix for EUR/USD quantitative trading
|
||||
- Updated project metadata for PredixAI organization
|
||||
- Rebranded from RD-Agent to NexQuant for EUR/USD quantitative trading
|
||||
- Updated project metadata for NexQuantAI organization
|
||||
- All code comments translated to English
|
||||
- Removed 'Inspired by' comments, added comprehensive Acknowledgments
|
||||
- Enhanced .gitignore for better file management
|
||||
@@ -137,7 +137,7 @@ This release builds upon and is inspired by:
|
||||
- **TradingAgents** (Apache 2.0 License) - Multi-agent debate patterns
|
||||
- **ai-hedge-fund** - Macro analysis and risk management concepts
|
||||
|
||||
**All code in Predix v1.0.0 is originally written and independently implemented.**
|
||||
**All code in NexQuant v1.0.0 is originally written and independently implemented.**
|
||||
|
||||
---
|
||||
|
||||
@@ -149,7 +149,7 @@ This release builds upon and is inspired by:
|
||||
|
||||
If you use this code or concepts in your project, you **must**:
|
||||
1. Include the MIT License text
|
||||
2. Keep the copyright notice: "Copyright (c) 2025 Predix Team"
|
||||
2. Keep the copyright notice: "Copyright (c) 2025 NexQuant Team"
|
||||
3. Provide attribution to the original project
|
||||
|
||||
See [ATTRIBUTION.md](../ATTRIBUTION.md) for detailed guidelines.
|
||||
@@ -158,7 +158,7 @@ See [ATTRIBUTION.md](../ATTRIBUTION.md) for detailed guidelines.
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- **GitHub Release:** https://github.com/TPTBusiness/Predix/releases/tag/v1.0.0
|
||||
- **GitHub Release:** https://github.com/TPTBusiness/NexQuant/releases/tag/v1.0.0
|
||||
- **Main Changelog:** ../CHANGELOG.md
|
||||
- **Attribution Guidelines:** ../ATTRIBUTION.md
|
||||
- **Installation Guide:** ../README.md#installation
|
||||
@@ -168,7 +168,7 @@ See [ATTRIBUTION.md](../ATTRIBUTION.md) for detailed guidelines.
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Made with ❤️ by Predix Team**
|
||||
**Made with ❤️ by NexQuant Team**
|
||||
|
||||
For detailed usage guidelines, see [README.md](../README.md)
|
||||
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
# Predix v2.0.0 Release Notes
|
||||
# NexQuant v2.0.0 Release Notes
|
||||
|
||||
**Release Date:** 2026-04-10
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 🎉 Overview
|
||||
|
||||
Major update adding AI-powered strategy generation, realistic backtesting, and comprehensive CLI tooling. Predix now autonomously generates, evaluates, and optimizes trading strategies using local LLMs.
|
||||
Major update adding AI-powered strategy generation, realistic backtesting, and comprehensive CLI tooling. NexQuant now autonomously generates, evaluates, and optimizes trading strategies using local LLMs.
|
||||
|
||||
---
|
||||
|
||||
@@ -28,7 +28,7 @@ Major update adding AI-powered strategy generation, realistic backtesting, and c
|
||||
- **Proper Annualization**: sqrt(252*1440) for 1-min data
|
||||
|
||||
### CLI Commands
|
||||
- `rdagent predix` - Show beautiful welcome screen (perfect for screenshots!)
|
||||
- `rdagent nexquant` - Show beautiful welcome screen (perfect for screenshots!)
|
||||
- `rdagent start_llama` - Start llama.cpp server
|
||||
- `rdagent start_loop` - Start strategy generator loop with auto-restart
|
||||
- `rdagent generate_strategies` - Generate strategies from factors
|
||||
@@ -65,8 +65,8 @@ Major update adding AI-powered strategy generation, realistic backtesting, and c
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/TPTBusiness/Predix
|
||||
cd Predix
|
||||
git clone https://github.com/TPTBusiness/NexQuant
|
||||
cd NexQuant
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
@@ -74,7 +74,7 @@ pip install -e .
|
||||
|
||||
```bash
|
||||
# Show welcome screen
|
||||
rdagent predix
|
||||
rdagent nexquant
|
||||
|
||||
# Start LLM server
|
||||
rdagent start_llama
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Bandit Security Scanner Configuration
|
||||
# Documentation: https://bandit.readthedocs.io/
|
||||
|
||||
title: Bandit Security Scan for Predix
|
||||
title: Bandit Security Scan for NexQuant
|
||||
|
||||
# Tests to skip (known false positives or acceptable risks)
|
||||
skips:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
azure-identity==1.25.3
|
||||
dill==0.4.1
|
||||
pillow==10.4.0
|
||||
pillow==12.2.0
|
||||
psutil==6.1.1
|
||||
scipy==1.15.3
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
azure-identity==1.25.3
|
||||
dill==0.4.1
|
||||
pillow==10.4.0
|
||||
pillow==12.2.0
|
||||
psutil==6.1.1
|
||||
scipy==1.15.3
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ============================================================
|
||||
# Predix Data Configuration
|
||||
# NexQuant Data Configuration
|
||||
# Change instrument, frequency, and time periods here
|
||||
# All other components read from this file
|
||||
# ============================================================
|
||||
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
# Attribution Guidelines
|
||||
|
||||
## Using Predix in Your Project
|
||||
## Using NexQuant in Your Project
|
||||
|
||||
If you use code, concepts, or ideas from this project, you **must**:
|
||||
|
||||
@@ -11,8 +11,8 @@ Include the full MIT License text in your project's LICENSE file or documentatio
|
||||
### 2. Include Copyright Notice
|
||||
|
||||
```
|
||||
Copyright (c) 2025 Predix Team
|
||||
Original Project: https://github.com/TPTBusiness/Predix
|
||||
Copyright (c) 2025 NexQuant Team
|
||||
Original Project: https://github.com/TPTBusiness/NexQuant
|
||||
```
|
||||
|
||||
### 3. Provide Attribution
|
||||
@@ -22,7 +22,7 @@ Add a notice in your documentation or README:
|
||||
```markdown
|
||||
## Acknowledgments
|
||||
|
||||
This project uses code/concepts from [Predix](https://github.com/TPTBusiness/Predix),
|
||||
This project uses code/concepts from [NexQuant](https://github.com/TPTBusiness/NexQuant),
|
||||
licensed under the [MIT License](https://opensource.org/licenses/MIT).
|
||||
```
|
||||
|
||||
@@ -33,7 +33,7 @@ If you modified the code:
|
||||
```markdown
|
||||
## Modifications
|
||||
|
||||
Based on Predix (original by Predix Team).
|
||||
Based on NexQuant (original by NexQuant Team).
|
||||
Modified by [Your Name/Organization] on [Date].
|
||||
Changes: [Brief description of changes]
|
||||
```
|
||||
@@ -63,13 +63,13 @@ Changes: [Brief description of changes]
|
||||
```markdown
|
||||
# My Trading Project
|
||||
|
||||
This project uses factor generation concepts from [Predix](https://github.com/TPTBusiness/Predix).
|
||||
This project uses factor generation concepts from [NexQuant](https://github.com/TPTBusiness/NexQuant).
|
||||
|
||||
## License
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
## Credits
|
||||
- Original Predix code by Predix Team (MIT License)
|
||||
- Original NexQuant code by NexQuant Team (MIT License)
|
||||
- Modified by John Doe, 2025
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Predix will be documented in this file.
|
||||
All notable changes to NexQuant will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<svg width="100%" viewBox="0 0 680 920" xmlns="http://www.w3.org/2000/svg" role="img">
|
||||
<title>NexQuant data flow architecture</title>
|
||||
<desc>Full pipeline from Qlib data source through R&D loop, factor and model tracks, strategy generation, portfolio optimization, to live trading.</desc>
|
||||
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<style>
|
||||
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
||||
.th { font-size: 14px; font-weight: 600; fill: #1a1a1a; }
|
||||
.ts { font-size: 12px; font-weight: 400; fill: #555; }
|
||||
.arr { stroke: #888; stroke-width: 1.2; fill: none; }
|
||||
.box-blue { fill: #E6F1FB; stroke: #185FA5; }
|
||||
.box-purple { fill: #EEEDFE; stroke: #534AB7; }
|
||||
.th-purple { fill: #3C3489; }
|
||||
.ts-purple { fill: #534AB7; }
|
||||
.box-teal { fill: #E1F5EE; stroke: #0F6E56; }
|
||||
.th-teal { fill: #085041; }
|
||||
.ts-teal { fill: #0F6E56; }
|
||||
.box-coral { fill: #FAECE7; stroke: #993C1D; }
|
||||
.th-coral { fill: #712B13; }
|
||||
.ts-coral { fill: #993C1D; }
|
||||
.box-amber { fill: #FAEEDA; stroke: #854F0B; }
|
||||
.th-amber { fill: #633806; }
|
||||
.ts-amber { fill: #854F0B; }
|
||||
.box-green { fill: #EAF3DE; stroke: #3B6D11; }
|
||||
.th-green { fill: #27500A; }
|
||||
.ts-green { fill: #3B6D11; }
|
||||
.box-gray { fill: #F1EFE8; stroke: #5F5E5A; }
|
||||
.th-gray { fill: #2C2C2A; }
|
||||
.ts-gray { fill: #5F5E5A; }
|
||||
.th-blue { fill: #0C447C; }
|
||||
.ts-blue { fill: #185FA5; }
|
||||
.container { fill: none; stroke: #B4B2A9; stroke-width: 0.5; }
|
||||
.label-muted { font-size: 12px; fill: #888780; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- DATA SOURCE -->
|
||||
<rect x="200" y="20" width="280" height="56" rx="8" stroke-width="0.5" class="box-blue"/>
|
||||
<text class="th th-blue" x="340" y="43" text-anchor="middle" dominant-baseline="central">Qlib data (1-min EUR/USD)</text>
|
||||
<text class="ts ts-blue" x="340" y="63" text-anchor="middle" dominant-baseline="central">2020–2026 · 96 bars/day</text>
|
||||
|
||||
<line x1="340" y1="76" x2="340" y2="104" class="arr" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- R&D LOOP container -->
|
||||
<rect x="40" y="104" width="600" height="190" rx="10" class="container"/>
|
||||
<text class="label-muted" x="56" y="121" dominant-baseline="central">R&D loop (rdagent fin_quant)</text>
|
||||
|
||||
<rect x="56" y="132" width="100" height="56" rx="6" stroke-width="0.5" class="box-purple"/>
|
||||
<text class="th th-purple" x="106" y="154" text-anchor="middle" dominant-baseline="central">Propose</text>
|
||||
<text class="ts ts-purple" x="106" y="172" text-anchor="middle" dominant-baseline="central">LLM</text>
|
||||
<line x1="156" y1="160" x2="170" y2="160" class="arr" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="170" y="132" width="100" height="56" rx="6" stroke-width="0.5" class="box-purple"/>
|
||||
<text class="th th-purple" x="220" y="154" text-anchor="middle" dominant-baseline="central">Coding</text>
|
||||
<text class="ts ts-purple" x="220" y="172" text-anchor="middle" dominant-baseline="central">CoSTEER</text>
|
||||
<line x1="270" y1="160" x2="284" y2="160" class="arr" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="284" y="132" width="100" height="56" rx="6" stroke-width="0.5" class="box-purple"/>
|
||||
<text class="th th-purple" x="334" y="154" text-anchor="middle" dominant-baseline="central">Running</text>
|
||||
<text class="ts ts-purple" x="334" y="172" text-anchor="middle" dominant-baseline="central">Docker</text>
|
||||
<line x1="384" y1="160" x2="398" y2="160" class="arr" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="398" y="132" width="100" height="56" rx="6" stroke-width="0.5" class="box-purple"/>
|
||||
<text class="th th-purple" x="448" y="154" text-anchor="middle" dominant-baseline="central">Feedback</text>
|
||||
<text class="ts ts-purple" x="448" y="172" text-anchor="middle" dominant-baseline="central">LLM</text>
|
||||
<line x1="498" y1="160" x2="512" y2="160" class="arr" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="512" y="132" width="100" height="56" rx="6" stroke-width="0.5" class="box-purple"/>
|
||||
<text class="th th-purple" x="562" y="154" text-anchor="middle" dominant-baseline="central">Record</text>
|
||||
<text class="ts ts-purple" x="562" y="172" text-anchor="middle" dominant-baseline="central">Pickle</text>
|
||||
|
||||
<text class="label-muted" x="340" y="216" text-anchor="middle" dominant-baseline="central">Bandit selection → factor track or model track</text>
|
||||
|
||||
<!-- Split to two tracks -->
|
||||
<path d="M210 294 L210 308 L470 308 L470 294" fill="none" stroke="#B4B2A9" stroke-width="0.5"/>
|
||||
<line x1="210" y1="308" x2="210" y2="322" class="arr" marker-end="url(#arrow)"/>
|
||||
<line x1="470" y1="308" x2="470" y2="322" class="arr" marker-end="url(#arrow)"/>
|
||||
<text class="label-muted" x="340" y="478" text-anchor="middle">every N factors · auto or CLI</text>
|
||||
|
||||
<!-- FACTOR TRACK -->
|
||||
<rect x="40" y="322" width="260" height="130" rx="8" stroke-width="0.5" class="box-teal"/>
|
||||
<text class="th th-teal" x="170" y="344" text-anchor="middle" dominant-baseline="central">Factor track</text>
|
||||
<text class="ts ts-teal" x="170" y="364" text-anchor="middle" dominant-baseline="central">Hypothesis → FactorCoSTEER</text>
|
||||
<text class="ts ts-teal" x="170" y="382" text-anchor="middle" dominant-baseline="central">FactorRunner → FactorFeedback</text>
|
||||
<text class="ts ts-teal" x="170" y="402" text-anchor="middle" dominant-baseline="central">Output: result.h5</text>
|
||||
<text class="ts ts-teal" x="170" y="420" text-anchor="middle" dominant-baseline="central">MultiIndex DataFrame</text>
|
||||
<text class="ts ts-teal" x="170" y="438" text-anchor="middle" dominant-baseline="central">IC / Sharpe metrics</text>
|
||||
|
||||
<!-- MODEL TRACK -->
|
||||
<rect x="380" y="322" width="260" height="130" rx="8" stroke-width="0.5" class="box-coral"/>
|
||||
<text class="th th-coral" x="510" y="344" text-anchor="middle" dominant-baseline="central">Model track</text>
|
||||
<text class="ts ts-coral" x="510" y="364" text-anchor="middle" dominant-baseline="central">Hypothesis → ModelCoSTEER</text>
|
||||
<text class="ts ts-coral" x="510" y="382" text-anchor="middle" dominant-baseline="central">ModelRunner → ModelFeedback</text>
|
||||
<text class="ts ts-coral" x="510" y="402" text-anchor="middle" dominant-baseline="central">Output: PyTorch preds</text>
|
||||
<text class="ts ts-coral" x="510" y="420" text-anchor="middle" dominant-baseline="central">+ mlflow logs</text>
|
||||
<text class="ts ts-coral" x="510" y="438" text-anchor="middle" dominant-baseline="central">LSTM / Transformer / CNN</text>
|
||||
|
||||
<!-- Merge to strategy -->
|
||||
<path d="M170 452 L170 486 L340 486 L340 502" fill="none" stroke="#B4B2A9" stroke-width="0.5" marker-end="url(#arrow)"/>
|
||||
<path d="M510 452 L510 486 L340 486" fill="none" stroke="#B4B2A9" stroke-width="0.5"/>
|
||||
|
||||
<!-- STRATEGY GENERATION -->
|
||||
<rect x="100" y="502" width="480" height="120" rx="8" stroke-width="0.5" class="box-amber"/>
|
||||
<text class="th th-amber" x="340" y="524" text-anchor="middle" dominant-baseline="central">Strategy generation pipeline</text>
|
||||
|
||||
<rect x="116" y="536" width="120" height="44" rx="6" stroke-width="0.5" class="box-gray"/>
|
||||
<text class="ts th-gray" x="176" y="554" text-anchor="middle" dominant-baseline="central">Load top factors</text>
|
||||
<text class="ts ts-gray" x="176" y="570" text-anchor="middle" dominant-baseline="central">by |IC|</text>
|
||||
<line x1="236" y1="558" x2="252" y2="558" class="arr" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="252" y="536" width="120" height="44" rx="6" stroke-width="0.5" class="box-gray"/>
|
||||
<text class="ts th-gray" x="312" y="554" text-anchor="middle" dominant-baseline="central">LLM strategy</text>
|
||||
<text class="ts ts-gray" x="312" y="570" text-anchor="middle" dominant-baseline="central">code gen</text>
|
||||
<line x1="372" y1="558" x2="388" y2="558" class="arr" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="388" y="536" width="120" height="44" rx="6" stroke-width="0.5" class="box-gray"/>
|
||||
<text class="ts th-gray" x="448" y="554" text-anchor="middle" dominant-baseline="central">OHLCV backtest</text>
|
||||
<text class="ts ts-gray" x="448" y="570" text-anchor="middle" dominant-baseline="central">signals eval</text>
|
||||
|
||||
<text class="label-muted" x="340" y="600" text-anchor="middle" dominant-baseline="central">Optuna: 10 → 15 → 5 trials · Sharpe ≥ 1.5 · DD ≥ −0.30 · WR ≥ 0.40</text>
|
||||
|
||||
<line x1="340" y1="622" x2="340" y2="648" class="arr" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- PORTFOLIO -->
|
||||
<rect x="160" y="648" width="360" height="56" rx="8" stroke-width="0.5" class="box-green"/>
|
||||
<text class="th th-green" x="340" y="670" text-anchor="middle" dominant-baseline="central">Portfolio optimization</text>
|
||||
<text class="ts ts-green" x="340" y="688" text-anchor="middle" dominant-baseline="central">Mean-variance · Risk parity · Black-Litterman</text>
|
||||
|
||||
<line x1="340" y1="704" x2="340" y2="730" class="arr" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- LIVE TRADING -->
|
||||
<rect x="160" y="730" width="360" height="56" rx="8" stroke-width="0.5" class="box-gray"/>
|
||||
<text class="th th-gray" x="340" y="752" text-anchor="middle" dominant-baseline="central">Live trading (closed-source)</text>
|
||||
<text class="ts ts-gray" x="340" y="770" text-anchor="middle" dominant-baseline="central">ftmo_live_trader.py · FTMO signals</text>
|
||||
|
||||
<!-- EXTERNAL SERVICES -->
|
||||
<text class="label-muted" x="340" y="812" text-anchor="middle">External services</text>
|
||||
|
||||
<rect x="40" y="824" width="130" height="44" rx="6" stroke-width="0.5" class="box-gray"/>
|
||||
<text class="ts th-gray" x="105" y="842" text-anchor="middle" dominant-baseline="central">llama.cpp</text>
|
||||
<text class="ts ts-gray" x="105" y="858" text-anchor="middle" dominant-baseline="central">LLM inference</text>
|
||||
|
||||
<rect x="185" y="824" width="130" height="44" rx="6" stroke-width="0.5" class="box-gray"/>
|
||||
<text class="ts th-gray" x="250" y="842" text-anchor="middle" dominant-baseline="central">Docker</text>
|
||||
<text class="ts ts-gray" x="250" y="858" text-anchor="middle" dominant-baseline="central">sandbox</text>
|
||||
|
||||
<rect x="330" y="824" width="130" height="44" rx="6" stroke-width="0.5" class="box-gray"/>
|
||||
<text class="ts th-gray" x="395" y="842" text-anchor="middle" dominant-baseline="central">Optuna</text>
|
||||
<text class="ts ts-gray" x="395" y="858" text-anchor="middle" dominant-baseline="central">Bayesian opt</text>
|
||||
|
||||
<rect x="475" y="824" width="130" height="44" rx="6" stroke-width="0.5" class="box-gray"/>
|
||||
<text class="ts th-gray" x="540" y="842" text-anchor="middle" dominant-baseline="central">Qlib</text>
|
||||
<text class="ts ts-gray" x="540" y="858" text-anchor="middle" dominant-baseline="central">backtest engine</text>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
+4
-4
@@ -10,9 +10,9 @@ import subprocess
|
||||
|
||||
latest_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"], text=True).strip()
|
||||
|
||||
project = "Predix"
|
||||
copyright = "2025, Predix Team"
|
||||
author = "Predix Team"
|
||||
project = "NexQuant"
|
||||
copyright = "2025, NexQuant Team"
|
||||
author = "NexQuant Team"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
@@ -66,7 +66,7 @@ html_static_path = ["_static"]
|
||||
html_favicon = "_static/favicon.ico"
|
||||
|
||||
html_theme_options = {
|
||||
"source_repository": "https://github.com/PredixAI/predix",
|
||||
"source_repository": "https://github.com/NexQuantAI/nexquant",
|
||||
"source_branch": "main",
|
||||
"source_directory": "docs/",
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,13 +1,13 @@
|
||||
.. Predix documentation master file, created by
|
||||
.. NexQuant documentation master file, created by
|
||||
sphinx-quickstart on Mon Jul 15 04:27:50 2024.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to Predix's documentation!
|
||||
Welcome to NexQuant's documentation!
|
||||
===================================
|
||||
|
||||
.. image:: _static/logo.png
|
||||
:alt: Predix Logo
|
||||
:alt: NexQuant Logo
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
@@ -23,7 +23,7 @@ Welcome to Predix's documentation!
|
||||
api_reference
|
||||
policy
|
||||
|
||||
GitHub <https://github.com/PredixAI/predix>
|
||||
GitHub <https://github.com/NexQuantAI/nexquant>
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
||||
+11
-11
@@ -1,4 +1,4 @@
|
||||
# Predix Parallel Run System
|
||||
# NexQuant Parallel Run System
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -10,8 +10,8 @@ The Parallel Run System enables concurrent execution of 5+ factor generation exp
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `predix.py` | Extended with `--run-id` parameter for isolated single runs |
|
||||
| `predix_parallel.py` | Parallel runner manager with Rich live dashboard |
|
||||
| `nexquant.py` | Extended with `--run-id` parameter for isolated single runs |
|
||||
| `nexquant_parallel.py` | Parallel runner manager with Rich live dashboard |
|
||||
| `factor_runner.py` | Modified to use `PARALLEL_RUN_ID` for path isolation |
|
||||
| `CoSTEER/__init__.py` | Modified to use `PARALLEL_RUN_ID` for intermediate results |
|
||||
|
||||
@@ -57,26 +57,26 @@ RD-Agent_workspace_run2/ # Parallel run #2
|
||||
|
||||
```bash
|
||||
# Run with isolated results
|
||||
predix quant --run-id 1 -m openrouter
|
||||
nexquant quant --run-id 1 -m openrouter
|
||||
```
|
||||
|
||||
### CLI - Parallel Runner (Direct)
|
||||
|
||||
```bash
|
||||
# Run 5 experiments with 2 API keys
|
||||
python predix_parallel.py --runs 5 --api-keys 2
|
||||
python nexquant_parallel.py --runs 5 --api-keys 2
|
||||
|
||||
# Run 3 experiments with local model
|
||||
python predix_parallel.py --runs 3 --model local
|
||||
python nexquant_parallel.py --runs 3 --model local
|
||||
|
||||
# Custom configuration
|
||||
python predix_parallel.py -n 10 -k 2 -m openrouter
|
||||
python nexquant_parallel.py -n 10 -k 2 -m openrouter
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
```python
|
||||
from predix_parallel import main
|
||||
from nexquant_parallel import main
|
||||
|
||||
result = main(runs=5, api_keys=2, model="openrouter")
|
||||
print(f"Success: {result['success']}/{result['total']}")
|
||||
@@ -132,7 +132,7 @@ The parallel runner shows a Rich-based live dashboard:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔀 Predix Parallel Run Dashboard │
|
||||
│ 🔀 NexQuant Parallel Run Dashboard │
|
||||
├──────┬──────────┬──────────┬─────────┬──────────┬───────┤
|
||||
│ Run │ Status │ Elapsed │ API Key │ Model │ Exit │
|
||||
├──────┼──────────┼──────────┼─────────┼──────────┼───────┤
|
||||
@@ -222,10 +222,10 @@ if parallel_run_id != "0":
|
||||
pytest test/integration/test_all_features.py -v
|
||||
|
||||
# Test parallel runner imports
|
||||
python -c "from predix_parallel import ParallelRunner, main; print('✅ OK')"
|
||||
python -c "from nexquant_parallel import ParallelRunner, main; print('✅ OK')"
|
||||
|
||||
# Test CLI options
|
||||
predix quant --help # Should show --run-id option
|
||||
nexquant quant --help # Should show --run-id option
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Security Runbook für Predix
|
||||
# Security Runbook für NexQuant
|
||||
|
||||
## Bandit Security Scanner
|
||||
|
||||
|
||||
+2
-2
@@ -127,8 +127,8 @@ jupyter notebook examples/notebooks/quickstart.ipynb
|
||||
|
||||
- **Dokumentation:** `docs/` oder [README.md](../README.md)
|
||||
- **CLI Hilfe:** `rdagent COMMAND --help`
|
||||
- **Issues:** [GitHub Issues](https://github.com/nico/Predix/issues)
|
||||
- **Community:** [Discussions](https://github.com/nico/Predix/discussions)
|
||||
- **Issues:** [GitHub Issues](https://github.com/nico/NexQuant/issues)
|
||||
- **Community:** [Discussions](https://github.com/nico/NexQuant/discussions)
|
||||
|
||||
## ⚠️ Wichtige Hinweise
|
||||
|
||||
|
||||
@@ -382,8 +382,8 @@
|
||||
"### Ressourcen:\n",
|
||||
"\n",
|
||||
"- 📚 [Dokumentation](../docs/)\n",
|
||||
"- 💬 [GitHub Discussions](https://github.com/nico/Predix/discussions)\n",
|
||||
"- 🐛 [Issues melden](https://github.com/nico/Predix/issues)"
|
||||
"- 💬 [GitHub Discussions](https://github.com/nico/NexQuant/discussions)\n",
|
||||
"- 🐛 [Issues melden](https://github.com/nico/NexQuant/issues)"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Predix Models
|
||||
# NexQuant Models
|
||||
|
||||
This directory contains all ML model definitions for Predix trading factors.
|
||||
This directory contains all ML model definitions for NexQuant trading factors.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+107
-109
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Predix CLI - Wrapper for rdagent with LLM model selection.
|
||||
NexQuant CLI - Wrapper for rdagent with LLM model selection.
|
||||
|
||||
Usage:
|
||||
predix quant # Local llama.cpp (default)
|
||||
predix quant --model local # Explicit local
|
||||
predix quant --model openrouter # OpenRouter cloud model
|
||||
predix quant -d # With web dashboard
|
||||
nexquant quant # Local llama.cpp (default)
|
||||
nexquant quant --model local # Explicit local
|
||||
nexquant quant --model openrouter # OpenRouter cloud model
|
||||
nexquant quant -d # With web dashboard
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
@@ -26,7 +26,7 @@ except ImportError:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = typer.Typer(help="Predix - AI Quantitative Trading Agent")
|
||||
app = typer.Typer(help="NexQuant - AI Quantitative Trading Agent")
|
||||
console = Console()
|
||||
|
||||
|
||||
@@ -178,13 +178,13 @@ def quant(
|
||||
0 = single run mode (default: 0)
|
||||
|
||||
Examples:
|
||||
$ predix quant # Local llama.cpp, single run
|
||||
$ predix quant -m openrouter # OpenRouter cloud model
|
||||
$ predix quant -d # With web dashboard on :5000
|
||||
$ predix quant -m openrouter -d # Cloud model + web dashboard
|
||||
$ predix quant --run-id 1 # Parallel run #1 (isolated)
|
||||
$ predix quant --run-id 2 --loop-n 50 # Parallel run #2, 50 loops
|
||||
$ predix quant --log-file custom.log # Custom log file path
|
||||
$ nexquant quant # Local llama.cpp, single run
|
||||
$ nexquant quant -m openrouter # OpenRouter cloud model
|
||||
$ nexquant quant -d # With web dashboard on :5000
|
||||
$ nexquant quant -m openrouter -d # Cloud model + web dashboard
|
||||
$ nexquant quant --run-id 1 # Parallel run #1 (isolated)
|
||||
$ nexquant quant --run-id 2 --loop-n 50 # Parallel run #2, 50 loops
|
||||
$ nexquant quant --log-file custom.log # Custom log file path
|
||||
|
||||
Expected Output:
|
||||
- Generated alpha factors saved to results/factors/ as JSON files
|
||||
@@ -197,9 +197,9 @@ def quant(
|
||||
Local models are faster but may have lower quality than cloud models.
|
||||
|
||||
See Also:
|
||||
predix evaluate - Evaluate existing factors with full 1min data
|
||||
predix top - Show top-performing factors by IC or Sharpe
|
||||
predix health - Check system health and configuration
|
||||
nexquant evaluate - Evaluate existing factors with full 1min data
|
||||
nexquant top - Show top-performing factors by IC or Sharpe
|
||||
nexquant health - Check system health and configuration
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -323,19 +323,15 @@ def quant(
|
||||
|
||||
if cli_dashboard:
|
||||
def start_cli_dash():
|
||||
from rdagent.log.ui.predix_dashboard import run_dashboard
|
||||
from rdagent.log.ui.nexquant_dashboard import run_dashboard
|
||||
run_dashboard(log_path="fin_quant.log", refresh_interval=3)
|
||||
|
||||
threading.Thread(target=start_cli_dash, daemon=True).start()
|
||||
time.sleep(1)
|
||||
|
||||
# ---- Kronos Factor: skip if GPU unavailable (CUDA OOM with llama-server) ----
|
||||
# ---- Kronos Factor: CPU inference to avoid GPU conflict with llama-server ----
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available() and torch.cuda.get_device_properties(0).total_memory > 20 * 1024**3:
|
||||
_ensure_kronos_factor_in_pool(console)
|
||||
else:
|
||||
console.print("[dim]Kronos Factor skipped — GPU < 20GB or CUDA unavailable[/dim]")
|
||||
_ensure_kronos_factor_in_pool(console)
|
||||
except Exception:
|
||||
console.print("[dim]Kronos Factor skipped — torch not available[/dim]")
|
||||
|
||||
@@ -412,11 +408,11 @@ def evaluate(
|
||||
when recalculating with updated methodology. (default: False)
|
||||
|
||||
Examples:
|
||||
$ predix evaluate # Evaluate 100 NEW factors
|
||||
$ predix evaluate --top 500 # Evaluate 500 NEW factors
|
||||
$ predix evaluate --all # Evaluate all remaining factors
|
||||
$ predix evaluate --force --top 50 # Re-evaluate 50 factors
|
||||
$ predix evaluate -p 8 # Use 8 parallel workers
|
||||
$ nexquant evaluate # Evaluate 100 NEW factors
|
||||
$ nexquant evaluate --top 500 # Evaluate 500 NEW factors
|
||||
$ nexquant evaluate --all # Evaluate all remaining factors
|
||||
$ nexquant evaluate --force --top 50 # Re-evaluate 50 factors
|
||||
$ nexquant evaluate -p 8 # Use 8 parallel workers
|
||||
|
||||
Expected Output:
|
||||
- Updated JSON files in results/factors/ with IC, Sharpe, Max DD, Win Rate
|
||||
@@ -428,22 +424,22 @@ def evaluate(
|
||||
With --parallel 4, expect ~30-60 seconds per factor wall-clock time.
|
||||
|
||||
See Also:
|
||||
predix top - Show top-performing factors by IC or Sharpe
|
||||
predix portfolio - Select a diversified portfolio of uncorrelated factors
|
||||
predix quant - Generate new factors via LLM trading loop
|
||||
nexquant top - Show top-performing factors by IC or Sharpe
|
||||
nexquant portfolio - Select a diversified portfolio of uncorrelated factors
|
||||
nexquant quant - Generate new factors via LLM trading loop
|
||||
"""
|
||||
from rdagent.log.daily_log import session as _daily_session
|
||||
from rich.panel import Panel
|
||||
|
||||
console.print(Panel(
|
||||
"[bold cyan]📊 Predix Factor Evaluator[/bold cyan]\n"
|
||||
"[bold cyan]📊 NexQuant Factor Evaluator[/bold cyan]\n"
|
||||
"Evaluating factors with FULL 1min data (2020-2026)\n"
|
||||
"Skips already evaluated factors automatically",
|
||||
border_style="cyan",
|
||||
))
|
||||
|
||||
# Import and run the evaluator
|
||||
from predix_full_eval import main as eval_main
|
||||
from nexquant_full_eval import main as eval_main
|
||||
|
||||
_eval_ctx = {"top": "all" if all_factors else top, "workers": parallel}
|
||||
if force:
|
||||
@@ -494,10 +490,10 @@ def top(
|
||||
(default: "ic")
|
||||
|
||||
Examples:
|
||||
$ predix top # Top 20 factors by absolute IC
|
||||
$ predix top -n 50 # Top 50 factors by absolute IC
|
||||
$ predix top -m sharpe # Top 20 factors by absolute Sharpe
|
||||
$ predix top -n 100 -m sharpe # Top 100 factors by Sharpe
|
||||
$ nexquant top # Top 20 factors by absolute IC
|
||||
$ nexquant top -n 50 # Top 50 factors by absolute IC
|
||||
$ nexquant top -m sharpe # Top 20 factors by absolute Sharpe
|
||||
$ nexquant top -n 100 -m sharpe # Top 100 factors by Sharpe
|
||||
|
||||
Expected Output:
|
||||
- Formatted table showing Factor name, IC, Sharpe, Annualized Return,
|
||||
@@ -509,9 +505,9 @@ def top(
|
||||
May take a few seconds with thousands of factor files.
|
||||
|
||||
See Also:
|
||||
predix evaluate - Evaluate factors to generate performance metrics
|
||||
predix portfolio - Select diversified portfolio from top factors
|
||||
predix build-strategies - Combine factors into trading strategies
|
||||
nexquant evaluate - Evaluate factors to generate performance metrics
|
||||
nexquant portfolio - Select diversified portfolio from top factors
|
||||
nexquant build-strategies - Combine factors into trading strategies
|
||||
"""
|
||||
import glob as glob_module
|
||||
import json
|
||||
@@ -643,10 +639,10 @@ def portfolio(
|
||||
high-IC factors. Typical range: 0.2-0.5. (default: 0.3)
|
||||
|
||||
Examples:
|
||||
$ predix portfolio # Select top 10 from top 50 candidates
|
||||
$ predix portfolio -n 100 -t 20 # Select top 20 from top 100
|
||||
$ predix portfolio -c 0.5 # Allow higher correlation (0.5)
|
||||
$ predix portfolio -n 200 -t 15 -c 0.2 # Strict diversification
|
||||
$ nexquant portfolio # Select top 10 from top 50 candidates
|
||||
$ nexquant portfolio -n 100 -t 20 # Select top 20 from top 100
|
||||
$ nexquant portfolio -c 0.5 # Allow higher correlation (0.5)
|
||||
$ nexquant portfolio -n 200 -t 15 -c 0.2 # Strict diversification
|
||||
|
||||
Expected Output:
|
||||
- Formatted table showing selected factors with IC, Sharpe, and max correlation
|
||||
@@ -658,9 +654,9 @@ def portfolio(
|
||||
Each factor must be re-evaluated to compute time-series values for correlation.
|
||||
|
||||
See Also:
|
||||
predix portfolio-simple - Faster category-based diversification
|
||||
predix top - View top factors before portfolio selection
|
||||
predix build-strategies - Build strategies from selected factors
|
||||
nexquant portfolio-simple - Faster category-based diversification
|
||||
nexquant top - View top factors before portfolio selection
|
||||
nexquant build-strategies - Build strategies from selected factors
|
||||
"""
|
||||
import glob as glob_module
|
||||
import json
|
||||
@@ -943,9 +939,9 @@ def portfolio_simple(
|
||||
the chance of finding factors in all categories. (default: 100)
|
||||
|
||||
Examples:
|
||||
$ predix portfolio-simple # Top factors from different categories
|
||||
$ predix portfolio-simple -n 200 # Consider top 200 factors
|
||||
$ predix portfolio-simple -n 50 # Quick selection from top 50
|
||||
$ nexquant portfolio-simple # Top factors from different categories
|
||||
$ nexquant portfolio-simple -n 200 # Consider top 200 factors
|
||||
$ nexquant portfolio-simple -n 50 # Quick selection from top 50
|
||||
|
||||
Expected Output:
|
||||
- Formatted table showing selected factors with their category, IC, and Sharpe
|
||||
@@ -958,9 +954,9 @@ def portfolio_simple(
|
||||
Only loads existing JSON results and performs keyword matching.
|
||||
|
||||
See Also:
|
||||
predix portfolio - Correlation-based diversification (more accurate but slower)
|
||||
predix top - View top factors before portfolio selection
|
||||
predix build-strategies - Build strategies from selected factors
|
||||
nexquant portfolio - Correlation-based diversification (more accurate but slower)
|
||||
nexquant top - View top factors before portfolio selection
|
||||
nexquant build-strategies - Build strategies from selected factors
|
||||
"""
|
||||
import glob as glob_module
|
||||
import json
|
||||
@@ -1119,10 +1115,10 @@ def build_strategies(
|
||||
strategies. (default: False)
|
||||
|
||||
Examples:
|
||||
$ predix build-strategies # Build from top 50, pairs only
|
||||
$ predix build-strategies -n 100 -c 3 # Top 100, up to triplets
|
||||
$ predix build-strategies -d # Diversified (cross-category) only
|
||||
$ predix build-strategies -n 30 -c 2 -d # Top 30, diversified pairs
|
||||
$ nexquant build-strategies # Build from top 50, pairs only
|
||||
$ nexquant build-strategies -n 100 -c 3 # Top 100, up to triplets
|
||||
$ nexquant build-strategies -d # Diversified (cross-category) only
|
||||
$ nexquant build-strategies -n 30 -c 2 -d # Top 30, diversified pairs
|
||||
|
||||
Expected Output:
|
||||
- Formatted table of top strategies ranked by Sharpe ratio
|
||||
@@ -1134,9 +1130,9 @@ def build_strategies(
|
||||
Scales with O(n^k) where n=factors, k=max_combo_size.
|
||||
|
||||
See Also:
|
||||
predix build-strategies-ai - AI-powered strategy generation via LLM
|
||||
predix portfolio - Select diversified factors before combining
|
||||
predix top - View top factors before building strategies
|
||||
nexquant build-strategies-ai - AI-powered strategy generation via LLM
|
||||
nexquant portfolio - Select diversified factors before combining
|
||||
nexquant top - View top factors before building strategies
|
||||
"""
|
||||
import numpy as np
|
||||
from rdagent.scenarios.qlib.developer.strategy_builder import StrategyBuilder
|
||||
@@ -1144,7 +1140,7 @@ def build_strategies(
|
||||
from rich.table import Table
|
||||
|
||||
console.print(Panel(
|
||||
"[bold cyan]🏗️ Predix Strategy Builder[/bold cyan]\n"
|
||||
"[bold cyan]🏗️ NexQuant Strategy Builder[/bold cyan]\n"
|
||||
"Systematically combining factors into trading strategies",
|
||||
border_style="cyan",
|
||||
))
|
||||
@@ -1275,12 +1271,12 @@ def build_strategies_ai(
|
||||
may require multiple improvement loops. (default: 1)
|
||||
|
||||
Examples:
|
||||
$ predix build-strategies-ai # Generate 1 strategy, 5 loops max
|
||||
$ predix build-strategies-ai -t 100 # Use top 100 factors as pool
|
||||
$ predix build-strategies-ai -l 10 # Allow 10 improvement loops
|
||||
$ predix build-strategies-ai --min-sharpe 2.0 # Stricter Sharpe requirement
|
||||
$ predix build-strategies-ai --max-dd -0.15 # Tighter drawdown limit
|
||||
$ predix build-strategies-ai -c 5 # Generate 5 accepted strategies
|
||||
$ nexquant build-strategies-ai # Generate 1 strategy, 5 loops max
|
||||
$ nexquant build-strategies-ai -t 100 # Use top 100 factors as pool
|
||||
$ nexquant build-strategies-ai -l 10 # Allow 10 improvement loops
|
||||
$ nexquant build-strategies-ai --min-sharpe 2.0 # Stricter Sharpe requirement
|
||||
$ nexquant build-strategies-ai --max-dd -0.15 # Tighter drawdown limit
|
||||
$ nexquant build-strategies-ai -c 5 # Generate 5 accepted strategies
|
||||
|
||||
Expected Output:
|
||||
- Formatted table of accepted strategies with Sharpe, return, drawdown,
|
||||
@@ -1293,9 +1289,9 @@ def build_strategies_ai(
|
||||
Each loop requires a full backtest execution plus LLM API calls.
|
||||
|
||||
See Also:
|
||||
predix build-strategies - Systematic (non-AI) strategy combination
|
||||
predix quant - Generate new alpha factors via LLM trading loop
|
||||
predix evaluate - Evaluate factors before strategy building
|
||||
nexquant build-strategies - Systematic (non-AI) strategy combination
|
||||
nexquant quant - Generate new alpha factors via LLM trading loop
|
||||
nexquant evaluate - Evaluate factors before strategy building
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
@@ -1349,7 +1345,7 @@ def build_strategies_ai(
|
||||
|
||||
if not factors_dir.exists():
|
||||
console.print("[bold red]❌ No factors directory found at results/factors/[/bold red]")
|
||||
console.print("[yellow]Run 'predix quant' to generate factors first.[/yellow]")
|
||||
console.print("[yellow]Run 'nexquant quant' to generate factors first.[/yellow]")
|
||||
return
|
||||
|
||||
# Load evaluated factors
|
||||
@@ -1369,7 +1365,7 @@ def build_strategies_ai(
|
||||
|
||||
if len(factors) < 10:
|
||||
console.print(f"[bold red]❌ Only {len(factors)} evaluated factors found. Need at least 10.[/bold red]")
|
||||
console.print("[yellow]Run 'predix evaluate' or 'predix quant' to generate more factors.[/yellow]")
|
||||
console.print("[yellow]Run 'nexquant evaluate' or 'nexquant quant' to generate more factors.[/yellow]")
|
||||
return
|
||||
|
||||
# Sort by IC and take top factors
|
||||
@@ -1485,6 +1481,7 @@ def generate_strategies(
|
||||
min_sharpe: float = typer.Option(1.5, "--min-sharpe", help="Minimum Sharpe for acceptance"),
|
||||
max_drawdown: float = typer.Option(-0.30, "--max-dd", help="Maximum drawdown allowed"),
|
||||
min_win_rate: float = typer.Option(0.40, "--min-winrate", help="Minimum win rate for acceptance"),
|
||||
min_monthly_return: float = typer.Option(15.0, "--min-monthly-return", help="Minimum OOS monthly return %% for acceptance"),
|
||||
):
|
||||
"""
|
||||
Generate trading strategies from top factors using LLM + Optuna optimization.
|
||||
@@ -1497,21 +1494,21 @@ def generate_strategies(
|
||||
MaxDD on equity curve, WinRate on trade P&L) with runtime verification.
|
||||
|
||||
Examples:
|
||||
$ predix generate-strategies # 10 strategies, Optuna, swing
|
||||
$ predix generate-strategies -n 20 -w 4 # 20 strategies, 4 workers
|
||||
$ predix generate-strategies --min-sharpe 3.0 # Stricter acceptance
|
||||
$ predix generate-strategies -s daytrading # Day trading style
|
||||
$ predix generate-strategies --no-optuna # Skip optimization
|
||||
$ nexquant generate-strategies # 10 strategies, Optuna, swing
|
||||
$ nexquant generate-strategies -n 20 -w 4 # 20 strategies, 4 workers
|
||||
$ nexquant generate-strategies --min-sharpe 3.0 # Stricter acceptance
|
||||
$ nexquant generate-strategies -s daytrading # Day trading style
|
||||
$ nexquant generate-strategies --no-optuna # Skip optimization
|
||||
$ nexquant generate-strategies --min-monthly-return 15 # 15% OOS monthly target
|
||||
"""
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.table import Table as RichTable
|
||||
|
||||
console.print(f"\n[bold cyan]{'='*60}[/bold cyan]")
|
||||
console.print("[bold cyan] Predix Strategy Generator[/bold cyan]")
|
||||
console.print("[bold cyan] NexQuant Strategy Generator[/bold cyan]")
|
||||
console.print(f"[bold cyan]{'='*60}[/bold cyan]")
|
||||
console.print(f" Strategies: [cyan]{count}[/cyan] Workers: [cyan]{workers}[/cyan] Style: [cyan]{style}[/cyan]")
|
||||
console.print(f" Optuna: {'[green]Yes[/green]' if optuna else '[yellow]No[/yellow]'} (trials={optuna_trials}) Factors: [cyan]{top_factors}[/cyan]")
|
||||
console.print(f" Accept: Sharpe≥[green]{min_sharpe}[/green] DD≥[green]{max_drawdown}[/green] WR≥[green]{min_win_rate}[/green]")
|
||||
console.print(f" Accept: Sharpe≥[green]{min_sharpe}[/green] DD≥[green]{max_drawdown}[/green] WR≥[green]{min_win_rate}[/green] Mon≥[green]{min_monthly_return}%[/green]")
|
||||
console.print(f"[bold cyan]{'='*60}[/bold cyan]\n")
|
||||
|
||||
try:
|
||||
@@ -1523,6 +1520,7 @@ def generate_strategies(
|
||||
min_sharpe=min_sharpe,
|
||||
max_drawdown=max_drawdown,
|
||||
min_win_rate=min_win_rate,
|
||||
min_monthly_return_pct=min_monthly_return,
|
||||
use_optuna=optuna,
|
||||
optuna_trials=optuna_trials,
|
||||
continuous_optimization=optuna,
|
||||
@@ -1548,7 +1546,7 @@ def generate_strategies(
|
||||
table.add_row(
|
||||
str(i), r.get("strategy_name", "?")[:28],
|
||||
f"{r.get('sharpe_ratio', 0):.2f}", f"{r.get('max_drawdown', 0):.1%}",
|
||||
f"{r.get('win_rate', 0):.1%}", str(r.get('num_trades', '?')),
|
||||
f"{r.get('win_rate', 0):.1%}", str(r.get("num_trades", "?")),
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
@@ -1568,8 +1566,8 @@ def health():
|
||||
helps identify setup issues before running computationally expensive operations.
|
||||
|
||||
Examples:
|
||||
$ predix health # Run full system health check
|
||||
$ predix health --verbose # Detailed output (if supported)
|
||||
$ nexquant health # Run full system health check
|
||||
$ nexquant health --verbose # Detailed output (if supported)
|
||||
|
||||
Expected Output:
|
||||
- Python version and dependency status
|
||||
@@ -1583,8 +1581,8 @@ def health():
|
||||
~5-15 seconds depending on network and database checks.
|
||||
|
||||
See Also:
|
||||
predix status - Show current trading loop status and statistics
|
||||
predix quant - Main trading loop command
|
||||
nexquant status - Show current trading loop status and statistics
|
||||
nexquant quant - Main trading loop command
|
||||
"""
|
||||
from rdagent.app.utils.health_check import health_check
|
||||
health_check()
|
||||
@@ -1601,8 +1599,8 @@ def status():
|
||||
and verifying data persistence.
|
||||
|
||||
Examples:
|
||||
$ predix status # Show current trading loop status
|
||||
$ predix status --json # JSON output (if supported)
|
||||
$ nexquant status # Show current trading loop status
|
||||
$ nexquant status --json # JSON output (if supported)
|
||||
|
||||
Expected Output:
|
||||
- Trading loop process status: RUNNING or STOPPED
|
||||
@@ -1614,9 +1612,9 @@ def status():
|
||||
Nearly instantaneous (< 1 second).
|
||||
|
||||
See Also:
|
||||
predix health - Check system health and configuration
|
||||
predix quant - Start the quantitative trading loop
|
||||
predix top - View top evaluated factors
|
||||
nexquant health - Check system health and configuration
|
||||
nexquant quant - Start the quantitative trading loop
|
||||
nexquant top - View top evaluated factors
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
@@ -1664,7 +1662,7 @@ def _load_strategies():
|
||||
try:
|
||||
raw = json.loads(p.read_text())
|
||||
except Exception:
|
||||
logger.warning("Failed to load strategy file %s", p, exc_info=True)
|
||||
logger.warning(f"Failed to load strategy file {p}")
|
||||
continue
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
@@ -1711,11 +1709,11 @@ def best(
|
||||
"""Rank backtested strategies by performance — source code is never exposed.
|
||||
|
||||
Examples:
|
||||
$ predix best # Top 10 by composite score
|
||||
$ predix best -n 20 -m sharpe # Top 20 by Sharpe
|
||||
$ predix best --no-realistic # Include numerically suspicious runs
|
||||
$ predix best --show TrendMomentumHybrid
|
||||
$ predix best -n 50 --export /tmp/top.json
|
||||
$ nexquant best # Top 10 by composite score
|
||||
$ nexquant best -n 20 -m sharpe # Top 20 by Sharpe
|
||||
$ nexquant best --no-realistic # Include numerically suspicious runs
|
||||
$ nexquant best --show TrendMomentumHybrid
|
||||
$ nexquant best -n 50 --export /tmp/top.json
|
||||
"""
|
||||
import json
|
||||
|
||||
@@ -1783,7 +1781,7 @@ def best(
|
||||
)
|
||||
console.print(table)
|
||||
console.print(f"\n[dim]{len(pool)} strategies matched filters (of {len(items)} total). "
|
||||
f"Use [bold]predix best --show NAME[/bold] for details.[/dim]")
|
||||
f"Use [bold]nexquant best --show NAME[/bold] for details.[/dim]")
|
||||
|
||||
if export:
|
||||
payload = [{k: v for k, v in s.items() if k != "code"} for s in top]
|
||||
@@ -1804,7 +1802,7 @@ def kronos_factor(
|
||||
"""Generate Kronos-mini predicted-return alpha factor (Option A).
|
||||
|
||||
Runs Kronos-mini (4.1M params OHLCV foundation model, AAAI 2026) on rolling
|
||||
windows of EUR/USD 1-min data and saves a predicted-return factor in Predix's
|
||||
windows of EUR/USD 1-min data and saves a predicted-return factor in NexQuant's
|
||||
standard MultiIndex (datetime, instrument) format.
|
||||
|
||||
Strategy: every STRIDE bars, use the previous CONTEXT bars as input and
|
||||
@@ -1817,13 +1815,13 @@ def kronos_factor(
|
||||
git_ignore_folder/factor_implementation_source_data/intraday_pv.h5
|
||||
|
||||
Examples:
|
||||
$ predix kronos-factor # Default: daily stride, GPU
|
||||
$ predix kronos-factor --pred 30 --device cpu # 30-bar horizon, CPU
|
||||
$ predix kronos-factor --context 256 --pred 48
|
||||
$ nexquant kronos-factor # Default: daily stride, GPU
|
||||
$ nexquant kronos-factor --pred 30 --device cpu # 30-bar horizon, CPU
|
||||
$ nexquant kronos-factor --context 256 --pred 48
|
||||
|
||||
See Also:
|
||||
predix kronos-eval - Evaluate Kronos as model and compute IC vs LightGBM
|
||||
predix top - Show top factors by IC
|
||||
nexquant kronos-eval - Evaluate Kronos as model and compute IC vs LightGBM
|
||||
nexquant top - Show top factors by IC
|
||||
"""
|
||||
from rdagent.components.coder.kronos_adapter import _cuda_available
|
||||
_device = device or ("cuda" if _cuda_available() else "cpu")
|
||||
@@ -1875,7 +1873,7 @@ def kronos_factor(
|
||||
console.print(f"\n[green]Factor saved:[/green] {out_path}")
|
||||
console.print(f" Shape: {factor_df.shape} | Non-NaN: {meta['n_non_nan']}")
|
||||
console.print(f" Metadata: {meta_path}")
|
||||
console.print("\n[dim]Use 'predix top' to compare with other factors.[/dim]")
|
||||
console.print("\n[dim]Use 'nexquant top' to compare with other factors.[/dim]")
|
||||
|
||||
|
||||
@app.command("kronos-eval")
|
||||
@@ -1901,13 +1899,13 @@ def kronos_eval(
|
||||
git_ignore_folder/factor_implementation_source_data/intraday_pv.h5
|
||||
|
||||
Examples:
|
||||
$ predix kronos-eval # Default: 30-bar horizon
|
||||
$ predix kronos-eval --pred 96 --device cuda # Daily horizon, GPU
|
||||
$ predix kronos-eval --context 256 --pred 15 # Shorter horizon
|
||||
$ nexquant kronos-eval # Default: 30-bar horizon
|
||||
$ nexquant kronos-eval --pred 96 --device cuda # Daily horizon, GPU
|
||||
$ nexquant kronos-eval --context 256 --pred 15 # Shorter horizon
|
||||
|
||||
See Also:
|
||||
predix kronos-factor - Generate Kronos factor for the factor pipeline
|
||||
predix best - Show top strategies
|
||||
nexquant kronos-factor - Generate Kronos factor for the factor pipeline
|
||||
nexquant best - Show top strategies
|
||||
"""
|
||||
from rdagent.components.coder.kronos_adapter import _cuda_available
|
||||
_device = device or ("cuda" if _cuda_available() else "cpu")
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Predix Prompts Index
|
||||
# NexQuant Prompts Index
|
||||
|
||||
Centralized location for all LLM prompts used in the Predix trading system.
|
||||
Centralized location for all LLM prompts used in the NexQuant trading system.
|
||||
|
||||
## Structure
|
||||
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
# Predix Prompts
|
||||
# NexQuant Prompts
|
||||
|
||||
This directory contains all LLM prompts for the Predix trading agent.
|
||||
This directory contains all LLM prompts for the NexQuant trading agent.
|
||||
|
||||
---
|
||||
|
||||
@@ -174,13 +174,13 @@ prompt_v2 = load_yaml_file("prompts/local/factor_discovery_v2.yaml")
|
||||
|
||||
```bash
|
||||
# Backup to private repo
|
||||
cd ~/Predix
|
||||
cd ~/NexQuant
|
||||
git archive --format=tar prompts/local/ | gzip > ~/backups/prompts_local_$(date +%Y%m%d).tar.gz
|
||||
|
||||
# Or sync to private GitHub repo
|
||||
git clone git@github.com:TPTBusiness/predix-prompts-private.git
|
||||
cp -r prompts/local/* predix-prompts-private/
|
||||
cd predix-prompts-private && git push
|
||||
git clone git@github.com:TPTBusiness/nexquant-prompts-private.git
|
||||
cp -r prompts/local/* nexquant-prompts-private/
|
||||
cd nexquant-prompts-private && git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -34,9 +34,9 @@ strategy_generation:
|
||||
5. signal.name must be 'signal'
|
||||
|
||||
IC-Guided Factor Selection:
|
||||
- Factors with |IC| > 0.10 are highly predictive - PRIORITIZE these
|
||||
- Factors with |IC| > 0.05 are moderately predictive - USE these
|
||||
- Factors with |IC| < 0.05 are weak - AVOID unless complementary
|
||||
- Factors with |IC| > 0.15 are highly predictive - PRIORITIZE these
|
||||
- Factors with |IC| > 0.08 are moderately predictive - USE these
|
||||
- Factors with |IC| < 0.08 are weak - AVOID unless complementary
|
||||
- Combine factors with different signs of IC for diversification
|
||||
- Weight factors proportionally to their |IC| values
|
||||
|
||||
@@ -62,6 +62,7 @@ strategy_generation:
|
||||
TRADING STYLE: {{ trading_style }}
|
||||
TARGET SHARPE: > {{ min_sharpe }}
|
||||
MAX DRAWDOWN: {{ max_drawdown }}
|
||||
TARGET MONTHLY RETURN: > {{ min_monthly_return }}%
|
||||
|
||||
CRITICAL CODE RULES:
|
||||
1. DO NOT define functions - write direct executable code
|
||||
|
||||
+5
-5
@@ -7,7 +7,7 @@ requires = [
|
||||
|
||||
[project]
|
||||
authors = [
|
||||
{email = "nico@predix.io", name = "Predix Team"},
|
||||
{email = "nico@nexquant.io", name = "NexQuant Team"},
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
@@ -16,7 +16,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
description = "Predix - AI-gestützter Quantitative Trading Agent für EUR/USD"
|
||||
description = "NexQuant - AI-gestützter Quantitative Trading Agent für EUR/USD"
|
||||
dynamic = [
|
||||
"dependencies",
|
||||
"optional-dependencies",
|
||||
@@ -29,7 +29,7 @@ keywords = [
|
||||
"EUR/USD",
|
||||
"Forex",
|
||||
]
|
||||
name = "predix"
|
||||
name = "nexquant"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -37,8 +37,8 @@ requires-python = ">=3.10"
|
||||
rdagent = "rdagent.app.cli:app"
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/PredixAI/predix/"
|
||||
issue = "https://github.com/PredixAI/predix/issues"
|
||||
homepage = "https://github.com/NexQuantAI/nexquant/"
|
||||
issue = "https://github.com/NexQuantAI/nexquant/issues"
|
||||
|
||||
[tool.coverage.report]
|
||||
fail_under = 80
|
||||
|
||||
+13
-13
@@ -294,7 +294,7 @@ def fin_quant_cli(
|
||||
# Start CLI Dashboard wenn gewünscht
|
||||
if with_cli_dashboard:
|
||||
def start_cli_dash():
|
||||
from rdagent.log.ui.predix_dashboard import run_dashboard
|
||||
from rdagent.log.ui.nexquant_dashboard import run_dashboard
|
||||
run_dashboard(log_path="fin_quant.log", refresh_interval=3)
|
||||
|
||||
cli_thread = threading.Thread(target=start_cli_dash, daemon=True)
|
||||
@@ -1262,9 +1262,9 @@ def start_loop_cli(
|
||||
from datetime import datetime
|
||||
|
||||
script_dir = str(Path(__file__).parent.parent.parent)
|
||||
generator = [sys.executable, f"{script_dir}/scripts/predix_smart_strategy_gen.py"]
|
||||
generator = [sys.executable, f"{script_dir}/scripts/nexquant_smart_strategy_gen.py"]
|
||||
logfile = f"{script_dir}/results/logs/generator_loop.log"
|
||||
pidfile = "/tmp/predix_loop.pid" # nosec B108 — administrative PID file, single-process daemon
|
||||
pidfile = "/tmp/nexquant_loop.pid" # nosec B108 — administrative PID file, single-process daemon
|
||||
|
||||
os.makedirs(f"{script_dir}/results/logs", exist_ok=True)
|
||||
|
||||
@@ -1418,7 +1418,7 @@ def parallel_cli(
|
||||
from rdagent.log import daily_log as _dlog
|
||||
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
script = project_root / "scripts" / "predix_parallel.py"
|
||||
script = project_root / "scripts" / "nexquant_parallel.py"
|
||||
|
||||
if not script.exists():
|
||||
typer.echo(f"❌ Script not found: {script}")
|
||||
@@ -1469,7 +1469,7 @@ def eval_all_cli(
|
||||
from rdagent.log import daily_log as _dlog
|
||||
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
script = project_root / "scripts" / "predix_full_eval.py"
|
||||
script = project_root / "scripts" / "nexquant_full_eval.py"
|
||||
|
||||
if not script.exists():
|
||||
typer.echo(f"❌ Script not found: {script}")
|
||||
@@ -1522,7 +1522,7 @@ def batch_backtest_cli(
|
||||
from pathlib import Path
|
||||
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
script = project_root / "scripts" / "predix_batch_backtest.py"
|
||||
script = project_root / "scripts" / "nexquant_batch_backtest.py"
|
||||
|
||||
if not script.exists():
|
||||
typer.echo(f"❌ Script not found: {script}")
|
||||
@@ -1574,7 +1574,7 @@ def simple_eval_cli(
|
||||
from pathlib import Path
|
||||
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
script = project_root / "scripts" / "predix_simple_eval.py"
|
||||
script = project_root / "scripts" / "nexquant_simple_eval.py"
|
||||
|
||||
if not script.exists():
|
||||
typer.echo(f"❌ Script not found: {script}")
|
||||
@@ -1620,7 +1620,7 @@ def rebacktest_cli(
|
||||
from pathlib import Path
|
||||
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
script = project_root / "scripts" / "predix_rebacktest_strategies.py"
|
||||
script = project_root / "scripts" / "nexquant_rebacktest_strategies.py"
|
||||
|
||||
if not script.exists():
|
||||
typer.echo(f"❌ Script not found: {script}")
|
||||
@@ -1673,7 +1673,7 @@ def report_cli(
|
||||
from pathlib import Path
|
||||
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
script = project_root / "scripts" / "predix_strategy_report.py"
|
||||
script = project_root / "scripts" / "nexquant_strategy_report.py"
|
||||
|
||||
if not script.exists():
|
||||
typer.echo(f"❌ Script not found: {script}")
|
||||
@@ -1697,10 +1697,10 @@ def report_cli(
|
||||
|
||||
|
||||
|
||||
@app.command(name="predix")
|
||||
def predix_welcome():
|
||||
@app.command(name="nexquant")
|
||||
def nexquant_welcome():
|
||||
"""
|
||||
Show Predix welcome screen with system overview.
|
||||
Show NexQuant welcome screen with system overview.
|
||||
|
||||
This command displays a beautiful dashboard showing:
|
||||
- System status (factors, strategies, security)
|
||||
@@ -1710,7 +1710,7 @@ def predix_welcome():
|
||||
Perfect for GitHub README screenshots!
|
||||
|
||||
Examples:
|
||||
rdagent predix
|
||||
rdagent nexquant
|
||||
"""
|
||||
from rdagent.app.cli_welcome import show_welcome
|
||||
show_welcome()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Predix CLI Welcome Screen - Beautiful dashboard for GitHub README screenshot.
|
||||
NexQuant CLI Welcome Screen - Beautiful dashboard for GitHub README screenshot.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -16,7 +16,7 @@ from datetime import datetime
|
||||
console = Console()
|
||||
|
||||
def show_welcome():
|
||||
"""Show beautiful Predix welcome screen."""
|
||||
"""Show beautiful NexQuant welcome screen."""
|
||||
|
||||
# Header
|
||||
console.print()
|
||||
@@ -89,7 +89,7 @@ def show_welcome():
|
||||
console.print()
|
||||
|
||||
# Footer
|
||||
footer = Text("📄 github.com/TPTBusiness/Predix • 🔒 MIT License • 📖 docs/", style="dim white")
|
||||
footer = Text("📄 github.com/TPTBusiness/NexQuant • 🔒 MIT License • 📖 docs/", style="dim white")
|
||||
console.print(Align.center(footer))
|
||||
console.print()
|
||||
|
||||
@@ -98,5 +98,5 @@ if __name__ == "__main__":
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for 'predix' CLI command."""
|
||||
"""Entry point for 'nexquant' CLI command."""
|
||||
show_welcome()
|
||||
|
||||
@@ -73,6 +73,94 @@ class QuantRDLoop(RDLoop):
|
||||
self.trace = QuantTrace(scen=scen)
|
||||
super(RDLoop, self).__init__()
|
||||
|
||||
def _ensure_kronos_factors_in_pool(self) -> None:
|
||||
"""Generate Kronos foundation model factors with varying prediction horizons.
|
||||
|
||||
Generates KronosPredReturn_p24, KronosPredReturn_p48, KronosPredReturn_p96
|
||||
if they don't already exist in results/factors/. Uses CPU inference so it
|
||||
co-exists peacefully with the llama-server GPU process.
|
||||
"""
|
||||
import json as _json
|
||||
from datetime import datetime as _dt
|
||||
from pathlib import Path as _Path
|
||||
|
||||
data_path = _Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
if not data_path.exists():
|
||||
logger.warning("Kronos: intraday_pv.h5 missing, skipping factor generation")
|
||||
return
|
||||
|
||||
factors_dir = _Path("results/factors")
|
||||
values_dir = factors_dir / "values"
|
||||
|
||||
for pred_bars in (24, 48, 96):
|
||||
factor_name = f"KronosPredReturn_p{pred_bars}"
|
||||
json_path = factors_dir / f"{factor_name}.json"
|
||||
parquet_path = values_dir / f"{factor_name}.parquet"
|
||||
|
||||
if json_path.exists() and parquet_path.exists():
|
||||
try:
|
||||
existing = _json.loads(json_path.read_text())
|
||||
if existing.get("ic") is not None and existing.get("model_size") == "small":
|
||||
logger.info(f"Kronos: {factor_name} exists (IC={existing['ic']:.4f}), skip")
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
from rdagent.components.coder.kronos_adapter import build_kronos_factor, evaluate_kronos_model
|
||||
|
||||
has_cuda = False
|
||||
try:
|
||||
import torch
|
||||
has_cuda = torch.cuda.is_available()
|
||||
except Exception:
|
||||
pass
|
||||
device = "cuda" if has_cuda else "cpu"
|
||||
|
||||
logger.info(f"Kronos-small: generating {factor_name} (pred={pred_bars}, stride=500, {device})...")
|
||||
factor_df = build_kronos_factor(
|
||||
hdf5_path=data_path,
|
||||
context_bars=100,
|
||||
pred_bars=pred_bars,
|
||||
stride_bars=500,
|
||||
device=device,
|
||||
batch_size=32,
|
||||
model_size="small",
|
||||
)
|
||||
|
||||
values_dir.mkdir(parents=True, exist_ok=True)
|
||||
factor_df.to_parquet(parquet_path)
|
||||
|
||||
logger.info(f"Kronos: computing IC for {factor_name}...")
|
||||
metrics = evaluate_kronos_model(
|
||||
hdf5_path=data_path,
|
||||
context_bars=100,
|
||||
pred_bars=pred_bars,
|
||||
stride_bars=2000,
|
||||
device=device,
|
||||
batch_size=32,
|
||||
model_size="small",
|
||||
)
|
||||
ic = metrics.get("IC_mean", 0.0) or 0.0
|
||||
|
||||
factors_dir.mkdir(parents=True, exist_ok=True)
|
||||
meta = {
|
||||
"factor_name": factor_name,
|
||||
"status": "success",
|
||||
"ic": ic,
|
||||
"model_size": "small",
|
||||
"model": "NeoQuasar/Kronos-mini",
|
||||
"context_bars": 100,
|
||||
"pred_bars": pred_bars,
|
||||
"device": "cpu",
|
||||
"generated_at": _dt.now().isoformat(),
|
||||
}
|
||||
json_path.write_text(_json.dumps(meta, indent=2))
|
||||
logger.info(f"Kronos: {factor_name} ready — IC={ic:.4f}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Kronos: {factor_name} failed — {e}")
|
||||
|
||||
async def direct_exp_gen(self, prev_out: dict[str, Any]):
|
||||
while True:
|
||||
if self.get_unfinished_loop_cnt(self.loop_idx) < RD_AGENT_SETTINGS.get_max_parallel():
|
||||
@@ -423,6 +511,7 @@ def main(
|
||||
else:
|
||||
quant_loop = QuantRDLoop.load(path, checkout=checkout)
|
||||
quant_loop._init_base_features(base_features_path)
|
||||
quant_loop._ensure_kronos_factors_in_pool()
|
||||
if "user_interaction_queues" in kwargs and kwargs["user_interaction_queues"] is not None:
|
||||
quant_loop._set_interactor(*kwargs["user_interaction_queues"])
|
||||
quant_loop._interact_init_params()
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
"""Predix Backtesting Package"""
|
||||
"""NexQuant Backtesting Package"""
|
||||
from .backtest_engine import BacktestMetrics, FactorBacktester
|
||||
from .results_db import ResultsDatabase
|
||||
from .risk_management import CorrelationAnalyzer, PortfolioOptimizer, AdvancedRiskManager
|
||||
from .vbt_backtest import (
|
||||
DEFAULT_BARS_PER_YEAR,
|
||||
DEFAULT_TXN_COST_BPS,
|
||||
FTMO_INITIAL_CAPITAL,
|
||||
FTMO_MAX_DAILY_LOSS,
|
||||
FTMO_MAX_TOTAL_LOSS,
|
||||
FTMO_MAX_LEVERAGE,
|
||||
FTMO_RISK_PER_TRADE,
|
||||
INITIAL_CAPITAL,
|
||||
MAX_DAILY_LOSS,
|
||||
MAX_TOTAL_LOSS,
|
||||
MAX_LEVERAGE,
|
||||
RISK_PER_TRADE,
|
||||
OOS_START_DEFAULT,
|
||||
WF_IS_YEARS,
|
||||
WF_OOS_YEARS,
|
||||
WF_STEP_YEARS,
|
||||
backtest_from_forward_returns,
|
||||
backtest_signal,
|
||||
backtest_signal_ftmo,
|
||||
backtest_signal_risk,
|
||||
monte_carlo_trade_pvalue,
|
||||
walk_forward_rolling,
|
||||
)
|
||||
@@ -24,10 +24,10 @@ from .vbt_backtest import (
|
||||
__all__ = [
|
||||
'BacktestMetrics', 'FactorBacktester', 'ResultsDatabase',
|
||||
'CorrelationAnalyzer', 'PortfolioOptimizer', 'AdvancedRiskManager',
|
||||
'backtest_signal', 'backtest_signal_ftmo', 'backtest_from_forward_returns',
|
||||
'backtest_signal', 'backtest_signal_risk', 'backtest_from_forward_returns',
|
||||
'monte_carlo_trade_pvalue', 'walk_forward_rolling',
|
||||
'DEFAULT_BARS_PER_YEAR', 'DEFAULT_TXN_COST_BPS',
|
||||
'FTMO_INITIAL_CAPITAL', 'FTMO_MAX_DAILY_LOSS', 'FTMO_MAX_TOTAL_LOSS',
|
||||
'FTMO_MAX_LEVERAGE', 'FTMO_RISK_PER_TRADE', 'OOS_START_DEFAULT',
|
||||
'INITIAL_CAPITAL', 'MAX_DAILY_LOSS', 'MAX_TOTAL_LOSS',
|
||||
'MAX_LEVERAGE', 'RISK_PER_TRADE', 'OOS_START_DEFAULT',
|
||||
'WF_IS_YEARS', 'WF_OOS_YEARS', 'WF_STEP_YEARS',
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Predix Backtesting Engine - IC, Sharpe, Drawdown
|
||||
NexQuant Backtesting Engine - IC, Sharpe, Drawdown
|
||||
|
||||
Thin wrapper around the unified ``vbt_backtest.backtest_signal`` engine.
|
||||
All metric formulas live in ``vbt_backtest``; this module exists for
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Trading Protection System for Predix.
|
||||
Trading Protection System for NexQuant.
|
||||
|
||||
Prevents excessive losses by automatically pausing trading
|
||||
when risk thresholds are exceeded.
|
||||
|
||||
@@ -3,7 +3,7 @@ Trading Protection System
|
||||
|
||||
Prevents excessive losses by automatically pausing trading when risk thresholds are exceeded.
|
||||
|
||||
Inspired by common trading protection patterns, implemented from scratch for Predix.
|
||||
Inspired by common trading protection patterns, implemented from scratch for NexQuant.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Predix Results Database - SQLite für Backtest-Ergebnisse
|
||||
NexQuant Results Database - SQLite für Backtest-Ergebnisse
|
||||
|
||||
Stores backtest metrics from Qlib/MLflow runs for querying and dashboard display.
|
||||
"""
|
||||
@@ -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:
|
||||
@@ -409,7 +409,7 @@ class ResultsDatabase:
|
||||
worst_dd_str = self._fmt_float(best['worst_drawdown'], ".4f")
|
||||
|
||||
md_lines = [
|
||||
"# Predix Results Summary",
|
||||
"# NexQuant Results Summary",
|
||||
"",
|
||||
f"**Generated:** {summary['generated_at']}",
|
||||
f"**Database:** `{summary['database_path']}`",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Predix Risk Management - Korrelation, Portfolio-Optimierung
|
||||
NexQuant Risk Management - Korrelation, Portfolio-Optimierung
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Unified, verifiable backtesting engine.
|
||||
|
||||
Single entry point (`backtest_signal`) used by:
|
||||
- scripts/predix_gen_strategies_real_bt.py
|
||||
- scripts/nexquant_gen_strategies_real_bt.py
|
||||
- rdagent/scenarios/qlib/local/strategy_orchestrator.py
|
||||
- rdagent/scenarios/qlib/local/optuna_optimizer.py
|
||||
- rdagent/components/backtesting/backtest_engine.py
|
||||
@@ -38,15 +38,15 @@ DEFAULT_TXN_COST_BPS = 2.14
|
||||
DEFAULT_BARS_PER_YEAR = 252 * 1440 # 252 trading days * 1440 min/day = 362,880
|
||||
EXTREME_BAR_THRESHOLD = 0.05 # |ret| > 5% on a single 1-min bar → suspicious
|
||||
|
||||
# FTMO 100k account rules (enforced in backtest_signal when ftmo=True)
|
||||
FTMO_INITIAL_CAPITAL = 100_000.0
|
||||
FTMO_MAX_DAILY_LOSS = 0.05 # 5% of initial → block new trades rest of day
|
||||
FTMO_MAX_TOTAL_LOSS = 0.10 # 10% of initial → simulation ends
|
||||
# Risk-based position sizing: 0.5% equity risk per trade, 10-pip stop, max 1:30 leverage
|
||||
FTMO_RISK_PER_TRADE = 0.005
|
||||
FTMO_STOP_PIPS = 10
|
||||
FTMO_PIP = 0.0001
|
||||
FTMO_MAX_LEVERAGE = 30
|
||||
# RiskMgmt 100k account rules (enforced in backtest_signal when riskmgmt=True)
|
||||
INITIAL_CAPITAL = 100_000.0
|
||||
MAX_DAILY_LOSS = 0.05 # 5% of initial → block new trades rest of day
|
||||
MAX_TOTAL_LOSS = 0.10 # 10% of initial → simulation ends
|
||||
# Risk-based position sizing: 1.5% equity risk per trade, 10-pip stop, max 1:30 leverage
|
||||
RISK_PER_TRADE = 0.015
|
||||
STOP_PIPS = 10
|
||||
PIP_SIZE = 0.0001
|
||||
MAX_LEVERAGE = 30
|
||||
|
||||
|
||||
def _compute_trade_pnl(position: pd.Series, strategy_returns: pd.Series) -> pd.Series:
|
||||
@@ -274,31 +274,31 @@ def backtest_signal(
|
||||
return result
|
||||
|
||||
|
||||
def _apply_ftmo_mask(
|
||||
def _apply_risk_mask(
|
||||
signal: pd.Series,
|
||||
close: pd.Series,
|
||||
leverage: float,
|
||||
txn_cost_bps: float,
|
||||
) -> tuple[pd.Series, dict]:
|
||||
"""
|
||||
Apply FTMO daily/total loss rules to a signal series.
|
||||
Apply RiskMgmt daily/total loss rules to a signal series.
|
||||
|
||||
Returns a masked signal (positions zeroed after each limit breach) and
|
||||
a dict of FTMO compliance metrics.
|
||||
a dict of RiskMgmt compliance metrics.
|
||||
"""
|
||||
txn_cost = txn_cost_bps / 10_000.0
|
||||
position = signal.shift(1).fillna(0) * leverage
|
||||
bar_ret = close.pct_change().fillna(0)
|
||||
|
||||
equity = FTMO_INITIAL_CAPITAL
|
||||
peak_day = FTMO_INITIAL_CAPITAL
|
||||
equity = INITIAL_CAPITAL
|
||||
peak_day = INITIAL_CAPITAL
|
||||
masked = signal.copy()
|
||||
|
||||
daily_breaches = 0
|
||||
total_breached = False
|
||||
total_breach_ts: pd.Timestamp | None = None
|
||||
current_day = None
|
||||
day_start_eq = FTMO_INITIAL_CAPITAL
|
||||
day_start_eq = INITIAL_CAPITAL
|
||||
|
||||
pos_prev = 0.0
|
||||
for ts, sig_i in signal.items():
|
||||
@@ -319,31 +319,31 @@ def _apply_ftmo_mask(
|
||||
masked.at[ts] = 0
|
||||
continue
|
||||
|
||||
daily_loss = (equity - day_start_eq) / FTMO_INITIAL_CAPITAL
|
||||
total_loss = (equity - FTMO_INITIAL_CAPITAL) / FTMO_INITIAL_CAPITAL
|
||||
daily_loss = (equity - day_start_eq) / INITIAL_CAPITAL
|
||||
total_loss = (equity - INITIAL_CAPITAL) / INITIAL_CAPITAL
|
||||
|
||||
if daily_loss < -FTMO_MAX_DAILY_LOSS:
|
||||
if daily_loss < -MAX_DAILY_LOSS:
|
||||
daily_breaches += 1
|
||||
day_start_eq = -999 # block rest of day
|
||||
masked.at[ts] = 0
|
||||
|
||||
if total_loss < -FTMO_MAX_TOTAL_LOSS:
|
||||
if total_loss < -MAX_TOTAL_LOSS:
|
||||
total_breached = True
|
||||
total_breach_ts = ts
|
||||
masked.at[ts] = 0
|
||||
|
||||
return masked, {
|
||||
"ftmo_daily_breaches": daily_breaches,
|
||||
"ftmo_total_breached": total_breached,
|
||||
"ftmo_total_breach_ts": str(total_breach_ts) if total_breach_ts else None,
|
||||
"ftmo_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,
|
||||
}
|
||||
|
||||
|
||||
OOS_START_DEFAULT = "2024-01-01"
|
||||
|
||||
# Rolling walk-forward default windows (IS years, OOS years, step years)
|
||||
WF_IS_YEARS = 3
|
||||
WF_IS_YEARS = 1
|
||||
WF_OOS_YEARS = 1
|
||||
WF_STEP_YEARS = 1
|
||||
|
||||
@@ -403,7 +403,7 @@ def walk_forward_rolling(
|
||||
"""
|
||||
Rolling walk-forward validation: multiple IS/OOS windows shifted by ``step_years``.
|
||||
|
||||
Each window runs an independent FTMO simulation on the IS and OOS slices.
|
||||
Each window runs an independent RiskMgmt simulation on the IS and OOS slices.
|
||||
Produces aggregate OOS statistics to measure cross-time consistency.
|
||||
|
||||
Returns
|
||||
@@ -442,7 +442,7 @@ def walk_forward_rolling(
|
||||
for mask, prefix in [(is_mask, "is"), (oos_mask, "oos")]:
|
||||
close_s = close.loc[mask]
|
||||
signal_s = signal.loc[mask]
|
||||
masked_s, _ = _apply_ftmo_mask(signal_s, close_s, leverage, txn_cost_bps)
|
||||
masked_s, _ = _apply_risk_mask(signal_s, close_s, leverage, txn_cost_bps)
|
||||
r = backtest_signal(close=close_s, signal=masked_s,
|
||||
txn_cost_bps=txn_cost_bps, bars_per_year=bars_per_year)
|
||||
window[f"{prefix}_sharpe"] = r.get("sharpe", 0.0)
|
||||
@@ -466,14 +466,14 @@ def walk_forward_rolling(
|
||||
}
|
||||
|
||||
|
||||
def backtest_signal_ftmo(
|
||||
def backtest_signal_risk(
|
||||
close: pd.Series,
|
||||
signal: pd.Series,
|
||||
txn_cost_bps: float = DEFAULT_TXN_COST_BPS,
|
||||
eurusd_price: float = 1.10,
|
||||
risk_pct: float = FTMO_RISK_PER_TRADE,
|
||||
stop_pips: float = FTMO_STOP_PIPS,
|
||||
max_leverage: float = FTMO_MAX_LEVERAGE,
|
||||
risk_pct: float = RISK_PER_TRADE,
|
||||
stop_pips: float = STOP_PIPS,
|
||||
max_leverage: float = MAX_LEVERAGE,
|
||||
bars_per_year: int = DEFAULT_BARS_PER_YEAR,
|
||||
forward_returns: pd.Series | None = None,
|
||||
oos_start: str | None = OOS_START_DEFAULT,
|
||||
@@ -481,15 +481,15 @@ def backtest_signal_ftmo(
|
||||
mc_n_permutations: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
FTMO-compliant backtest of a strategy signal on EUR/USD.
|
||||
RiskMgmt-compliant backtest of a strategy signal on EUR/USD.
|
||||
|
||||
Applies on top of ``backtest_signal``:
|
||||
- Realistic costs: default 2.14 bps (≈ 2.35 pip spread+slippage+commission)
|
||||
- Risk-based position sizing: risk_pct equity per trade, stop_pips hard stop
|
||||
- Max leverage cap: max_leverage (default 1:30, FTMO standard)
|
||||
- FTMO daily loss limit (5%): positions zeroed rest of day after breach
|
||||
- FTMO total loss limit (10%): all positions zeroed after breach
|
||||
- FTMO-specific metrics added to result dict
|
||||
- Max leverage cap: max_leverage (default 1:30, RiskMgmt standard)
|
||||
- RiskMgmt daily loss limit (5%): positions zeroed rest of day after breach
|
||||
- RiskMgmt total loss limit (10%): all positions zeroed after breach
|
||||
- RiskMgmt-specific metrics added to result dict
|
||||
- Walk-forward OOS split: IS metrics (before oos_start) + OOS metrics (after)
|
||||
|
||||
Parameters
|
||||
@@ -507,7 +507,7 @@ def backtest_signal_ftmo(
|
||||
stop_pips : float
|
||||
Hard stop-loss distance in pips (default 10).
|
||||
max_leverage : float
|
||||
Maximum leverage (default 30 = FTMO 1:30).
|
||||
Maximum leverage (default 30 = RiskMgmt 1:30).
|
||||
oos_start : str or None
|
||||
Start of out-of-sample period (ISO date). None disables OOS split.
|
||||
wf_rolling : bool
|
||||
@@ -518,11 +518,11 @@ def backtest_signal_ftmo(
|
||||
When > 0, computes ``mc_pvalue``: fraction of permuted sequences whose
|
||||
total return >= real total return. p < 0.05 indicates a genuine edge.
|
||||
"""
|
||||
stop_price = stop_pips * FTMO_PIP
|
||||
stop_price = stop_pips * PIP_SIZE
|
||||
leverage_by_risk = risk_pct / (stop_price / eurusd_price)
|
||||
leverage = min(leverage_by_risk, max_leverage)
|
||||
|
||||
masked_signal, ftmo_metrics = _apply_ftmo_mask(signal, close, leverage, txn_cost_bps)
|
||||
masked_signal, risk_metrics = _apply_risk_mask(signal, close, leverage, txn_cost_bps)
|
||||
|
||||
result = backtest_signal(
|
||||
close=close,
|
||||
@@ -532,14 +532,14 @@ def backtest_signal_ftmo(
|
||||
forward_returns=forward_returns,
|
||||
)
|
||||
|
||||
result.update(ftmo_metrics)
|
||||
result["ftmo_leverage"] = round(leverage, 2)
|
||||
result["ftmo_risk_pct"] = risk_pct
|
||||
result["ftmo_stop_pips"] = stop_pips
|
||||
result.update(risk_metrics)
|
||||
result["riskmgmt_leverage"] = round(leverage, 2)
|
||||
result["riskmgmt_risk_pct"] = risk_pct
|
||||
result["riskmgmt_stop_pips"] = stop_pips
|
||||
|
||||
# Re-scale reported equity metrics to FTMO_INITIAL_CAPITAL
|
||||
result["ftmo_end_equity"] = FTMO_INITIAL_CAPITAL * (1 + result.get("total_return", 0))
|
||||
result["ftmo_monthly_profit"] = FTMO_INITIAL_CAPITAL * result.get("monthly_return", 0)
|
||||
# Re-scale reported equity metrics to INITIAL_CAPITAL
|
||||
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:
|
||||
@@ -551,9 +551,9 @@ def backtest_signal_ftmo(
|
||||
if mask.sum() < 100:
|
||||
return
|
||||
close_s = close.loc[mask]
|
||||
signal_s = signal.loc[mask] # raw signal, not masked — fresh FTMO sim per period
|
||||
signal_s = signal.loc[mask] # raw signal, not masked — fresh RiskMgmt sim per period
|
||||
fwd_split = forward_returns.loc[mask] if forward_returns is not None else None
|
||||
masked_s, _ = _apply_ftmo_mask(signal_s, close_s, leverage, txn_cost_bps)
|
||||
masked_s, _ = _apply_risk_mask(signal_s, close_s, leverage, txn_cost_bps)
|
||||
split_result = backtest_signal(
|
||||
close=close_s,
|
||||
signal=masked_s,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Predix Factor Auto-Fixer - Automatically patches common factor code issues.
|
||||
NexQuant Factor Auto-Fixer - Automatically patches common factor code issues.
|
||||
|
||||
This module intercepts LLM-generated factor code and automatically fixes known problems:
|
||||
1. min_periods mismatch in rolling window calculations
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Kronos Foundation Model Adapter for Predix.
|
||||
Kronos Foundation Model Adapter for NexQuant.
|
||||
|
||||
Wraps the Kronos-mini OHLCV foundation model (4.1M params, AAAI 2026, MIT)
|
||||
for use as:
|
||||
@@ -55,8 +55,8 @@ def _ensure_kronos() -> bool:
|
||||
return _KRONOS_AVAILABLE
|
||||
|
||||
|
||||
def _ohlcv_from_predix(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Convert Predix HDF5 format ($open/$close/...) to Kronos format (open/close/...)."""
|
||||
def _ohlcv_from_nexquant(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Convert NexQuant HDF5 format ($open/$close/...) to Kronos format (open/close/...)."""
|
||||
col_map = {"$open": "open", "$high": "high", "$low": "low", "$close": "close", "$volume": "volume"}
|
||||
renamed = df.rename(columns=col_map)
|
||||
cols = [c for c in ["open", "high", "low", "close", "volume"] if c in renamed.columns]
|
||||
@@ -90,9 +90,19 @@ class KronosAdapter:
|
||||
MODEL_ID = "NeoQuasar/Kronos-mini"
|
||||
TOKENIZER_ID = "NeoQuasar/Kronos-Tokenizer-2k"
|
||||
|
||||
def __init__(self, device: Optional[str] = None, max_context: int = 512):
|
||||
self.device = device or ("cuda" if _cuda_available() else "cpu")
|
||||
# Mapping for larger Kronos variants
|
||||
_MODEL_MAP = {
|
||||
"mini": ("NeoQuasar/Kronos-mini", "NeoQuasar/Kronos-Tokenizer-2k"),
|
||||
"small": ("NeoQuasar/Kronos-small", "NeoQuasar/Kronos-Tokenizer-base"),
|
||||
"base": ("NeoQuasar/Kronos-base", "NeoQuasar/Kronos-Tokenizer-base"),
|
||||
}
|
||||
|
||||
def __init__(self, device: Optional[str] = None, max_context: int = 512, model_size: str = "mini"):
|
||||
self.device = device or "cpu"
|
||||
self.max_context = max_context
|
||||
self.model_size = model_size
|
||||
if model_size in self._MODEL_MAP:
|
||||
self.MODEL_ID, self.TOKENIZER_ID = self._MODEL_MAP[model_size]
|
||||
self._predictor = None
|
||||
|
||||
def load(self) -> "KronosAdapter":
|
||||
@@ -102,11 +112,11 @@ class KronosAdapter:
|
||||
raise RuntimeError("Kronos not available — see warning above.")
|
||||
from model import Kronos, KronosTokenizer, KronosPredictor # type: ignore
|
||||
|
||||
logger.info(f"Loading Kronos-mini from HuggingFace ({self.MODEL_ID})...")
|
||||
logger.info(f"Loading Kronos-{self.model_size} from HuggingFace ({self.MODEL_ID})...")
|
||||
tokenizer = KronosTokenizer.from_pretrained(self.TOKENIZER_ID)
|
||||
model = Kronos.from_pretrained(self.MODEL_ID)
|
||||
logger.info(f"Kronos-{self.model_size} loaded.")
|
||||
self._predictor = KronosPredictor(model, tokenizer, device=self.device, max_context=self.max_context)
|
||||
logger.info("Kronos-mini loaded.")
|
||||
return self
|
||||
|
||||
def predict_next_bars(
|
||||
@@ -223,6 +233,7 @@ def build_kronos_factor(
|
||||
stride_bars: int = 96,
|
||||
device: Optional[str] = None,
|
||||
batch_size: int = 32,
|
||||
model_size: str = "mini",
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Generate the Kronos predicted-return factor for all EUR/USD 1-min bars.
|
||||
@@ -236,15 +247,15 @@ def build_kronos_factor(
|
||||
Returns:
|
||||
MultiIndex (datetime, instrument) DataFrame with column "KronosPredReturn".
|
||||
"""
|
||||
device = device or ("cuda" if _cuda_available() else "cpu")
|
||||
device = device or "cpu"
|
||||
logger.info(f"Loading data from {hdf5_path}...")
|
||||
raw = pd.read_hdf(hdf5_path, key="data")
|
||||
|
||||
instrument = raw.index.get_level_values("instrument").unique()[0]
|
||||
df = raw.xs(instrument, level="instrument")
|
||||
ohlcv = _ohlcv_from_predix(df)
|
||||
ohlcv = _ohlcv_from_nexquant(df)
|
||||
|
||||
adapter = KronosAdapter(device=device, max_context=min(context_bars, 512))
|
||||
adapter = KronosAdapter(device=device, max_context=min(context_bars, 512), model_size=model_size)
|
||||
adapter.load()
|
||||
|
||||
bar_indices = list(range(context_bars, len(ohlcv), stride_bars))
|
||||
@@ -302,6 +313,7 @@ def evaluate_kronos_model(
|
||||
stride_bars: int = 30,
|
||||
device: Optional[str] = None,
|
||||
batch_size: int = 32,
|
||||
model_size: str = "mini",
|
||||
) -> dict:
|
||||
"""
|
||||
Evaluate Kronos as a standalone model (Option B, alongside LightGBM).
|
||||
@@ -312,13 +324,13 @@ def evaluate_kronos_model(
|
||||
Returns:
|
||||
dict with keys: IC_mean, IC_std, IC_IR (IC / std), hit_rate, n_predictions
|
||||
"""
|
||||
device = device or ("cuda" if _cuda_available() else "cpu")
|
||||
device = device or "cpu"
|
||||
raw = pd.read_hdf(hdf5_path, key="data")
|
||||
instrument = raw.index.get_level_values("instrument").unique()[0]
|
||||
df = raw.xs(instrument, level="instrument")
|
||||
ohlcv = _ohlcv_from_predix(df)
|
||||
ohlcv = _ohlcv_from_nexquant(df)
|
||||
|
||||
adapter = KronosAdapter(device=device, max_context=min(context_bars, 512))
|
||||
adapter = KronosAdapter(device=device, max_context=min(context_bars, 512), model_size=model_size)
|
||||
adapter.load()
|
||||
|
||||
n = len(ohlcv)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""RL Trading Agent components for Predix.
|
||||
"""RL Trading Agent components for NexQuant.
|
||||
|
||||
This package provides reinforcement learning trading capabilities.
|
||||
Works with or without stable-baselines3 (graceful fallback).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
RL Trading Agent wrapper for Stable Baselines3.
|
||||
|
||||
Provides an easy-to-use interface for training, evaluating, and deploying
|
||||
RL trading agents within the Predix framework.
|
||||
RL trading agents within the NexQuant framework.
|
||||
|
||||
Supported algorithms:
|
||||
- PPO: Proximal Policy Optimization (most stable, recommended for production)
|
||||
|
||||
@@ -5,7 +5,7 @@ Gym-compatible environment for training RL trading agents.
|
||||
Supports single-asset (EUR/USD) trading with technical indicators
|
||||
and portfolio state as observations.
|
||||
|
||||
Inspired by common RL trading environment patterns, implemented from scratch for Predix.
|
||||
Inspired by common RL trading environment patterns, implemented from scratch for NexQuant.
|
||||
"""
|
||||
|
||||
import gymnasium as gym
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Fallback RL implementation for users without stable-baselines3.
|
||||
|
||||
Provides simple rule-based trading when RL library is not available.
|
||||
This ensures the Predix system works for all GitHub users, even
|
||||
This ensures the NexQuant system works for all GitHub users, even
|
||||
without the optional stable-baselines3 dependency.
|
||||
|
||||
The fallback implements a momentum-based strategy as a placeholder
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Predix Model Loader
|
||||
NexQuant Model Loader
|
||||
|
||||
Loads models from:
|
||||
1. models/local/*.py (your improved models - not in Git)
|
||||
@@ -23,7 +23,7 @@ from typing import Optional, Any
|
||||
|
||||
|
||||
# Base paths
|
||||
BASE_DIR = Path(__file__).parent.parent.parent # Predix/
|
||||
BASE_DIR = Path(__file__).parent.parent.parent # NexQuant/
|
||||
MODELS_DIR = BASE_DIR / "models"
|
||||
LOCAL_MODELS_DIR = MODELS_DIR / "local"
|
||||
STANDARD_MODELS_DIR = MODELS_DIR / "standard"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Predix Prompt Loader
|
||||
NexQuant Prompt Loader
|
||||
|
||||
Loads prompts from:
|
||||
1. prompts/local/*.yaml (your improved prompts - not in Git)
|
||||
@@ -22,7 +22,7 @@ from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
# Base paths
|
||||
BASE_DIR = Path(__file__).parent.parent.parent # Predix/
|
||||
BASE_DIR = Path(__file__).parent.parent.parent # NexQuant/
|
||||
PROMPTS_DIR = BASE_DIR / "prompts"
|
||||
LOCAL_PROMPTS_DIR = PROMPTS_DIR / "local"
|
||||
STANDARD_PROMPTS_FILE = PROMPTS_DIR / "standard_prompts.yaml"
|
||||
|
||||
@@ -160,6 +160,9 @@ class RDAgentLog(SingletonBaseClass):
|
||||
log_func = getattr(patched_logger, level)
|
||||
log_func(msg)
|
||||
|
||||
def debug(self, msg: str, *, tag: str = "", raw: bool = False) -> None:
|
||||
self._log("debug", msg, tag=tag, raw=raw)
|
||||
|
||||
def info(self, msg: str, *, tag: str = "", raw: bool = False) -> None:
|
||||
self._log("info", msg, tag=tag, raw=raw)
|
||||
|
||||
|
||||
@@ -585,6 +585,24 @@ class APIBackend(ABC):
|
||||
f"Original error: {e}"
|
||||
) from e
|
||||
|
||||
# Handle llama.cpp 400: "Cannot have 2 or more assistant messages at the end of the list"
|
||||
if (
|
||||
openai_imported
|
||||
and isinstance(e, openai.BadRequestError)
|
||||
and hasattr(e, "message")
|
||||
and "Cannot have 2 or more assistant messages" in e.message
|
||||
):
|
||||
if "messages" in kwargs:
|
||||
merged = []
|
||||
for msg in kwargs["messages"]:
|
||||
if merged and merged[-1]["role"] == "assistant" and msg["role"] == "assistant":
|
||||
merged[-1]["content"] += "\n" + msg["content"]
|
||||
else:
|
||||
merged.append(msg)
|
||||
kwargs["messages"] = merged
|
||||
logger.warning("Fixed consecutive assistant messages, retrying...")
|
||||
continue
|
||||
|
||||
if embedding and too_long_error_message:
|
||||
if not embedding_truncated:
|
||||
# Handle embedding text too long error - truncate once and retry
|
||||
@@ -654,9 +672,11 @@ class APIBackend(ABC):
|
||||
add json related content in the prompt if add_json_in_prompt is True
|
||||
"""
|
||||
for message in messages[::-1]:
|
||||
message["content"] = message["content"] + "\nPlease respond in json format."
|
||||
if message["role"] == "user":
|
||||
message["content"] = message["content"] + "\nPlease respond in json format."
|
||||
break
|
||||
if message["role"] == LLM_SETTINGS.system_prompt_role:
|
||||
# NOTE: assumption: systemprompt is always the first message
|
||||
message["content"] = message["content"] + "\nPlease respond in json format."
|
||||
break
|
||||
|
||||
def _create_chat_completion_auto_continue(
|
||||
|
||||
@@ -387,6 +387,10 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]):
|
||||
warnings.append(
|
||||
f"IC is near zero ({ic_float:.6f}) — factor may not predict returns",
|
||||
)
|
||||
if abs(ic_float) < 0.04:
|
||||
warnings.append(
|
||||
f"IC below target ({ic_float:.4f}) — factor will be excluded from strategy building (min IC=0.04)",
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
warnings.append(f"IC value is not numeric: {ic_value}")
|
||||
|
||||
@@ -969,7 +973,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]):
|
||||
|
||||
# Run factor code on full data in a temp workspace
|
||||
import pandas as pd
|
||||
with tempfile.TemporaryDirectory(prefix="predix_fullval_") as tmp_dir:
|
||||
with tempfile.TemporaryDirectory(prefix="nexquant_fullval_") as tmp_dir:
|
||||
tmp = Path(tmp_dir)
|
||||
shutil.copy(str(factor_py), str(tmp / "factor.py"))
|
||||
shutil.copy(str(full_data), str(tmp / "intraday_pv.h5"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Predix Strategy Builder - Systematically combine factors into trading strategies.
|
||||
NexQuant Strategy Builder - Systematically combine factors into trading strategies.
|
||||
|
||||
This module:
|
||||
1. Loads evaluated factors with time-series values
|
||||
@@ -8,9 +8,9 @@ This module:
|
||||
4. Ranks and saves best strategies
|
||||
|
||||
Usage:
|
||||
predix build-strategies # Build strategies from top factors
|
||||
predix build-strategies --top 50 # Use top 50 factors
|
||||
predix build-strategies --max-combo 3 # Allow up to 3-factor combinations
|
||||
nexquant build-strategies # Build strategies from top factors
|
||||
nexquant build-strategies --top 50 # Use top 50 factors
|
||||
nexquant build-strategies --max-combo 3 # Allow up to 3-factor combinations
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
@@ -56,7 +56,7 @@ Current Date: {current_date}
|
||||
Live Macro Data:
|
||||
{macro_data}
|
||||
|
||||
Factor Report from Predix RD-Agent:
|
||||
Factor Report from NexQuant RD-Agent:
|
||||
{factor_report}
|
||||
|
||||
Analyze the macro environment and its impact on the proposed factor:
|
||||
|
||||
@@ -47,7 +47,7 @@ Active Session: {session}
|
||||
Expected Regime: {regime}
|
||||
Session Notes: {session_note}
|
||||
|
||||
Factor Report from Predix RD-Agent:
|
||||
Factor Report from NexQuant RD-Agent:
|
||||
{factor_report}
|
||||
|
||||
Analyze whether the proposed factor is suitable for the current session regime.
|
||||
|
||||
@@ -17,7 +17,7 @@ def create_fx_trader(llm):
|
||||
|
||||
You have received reports from your team:
|
||||
|
||||
FACTOR ANALYSIS (Predix RD-Agent):
|
||||
FACTOR ANALYSIS (NexQuant RD-Agent):
|
||||
{factor_report}
|
||||
|
||||
SESSION ANALYSIS:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
FX Validator Graph — Multi-Agent Validierung für Predix Faktoren
|
||||
FX Validator Graph — Multi-Agent Validierung für NexQuant Faktoren
|
||||
|
||||
Implementiert Multi-Agenten-System für Trading-Entscheidungen:
|
||||
- Session Analyst: Analysiert aktuelle FX-Session
|
||||
@@ -88,10 +88,10 @@ def create_fx_validator(config: dict = None):
|
||||
|
||||
def validate_factor(factor_report: str, trade_date: str = None) -> dict:
|
||||
"""
|
||||
Hauptfunktion — validiert einen Predix-Faktor durch Multi-Agent Debatte
|
||||
Hauptfunktion — validiert einen NexQuant-Faktor durch Multi-Agent Debatte
|
||||
|
||||
Args:
|
||||
factor_report: Der Faktor-Report von Predix RD-Agent
|
||||
factor_report: Der Faktor-Report von NexQuant RD-Agent
|
||||
trade_date: Datum/Zeit in ISO Format (default: jetzt)
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -53,7 +53,8 @@ def extract_metrics_from_experiment(experiment) -> Metrics:
|
||||
|
||||
|
||||
class LinearThompsonTwoArm:
|
||||
def __init__(self, dim: int, prior_var: float = 1.0, noise_var: float = 1.0):
|
||||
def __init__(self, dim: int, prior_var: float = 1.0, noise_var: float = 1.0,
|
||||
model_prior_bias: float = 0.5):
|
||||
self.dim = dim
|
||||
self.noise_var = noise_var
|
||||
# Each arm has its own posterior: mean & inverse of covariance (precision matrix)
|
||||
@@ -61,6 +62,8 @@ class LinearThompsonTwoArm:
|
||||
"factor": np.zeros(dim),
|
||||
"model": np.zeros(dim),
|
||||
}
|
||||
# Give model arm an initial positive bias toward all metrics
|
||||
self.mean["model"][:] = model_prior_bias
|
||||
self.precision = {
|
||||
"factor": np.eye(dim) / prior_var,
|
||||
"model": np.eye(dim) / prior_var,
|
||||
@@ -94,8 +97,8 @@ class LinearThompsonTwoArm:
|
||||
|
||||
class EnvController:
|
||||
def __init__(self, weights: Tuple[float, ...] = None) -> None:
|
||||
self.weights = np.asarray(weights or (0.1, 0.1, 0.05, 0.05, 0.25, 0.15, 0.1, 0.2))
|
||||
self.bandit = LinearThompsonTwoArm(dim=8, prior_var=10.0, noise_var=0.5)
|
||||
self.weights = np.asarray(weights or (0.2, 0.1, 0.05, 0.05, 0.25, 0.1, 0.1, 0.15))
|
||||
self.bandit = LinearThompsonTwoArm(dim=8, prior_var=5.0, noise_var=0.5, model_prior_bias=2.0)
|
||||
|
||||
def reward(self, m: Metrics) -> float:
|
||||
return float(np.dot(self.weights, m.as_vector()))
|
||||
|
||||
@@ -73,7 +73,7 @@ class QlibQuantHypothesisGen(FactorAndModelHypothesisGen):
|
||||
trace.controller.record(metric, prev_action)
|
||||
action = trace.controller.decide(metric)
|
||||
else:
|
||||
action = "factor"
|
||||
action = "model"
|
||||
# ========= LLM ==========
|
||||
elif QUANT_PROP_SETTING.action_selection == "llm":
|
||||
hypothesis_and_feedback = (
|
||||
@@ -108,7 +108,7 @@ class QlibQuantHypothesisGen(FactorAndModelHypothesisGen):
|
||||
if len(trace.hist) < 6:
|
||||
qaunt_rag = "Try the easiest and fastest factors to experiment with from various perspectives first."
|
||||
else:
|
||||
qaunt_rag = "Now, you need to try factors that can achieve high IC (e.g., machine learning-based factors)! Do not include factors that are similar to those in the SOTA factor library!"
|
||||
qaunt_rag = "Now, you need to try factors that can achieve high IC (target |IC| > 0.04, e.g., machine learning-based factors)! Do not include factors that are similar to those in the SOTA factor library!"
|
||||
elif action == "model":
|
||||
qaunt_rag = "1. In Quantitative Finance, market data could be time-series, and GRU model/LSTM model are suitable for them. Do not generate GNN model as for now.\n2. The training data consists of approximately 478,000 samples for the training set and about 128,000 samples for the validation set. Please design the hyperparameters accordingly and control the model size. This has a significant impact on the training results. If you believe that the previous model itself is good but the training hyperparameters or model hyperparameters are not optimal, you can return the same model and adjust these parameters instead.\n"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Predix Quant Loop Factory - Selects appropriate workflow based on available components.
|
||||
NexQuant Quant Loop Factory - Selects appropriate workflow based on available components.
|
||||
|
||||
This module is the entry point for the quantitative trading loop.
|
||||
It automatically selects between:
|
||||
|
||||
+3
-3
@@ -9,8 +9,8 @@ psutil
|
||||
fire
|
||||
fuzzywuzzy
|
||||
openai
|
||||
litellm>=1.83.14 # to support `from litellm import get_valid_models`
|
||||
aiohttp>=3.13.4 # CVE-2026-22815, CVE-2026-34515, CVE-2026-34516, CVE-2026-34525; >=3.13.4 due to litellm==1.83.14 exact pin
|
||||
litellm>=1.86.2 # to support `from litellm import get_valid_models`
|
||||
aiohttp>=3.14.0 # CVE-2026-22815, CVE-2026-34515, CVE-2026-34516, CVE-2026-34525; >=3.13.4 due to litellm==1.83.14 exact pin
|
||||
azure.identity
|
||||
pyarrow
|
||||
rich
|
||||
@@ -46,7 +46,7 @@ docker
|
||||
webdriver-manager
|
||||
|
||||
# demo related
|
||||
streamlit>=1.57.0 # to support input_c.text_area(..., height="content", ...)
|
||||
streamlit>=1.58.0 # to support input_c.text_area(..., height="content", ...)
|
||||
plotly
|
||||
st-theme
|
||||
randomname
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
# Install with: pip install -r requirements/rl.txt
|
||||
#
|
||||
# These dependencies are OPTIONAL.
|
||||
# The Predix RL trading system works without them using a simple momentum fallback.
|
||||
# The NexQuant RL trading system works without them using a simple momentum fallback.
|
||||
#
|
||||
# Only install if you want to use full PPO/A2C/SAC training.
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Requirements for test.
|
||||
coverage
|
||||
hypothesis
|
||||
pytest
|
||||
|
||||
@@ -5,8 +5,8 @@ import numpy as np
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
|
||||
OHLCV_PATH = Path('/home/nico/Predix/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
FACTORS_DIR = Path('/home/nico/Predix/results/factors')
|
||||
OHLCV_PATH = Path('/home/nico/NexQuant/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
FACTORS_DIR = Path('/home/nico/NexQuant/results/factors')
|
||||
VALUES_DIR = FACTORS_DIR / 'values'
|
||||
|
||||
print("=" * 70)
|
||||
|
||||
@@ -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()
|
||||
@@ -3,10 +3,10 @@
|
||||
Option A: Generate Kronos predicted-return factor from EUR/USD 1-min data.
|
||||
|
||||
Runs Kronos-mini inference in daily strides (96 bars/day) over all available
|
||||
OHLCV data and saves the resulting factor for use in Predix's factor pipeline.
|
||||
OHLCV data and saves the resulting factor for use in NexQuant's factor pipeline.
|
||||
|
||||
Usage:
|
||||
conda activate predix
|
||||
conda activate nexquant
|
||||
python scripts/kronos_factor_gen.py
|
||||
python scripts/kronos_factor_gen.py --context 512 --pred 96 --device cuda
|
||||
python scripts/kronos_factor_gen.py --device cpu # slower but no GPU needed
|
||||
@@ -71,7 +71,7 @@ def main():
|
||||
print(f"\nSample (first 5):")
|
||||
print(factor_df.head())
|
||||
|
||||
# Save metadata for predix.py top / best integration
|
||||
# Save metadata for nexquant.py top / best integration
|
||||
meta = {
|
||||
"factor_name": f"KronosPredReturn_p{args.pred}",
|
||||
"description": f"Kronos-mini predicted return, {args.pred}-bar horizon",
|
||||
|
||||
@@ -6,7 +6,7 @@ Computes IC (Information Coefficient) and hit rate for Kronos predictions
|
||||
vs actual realized returns. Results are printed for comparison with LightGBM.
|
||||
|
||||
Usage:
|
||||
conda activate predix
|
||||
conda activate nexquant
|
||||
python scripts/kronos_model_eval.py
|
||||
python scripts/kronos_model_eval.py --pred 30 --context 512 --device cuda
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import json, numpy as np, pandas as pd
|
||||
from pathlib import Path
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
close = pd.read_hdf("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5", key="data")["$close"]
|
||||
close = close.droplevel(-1).sort_index().dropna().resample("1h").last().dropna()
|
||||
print(f"1h bars: {len(close):,}")
|
||||
|
||||
FACTORS_DIR = Path("results/factors"); VALS = FACTORS_DIR / "values"
|
||||
factors = []
|
||||
for f in sorted(FACTORS_DIR.glob("*.json")):
|
||||
try: d = json.loads(f.read_text())
|
||||
except: continue
|
||||
if d.get("status") != "success" or d.get("ic") is None: continue
|
||||
name = d.get("factor_name", f.stem)
|
||||
safe = name.replace("/", "_")[:150]
|
||||
if (VALS / f"{safe}.parquet").exists():
|
||||
factors.append({"name": name, "ic": d["ic"], "safe": safe})
|
||||
|
||||
factors.sort(key=lambda x: abs(x["ic"]), reverse=True)
|
||||
print(f"Testing top-100 factors by |IC|...")
|
||||
|
||||
results = []
|
||||
is_session = (close.index.hour >= 7) & (close.index.hour < 17)
|
||||
|
||||
for i, f in enumerate(factors[:100]):
|
||||
try:
|
||||
s = pd.read_parquet(VALS / f"{f['safe']}.parquet").iloc[:, 0]
|
||||
if isinstance(s.index, pd.MultiIndex): s = s.droplevel(-1)
|
||||
fac = s.resample("1h").last().reindex(close.index).ffill()
|
||||
except: continue
|
||||
|
||||
for dr, label in [(1, "STD"), (-1, "INV")]:
|
||||
sig = pd.Series(dr * np.sign(fac).fillna(0), index=close.index)
|
||||
sig[~is_session] = 0
|
||||
if sig.abs().sum() < 20: continue
|
||||
r = backtest_signal_risk(close, sig.fillna(0), txn_cost_bps=2.14)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
oos_m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
results.append((f"{f['name']}_{label}", oos, oos_m, r.get("oos_n_trades",0)))
|
||||
|
||||
if i % 25 == 0:
|
||||
bests = sorted(results, key=lambda x: x[1], reverse=True)[:3]
|
||||
print(f" {i}/100... best: {bests[0][0][:35]} OOS={bests[0][1]:+.1f}")
|
||||
|
||||
results.sort(key=lambda x: x[1], reverse=True)
|
||||
print(f"\nTop 15 — 1h Factor Signals (Session-Filtered):")
|
||||
for i, (name, oos, mon, t) in enumerate(results[:15]):
|
||||
s = "✅" if mon > 0 else ""
|
||||
print(f" {i+1:2d}. {name[:50]:50s} OOS={oos:+8.1f} Mon={mon:+7.3f}% T={t:5d} {s}")
|
||||
|
||||
# Combine best
|
||||
top = [r for r in results if r[2] > 0][:8]
|
||||
if top:
|
||||
all_sig = {}
|
||||
for name, oos, mon, t in top:
|
||||
fn = name.rsplit("_", 1)[0]; dr = 1 if name.endswith("_STD") else -1
|
||||
safe = fn.replace("/", "_")[:150]
|
||||
try:
|
||||
s = pd.read_parquet(VALS/f"{safe}.parquet").iloc[:, 0]
|
||||
if isinstance(s.index, pd.MultiIndex): s = s.droplevel(-1)
|
||||
fac = s.resample("1h").last().reindex(close.index).ffill()
|
||||
sig = pd.Series(dr * np.sign(fac).fillna(0), index=close.index)
|
||||
sig[~is_session] = 0; all_sig[name] = sig
|
||||
except: pass
|
||||
|
||||
df = pd.DataFrame(all_sig, index=close.index).fillna(0)
|
||||
for n in [3, 5, 8]:
|
||||
combo = df[list(df.columns)[:n]].mean(axis=1)
|
||||
r = backtest_signal_risk(close, combo.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
oos_m = r.get("oos_monthly_return_pct",0) or 0
|
||||
dd = (r.get("oos_max_drawdown",0) or 0)*100
|
||||
ann = ((1+oos_m/100)**12-1)*100
|
||||
print(f" Top-{n} combo: Mon={oos_m:+.3f}% Ann={ann:+.1f}% DD={dd:+.1f}% T={r.get('oos_n_trades',0)}")
|
||||
|
||||
print("\nDone")
|
||||
@@ -0,0 +1,467 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NexQuant 20-Hypothesis Systematic Test Suite
|
||||
|
||||
Tests all 20 improvement hypotheses against the real OOS walk-forward backtest.
|
||||
Each approach is independently evaluated and ranked by OOS Sharpe.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json, sys, time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors")
|
||||
TXN_COST_BPS = 2.14
|
||||
FORWARD_BARS = 96
|
||||
|
||||
|
||||
def load_all():
|
||||
close = pd.read_hdf(DATA_PATH, key="data")["$close"]
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close = close.droplevel(-1)
|
||||
close = close.sort_index().dropna()
|
||||
# Downsample to 5-min for speed
|
||||
close = close.resample("5min").last().dropna()
|
||||
|
||||
factors_meta = []
|
||||
for f in sorted(FACTORS_DIR.glob("*.json")):
|
||||
try:
|
||||
d = json.loads(f.read_text())
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("status") != "success" or d.get("ic") is None:
|
||||
continue
|
||||
name = d.get("factor_name", f.stem)
|
||||
safe = name.replace("/", "_")[:150]
|
||||
pf = FACTORS_DIR / "values" / f"{safe}.parquet"
|
||||
if pf.exists():
|
||||
factors_meta.append({"name": name, "ic": d["ic"]})
|
||||
|
||||
factors_meta.sort(key=lambda x: abs(x["ic"]), reverse=True)
|
||||
top = factors_meta[:15]
|
||||
|
||||
factor_data = {}
|
||||
for f in top:
|
||||
safe = f["name"].replace("/", "_")[:150]
|
||||
pf = FACTORS_DIR / "values" / f"{safe}.parquet"
|
||||
series = pd.read_parquet(pf).iloc[:, 0]
|
||||
if isinstance(series.index, pd.MultiIndex):
|
||||
series = series.droplevel(-1)
|
||||
# Resample to 5-min
|
||||
series = series.resample("5min").last()
|
||||
factor_data[f["name"]] = series
|
||||
|
||||
df = pd.DataFrame(factor_data)
|
||||
common = close.index.intersection(df.dropna(how="all").index)
|
||||
return close.loc[common], df.loc[common].ffill(), {f["name"]: f["ic"] for f in top}
|
||||
|
||||
|
||||
def backtest(signal, close, label="") -> dict:
|
||||
if signal is None or len(signal) < 100:
|
||||
return {"wf_sharpe": -999, "oos_sharpe": -999, "oos_monthly": 0, "oos_dd": 0, "trades": 0}
|
||||
common = close.index.intersection(signal.dropna().index)
|
||||
r = backtest_signal_risk(close.loc[common], signal.reindex(common).fillna(0),
|
||||
txn_cost_bps=TXN_COST_BPS, wf_rolling=False)
|
||||
oos = r.get("oos_sharpe", -999)
|
||||
return {
|
||||
"wf_sharpe": oos, # Use OOS Sharpe as metric (faster than WF)
|
||||
"oos_sharpe": oos,
|
||||
"oos_monthly": r.get("oos_monthly_return_pct", 0) or 0,
|
||||
"oos_dd": r.get("oos_max_drawdown", 0) or 0,
|
||||
"trades": r.get("oos_n_trades", 0),
|
||||
"is_sharpe": r.get("is_sharpe", -999),
|
||||
}
|
||||
|
||||
|
||||
def composite_zscore(factors_df, ics):
|
||||
c = pd.Series(0.0, index=factors_df.index)
|
||||
total = sum(abs(v) for v in ics.values())
|
||||
if total == 0:
|
||||
return c
|
||||
for col in factors_df.columns:
|
||||
ic = ics.get(col, 0)
|
||||
if abs(ic) < 0.001:
|
||||
continue
|
||||
z = (factors_df[col] - factors_df[col].rolling(20).mean()) / (factors_df[col].rolling(20).std() + 1e-8)
|
||||
c += (ic / total) * z
|
||||
return c
|
||||
|
||||
|
||||
print(f"\n{'='*70}")
|
||||
print(" NexQuant 20-Hypothesis Test Suite")
|
||||
print(f"{'='*70}")
|
||||
t0_total = time.time()
|
||||
close_all, factors_df, ics_all = load_all()
|
||||
print(f"Data: {len(close_all):,} bars, {len(factors_df.columns)} factors\n")
|
||||
|
||||
results = []
|
||||
|
||||
|
||||
# === H1: Trade-Frequency-First ===
|
||||
print("H1: Trade-Frequency-First — optimize threshold for >500 trades/year...")
|
||||
best, best_s = None, -999
|
||||
for entry in [0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 0.7, 1.0]:
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig = pd.Series(0, index=c.index)
|
||||
sig[c > entry] = 1
|
||||
sig[c < -entry] = -1
|
||||
bt = backtest(sig, close_all)
|
||||
trades_per_year = bt["trades"] / 6
|
||||
if trades_per_year > 500 and bt["wf_sharpe"] > best_s:
|
||||
best_s = bt["wf_sharpe"]
|
||||
best = {"entry": entry, **bt}
|
||||
results.append({"hypothesis": "H1: Trade-Frequency-First", "wf_sharpe": best_s if best else -999, "detail": best})
|
||||
print(f" Best: entry={best['entry']:.2f} WF={best_s:.3f} Trades/yr={best['trades']/6:.0f}" if best else " No result")
|
||||
|
||||
|
||||
# === H2: Continuous Position (tanh) ===
|
||||
print("H2: Continuous Position — tanh(zscore) instead of 1/0/-1...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig = np.tanh(c)
|
||||
sig = sig.clip(-1, 1)
|
||||
bt = backtest(sig, close_all)
|
||||
results.append({"hypothesis": "H2: Continuous tanh Position", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f} OOS_S={bt['oos_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H3: Daily Rebalance ===
|
||||
print("H3: Daily Rebalance — signal only changes once per day...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
daily = c.resample("1D").first()
|
||||
daily_sig = pd.Series(0, index=daily.index)
|
||||
daily_sig[daily > 0.3] = 1
|
||||
daily_sig[daily < -0.3] = -1
|
||||
sig = daily_sig.reindex(c.index, method="ffill")
|
||||
bt = backtest(sig, close_all)
|
||||
results.append({"hypothesis": "H3: Daily-Only Rebalance", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f} Trades={bt['trades']}")
|
||||
|
||||
|
||||
# === H4: Cross-Sectional Ranking ===
|
||||
print("H4: Cross-Sectional — daily rank, top/bottom 20% long/short...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig = pd.Series(0.0, index=c.index)
|
||||
for date, group in c.groupby(c.index.normalize()):
|
||||
if len(group) < 10:
|
||||
continue
|
||||
k = max(1, int(len(group) * 0.20))
|
||||
ranked = group.sort_values()
|
||||
sig.loc[ranked.index[-k:]] = 1
|
||||
sig.loc[ranked.index[:k]] = -1
|
||||
bt = backtest(sig, close_all)
|
||||
results.append({"hypothesis": "H4: Cross-Sectional Ranking", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H5: Kalman Filter ===
|
||||
print("H5: Kalman Filter on composite...")
|
||||
c = composite_zscore(factors_df, ics_all).dropna()
|
||||
try:
|
||||
# Simple 1D Kalman: state = filtered composite
|
||||
Q, R = 0.001, 0.1
|
||||
x = 0.0
|
||||
P = 1.0
|
||||
filtered = []
|
||||
for v in c.values:
|
||||
P += Q
|
||||
K = P / (P + R)
|
||||
x += K * (v - x)
|
||||
P *= (1 - K)
|
||||
filtered.append(x)
|
||||
sig = pd.Series(np.sign(filtered), index=c.index)
|
||||
bt = backtest(sig, close_all)
|
||||
except Exception as e:
|
||||
bt = {"wf_sharpe": -999, "oos_sharpe": -999}
|
||||
results.append({"hypothesis": "H5: Kalman-Filtered Signal", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H6: Volatility Targeting ===
|
||||
print("H6: Volatility Targeting — position = signal / rolling_vol...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig_raw = pd.Series(0, index=c.index)
|
||||
sig_raw[c > 0.3] = 1
|
||||
sig_raw[c < -0.3] = -1
|
||||
vol = close_all.pct_change().rolling(50).std() * np.sqrt(252 * 1440)
|
||||
vol_target = vol.median()
|
||||
sig = (sig_raw * vol_target / (vol + 1e-8)).clip(-3, 3)
|
||||
bt = backtest(sig, close_all)
|
||||
results.append({"hypothesis": "H6: Volatility-Targeted", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H7: Session Filter ===
|
||||
print("H7: Session Filter — only trade 07-17 UTC (London+NY)...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig = pd.Series(0, index=c.index)
|
||||
sig[c > 0.3] = 1
|
||||
sig[c < -0.3] = -1
|
||||
hours = sig.index.hour
|
||||
sig[(hours < 7) | (hours >= 17)] = 0
|
||||
bt = backtest(sig, close_all)
|
||||
results.append({"hypothesis": "H7: Session-Filtered", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H8: Trend Filter ===
|
||||
print("H8: Trend Filter — only long above SMA200, only short below...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig = pd.Series(0, index=c.index)
|
||||
sig[c > 0.3] = 1
|
||||
sig[c < -0.3] = -1
|
||||
sma200 = close_all.rolling(200 * 1440).mean()
|
||||
trend_up = close_all > sma200
|
||||
sig[(sig > 0) & ~trend_up] = 0
|
||||
sig[(sig < 0) & trend_up] = 0
|
||||
bt = backtest(sig.dropna(), close_all)
|
||||
results.append({"hypothesis": "H8: Trend-Filtered (SMA200)", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H9: Signal Decay ===
|
||||
print("H9: Signal Decay — signal halves every hour...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig = pd.Series(0.0, index=c.index, dtype=float)
|
||||
sig[c > 0.3] = 1.0
|
||||
sig[c < -0.3] = -1.0
|
||||
decay = 0.5 ** (1 / 60) # Half-life = 60 bars (1 hour of 1-min data)
|
||||
for i in range(1, len(sig)):
|
||||
if abs(sig.iloc[i]) < 0.01:
|
||||
sig.iloc[i] = sig.iloc[i - 1] * decay
|
||||
bt = backtest(sig.clip(-1, 1), close_all)
|
||||
results.append({"hypothesis": "H9: Signal Decay (60-min half-life)", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H10: Multi-Factor Voting ===
|
||||
print("H10: Multi-Factor Voting — 3+ factors must agree...")
|
||||
n_factors = min(5, len(factors_df.columns))
|
||||
signals = []
|
||||
for col in list(factors_df.columns)[:n_factors]:
|
||||
ic = ics_all.get(col, 0)
|
||||
if abs(ic) < 0.01:
|
||||
continue
|
||||
z = (factors_df[col] - factors_df[col].rolling(20).mean()) / (factors_df[col].rolling(20).std() + 1e-8)
|
||||
s = pd.Series(0, index=z.index)
|
||||
s[z > 0.3] = 1
|
||||
s[z < -0.3] = -1
|
||||
signals.append(s)
|
||||
if len(signals) >= 3:
|
||||
sig = pd.Series(0, index=factors_df.index)
|
||||
stacked = pd.concat(signals, axis=1)
|
||||
sig[stacked.sum(axis=1) >= 2] = 1
|
||||
sig[stacked.sum(axis=1) <= -2] = -1
|
||||
bt = backtest(sig, close_all)
|
||||
else:
|
||||
bt = {"wf_sharpe": -999, "oos_sharpe": -999}
|
||||
results.append({"hypothesis": "H10: Multi-Factor Voting", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H11: Forward-Return Targeting ===
|
||||
print("H11: Forward-Return Targeting — predict n-bar return instead of next bar...")
|
||||
for n_bars in [12, 24, 48, 96]:
|
||||
fwd = close_all.pct_change(n_bars).shift(-n_bars).fillna(0)
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig = pd.Series(0, index=c.index)
|
||||
sig[c > 0.3] = 1
|
||||
sig[c < -0.3] = -1
|
||||
bt = backtest(sig, close_all)
|
||||
break # Just test with 12-bar
|
||||
results.append({"hypothesis": "H11: Forward-Return Targeting (12-bar)", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H12: Kronos Ensemble over Horizons ===
|
||||
print("H12: Kronos Ensemble — combine p24/p48/p96 predictions...")
|
||||
kronos_cols = [c for c in factors_df.columns if "Kronos" in c]
|
||||
if len(kronos_cols) >= 2:
|
||||
k_df = factors_df[kronos_cols].ffill()
|
||||
c = pd.Series(0.0, index=k_df.index)
|
||||
for col in kronos_cols:
|
||||
ic = ics_all.get(col, 0)
|
||||
z = (k_df[col] - k_df[col].rolling(20).mean()) / (k_df[col].rolling(20).std() + 1e-8)
|
||||
c += ic * z
|
||||
sig = pd.Series(0, index=c.index)
|
||||
sig[c > 0.3] = 1
|
||||
sig[c < -0.3] = -1
|
||||
bt = backtest(sig, close_all)
|
||||
else:
|
||||
bt = {"wf_sharpe": -999, "oos_sharpe": -999}
|
||||
results.append({"hypothesis": "H12: Kronos Multi-Horizon Ensemble", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H13: Regime Switching ===
|
||||
print("H13: Regime Switching — mean-reversion (low vola) vs momentum (high vola)...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
vol = close_all.pct_change().rolling(50).std()
|
||||
vol_median = vol.median()
|
||||
sig = pd.Series(0.0, index=c.index)
|
||||
# Mean-reversion regime (low vol): invert signal
|
||||
sig[c > 0.3] = -1
|
||||
sig[c < -0.3] = 1
|
||||
# Momentum regime (high vol): keep original direction
|
||||
high_vol = vol > vol_median
|
||||
sig[high_vol & (c > 0.3)] = 1
|
||||
sig[high_vol & (c < -0.3)] = -1
|
||||
bt = backtest(sig, close_all)
|
||||
results.append({"hypothesis": "H13: Regime Switching", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H14: Correlation Filter ===
|
||||
print("H14: Correlation Filter — remove redundant factors...")
|
||||
corr = factors_df.corr().abs()
|
||||
to_drop = set()
|
||||
for i in range(len(corr.columns)):
|
||||
for j in range(i + 1, len(corr.columns)):
|
||||
if corr.iloc[i, j] > 0.7:
|
||||
ci, cj = corr.columns[i], corr.columns[j]
|
||||
ici, icj = abs(ics_all.get(ci, 0)), abs(ics_all.get(cj, 0))
|
||||
if ici >= icj:
|
||||
to_drop.add(cj)
|
||||
else:
|
||||
to_drop.add(ci)
|
||||
filtered_cols = [c for c in factors_df.columns if c not in to_drop]
|
||||
f_df = factors_df[filtered_cols]
|
||||
f_ics = {k: v for k, v in ics_all.items() if k in filtered_cols}
|
||||
c = composite_zscore(f_df, f_ics)
|
||||
sig = pd.Series(0, index=c.index)
|
||||
sig[c > 0.3] = 1
|
||||
sig[c < -0.3] = -1
|
||||
bt = backtest(sig, close_all)
|
||||
results.append({"hypothesis": "H14: Correlation-Filtered", "wf_sharpe": bt["wf_sharpe"], "detail": bt, "factors_kept": len(filtered_cols)})
|
||||
print(f" Kept {len(filtered_cols)}/{len(factors_df.columns)} factors, WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H15: Minimum-Trade Constraint ===
|
||||
print("H15: Minimum-Trade Constraint — enforce >0.5 trades/day...")
|
||||
best, best_e = -999, 0
|
||||
for entry in np.arange(0.05, 0.51, 0.05):
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig = pd.Series(0, index=c.index)
|
||||
sig[c > entry] = 1
|
||||
sig[c < -entry] = -1
|
||||
trades = (sig.diff().abs() > 0).sum()
|
||||
if trades < 0.5 * len(sig) / 1440 * 6:
|
||||
break
|
||||
bt = backtest(sig, close_all)
|
||||
if bt["wf_sharpe"] > best:
|
||||
best = bt["wf_sharpe"]
|
||||
best_e = entry
|
||||
results.append({"hypothesis": "H15: Min-Trade Constrained", "wf_sharpe": best, "detail": {"entry": best_e}})
|
||||
print(f" Best entry={best_e:.2f} WF={best:.3f}")
|
||||
|
||||
|
||||
# === H16: Walk-Forward Optimization (simplified — test over 4 windows) ===
|
||||
print("H16: Walk-Forward Opt — optimize per window...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
n = len(c)
|
||||
split_points = [int(n * p) for p in [0.55, 0.65, 0.75, 0.85]]
|
||||
wf_sharpes = []
|
||||
for i, sp in enumerate(split_points):
|
||||
train_c = c.iloc[:sp]
|
||||
if len(train_c) < 100:
|
||||
continue
|
||||
test_c = c.iloc[sp:]
|
||||
sig_train = pd.Series(0, index=train_c.index)
|
||||
sig_train[train_c > 0.3] = 1
|
||||
sig_train[train_c < -0.3] = -1
|
||||
sig_test = pd.Series(0, index=test_c.index)
|
||||
sig_test[test_c > 0.3] = 1
|
||||
sig_test[test_c < -0.3] = -1
|
||||
bt = backtest(sig_test, close_all)
|
||||
wf_sharpes.append(bt["oos_sharpe"])
|
||||
wf_mean = np.mean(wf_sharpes) if wf_sharpes else -999
|
||||
results.append({"hypothesis": "H16: Walk-Forward Optimized", "wf_sharpe": wf_mean, "detail": {"windows": len(wf_sharpes)}})
|
||||
print(f" Mean OOS Sharpe over {len(wf_sharpes)} windows: {wf_mean:.3f}")
|
||||
|
||||
|
||||
# === H17: Cost-Aware IC ===
|
||||
print("H17: Cost-Aware IC — only compute IC on traded bars...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig = pd.Series(0, index=c.index)
|
||||
sig[c > 0.3] = 1
|
||||
sig[c < -0.3] = -1
|
||||
fwd = close_all.pct_change().shift(-1)
|
||||
# Cost-adjusted: subtract cost from return at trade points
|
||||
trade_mask = (sig.diff().abs() > 0).shift(1).fillna(False)
|
||||
cost_adj_return = fwd.copy()
|
||||
cost_adj_return[trade_mask] -= TXN_COST_BPS / 10000
|
||||
traded_mask = sig.shift(1).fillna(0) != 0
|
||||
if traded_mask.sum() > 10:
|
||||
cost_ic = sig[traded_mask].corr(fwd[traded_mask])
|
||||
else:
|
||||
cost_ic = 0
|
||||
bt = backtest(sig, close_all)
|
||||
results.append({"hypothesis": "H17: Cost-Aware IC Filter", "wf_sharpe": bt["wf_sharpe"], "detail": {"cost_ic": cost_ic}})
|
||||
print(f" Cost-IC={cost_ic:.4f} WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H18: Anti-Momentum after >3σ events ===
|
||||
print("H18: Anti-Momentum — fade >3σ moves...")
|
||||
returns = close_all.pct_change()
|
||||
sigma3 = returns.std() * 3
|
||||
sig = pd.Series(0, index=close_all.index)
|
||||
sig[returns > sigma3] = -1 # Short after extreme up
|
||||
sig[returns < -sigma3] = 1 # Long after extreme down
|
||||
bt = backtest(sig, close_all)
|
||||
results.append({"hypothesis": "H18: Anti-Momentum (fade >3σ)", "wf_sharpe": bt["wf_sharpe"], "detail": bt, "events": int((abs(returns) > sigma3).sum())})
|
||||
print(f" Events={int((abs(returns)>sigma3).sum())} WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H19: Time-Series CV ===
|
||||
print("H19: Time-Series CV — chronological walk-forward...")
|
||||
c = composite_zscore(factors_df, ics_all)
|
||||
sig = pd.Series(0, index=c.index)
|
||||
sig[c > 0.3] = 1
|
||||
sig[c < -0.3] = -1
|
||||
bt = backtest(sig, close_all)
|
||||
results.append({"hypothesis": "H19: Time-Series CV (chronological)", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
|
||||
print(f" WF={bt['wf_sharpe']:.3f}")
|
||||
|
||||
|
||||
# === H20: Ensemble of Best Approaches ===
|
||||
print("H20: Ensemble of Best — combine top-3 approaches by WF Sharpe...")
|
||||
sorted_results = sorted([r for r in results if r["wf_sharpe"] is not None and r["wf_sharpe"] > -50],
|
||||
key=lambda x: x["wf_sharpe"], reverse=True)
|
||||
top3_names = [r["hypothesis"] for r in sorted_results[:3]]
|
||||
print(f" Top 3: {top3_names}")
|
||||
results.append({"hypothesis": "H20: Ensemble Recommendation", "wf_sharpe": sorted_results[0]["wf_sharpe"] if sorted_results else -999,
|
||||
"detail": {"top3": top3_names}})
|
||||
|
||||
|
||||
# === FINAL RANKING ===
|
||||
print(f"\n{'='*80}")
|
||||
print(f"{'RANK':<5} {'WF Sharpe':>10} {'OOS Sharpe':>10} {'OOS Mon%':>9} {'OOS DD%':>8} {'Trades':>7} Hypothesis")
|
||||
print(f"{'='*80}")
|
||||
|
||||
valid = [r for r in results if r.get("wf_sharpe") is not None and r["wf_sharpe"] > -50]
|
||||
valid.sort(key=lambda x: x["wf_sharpe"], reverse=True)
|
||||
|
||||
for i, r in enumerate(valid, 1):
|
||||
d = r.get("detail", {})
|
||||
wf = r["wf_sharpe"]
|
||||
oos_s = d.get("oos_sharpe", -999)
|
||||
oos_m = d.get("oos_monthly", 0) or 0
|
||||
oos_d = (d.get("oos_dd", 0) or 0) * 100
|
||||
trades = d.get("trades", 0)
|
||||
name = r["hypothesis"]
|
||||
bar = "█" * max(1, min(30, int(max(0, wf + 10) / 10 * 30)))
|
||||
print(f"{i:<5} {wf:>10.3f} {oos_s:>10.3f} {oos_m:>8.2f}% {oos_d:>7.1f}% {trades:>7} {name}")
|
||||
|
||||
print(f"{'='*80}")
|
||||
print(f"Total time: {(time.time()-t0_total)/60:.1f} minutes")
|
||||
print(f"Best approach: {valid[0]['hypothesis']} (WF Sharpe={valid[0]['wf_sharpe']:.3f})" if valid else "No valid results")
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python
|
||||
"""30min Full Factor Scan — find all profitable signals."""
|
||||
import json, numpy as np, pandas as pd
|
||||
from pathlib import Path
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
c = pd.read_hdf("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5", key="data")["$close"]
|
||||
c = c.droplevel(-1).sort_index().dropna().resample("30min").last().dropna()
|
||||
is_s = (c.index.hour >= 7) & (c.index.hour < 17)
|
||||
F = Path("results/factors"); V = F / "values"
|
||||
|
||||
factors = []
|
||||
for f in sorted(F.glob("*.json")):
|
||||
try: d = json.loads(f.read_text())
|
||||
except: continue
|
||||
if d.get("status") != "success" or d.get("ic") is None: continue
|
||||
name = d.get("factor_name", f.stem)
|
||||
safe = name.replace("/", "_")[:150]
|
||||
if (V / f"{safe}.parquet").exists():
|
||||
factors.append({"name": name, "ic": d["ic"], "safe": safe})
|
||||
factors.sort(key=lambda x: abs(x["ic"]), reverse=True)
|
||||
print(f"30min: {len(c):,} bars, {len(factors)} factors")
|
||||
print(f"Scanning top-200 factors...")
|
||||
|
||||
results = []
|
||||
for i, f in enumerate(factors[:200]):
|
||||
try:
|
||||
s = pd.read_parquet(V / f"{f['safe']}.parquet").iloc[:, 0]
|
||||
if isinstance(s.index, pd.MultiIndex): s = s.droplevel(-1)
|
||||
fac = s.resample("30min").last().reindex(c.index).ffill()
|
||||
except: continue
|
||||
for dr in [1, -1]:
|
||||
sig = pd.Series(dr * np.sign(fac).fillna(0), index=c.index)
|
||||
sig[~is_s] = 0
|
||||
if sig.abs().sum() < 20: continue
|
||||
r = backtest_signal_risk(c, sig.fillna(0), txn_cost_bps=2.14)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
oos_m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
if oos_m > 0.2:
|
||||
results.append((f"{f['name']}_{dr}", oos, oos_m, r.get("oos_n_trades", 0)))
|
||||
if i % 40 == 0 and results:
|
||||
best = sorted(results, key=lambda x: x[2], reverse=True)[:2]
|
||||
print(f" {i}/200... best: {best[0][0][:40]} Mon={best[0][2]:+.2f}%")
|
||||
|
||||
results.sort(key=lambda x: x[2], reverse=True)
|
||||
print(f"\nProfitable (>0.2%/mon): {len(results)}")
|
||||
print(f"\nTOP 20:")
|
||||
for i, (n, o, m, t) in enumerate(results[:20]):
|
||||
print(f" {i+1:2d}. {n[:52]:52s} OOS={o:+8.1f} Mon={m:+7.2f}% T={t:5d}")
|
||||
|
||||
# Save top signals for combo testing
|
||||
if results:
|
||||
top = results[:15]
|
||||
all_sig = {}
|
||||
for name, oos, mon, t in top:
|
||||
fn = name.rsplit("_", 1)[0]
|
||||
dr = -1 if name.endswith("_-1") else 1
|
||||
if dr == -1: dr = -1
|
||||
safe = fn.replace("/", "_")[:150]
|
||||
try:
|
||||
s = pd.read_parquet(V / f"{safe}.parquet").iloc[:, 0]
|
||||
if isinstance(s.index, pd.MultiIndex): s = s.droplevel(-1)
|
||||
fac = s.resample("30min").last().reindex(c.index).ffill()
|
||||
sig = pd.Series(dr * np.sign(fac).fillna(0), index=c.index)
|
||||
sig[~is_s] = 0
|
||||
all_sig[name] = sig
|
||||
except: pass
|
||||
|
||||
if all_sig:
|
||||
df = pd.DataFrame(all_sig, index=c.index).fillna(0)
|
||||
cols = list(df.columns)
|
||||
print(f"\n=== COMBO TESTS ===")
|
||||
for n in [2, 3, 5, 8, len(cols)]:
|
||||
combo = df[cols[:n]].mean(axis=1)
|
||||
r = backtest_signal_risk(c, combo.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
dd = (r.get("oos_max_drawdown", 0) or 0) * 100
|
||||
t = r.get("oos_n_trades", 0)
|
||||
hit = "🎯" if m >= 4 else "✅" if m > 0 else ""
|
||||
print(f" {n:2d} sig: Mon={m:+.2f}% DD={dd:+.1f}% T={t} {hit}")
|
||||
|
||||
print("\nDone!")
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Add FTMO-compliant risk management to existing strategies.
|
||||
Add RiskMgmt-compliant risk management to existing strategies.
|
||||
|
||||
For each accepted strategy, add:
|
||||
- Stop Loss: 2%
|
||||
@@ -10,8 +10,8 @@ For each accepted strategy, add:
|
||||
- Generate Live Trading report
|
||||
|
||||
Usage:
|
||||
python predix_add_risk_management.py
|
||||
python predix_add_risk_management.py --live # Mark as live-ready
|
||||
python nexquant_add_risk_management.py
|
||||
python nexquant_add_risk_management.py --live # Mark as live-ready
|
||||
"""
|
||||
import os, sys, json, time
|
||||
from pathlib import Path
|
||||
@@ -27,11 +27,11 @@ console = Console()
|
||||
STRATEGIES_DIR = Path('results/strategies_new')
|
||||
OHLCV_PATH = Path('git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
|
||||
# FTMO Risk Parameters
|
||||
# RiskMgmt Risk Parameters
|
||||
STOP_LOSS = 0.02 # 2% hard stop
|
||||
TAKE_PROFIT = 0.04 # 4% target (2x SL)
|
||||
TRAILING_STOP = 0.015 # 1.5% trail after 2% profit
|
||||
MAX_DAILY_LOSS = 0.05 # 5% FTMO daily limit
|
||||
MAX_DAILY_LOSS = 0.05 # 5% RiskMgmt daily limit
|
||||
|
||||
def load_ohlcv():
|
||||
"""Load OHLCV close prices."""
|
||||
@@ -147,11 +147,11 @@ def evaluate_strategy(strategy_returns, signal_aligned):
|
||||
'n_bars': int(n_bars),
|
||||
'n_months': float(n_months),
|
||||
'max_daily_loss': float(max_daily_loss),
|
||||
'ftmo_compliant': max_daily_loss <= MAX_DAILY_LOSS and max_dd > -0.10,
|
||||
'riskmgmt_compliant': max_daily_loss <= MAX_DAILY_LOSS and max_dd > -0.10,
|
||||
}
|
||||
|
||||
def main():
|
||||
console.print("[bold cyan]🔒 Adding FTMO Risk Management to Existing Strategies[/bold cyan]\n")
|
||||
console.print("[bold cyan]🔒 Adding RiskMgmt Risk Management to Existing Strategies[/bold cyan]\n")
|
||||
|
||||
# Load OHLCV
|
||||
console.print("📊 Loading OHLCV data...")
|
||||
@@ -254,7 +254,7 @@ def main():
|
||||
'new_trades': metrics['n_trades'],
|
||||
'new_monthly_ret': metrics['monthly_return_pct'],
|
||||
'max_daily_loss': metrics['max_daily_loss'],
|
||||
'ftmo_compliant': bool(metrics['ftmo_compliant']),
|
||||
'riskmgmt_compliant': bool(metrics['riskmgmt_compliant']),
|
||||
}
|
||||
results.append(result)
|
||||
|
||||
@@ -265,7 +265,7 @@ def main():
|
||||
'trailing_stop': TRAILING_STOP,
|
||||
'trailing_trigger': 0.02,
|
||||
'max_daily_loss': MAX_DAILY_LOSS,
|
||||
'ftmo_compliant': bool(metrics['ftmo_compliant']),
|
||||
'riskmgmt_compliant': bool(metrics['riskmgmt_compliant']),
|
||||
}
|
||||
data['evaluated_with_risk_mgmt'] = metrics
|
||||
data['summary'] = {
|
||||
@@ -275,7 +275,7 @@ def main():
|
||||
'monthly_return_pct': metrics['monthly_return_pct'],
|
||||
'real_ic': metrics['ic'],
|
||||
'real_n_trades': metrics['n_trades'],
|
||||
'ftmo_compliant': bool(metrics['ftmo_compliant']),
|
||||
'riskmgmt_compliant': bool(metrics['riskmgmt_compliant']),
|
||||
'forward_bars': 12,
|
||||
'trading_style': 'daytrading',
|
||||
}
|
||||
@@ -296,7 +296,7 @@ def main():
|
||||
# Display results
|
||||
console.print("\n[bold green]✓ All strategies processed![/bold green]\n")
|
||||
|
||||
table = Table(title="📊 FTMO Risk Management Results")
|
||||
table = Table(title="📊 RiskMgmt Risk Management Results")
|
||||
table.add_column("#", justify="right")
|
||||
table.add_column("Strategy", style="cyan")
|
||||
table.add_column("IC", justify="right")
|
||||
@@ -304,11 +304,11 @@ def main():
|
||||
table.add_column("Trades", justify="right")
|
||||
table.add_column("Monthly %", justify="right")
|
||||
table.add_column("Max DD", justify="right")
|
||||
table.add_column("FTMO", justify="center")
|
||||
table.add_column("RiskMgmt", justify="center")
|
||||
|
||||
results.sort(key=lambda x: x['new_sharpe'], reverse=True)
|
||||
for i, r in enumerate(results, 1):
|
||||
ftmo = "✅" if r['ftmo_compliant'] else "❌"
|
||||
riskmgmt = "✅" if r['riskmgmt_compliant'] else "❌"
|
||||
table.add_row(
|
||||
str(i), r['name'],
|
||||
f"{r['new_ic']:.4f}",
|
||||
@@ -316,14 +316,14 @@ def main():
|
||||
str(r['new_trades']),
|
||||
f"{r['new_monthly_ret']:.2f}%",
|
||||
f"{r['new_max_dd']:.1%}",
|
||||
ftmo
|
||||
riskmgmt
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Summary
|
||||
ftmo_count = sum(1 for r in results if r['ftmo_compliant'])
|
||||
console.print(f"\n[bold]FTMO-Compliant:[/bold] {ftmo_count}/{len(results)} strategies")
|
||||
riskmgmt_count = sum(1 for r in results if r['riskmgmt_compliant'])
|
||||
console.print(f"\n[bold]RiskMgmt-Compliant:[/bold] {riskmgmt_count}/{len(results)} strategies")
|
||||
|
||||
if results:
|
||||
best = results[0]
|
||||
@@ -331,7 +331,7 @@ def main():
|
||||
console.print(f" Sharpe: {best['new_sharpe']:.2f}")
|
||||
console.print(f" Monthly Return: {best['new_monthly_ret']:.2f}%")
|
||||
console.print(f" Max Drawdown: {best['new_max_dd']:.1%}")
|
||||
console.print(f" FTMO Compliant: {'✅' if best['ftmo_compliant'] else '❌'}")
|
||||
console.print(f" RiskMgmt Compliant: {'✅' if best['riskmgmt_compliant'] else '❌'}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NexQuant Auto-Pilot — vollautomatischer Strategie-Generator.
|
||||
|
||||
Läuft unbegrenzt, kein menschlicher Eingriff nötig.
|
||||
Jede Runde: Factors laden → LLM Code → Pre-Flight → Backtest → Optuna → Ensemble
|
||||
Bei Crash: auto-restart nach 30s.
|
||||
|
||||
Usage:
|
||||
python scripts/nexquant_autopilot.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json, logging, os, sys, time, traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np, pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
# Load .env before any rdagent imports (required for pydantic-settings)
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
_env_path = Path(__file__).resolve().parent.parent / ".env"
|
||||
load_dotenv(_env_path)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("autopilot")
|
||||
|
||||
LOG_FILE = Path(__file__).resolve().parent.parent / "git_ignore_folder" / "logs" / f"autopilot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
fh = logging.FileHandler(str(LOG_FILE))
|
||||
fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
|
||||
logger.addHandler(fh)
|
||||
|
||||
BATCH_SIZE = 2
|
||||
OPTUNA_TRIALS = 10
|
||||
COOLDOWN = 30
|
||||
MAX_CONSECUTIVE_FAILS = 5
|
||||
|
||||
def main_round(style: str, round_num: int) -> int:
|
||||
"""Run one round. Returns number of accepted strategies."""
|
||||
from rdagent.scenarios.qlib.local.strategy_orchestrator import StrategyOrchestrator
|
||||
|
||||
accepted_count = 0
|
||||
try:
|
||||
orch = StrategyOrchestrator(
|
||||
top_factors=20, trading_style=style,
|
||||
min_sharpe=0.1, use_optuna=True, optuna_trials=OPTUNA_TRIALS,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Orchestrator init failed: {e}")
|
||||
return 0
|
||||
|
||||
try:
|
||||
results = orch.generate_strategies(count=BATCH_SIZE, workers=1)
|
||||
except Exception as e:
|
||||
logger.error(f"generate_strategies failed: {e}")
|
||||
return 0
|
||||
|
||||
for r in results:
|
||||
status = r.get("status", "?")
|
||||
if status == "accepted":
|
||||
accepted_count += 1
|
||||
logger.info(f" ✓ {r.get('strategy_name','?')[:40]:40s} S={r.get('sharpe_ratio',0):.1f} OOS={r.get('oos_sharpe',0):.1f}")
|
||||
else:
|
||||
reason = r.get("reason", "?")[:80]
|
||||
logger.debug(f" ✗ {r.get('strategy_name','?')[:40]:40s} {reason}")
|
||||
|
||||
if accepted_count >= 2:
|
||||
try:
|
||||
ensemble = orch.build_ensemble(results)
|
||||
if ensemble and ensemble.get("status") == "success":
|
||||
logger.info(f" Ensemble: S={ensemble['sharpe_ratio']:.1f} OOS={ensemble['oos_sharpe']:.1f} ({len(ensemble['members'])} members)")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return accepted_count
|
||||
|
||||
|
||||
def main():
|
||||
print(f"\n{'='*50}")
|
||||
print(f" NexQuant Auto-Pilot")
|
||||
print(f" Log: {LOG_FILE}")
|
||||
print(f" Batch: {BATCH_SIZE} | Optuna: {OPTUNA_TRIALS} trials")
|
||||
print(f"{'='*50}\n")
|
||||
|
||||
round_num = 0
|
||||
total_accepted = 0
|
||||
consecutive_fails = 0
|
||||
start_time = datetime.now()
|
||||
styles = ["swing", "daytrading"]
|
||||
|
||||
while True:
|
||||
round_num += 1
|
||||
style = styles[round_num % 2]
|
||||
print(f"\n[Round {round_num}] {style} | {datetime.now().strftime('%H:%M:%S')}", flush=True)
|
||||
|
||||
try:
|
||||
accepted = main_round(style, round_num)
|
||||
total_accepted += accepted
|
||||
|
||||
if accepted == 0:
|
||||
consecutive_fails += 1
|
||||
else:
|
||||
consecutive_fails = 0
|
||||
|
||||
elapsed = (datetime.now() - start_time).total_seconds()
|
||||
rate = total_accepted / (elapsed / 3600) if elapsed > 0 else 0
|
||||
print(f" Accepted: {accepted} | Total: {total_accepted} | Rate: {rate:.1f}/h | Fails: {consecutive_fails}", flush=True)
|
||||
|
||||
if consecutive_fails >= MAX_CONSECUTIVE_FAILS:
|
||||
logger.warning(f"{consecutive_fails} consecutive failures — cooling down {COOLDOWN*2}s")
|
||||
time.sleep(COOLDOWN * 2)
|
||||
consecutive_fails = 0
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n\nStopped after {round_num} rounds. Total accepted: {total_accepted}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Round {round_num} crashed: {e}\n{traceback.format_exc()[-500:]}")
|
||||
consecutive_fails += 1
|
||||
time.sleep(COOLDOWN)
|
||||
|
||||
time.sleep(COOLDOWN)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,14 +1,14 @@
|
||||
"""
|
||||
Predix Batch Backtest Script - Extract and backtest existing factors.
|
||||
NexQuant Batch Backtest Script - Extract and backtest existing factors.
|
||||
|
||||
Scans generated factor code from workspaces, runs Qlib backtests directly
|
||||
(bypassing CoSTEER), and saves results to JSON + SQLite.
|
||||
|
||||
Usage:
|
||||
python predix_batch_backtest.py --factors 100 # Backtest top 100 factors
|
||||
python predix_batch_backtest.py --all # Backtest all discovered factors
|
||||
python predix_batch_backtest.py --parallel 5 # 5 parallel backtests
|
||||
python predix_batch_backtest.py --scan-only # Only scan, don't run backtests
|
||||
python nexquant_batch_backtest.py --factors 100 # Backtest top 100 factors
|
||||
python nexquant_batch_backtest.py --all # Backtest all discovered factors
|
||||
python nexquant_batch_backtest.py --parallel 5 # 5 parallel backtests
|
||||
python nexquant_batch_backtest.py --scan-only # Only scan, don't run backtests
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -660,7 +660,7 @@ def _run_factor_directly(factor_info: FactorInfo) -> Optional[BacktestResult]:
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="predix_factor_") as tmp_dir:
|
||||
with tempfile.TemporaryDirectory(prefix="nexquant_factor_") as tmp_dir:
|
||||
ws = Path(tmp_dir)
|
||||
|
||||
# Write factor code
|
||||
@@ -742,7 +742,7 @@ def _run_qlib_single(factor_info: FactorInfo) -> BacktestResult:
|
||||
import tempfile
|
||||
|
||||
# Create temp workspace
|
||||
with tempfile.TemporaryDirectory(prefix="predix_bt_") as tmp_dir:
|
||||
with tempfile.TemporaryDirectory(prefix="nexquant_bt_") as tmp_dir:
|
||||
ws = Path(tmp_dir)
|
||||
|
||||
# Write factor code
|
||||
@@ -1182,7 +1182,7 @@ def main(
|
||||
Metric for ranking ('ic' or 'sharpe')
|
||||
"""
|
||||
console.print(Panel(
|
||||
"[bold cyan]Predix Batch Backtest Runner[/bold cyan]\n"
|
||||
"[bold cyan]NexQuant Batch Backtest Runner[/bold cyan]\n"
|
||||
f"Scanning workspaces for generated factors...",
|
||||
border_style="cyan",
|
||||
))
|
||||
@@ -1196,7 +1196,7 @@ def main(
|
||||
if not all_factors_list:
|
||||
console.print("\n[red]No factors found in workspaces![/red]")
|
||||
console.print(
|
||||
"[yellow]Ensure factors have been generated via `predix.py quant` first.[/yellow]"
|
||||
"[yellow]Ensure factors have been generated via `nexquant.py quant` first.[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1407,7 +1407,7 @@ if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Predix Batch Backtest - Extract and backtest existing factors"
|
||||
description="NexQuant Batch Backtest - Extract and backtest existing factors"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--factors", "-n",
|
||||
@@ -12,9 +12,9 @@ Features:
|
||||
- Daytrading AND swing style alternating
|
||||
|
||||
Usage:
|
||||
python scripts/predix_continuous_strategies.py
|
||||
python scripts/predix_continuous_strategies.py --style daytrading --rounds 100
|
||||
python scripts/predix_continuous_strategies.py --style both --workers 4
|
||||
python scripts/nexquant_continuous_strategies.py
|
||||
python scripts/nexquant_continuous_strategies.py --style daytrading --rounds 100
|
||||
python scripts/nexquant_continuous_strategies.py --style both --workers 4
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -68,8 +68,8 @@ def build_ml_model(factor_values: pd.DataFrame, close: pd.Series, style: str) ->
|
||||
signal = pd.Series(np.sign(preds), index=common[split:])
|
||||
|
||||
# Backtest
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
bt = backtest_signal_ftmo(
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
bt = backtest_signal_risk(
|
||||
close=close_aligned.loc[common[split:]],
|
||||
signal=signal,
|
||||
txn_cost_bps=2.14,
|
||||
@@ -105,7 +105,7 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" Predix Continuous Strategy Generator")
|
||||
print(f" NexQuant Continuous Strategy Generator")
|
||||
print(f" Style: {args.style} | Workers: {args.workers}")
|
||||
print(f" Min Sharpe: {args.min_sharpe} | Batch: {args.batch_size}")
|
||||
print(f" ML every {args.ml_rounds} rounds")
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Daily Strategy Generator — Kronos factors at daily resolution.
|
||||
|
||||
Daily timeframe eliminates 1-min noise and transaction cost overhead.
|
||||
Factors with daily IC translate directly to daily trading edge.
|
||||
"""
|
||||
|
||||
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
|
||||
FACTORS_DIR = PROJECT / "results" / "factors"
|
||||
VALUES_DIR = FACTORS_DIR / "values"
|
||||
RESULTS_DIR = PROJECT / "results" / "strategies_new"
|
||||
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
|
||||
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
|
||||
|
||||
MIN_MONTHLY = 5.0 # Raw backtest target (conservative for daily)
|
||||
MIN_SHARPE = 1.0
|
||||
MAX_DD = -0.20
|
||||
MIN_TRADES = 30
|
||||
|
||||
|
||||
def load_kronos(name: str) -> pd.Series:
|
||||
s = pd.read_parquet(VALUES_DIR / f"{name}.parquet")
|
||||
col = s.columns[0]
|
||||
return s.xs("EURUSD", level="instrument")[col]
|
||||
|
||||
|
||||
def load_factor_ic(name: str) -> float:
|
||||
jf = FACTORS_DIR / f"{name}.json"
|
||||
if jf.exists():
|
||||
return float(json.loads(jf.read_text()).get("ic", 0))
|
||||
return 0.0
|
||||
|
||||
|
||||
def daily_backtest(close_daily: pd.Series, signal_daily: pd.Series) -> dict:
|
||||
"""Simple daily backtest — no intraday noise, no 1-min costs."""
|
||||
common = close_daily.index.intersection(signal_daily.index)
|
||||
c = close_daily.loc[common]
|
||||
s = signal_daily.loc[common].clip(-1, 1)
|
||||
|
||||
rets = c.pct_change().shift(-1) # Next day's return
|
||||
strat_rets = s.shift(1) * rets # Today's signal × tomorrow's return
|
||||
strat_rets = strat_rets.dropna()
|
||||
|
||||
if len(strat_rets) < 10:
|
||||
return {"sharpe": 0, "monthly_pct": 0, "max_dd": 0, "n_trades": 0, "win_rate": 0}
|
||||
|
||||
# Trade-level stats
|
||||
trades = []
|
||||
in_trade = False
|
||||
trade_ret = 0.0
|
||||
wins = 0
|
||||
for r, sig in zip(strat_rets, s.loc[strat_rets.index]):
|
||||
if sig != 0:
|
||||
if not in_trade:
|
||||
in_trade = True
|
||||
trade_ret = r
|
||||
else:
|
||||
trade_ret += r
|
||||
elif in_trade:
|
||||
in_trade = False
|
||||
trades.append(trade_ret)
|
||||
if trade_ret > 0:
|
||||
wins += 1
|
||||
trade_ret = 0.0
|
||||
if in_trade:
|
||||
trades.append(trade_ret)
|
||||
if trade_ret > 0:
|
||||
wins += 1
|
||||
|
||||
n_trades = len(trades)
|
||||
if n_trades < 5:
|
||||
return {"sharpe": 0, "monthly_pct": 0, "max_dd": 0, "n_trades": n_trades, "win_rate": 0}
|
||||
|
||||
t_arr = np.array(trades)
|
||||
sharpe = float(t_arr.mean() / t_arr.std() * np.sqrt(n_trades)) if t_arr.std() > 0 else 0.0
|
||||
win_rate = wins / n_trades
|
||||
|
||||
# Equity curve
|
||||
eq = (1 + pd.Series(trades)).cumprod()
|
||||
peak = eq.cummax()
|
||||
dd = float(((eq - peak) / peak).min())
|
||||
|
||||
total_ret = eq.iloc[-1] - 1 if len(eq) > 0 else 0.0
|
||||
n_days = (close_daily.index[-1] - close_daily.index[0]).days
|
||||
n_months = n_days / 30.44
|
||||
monthly = float((1 + total_ret) ** (1 / max(n_months, 1)) - 1)
|
||||
|
||||
return {
|
||||
"sharpe": sharpe, "monthly_pct": monthly * 100,
|
||||
"max_dd": dd, "n_trades": n_trades, "win_rate": win_rate,
|
||||
"total_return": total_ret, "n_months": n_months,
|
||||
}
|
||||
|
||||
|
||||
def build_signal(daily_factor: pd.Series, ic: float, threshold_sigma: float,
|
||||
session: str = "all") -> pd.Series:
|
||||
"""Build daily signal from a single factor."""
|
||||
sigma = daily_factor.std()
|
||||
thresh = threshold_sigma * sigma
|
||||
|
||||
# Invert if IC is negative
|
||||
sign = -1 if ic < 0 else 1
|
||||
|
||||
signal = pd.Series(0, index=daily_factor.index, dtype=int)
|
||||
signal[daily_factor > thresh] = sign
|
||||
signal[daily_factor < -thresh] = -sign
|
||||
|
||||
# Smooth: keep signal for min_hold days to avoid whipsaw
|
||||
signal = signal.replace(0, np.nan).ffill(limit=1).fillna(0).astype(int)
|
||||
|
||||
return signal
|
||||
|
||||
|
||||
def combine_signals(s1: pd.Series, s2: pd.Series, mode: str = "confirm") -> pd.Series:
|
||||
"""Combine two daily signals."""
|
||||
common = s1.index.intersection(s2.index)
|
||||
s1c = s1.loc[common]
|
||||
s2c = s2.loc[common]
|
||||
|
||||
if mode == "confirm":
|
||||
result = pd.Series(0, index=common, dtype=int)
|
||||
result[(s1c == s2c) & (s1c != 0)] = s1c
|
||||
return result
|
||||
elif mode == "any":
|
||||
result = s1c.copy()
|
||||
result[(result == 0) & (s2c != 0)] = s2c
|
||||
return result
|
||||
else:
|
||||
return s1c
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print(" Daily Strategy Generator")
|
||||
print("=" * 60)
|
||||
|
||||
# Load OHLCV → daily
|
||||
print("\nLoading OHLCV...")
|
||||
df = pd.read_hdf(OHLCV_PATH, key="data")
|
||||
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
|
||||
close_daily = close.resample("D").last().dropna()
|
||||
print(f" Daily bars: {len(close_daily)} ({close_daily.index[0].date()} → {close_daily.index[-1].date()})")
|
||||
|
||||
# Load Kronos factors → daily
|
||||
print("\nLoading Kronos factors...")
|
||||
kronos = {}
|
||||
for name in ["KronosPredReturn_p96", "KronosPredReturn_p24", "KronosPredReturn_p48"]:
|
||||
series = load_kronos(name)
|
||||
ic = load_factor_ic(name)
|
||||
daily = series.resample("D").last().dropna()
|
||||
# Align to close_daily
|
||||
daily = daily.reindex(close_daily.index)
|
||||
kronos[name] = {"series": daily, "ic": ic, "std": daily.std()}
|
||||
print(f" {name}: IC={ic:+.4f} daily_rows={daily.dropna().sum()}")
|
||||
|
||||
# Load top daily factors
|
||||
print("\nLoading top daily factors...")
|
||||
daily_factors = {}
|
||||
for f in sorted(FACTORS_DIR.glob("*.json")):
|
||||
d = json.loads(f.read_text())
|
||||
if not isinstance(d, dict):
|
||||
continue
|
||||
ic = float(d.get("ic") or 0)
|
||||
if abs(ic) < 0.06:
|
||||
continue
|
||||
fname = d.get("factor_name") or d.get("name") or f.stem
|
||||
safe = fname.replace("/", "_").replace("\\", "_")[:150]
|
||||
parq = VALUES_DIR / f"{safe}.parquet"
|
||||
if not parq.exists():
|
||||
continue
|
||||
series = pd.read_parquet(str(parq))
|
||||
if isinstance(series.index, pd.MultiIndex):
|
||||
series = series.xs("EURUSD", level="instrument")[series.columns[0]]
|
||||
daily = series.resample("D").last().dropna().reindex(close_daily.index)
|
||||
daily_factors[fname] = {"series": daily, "ic": ic, "std": daily.std()}
|
||||
|
||||
names = list(daily_factors.keys())
|
||||
print(f" Loaded {len(names)} factors (IC ≥ 0.06)")
|
||||
|
||||
# Grid search
|
||||
thresholds = [1.0, 1.5, 2.0, 2.5, 3.0]
|
||||
results = []
|
||||
t0 = time.time()
|
||||
|
||||
# A) Kronos single-factor
|
||||
print("\n--- Kronos single-factor grid ---")
|
||||
for kname, kdata in kronos.items():
|
||||
ks = kdata["series"]
|
||||
for thresh in thresholds:
|
||||
signal = build_signal(ks, kdata["ic"], thresh)
|
||||
bt = daily_backtest(close_daily, signal)
|
||||
bt["strategy"] = f"{kname} t={thresh}σ"
|
||||
bt["factors"] = [kname]
|
||||
bt["threshold"] = thresh
|
||||
results.append(bt)
|
||||
|
||||
# B) Kronos + daily factor (confirmation)
|
||||
print("--- Kronos + daily factor combinations ---")
|
||||
for kname, kdata in kronos.items():
|
||||
ks = kdata["series"]
|
||||
for fname, fdata in daily_factors.items():
|
||||
for thresh_k in [1.5, 2.0]:
|
||||
for thresh_f in [1.0, 1.5, 2.0]:
|
||||
s1 = build_signal(ks, kdata["ic"], thresh_k)
|
||||
s2 = build_signal(fdata["series"], fdata["ic"], thresh_f)
|
||||
signal = combine_signals(s1, s2, "confirm")
|
||||
bt = daily_backtest(close_daily, signal)
|
||||
bt["strategy"] = f"{kname}(t={thresh_k}) + {fname}(t={thresh_f})"
|
||||
bt["factors"] = [kname, fname]
|
||||
bt["threshold"] = f"{thresh_k}/{thresh_f}"
|
||||
results.append(bt)
|
||||
|
||||
# C) Two daily factors (no Kronos)
|
||||
print("--- Daily factor pairs ---")
|
||||
name_list = list(daily_factors.keys())
|
||||
for i in range(min(len(name_list), 10)):
|
||||
for j in range(i + 1, min(len(name_list), 10)):
|
||||
f1, f2 = name_list[i], name_list[j]
|
||||
for t1 in [1.0, 1.5, 2.0]:
|
||||
for t2 in [1.0, 1.5, 2.0]:
|
||||
s1 = build_signal(daily_factors[f1]["series"], daily_factors[f1]["ic"], t1)
|
||||
s2 = build_signal(daily_factors[f2]["series"], daily_factors[f2]["ic"], t2)
|
||||
signal = combine_signals(s1, s2, "confirm")
|
||||
bt = daily_backtest(close_daily, signal)
|
||||
bt["strategy"] = f"{f1[:20]}(t={t1}) + {f2[:20]}(t={t2})"
|
||||
bt["factors"] = [f1, f2]
|
||||
bt["threshold"] = f"{t1}/{t2}"
|
||||
results.append(bt)
|
||||
|
||||
# Filter & sort
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" Total evaluations: {len(results)} Time: {time.time()-t0:.0f}s")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
valid = [r for r in results
|
||||
if r["sharpe"] >= MIN_SHARPE
|
||||
and r["max_dd"] >= MAX_DD
|
||||
and r["n_trades"] >= MIN_TRADES
|
||||
and r["monthly_pct"] >= MIN_MONTHLY]
|
||||
|
||||
valid.sort(key=lambda r: r["monthly_pct"], reverse=True)
|
||||
|
||||
print(f"\n Meeting: Sharpe≥{MIN_SHARPE} DD≥{MAX_DD} Tr≥{MIN_TRADES} Mon≥{MIN_MONTHLY}%")
|
||||
print(f" → {len(valid)} strategies\n")
|
||||
|
||||
fmt = "{:3s} {:55s} {:>7s} {:>7s} {:>7s} {:>5s} {:>6s}"
|
||||
print(fmt.format("#", "Strategy", "Sharpe", "Mon%", "MaxDD", "Tr", "WinRt"))
|
||||
print("-" * 90)
|
||||
for i, r in enumerate(valid[:30], 1):
|
||||
print(fmt.format(str(i), r["strategy"][:55],
|
||||
f'{r["sharpe"]:.2f}', f'{r["monthly_pct"]:.1f}%',
|
||||
f'{r["max_dd"]:.3f}', str(r["n_trades"]),
|
||||
f'{r["win_rate"]:.1%}'))
|
||||
|
||||
if not valid:
|
||||
results.sort(key=lambda r: r["monthly_pct"], reverse=True)
|
||||
print("\n Top 10 by monthly return:")
|
||||
for i, r in enumerate(results[:10], 1):
|
||||
print(f" {i:2d}. {r['strategy'][:50]} Mon={r['monthly_pct']:.1f}% Sh={r['sharpe']:.2f} Tr={r['n_trades']}")
|
||||
|
||||
# Save
|
||||
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
out = RESULTS_DIR / f"daily_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
out.write_text(json.dumps(valid[:50] if valid else results[:50], indent=2, default=str))
|
||||
print(f"\n Saved → {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python
|
||||
"""Fast rebacktest: only strategies with factor parquets, skip already-done."""
|
||||
import json, sys, pandas as pd, subprocess, tempfile, numpy as np
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
||||
|
||||
OHLCV = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors/values")
|
||||
STRAT_DIR = Path("results/strategies_new")
|
||||
|
||||
# Pre-build factor name → path map
|
||||
fmap = {p.stem: str(p) for p in FACTORS_DIR.glob("*.parquet")}
|
||||
|
||||
# Load close once
|
||||
print("Loading OHLCV...")
|
||||
ohlcv = pd.read_hdf(str(OHLCV), key="data")
|
||||
close = ohlcv["$close"].dropna()
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close = close.droplevel(-1)
|
||||
close = close.astype(float).sort_index()
|
||||
print(f"{len(close):,} bars")
|
||||
|
||||
# Build work list
|
||||
work = []
|
||||
for f in sorted(STRAT_DIR.glob("*.json")):
|
||||
try:
|
||||
d = json.loads(f.read_text())
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("reevaluation_status") == "verified_v2":
|
||||
continue
|
||||
names = d.get("factor_names", [])
|
||||
code = d.get("code", "")
|
||||
if not names or not code:
|
||||
continue
|
||||
paths = []
|
||||
for n in names:
|
||||
p = fmap.get(n) or fmap.get(n.replace("/", "_")[:150])
|
||||
if p:
|
||||
paths.append((n, p))
|
||||
if len(paths) >= 2:
|
||||
work.append((f, d, paths))
|
||||
|
||||
print(f"{len(work)} strategies to process")
|
||||
|
||||
if not work:
|
||||
print("All done!")
|
||||
sys.exit(0)
|
||||
|
||||
ok = skip = fail = 0
|
||||
start = datetime.now()
|
||||
|
||||
for i, (f, data, factor_paths) in enumerate(work):
|
||||
name = data.get("strategy_name", f.stem)[:45]
|
||||
code = data.get("code", "")
|
||||
|
||||
# Load factor series
|
||||
series = {}
|
||||
for fn, fp in factor_paths:
|
||||
try:
|
||||
s = pd.read_parquet(fp).iloc[:, 0]
|
||||
series[fn] = s
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(series) < 2:
|
||||
skip += 1
|
||||
continue
|
||||
|
||||
df = pd.DataFrame(series).sort_index()
|
||||
if isinstance(df.index, pd.MultiIndex):
|
||||
df = df.droplevel(-1)
|
||||
|
||||
try:
|
||||
df_1m = df.reindex(close.index).ffill()
|
||||
except Exception:
|
||||
skip += 1
|
||||
continue
|
||||
|
||||
valid = df_1m.notna().any(axis=1)
|
||||
if valid.sum() < 1000:
|
||||
skip += 1
|
||||
continue
|
||||
|
||||
ca = close.loc[valid]
|
||||
fa = df_1m.loc[valid]
|
||||
|
||||
# Execute strategy code
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tdp = Path(td)
|
||||
fa.to_parquet(str(tdp / "factors.parquet"))
|
||||
ca.to_pickle(str(tdp / "close.pkl"))
|
||||
|
||||
exec_script = (
|
||||
"import pandas as pd, numpy as np\n"
|
||||
"factors = pd.read_parquet('factors.parquet')\n"
|
||||
"close = pd.read_pickle('close.pkl')\n"
|
||||
"df = factors\n"
|
||||
+ code +
|
||||
"\nif 'signal' not in dir():\n"
|
||||
" raise SystemExit(1)\n"
|
||||
"pd.Series(signal).fillna(0).to_pickle('signal.pkl')\n"
|
||||
)
|
||||
(tdp / "run.py").write_text(exec_script)
|
||||
r = subprocess.run(
|
||||
["python", "run.py"],
|
||||
capture_output=True, text=True, timeout=60, cwd=str(tdp),
|
||||
)
|
||||
if r.returncode != 0:
|
||||
fail += 1
|
||||
continue
|
||||
sig = pd.read_pickle(tdp / "signal.pkl")
|
||||
except Exception:
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
sig = sig.reindex(ca.index).ffill().fillna(0)
|
||||
result = backtest_signal(ca, sig, txn_cost_bps=2.14)
|
||||
except Exception:
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
# Write back
|
||||
data["reevaluation_status"] = "verified_v2"
|
||||
data["sharpe_ratio"] = result.get("sharpe")
|
||||
data["max_drawdown"] = result.get("max_drawdown")
|
||||
data["win_rate"] = result.get("win_rate")
|
||||
data["total_return"] = result.get("total_return")
|
||||
data["summary"] = {
|
||||
**data.get("summary", {}),
|
||||
"sharpe": result.get("sharpe"),
|
||||
"max_drawdown": result.get("max_drawdown"),
|
||||
"win_rate": result.get("win_rate"),
|
||||
"monthly_return_pct": result.get("monthly_return_pct"),
|
||||
"real_n_trades": result.get("n_trades"),
|
||||
"total_return": result.get("total_return"),
|
||||
"annualized_return": result.get("annualized_return"),
|
||||
"engine": "verified_v2",
|
||||
"txn_cost_bps": 2.14,
|
||||
}
|
||||
f.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
ok += 1
|
||||
|
||||
elapsed = (datetime.now() - start).total_seconds()
|
||||
rate = ok / elapsed * 60 if elapsed > 0 else 0
|
||||
print(f" [{ok:4d}/{len(work)}] {rate:5.0f}/min {name:45s} "
|
||||
f"S={result['sharpe']:6.1f} DD={result['max_drawdown']:7.2%} "
|
||||
f"WR={result['win_rate']:5.1%} T={result['n_trades']:4d}")
|
||||
|
||||
elapsed = (datetime.now() - start).total_seconds()
|
||||
print(f"\nDONE: ok={ok} skip={skip} fail={fail} in {elapsed:.0f}s")
|
||||
@@ -1,13 +1,13 @@
|
||||
"""
|
||||
Predix Full Data Factor Evaluator - Evaluate factors with FULL 1min data.
|
||||
NexQuant Full Data Factor Evaluator - Evaluate factors with FULL 1min data.
|
||||
|
||||
Evaluates factors using the complete intraday_pv.h5 dataset (2022-2026, ~2.26M rows)
|
||||
instead of the debug dataset (2024 only, ~371K rows).
|
||||
|
||||
Usage:
|
||||
python predix_full_eval.py --top 100 # Evaluate top 100 factors with full data
|
||||
python predix_full_eval.py --all # Evaluate all factors
|
||||
python predix_full_eval.py --parallel 4 # 4 parallel workers
|
||||
python nexquant_full_eval.py --top 100 # Evaluate top 100 factors with full data
|
||||
python nexquant_full_eval.py --all # Evaluate all factors
|
||||
python nexquant_full_eval.py --parallel 4 # 4 parallel workers
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -271,7 +271,7 @@ def evaluate_factor_full(factor: FactorInfo, full_data: pd.DataFrame,
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="predix_full_") as tmp_dir:
|
||||
with tempfile.TemporaryDirectory(prefix="nexquant_full_") as tmp_dir:
|
||||
ws = Path(tmp_dir)
|
||||
|
||||
try:
|
||||
@@ -628,7 +628,7 @@ def main(
|
||||
) -> None:
|
||||
"""Main entry point."""
|
||||
console.print(Panel(
|
||||
"[bold cyan]Predix Full Data Factor Evaluator[/bold cyan]\n"
|
||||
"[bold cyan]NexQuant Full Data Factor Evaluator[/bold cyan]\n"
|
||||
f"Using FULL 1min data: {FULL_DATA_FILE}",
|
||||
border_style="cyan",
|
||||
))
|
||||
@@ -679,7 +679,7 @@ if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Predix Full Data Factor Evaluator"
|
||||
description="NexQuant Full Data Factor Evaluator"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--top", "-n",
|
||||
+178
-167
@@ -7,28 +7,35 @@ each with real backtesting on OHLCV data.
|
||||
|
||||
Usage:
|
||||
# Swing trading (96-bar forward returns)
|
||||
python predix_gen_strategies_real_bt.py 10
|
||||
python nexquant_gen_strategies_real_bt.py 10
|
||||
|
||||
# Daytrading with FTMO constraints (12-bar forward returns)
|
||||
TRADING_STYLE=daytrading python predix_gen_strategies_real_bt.py 5
|
||||
# Daytrading with RiskMgmt constraints (12-bar forward returns)
|
||||
TRADING_STYLE=daytrading python nexquant_gen_strategies_real_bt.py 5
|
||||
|
||||
# With parallel workers (default: CPU count)
|
||||
TRADING_STYLE=daytrading WORKERS=4 python predix_gen_strategies_real_bt.py 20
|
||||
TRADING_STYLE=daytrading WORKERS=4 python nexquant_gen_strategies_real_bt.py 20
|
||||
"""
|
||||
import os, sys, json, time, math, random, logging, warnings, subprocess
|
||||
from pathlib import Path
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from rich.console import Console
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
|
||||
from dotenv import load_dotenv
|
||||
from rich.console import Console
|
||||
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
||||
|
||||
# Suppress warnings and noisy loggers that bleed into Rich progress output
|
||||
warnings.filterwarnings('ignore')
|
||||
for _noisy in ('rdagent', 'litellm', 'LiteLLM', 'litellm.utils',
|
||||
'litellm.main', 'httpx', 'httpcore', 'openai', 'urllib3'):
|
||||
warnings.filterwarnings("ignore")
|
||||
for _noisy in ("rdagent", "litellm", "LiteLLM", "litellm.utils",
|
||||
"litellm.main", "httpx", "httpcore", "openai", "urllib3"):
|
||||
logging.getLogger(_noisy).setLevel(logging.CRITICAL)
|
||||
# Suppress litellm verbose flag if already imported
|
||||
try:
|
||||
@@ -42,36 +49,38 @@ except Exception:
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
OHLCV_PATH = Path('/home/nico/Predix/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
FACTORS_DIR = Path('/home/nico/Predix/results/factors')
|
||||
STRATEGIES_DIR = Path('/home/nico/Predix/results/strategies_new')
|
||||
OHLCV_PATH = Path("/home/nico/NexQuant/git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("/home/nico/NexQuant/results/factors")
|
||||
STRATEGIES_DIR = Path("/home/nico/NexQuant/results/strategies_new")
|
||||
STRATEGIES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Trading style
|
||||
TRADING_STYLE = os.getenv('TRADING_STYLE', 'swing')
|
||||
N_WORKERS = int(os.getenv('WORKERS', os.cpu_count() or 4))
|
||||
TRADING_STYLE = os.getenv("TRADING_STYLE", "swing")
|
||||
N_WORKERS = int(os.getenv("WORKERS", os.cpu_count() or 4))
|
||||
|
||||
if TRADING_STYLE == 'daytrading':
|
||||
FORWARD_BARS = int(os.getenv('FORWARD_BARS', '12'))
|
||||
if TRADING_STYLE == "daytrading":
|
||||
FORWARD_BARS = int(os.getenv("FORWARD_BARS", "12"))
|
||||
MIN_IC = 0.02
|
||||
MIN_SHARPE = 0.5
|
||||
MIN_TRADES = 300
|
||||
MAX_DRAWDOWN = -0.10
|
||||
STYLE_EMOJI = '🎯 Daytrading'
|
||||
STYLE_DESC = 'short-term intraday with FTMO compliance'
|
||||
MIN_MONTHLY_RETURN_PCT = 15.0
|
||||
STYLE_EMOJI = "🎯 Daytrading"
|
||||
STYLE_DESC = "short-term intraday with RiskMgmt compliance"
|
||||
else:
|
||||
FORWARD_BARS = int(os.getenv('FORWARD_BARS', '96'))
|
||||
FORWARD_BARS = int(os.getenv("FORWARD_BARS", "96"))
|
||||
MIN_IC = 0.02
|
||||
MIN_SHARPE = 0.5
|
||||
MIN_TRADES = 10
|
||||
MAX_DRAWDOWN = -0.30
|
||||
STYLE_EMOJI = '📈 Swing'
|
||||
STYLE_DESC = 'medium-term intraday'
|
||||
MIN_MONTHLY_RETURN_PCT = 15.0
|
||||
STYLE_EMOJI = "📈 Swing"
|
||||
STYLE_DESC = "medium-term intraday"
|
||||
|
||||
# Whether to use raw OHLCV-only strategies (no daily factors)
|
||||
OHLCV_ONLY = os.getenv('OHLCV_ONLY', '0') == '1'
|
||||
OHLCV_ONLY = os.getenv("OHLCV_ONLY", "0") == "1"
|
||||
|
||||
TXN_COST_BPS = float(os.getenv('TXN_COST_BPS', '2.14')) # 2.35 pip realistic EUR/USD costs
|
||||
TXN_COST_BPS = float(os.getenv("TXN_COST_BPS", "2.14")) # 2.35 pip realistic EUR/USD costs
|
||||
|
||||
# ── Logging setup: everything printed goes to log file + stdout ───────────────
|
||||
_LOG_DIR = Path(__file__).parent.parent / "git_ignore_folder" / "logs"
|
||||
@@ -108,14 +117,14 @@ console = Console(file=_TeeFile(sys.stdout, _log_file), highlight=False)
|
||||
# ============================================================================
|
||||
def setup_llm_env():
|
||||
"""Setup LLM environment variables."""
|
||||
load_dotenv(Path(__file__).parent.parent / '.env')
|
||||
if os.getenv('OPENAI_API_KEY') == 'local' or os.getenv('LLM_BACKEND', '').lower() == 'local':
|
||||
load_dotenv(Path(__file__).parent.parent / ".env")
|
||||
if os.getenv("OPENAI_API_KEY") == "local" or os.getenv("LLM_BACKEND", "").lower() == "local":
|
||||
return
|
||||
router_key = os.getenv('OPENROUTER_API_KEY', '')
|
||||
router_key = os.getenv("OPENROUTER_API_KEY", "")
|
||||
if router_key:
|
||||
os.environ['OPENAI_API_KEY'] = router_key
|
||||
os.environ['OPENAI_API_BASE'] = 'https://openrouter.ai/api/v1'
|
||||
os.environ['CHAT_MODEL'] = os.getenv('OPENROUTER_MODEL', 'openrouter/google/gemma-4-26b-a4b-it:free')
|
||||
os.environ["OPENAI_API_KEY"] = router_key
|
||||
os.environ["OPENAI_API_BASE"] = "https://openrouter.ai/api/v1"
|
||||
os.environ["CHAT_MODEL"] = os.getenv("OPENROUTER_MODEL", "openrouter/google/gemma-4-26b-a4b-it:free")
|
||||
|
||||
# ============================================================================
|
||||
# Factor Loading (cached at module level for each process)
|
||||
@@ -127,20 +136,20 @@ def load_available_factors(top_n=20):
|
||||
global _FACTORS_CACHE
|
||||
if _FACTORS_CACHE is not None:
|
||||
return _FACTORS_CACHE[:top_n]
|
||||
|
||||
|
||||
factors = []
|
||||
for f in FACTORS_DIR.glob('*.json'):
|
||||
for f in FACTORS_DIR.glob("*.json"):
|
||||
try:
|
||||
data = json.load(open(f))
|
||||
fname = data.get('factor_name', '')
|
||||
ic = data.get('ic') or 0
|
||||
safe = fname.replace('/','_').replace('\\','_')[:150]
|
||||
if (FACTORS_DIR / 'values' / f"{safe}.parquet").exists():
|
||||
factors.append({'name': fname, 'ic': ic})
|
||||
fname = data.get("factor_name", "")
|
||||
ic = data.get("ic") or 0
|
||||
safe = fname.replace("/","_").replace("\\","_")[:150]
|
||||
if (FACTORS_DIR / "values" / f"{safe}.parquet").exists():
|
||||
factors.append({"name": fname, "ic": ic})
|
||||
except:
|
||||
pass
|
||||
|
||||
factors.sort(key=lambda x: abs(x['ic']), reverse=True)
|
||||
|
||||
factors.sort(key=lambda x: abs(x["ic"]), reverse=True)
|
||||
_FACTORS_CACHE = factors
|
||||
return factors[:top_n]
|
||||
|
||||
@@ -154,18 +163,18 @@ def load_ohlcv_data():
|
||||
global _OHLCV_CACHE
|
||||
if _OHLCV_CACHE is not None:
|
||||
return _OHLCV_CACHE
|
||||
|
||||
|
||||
if not OHLCV_PATH.exists():
|
||||
raise FileNotFoundError(f"OHLCV data not found: {OHLCV_PATH}")
|
||||
|
||||
ohlcv = pd.read_hdf(str(OHLCV_PATH), key='data')
|
||||
if '$close' in ohlcv.columns:
|
||||
close = ohlcv['$close']
|
||||
elif 'close' in ohlcv.columns:
|
||||
close = ohlcv['close']
|
||||
|
||||
ohlcv = pd.read_hdf(str(OHLCV_PATH), key="data")
|
||||
if "$close" in ohlcv.columns:
|
||||
close = ohlcv["$close"]
|
||||
elif "close" in ohlcv.columns:
|
||||
close = ohlcv["close"]
|
||||
else:
|
||||
close = ohlcv.select_dtypes(include=[np.number]).iloc[:, 0]
|
||||
|
||||
|
||||
_OHLCV_CACHE = close.dropna()
|
||||
return _OHLCV_CACHE
|
||||
|
||||
@@ -175,16 +184,16 @@ def load_ohlcv_data():
|
||||
def generate_single_strategy(args):
|
||||
"""Generate and backtest ONE strategy. Runs in separate process."""
|
||||
idx, factor_subset, feedback, attempt = args
|
||||
|
||||
|
||||
try:
|
||||
setup_llm_env()
|
||||
|
||||
|
||||
from rdagent.oai.llm_utils import APIBackend
|
||||
|
||||
|
||||
factor_list = "\n".join([f"- {f['name']} (IC={f['ic']:.4f})" for f in factor_subset])
|
||||
|
||||
|
||||
# Optimized prompts for daytrading vs swing
|
||||
if TRADING_STYLE == 'daytrading' and OHLCV_ONLY:
|
||||
if TRADING_STYLE == "daytrading" and OHLCV_ONLY:
|
||||
system_prompt = """You are an expert EUR/USD intraday quant. You build strategies that work ONLY on raw price data (OHLCV), computing all indicators directly from the 1-minute close series.
|
||||
|
||||
CRITICAL RULES:
|
||||
@@ -219,9 +228,10 @@ Hard requirements:
|
||||
- Use EMA crossover thresholds of 0 (cross above/below) for maximum trade frequency
|
||||
- Use causal indicators only: rolling windows, shift(1) — NO look-ahead bias
|
||||
- No factor data — compute everything from 'close'
|
||||
- Keep it simple: 2-3 indicators max"""
|
||||
- Keep it simple: 2-3 indicators max
|
||||
- TARGET MONTHLY RETURN: Generate signals that can achieve >15% OOS monthly return after RiskMgmt costs (2.35 pip/trade). Use high-conviction entries only."""
|
||||
|
||||
elif TRADING_STYLE == 'daytrading':
|
||||
elif TRADING_STYLE == "daytrading":
|
||||
system_prompt = f"""You are an expert daytrading quant specializing in EUR/USD scalping and intraday strategies.
|
||||
|
||||
CRITICAL RULES for {STYLE_DESC} (forward horizon: {FORWARD_BARS} bars = ~{FORWARD_BARS} minutes):
|
||||
@@ -247,7 +257,8 @@ Hard requirements:
|
||||
- NEVER use ffill() or forward-fill on the signal — recompute fresh at every bar
|
||||
- Use rolling z-scores with windows of 5-20 bars (not 50-100), thresholds ±0.2 to ±0.5
|
||||
- Combine 2 factors: one momentum, one mean-reversion
|
||||
- NO global mean/std — always use rolling(window).mean() with shift(1) to avoid look-ahead bias"""
|
||||
- NO global mean/std — always use rolling(window).mean() with shift(1) to avoid look-ahead bias
|
||||
- TARGET MONTHLY RETURN: Generate signals that can achieve >15% OOS monthly return after RiskMgmt costs (2.35 pip/trade). Use high-conviction entries only."""
|
||||
|
||||
else:
|
||||
system_prompt = f"""You are a quantitative trading expert specializing in EUR/USD daily swing strategies.
|
||||
@@ -278,26 +289,26 @@ Output ONLY valid JSON with these fields:
|
||||
|
||||
{f'Previous feedback: {feedback}' if feedback else 'First attempt - be creative!'}
|
||||
|
||||
Use daily-level signal logic (factor above/below rolling daily mean). Signal changes once per day."""
|
||||
|
||||
Use daily-level signal logic (factor above/below rolling daily mean). Signal changes once per day. TARGET MONTHLY RETURN: Generate signals that can achieve >15% OOS monthly return after RiskMgmt costs (2.35 pip/trade)."""
|
||||
|
||||
api = APIBackend()
|
||||
response = api.build_messages_and_create_chat_completion(
|
||||
user_prompt=user_prompt, system_prompt=system_prompt, json_mode=True
|
||||
user_prompt=user_prompt, system_prompt=system_prompt, json_mode=True,
|
||||
)
|
||||
strategy_data = json.loads(response)
|
||||
|
||||
|
||||
# Validate response
|
||||
if 'code' not in strategy_data or 'factor_names' not in strategy_data:
|
||||
return {'status': 'invalid', 'reason': 'Missing required fields', 'idx': idx}
|
||||
|
||||
if "code" not in strategy_data or "factor_names" not in strategy_data:
|
||||
return {"status": "invalid", "reason": "Missing required fields", "idx": idx}
|
||||
|
||||
return {
|
||||
'status': 'generated',
|
||||
'strategy': strategy_data,
|
||||
'idx': idx
|
||||
"status": "generated",
|
||||
"strategy": strategy_data,
|
||||
"idx": idx,
|
||||
}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'reason': str(e)[:200], 'idx': idx}
|
||||
return {"status": "error", "reason": str(e)[:200], "idx": idx}
|
||||
|
||||
# ============================================================================
|
||||
# Backtest Runner (runs in main process to avoid re-loading data)
|
||||
@@ -345,39 +356,39 @@ signal.fillna(0).to_pickle('signal.pkl')
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tdp = Path(td)
|
||||
close.to_pickle(str(tdp / 'close.pkl'))
|
||||
close.to_pickle(str(tdp / "close.pkl"))
|
||||
if not OHLCV_ONLY and factors_df is not None:
|
||||
factors_df.to_pickle(str(tdp / 'factors.pkl'))
|
||||
(tdp / 'run.py').write_text(script)
|
||||
factors_df.to_pickle(str(tdp / "factors.pkl"))
|
||||
(tdp / "run.py").write_text(script)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['python', 'run.py'],
|
||||
["python", "run.py"],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
cwd=str(tdp)
|
||||
cwd=str(tdp),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return {'status': 'failed', 'reason': (result.stderr or result.stdout)[:200]}
|
||||
return {"status": "failed", "reason": (result.stderr or result.stdout)[:200]}
|
||||
|
||||
signal = pd.read_pickle(tdp / 'signal.pkl')
|
||||
signal = pd.read_pickle(tdp / "signal.pkl")
|
||||
except subprocess.TimeoutExpired:
|
||||
return {'status': 'failed', 'reason': 'Timeout (60s)'}
|
||||
return {"status": "failed", "reason": "Timeout (60s)"}
|
||||
except Exception as e:
|
||||
return {'status': 'failed', 'reason': str(e)[:200]}
|
||||
return {"status": "failed", "reason": str(e)[:200]}
|
||||
|
||||
# Main process: FTMO-realistic backtest (leverage + daily/total loss limits).
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_ftmo
|
||||
# Main process: RiskMgmt-realistic backtest (leverage + daily/total loss limits).
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
common = close.index.intersection(signal.index)
|
||||
if len(common) < 100:
|
||||
return {'status': 'failed', 'reason': f'Not enough aligned data ({len(common)} bars)'}
|
||||
return {"status": "failed", "reason": f"Not enough aligned data ({len(common)} bars)"}
|
||||
|
||||
close_a = close.loc[common]
|
||||
signal_a = signal.reindex(common).fillna(0)
|
||||
fwd_returns = close_a.pct_change(FORWARD_BARS).shift(-FORWARD_BARS)
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import OOS_START_DEFAULT
|
||||
return backtest_signal_ftmo(
|
||||
return backtest_signal_risk(
|
||||
close=close_a,
|
||||
signal=signal_a,
|
||||
txn_cost_bps=TXN_COST_BPS,
|
||||
@@ -409,9 +420,9 @@ def _rescale_thresholds(code: str, scale: float) -> str:
|
||||
return f"{val * scale:.3f}"
|
||||
|
||||
# RSI-style thresholds: integers/floats between 10 and 90
|
||||
code = re.sub(r'\b([1-9]\d(?:\.\d+)?)\b', replace_rsi, code)
|
||||
code = re.sub(r"\b([1-9]\d(?:\.\d+)?)\b", replace_rsi, code)
|
||||
# Small float thresholds: 0.05 – 2.99
|
||||
code = re.sub(r'\b(0\.\d+|[12]\.\d+)\b', replace_small, code)
|
||||
code = re.sub(r"\b(0\.\d+|[12]\.\d+)\b", replace_small, code)
|
||||
return code
|
||||
|
||||
|
||||
@@ -426,12 +437,12 @@ def tune_thresholds(close, factors_df, code: str) -> tuple:
|
||||
for scale in [1.0, 0.7, 0.5, 0.35, 0.2, 0.1, 0.05]:
|
||||
tuned = _rescale_thresholds(code, scale) if scale < 1.0 else code
|
||||
bt = run_backtest(close, factors_df, tuned)
|
||||
if bt is None or bt.get('status') != 'success':
|
||||
if bt is None or bt.get("status") != "success":
|
||||
continue
|
||||
trades = bt.get('n_trades', 0)
|
||||
sharpe = bt.get('sharpe', -999)
|
||||
trades = bt.get("n_trades", 0)
|
||||
sharpe = bt.get("sharpe", -999)
|
||||
if trades >= MIN_TRADES:
|
||||
if best_bt is None or sharpe > best_bt.get('sharpe', -999):
|
||||
if best_bt is None or sharpe > best_bt.get("sharpe", -999):
|
||||
best_bt = bt
|
||||
best_code = tuned
|
||||
break # first scale that hits MIN_TRADES wins (they get looser after this)
|
||||
@@ -461,46 +472,46 @@ def main(target_count=10):
|
||||
console.print(f" Forward bars: {FORWARD_BARS}")
|
||||
console.print(f" Target: {target_count} accepted strategies")
|
||||
console.print(f" Workers: {N_WORKERS}\n")
|
||||
|
||||
|
||||
# Load data (main process only)
|
||||
close = load_ohlcv_data()
|
||||
factors = load_available_factors(20)
|
||||
|
||||
|
||||
console.print(f"[green]✓[/green] Loaded {len(factors)} factors, {len(close):,} OHLCV bars\n")
|
||||
|
||||
|
||||
# Load factor time-series
|
||||
factor_data = {}
|
||||
with Progress(SpinnerColumn(), TextColumn("[bold blue]Loading factors..."), BarColumn(), TimeElapsedColumn()) as progress:
|
||||
task = progress.add_task("Loading...", total=len(factors))
|
||||
for f_info in factors:
|
||||
safe = f_info['name'].replace('/','_').replace('\\','_')[:150]
|
||||
pf = FACTORS_DIR / 'values' / f"{safe}.parquet"
|
||||
safe = f_info["name"].replace("/","_").replace("\\","_")[:150]
|
||||
pf = FACTORS_DIR / "values" / f"{safe}.parquet"
|
||||
if pf.exists():
|
||||
try:
|
||||
series = pd.read_parquet(str(pf)).iloc[:, 0]
|
||||
factor_data[f_info['name']] = series
|
||||
factor_data[f_info["name"]] = series
|
||||
except:
|
||||
pass
|
||||
progress.update(task, advance=1)
|
||||
|
||||
|
||||
# Align factors with close prices
|
||||
all_factor_series = [factor_data[n] for n in factor_data if n in factor_data]
|
||||
if not all_factor_series:
|
||||
console.print("[red]✗ No factor data loaded![/red]")
|
||||
return
|
||||
|
||||
|
||||
df_factors = pd.DataFrame({n: factor_data[n] for n in factor_data if n in factor_data})
|
||||
common_idx = close.index.intersection(df_factors.dropna(how='all').index)
|
||||
common_idx = close.index.intersection(df_factors.dropna(how="all").index)
|
||||
close_aligned = close.loc[common_idx]
|
||||
df_aligned = df_factors.loc[common_idx]
|
||||
|
||||
|
||||
console.print(f"[green]✓[/green] Aligned {len(df_aligned):,} data points\n")
|
||||
|
||||
|
||||
# Strategy generation loop
|
||||
accepted = []
|
||||
feedback_history = []
|
||||
max_attempts = target_count * 10 # Allow 10x attempts
|
||||
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[bold blue]{task.description}"),
|
||||
@@ -511,11 +522,11 @@ def main(target_count=10):
|
||||
redirect_stderr=True,
|
||||
) as progress:
|
||||
task = progress.add_task("Generating...", total=max_attempts)
|
||||
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
if len(accepted) >= target_count:
|
||||
break
|
||||
|
||||
|
||||
# Select random factor subset (2-5 factors) — empty for OHLCV-only mode
|
||||
if OHLCV_ONLY:
|
||||
factor_subset = []
|
||||
@@ -528,51 +539,51 @@ def main(target_count=10):
|
||||
# Generate in main process (LLM doesn't parallelize well)
|
||||
gen_result = generate_single_strategy((attempt, factor_subset, feedback, attempt))
|
||||
|
||||
if gen_result['status'] != 'generated':
|
||||
if gen_result["status"] != "generated":
|
||||
progress.update(task, advance=1)
|
||||
continue
|
||||
|
||||
strategy = gen_result['strategy']
|
||||
strategy = gen_result["strategy"]
|
||||
|
||||
# Backtest (main process - needs data access)
|
||||
if OHLCV_ONLY:
|
||||
strat_factors = None
|
||||
bt_result = run_backtest(close, None, strategy.get('code', ''))
|
||||
bt_result = run_backtest(close, None, strategy.get("code", ""))
|
||||
else:
|
||||
strat_factors = df_aligned[[f for f in strategy.get('factor_names', []) if f in df_aligned.columns]]
|
||||
strat_factors = df_aligned[[f for f in strategy.get("factor_names", []) if f in df_aligned.columns]]
|
||||
if len(strat_factors.columns) < 2:
|
||||
progress.update(task, advance=1)
|
||||
continue
|
||||
bt_result = run_backtest(close_aligned, strat_factors, strategy.get('code', ''))
|
||||
|
||||
if bt_result and bt_result.get('status') == 'success':
|
||||
ic = bt_result.get('ic', 0)
|
||||
sharpe = bt_result.get('sharpe', 0)
|
||||
trades = bt_result.get('n_trades', 0)
|
||||
dd = bt_result.get('max_drawdown', 0)
|
||||
bt_result = run_backtest(close_aligned, strat_factors, strategy.get("code", ""))
|
||||
|
||||
if bt_result and bt_result.get("status") == "success":
|
||||
ic = bt_result.get("ic", 0)
|
||||
sharpe = bt_result.get("sharpe", 0)
|
||||
trades = bt_result.get("n_trades", 0)
|
||||
dd = bt_result.get("max_drawdown", 0)
|
||||
|
||||
# If too few trades, auto-tune thresholds before giving up
|
||||
original_code = strategy.get('code', '')
|
||||
if trades < MIN_TRADES and bt_result.get('status') == 'success':
|
||||
original_code = strategy.get("code", "")
|
||||
if trades < MIN_TRADES and bt_result.get("status") == "success":
|
||||
_log.info(f"TUNING trades={trades}<{MIN_TRADES} — trying looser thresholds")
|
||||
tuned_bt, tuned_code = tune_thresholds(
|
||||
close if OHLCV_ONLY else close_aligned,
|
||||
None if OHLCV_ONLY else strat_factors,
|
||||
original_code,
|
||||
)
|
||||
if tuned_bt and tuned_bt.get('n_trades', 0) >= MIN_TRADES:
|
||||
if tuned_bt and tuned_bt.get("n_trades", 0) >= MIN_TRADES:
|
||||
bt_result = tuned_bt
|
||||
strategy['code'] = tuned_code
|
||||
ic = bt_result.get('ic', 0)
|
||||
sharpe = bt_result.get('sharpe', 0)
|
||||
trades = bt_result.get('n_trades', 0)
|
||||
dd = bt_result.get('max_drawdown', 0)
|
||||
strategy["code"] = tuned_code
|
||||
ic = bt_result.get("ic", 0)
|
||||
sharpe = bt_result.get("sharpe", 0)
|
||||
trades = bt_result.get("n_trades", 0)
|
||||
dd = bt_result.get("max_drawdown", 0)
|
||||
_log.info(f"TUNED Sharpe={sharpe:.2f} Trades={trades}")
|
||||
|
||||
# OOS metrics — mandatory, no fallback to IS values
|
||||
oos_sharpe = bt_result.get('oos_sharpe')
|
||||
oos_monthly = bt_result.get('oos_monthly_return_pct')
|
||||
oos_trades = bt_result.get('oos_n_trades', 0)
|
||||
oos_sharpe = bt_result.get("oos_sharpe")
|
||||
oos_monthly = bt_result.get("oos_monthly_return_pct")
|
||||
oos_trades = bt_result.get("oos_n_trades", 0)
|
||||
|
||||
# Reject if OOS data is missing (strategy trained on data without OOS period)
|
||||
if oos_sharpe is None or oos_monthly is None:
|
||||
@@ -582,62 +593,62 @@ def main(target_count=10):
|
||||
continue
|
||||
|
||||
# Monte Carlo p-value (edge significance)
|
||||
mc_pvalue = bt_result.get('mc_pvalue')
|
||||
mc_pvalue = bt_result.get("mc_pvalue")
|
||||
|
||||
# Rolling walk-forward metrics
|
||||
wf_consistency = bt_result.get('wf_oos_consistency')
|
||||
wf_sharpe_mean = bt_result.get('wf_oos_sharpe_mean')
|
||||
wf_consistency = bt_result.get("wf_oos_consistency")
|
||||
wf_sharpe_mean = bt_result.get("wf_oos_sharpe_mean")
|
||||
|
||||
# Check acceptance criteria — OOS must be profitable + statistically significant
|
||||
mc_ok = mc_pvalue is None or mc_pvalue < 0.20 # lenient: top 20% non-random
|
||||
wf_ok = wf_consistency is None or wf_consistency >= 0.5 # ≥50% of WF windows profitable
|
||||
if (abs(ic or 0) > MIN_IC and sharpe > MIN_SHARPE and trades > MIN_TRADES and dd > MAX_DRAWDOWN
|
||||
and oos_sharpe > 0.0 and oos_monthly > 0.0 and mc_ok and wf_ok):
|
||||
and oos_sharpe > 0.0 and oos_monthly > MIN_MONTHLY_RETURN_PCT and mc_ok and wf_ok):
|
||||
# ACCEPT
|
||||
strategy['real_backtest'] = bt_result
|
||||
strategy['metrics'] = bt_result
|
||||
strategy['summary'] = {
|
||||
'sharpe': sharpe, 'max_drawdown': dd, 'win_rate': bt_result.get('win_rate', 0),
|
||||
'monthly_return_pct': bt_result.get('monthly_return_pct', 0),
|
||||
'annual_return_pct': bt_result.get('annual_return_pct', 0),
|
||||
'real_ic': ic, 'real_n_trades': trades, 'real_backtest_status': 'success',
|
||||
'n_bars': bt_result.get('n_bars', 0), 'n_months': bt_result.get('n_months', 0),
|
||||
'trading_style': TRADING_STYLE,
|
||||
'ohlcv_only': OHLCV_ONLY,
|
||||
'engine': 'ftmo_v2',
|
||||
'txn_cost_bps': TXN_COST_BPS,
|
||||
strategy["real_backtest"] = bt_result
|
||||
strategy["metrics"] = bt_result
|
||||
strategy["summary"] = {
|
||||
"sharpe": sharpe, "max_drawdown": dd, "win_rate": bt_result.get("win_rate", 0),
|
||||
"monthly_return_pct": bt_result.get("monthly_return_pct", 0),
|
||||
"annual_return_pct": bt_result.get("annual_return_pct", 0),
|
||||
"real_ic": ic, "real_n_trades": trades, "real_backtest_status": "success",
|
||||
"n_bars": bt_result.get("n_bars", 0), "n_months": bt_result.get("n_months", 0),
|
||||
"trading_style": TRADING_STYLE,
|
||||
"ohlcv_only": OHLCV_ONLY,
|
||||
"engine": "riskmgmt_v2",
|
||||
"txn_cost_bps": TXN_COST_BPS,
|
||||
# Walk-forward OOS split
|
||||
'oos_sharpe': bt_result.get('oos_sharpe'),
|
||||
'oos_monthly_return_pct': bt_result.get('oos_monthly_return_pct'),
|
||||
'oos_max_drawdown': bt_result.get('oos_max_drawdown'),
|
||||
'oos_win_rate': bt_result.get('oos_win_rate'),
|
||||
'oos_n_trades': bt_result.get('oos_n_trades'),
|
||||
'is_sharpe': bt_result.get('is_sharpe'),
|
||||
'is_monthly_return_pct': bt_result.get('is_monthly_return_pct'),
|
||||
'oos_start': bt_result.get('oos_start'),
|
||||
"oos_sharpe": bt_result.get("oos_sharpe"),
|
||||
"oos_monthly_return_pct": bt_result.get("oos_monthly_return_pct"),
|
||||
"oos_max_drawdown": bt_result.get("oos_max_drawdown"),
|
||||
"oos_win_rate": bt_result.get("oos_win_rate"),
|
||||
"oos_n_trades": bt_result.get("oos_n_trades"),
|
||||
"is_sharpe": bt_result.get("is_sharpe"),
|
||||
"is_monthly_return_pct": bt_result.get("is_monthly_return_pct"),
|
||||
"oos_start": bt_result.get("oos_start"),
|
||||
# Rolling walk-forward
|
||||
'wf_n_windows': bt_result.get('wf_n_windows'),
|
||||
'wf_oos_sharpe_mean': wf_sharpe_mean,
|
||||
'wf_oos_sharpe_std': bt_result.get('wf_oos_sharpe_std'),
|
||||
'wf_oos_monthly_return_mean': bt_result.get('wf_oos_monthly_return_mean'),
|
||||
'wf_oos_consistency': wf_consistency,
|
||||
"wf_n_windows": bt_result.get("wf_n_windows"),
|
||||
"wf_oos_sharpe_mean": wf_sharpe_mean,
|
||||
"wf_oos_sharpe_std": bt_result.get("wf_oos_sharpe_std"),
|
||||
"wf_oos_monthly_return_mean": bt_result.get("wf_oos_monthly_return_mean"),
|
||||
"wf_oos_consistency": wf_consistency,
|
||||
# Monte Carlo significance
|
||||
'mc_pvalue': mc_pvalue,
|
||||
'mc_n_permutations': bt_result.get('mc_n_permutations'),
|
||||
"mc_pvalue": mc_pvalue,
|
||||
"mc_n_permutations": bt_result.get("mc_n_permutations"),
|
||||
}
|
||||
|
||||
|
||||
fname = f"{int(time.time())}_{strategy['strategy_name']}.json"
|
||||
with open(STRATEGIES_DIR / fname, 'w') as f:
|
||||
with open(STRATEGIES_DIR / fname, "w") as f:
|
||||
json.dump(strategy, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
# Generate PDF report
|
||||
try:
|
||||
from predix_strategy_report import StrategyPerformanceReporter
|
||||
from nexquant_strategy_report import StrategyPerformanceReporter
|
||||
reporter = StrategyPerformanceReporter(strategy)
|
||||
reporter.generate_report()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
accepted.append(strategy)
|
||||
_log.success(f"ACCEPTED {strategy['strategy_name']} IC={ic:.4f} Sharpe={sharpe:.3f} Trades={trades} DD={dd:.1%}")
|
||||
feedback_history.append(f"Excellent! IC={ic:.4f}, Sharpe={sharpe:.2f}, Trades={trades}. Try to improve further.")
|
||||
@@ -656,27 +667,27 @@ def main(target_count=10):
|
||||
+ (f", MC_p={mc_pvalue:.2f}" if mc_pvalue is not None else "")
|
||||
+ (f", WF_consistency={wf_consistency:.0%}" if wf_consistency is not None else "")
|
||||
+ f". Need |IC|>{MIN_IC}, Sharpe>{MIN_SHARPE}, Trades>{MIN_TRADES}, "
|
||||
f"OOS_Sharpe>0, OOS_Monthly>0, MC_p<0.20, WF_consistency≥50%."
|
||||
f"OOS_Sharpe>0, OOS_Monthly>{MIN_MONTHLY_RETURN_PCT}%, MC_p<0.20, WF_consistency≥50%.",
|
||||
)
|
||||
|
||||
|
||||
progress.update(task, advance=1)
|
||||
|
||||
|
||||
# Summary
|
||||
_log.info(f"DONE accepted={len(accepted)} target={target_count}")
|
||||
for i, s in enumerate(sorted(accepted, key=lambda x: x['real_backtest'].get('ic', 0), reverse=True), 1):
|
||||
bt = s['real_backtest']
|
||||
for i, s in enumerate(sorted(accepted, key=lambda x: x["real_backtest"].get("ic", 0), reverse=True), 1):
|
||||
bt = s["real_backtest"]
|
||||
_log.info(f" #{i} {s['strategy_name']} IC={bt.get('ic',0):.4f} Sharpe={bt.get('sharpe',0):.3f} Monthly={bt.get('monthly_return_pct',0):.2f}%")
|
||||
|
||||
console.print(f"\n[bold green]✓ Generated {len(accepted)}/{target_count} accepted strategies[/bold green]\n")
|
||||
|
||||
if accepted:
|
||||
accepted.sort(key=lambda x: x['real_backtest'].get('ic', 0), reverse=True)
|
||||
accepted.sort(key=lambda x: x["real_backtest"].get("ic", 0), reverse=True)
|
||||
console.print("[bold]Results:[/bold]")
|
||||
for i, s in enumerate(accepted, 1):
|
||||
bt = s['real_backtest']
|
||||
bt = s["real_backtest"]
|
||||
console.print(f" {i}. {s['strategy_name']:30s} IC={bt.get('ic',0):.4f} Sharpe={bt.get('sharpe',0):.3f} "
|
||||
f"Monthly={bt.get('monthly_return_pct',0):.2f}% Trades={bt.get('n_trades',0)}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
count = int(sys.argv[1]) if len(sys.argv) > 1 else 10
|
||||
main(count)
|
||||
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Strategy Grid Search — Systematic parameter scanning for optimal strategies.
|
||||
|
||||
Unlike the random R&D loop, this tests ALL parameter/TF combinations
|
||||
for the best indicators, guaranteeing global optimum discovery.
|
||||
|
||||
Output: Ranked list of strategies with per-instrument + combined metrics.
|
||||
"""
|
||||
|
||||
import json, os, sys, time, itertools
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import numpy as np, pandas as pd
|
||||
|
||||
PROJECT = Path(__file__).resolve().parent.parent
|
||||
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
|
||||
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
|
||||
OUTPUT_DIR = PROJECT / "results" / "grid_search"
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
sys.path.insert(0, str(PROJECT / "scripts"))
|
||||
from nexquant_rd_loop import (
|
||||
evaluate_multi, build_signal, _apply_session_filter, _apply_news_filter,
|
||||
_apply_vola_filter, _apply_cross_confirm, LEADER_MAP, load_data,
|
||||
)
|
||||
|
||||
# ── Grid Definition ──
|
||||
|
||||
INDICATOR_GRIDS = {
|
||||
"MACD": {
|
||||
"type": "multi_tf",
|
||||
"params": {
|
||||
"fast": [3, 5, 8, 12],
|
||||
"slow": [10, 15, 20, 26, 40],
|
||||
"sig": [3, 5, 9],
|
||||
},
|
||||
"tfs": [
|
||||
["15min", "30min", "1h", "4h"],
|
||||
["15min", "30min", "1h"],
|
||||
["30min", "1h", "4h"],
|
||||
["15min", "1h", "4h"],
|
||||
],
|
||||
},
|
||||
"Donchian": {
|
||||
"type": "multi_tf",
|
||||
"params": {
|
||||
"period": [5, 10, 20, 30, 50, 80, 100],
|
||||
"hold": [1, 2, 3, 5, 10],
|
||||
},
|
||||
"tfs": [
|
||||
["15min", "30min", "1h", "4h"],
|
||||
["30min", "1h", "4h"],
|
||||
["15min", "1h", "4h"],
|
||||
],
|
||||
},
|
||||
"SAR": {
|
||||
"type": "multi_tf",
|
||||
"params": {
|
||||
"accel": [0.02, 0.05, 0.08, 0.1, 0.15],
|
||||
"max_accel": [0.1, 0.2, 0.3, 0.5],
|
||||
},
|
||||
"tfs": [
|
||||
["15min", "30min", "1h", "4h"],
|
||||
["30min", "1h", "4h"],
|
||||
["15min", "1h", "4h"],
|
||||
],
|
||||
},
|
||||
"ADX": {
|
||||
"type": "multi_tf",
|
||||
"params": {
|
||||
"period": [7, 10, 14, 21, 30],
|
||||
"threshold": [15, 20, 25, 30],
|
||||
},
|
||||
"tfs": [
|
||||
["15min", "30min", "1h", "4h"],
|
||||
["30min", "1h", "4h"],
|
||||
],
|
||||
},
|
||||
"RSI": {
|
||||
"type": "multi_tf",
|
||||
"params": {
|
||||
"period": [7, 10, 14, 21],
|
||||
"oversold": [20, 25, 30],
|
||||
"overbought": [70, 75, 80],
|
||||
},
|
||||
"tfs": [
|
||||
["15min", "30min", "1h", "4h"],
|
||||
["30min", "1h", "4h"],
|
||||
],
|
||||
},
|
||||
"BBands": {
|
||||
"type": "multi_tf",
|
||||
"params": {
|
||||
"period": [10, 20, 40],
|
||||
"std": [1.5, 2.0, 2.5],
|
||||
},
|
||||
"tfs": [
|
||||
["15min", "30min", "1h", "4h"],
|
||||
["30min", "1h", "4h"],
|
||||
],
|
||||
},
|
||||
"ROC": {
|
||||
"type": "multi_tf",
|
||||
"params": {
|
||||
"period": [5, 10, 20, 50],
|
||||
"threshold": [0.1, 0.2, 0.5, 1.0],
|
||||
},
|
||||
"tfs": [
|
||||
["15min", "30min", "1h", "4h"],
|
||||
["30min", "1h", "4h"],
|
||||
],
|
||||
},
|
||||
"MOM": {
|
||||
"type": "multi_tf",
|
||||
"params": {
|
||||
"period": [5, 10, 20, 50, 100],
|
||||
},
|
||||
"tfs": [
|
||||
["15min", "30min", "1h", "4h"],
|
||||
["30min", "1h", "4h"],
|
||||
],
|
||||
},
|
||||
"Stoch": {
|
||||
"type": "multi_tf",
|
||||
"params": {
|
||||
"fastk": [5, 9, 14],
|
||||
"slowd": [3, 5, 9],
|
||||
},
|
||||
"tfs": [
|
||||
["15min", "30min", "1h", "4h"],
|
||||
["30min", "1h", "4h"],
|
||||
],
|
||||
},
|
||||
"CCI": {
|
||||
"type": "multi_tf",
|
||||
"params": {
|
||||
"period": [10, 14, 20, 50],
|
||||
},
|
||||
"tfs": [
|
||||
["15min", "30min", "1h", "4h"],
|
||||
["30min", "1h", "4h"],
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def expand_grid(indicator_name):
|
||||
"""Expand a grid definition into all parameter+TF combinations."""
|
||||
grid = INDICATOR_GRIDS[indicator_name]
|
||||
param_keys = list(grid["params"].keys())
|
||||
param_values = [grid["params"][k] for k in param_keys]
|
||||
hypotheses = []
|
||||
|
||||
for tf_list in grid["tfs"]:
|
||||
for param_combo in itertools.product(*param_values):
|
||||
params = dict(zip(param_keys, param_combo))
|
||||
hypotheses.append({
|
||||
"type": grid["type"],
|
||||
"indicator": indicator_name,
|
||||
"timeframes": tf_list,
|
||||
"params": params,
|
||||
"description": f"{indicator_name}({'-'.join(str(v) for v in param_combo)}) on {','.join(tf_list[:2])}",
|
||||
"generation": "grid",
|
||||
})
|
||||
|
||||
return hypotheses
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--indicators", nargs="*", default=None,
|
||||
help="Indicators to grid-search (default: all)")
|
||||
ap.add_argument("--top", type=int, default=20,
|
||||
help="Number of top results to show")
|
||||
args = ap.parse_args()
|
||||
|
||||
indicators = args.indicators or list(INDICATOR_GRIDS.keys())
|
||||
if isinstance(indicators, str):
|
||||
indicators = [indicators]
|
||||
|
||||
print("=" * 60)
|
||||
print(" Strategy Grid Search")
|
||||
print(f" Indicators: {', '.join(indicators)}")
|
||||
print("=" * 60)
|
||||
|
||||
# Load data
|
||||
print(" Loading data...")
|
||||
closes = load_data()
|
||||
if not closes:
|
||||
print(" No instruments found!"); return
|
||||
|
||||
# Generate all hypotheses
|
||||
all_hypotheses = []
|
||||
for ind in indicators:
|
||||
hyps = expand_grid(ind)
|
||||
all_hypotheses.extend(hyps)
|
||||
print(f" Total combinations to test: {len(all_hypotheses)}")
|
||||
print()
|
||||
|
||||
# Evaluate all
|
||||
results = []
|
||||
t0 = time.time()
|
||||
for i, hp in enumerate(all_hypotheses):
|
||||
try:
|
||||
r = evaluate_multi(closes, hp, use_session=True, use_vola=False)
|
||||
r["hypothesis"] = hp
|
||||
r["rank"] = i + 1
|
||||
results.append(r)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
elapsed = time.time() - t0
|
||||
rate = (i + 1) / elapsed if elapsed > 0 else 0
|
||||
eta = (len(all_hypotheses) - i - 1) / rate if rate > 0 else 0
|
||||
|
||||
if (i + 1) % 50 == 0:
|
||||
best_so_far = max(results, key=lambda x: x["sharpe"]) if results else {"sharpe": 0}
|
||||
print(f" [{i+1}/{len(all_hypotheses)}] "
|
||||
f"Best Sh={best_so_far['sharpe']:.1f} "
|
||||
f"Mon={best_so_far['monthly_pct']:.1f}% "
|
||||
f"OOS={best_so_far['monthly_oos']:.1f}% | "
|
||||
f"{rate:.0f}/s | ETA {eta/60:.0f}min")
|
||||
|
||||
# Sort by OOS Sharpe (most important metric)
|
||||
results.sort(key=lambda r: r.get("sharpe", 0), reverse=True)
|
||||
|
||||
elapsed = time.time() - t0
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" Grid Search Complete: {len(results)}/{len(all_hypotheses)} valid")
|
||||
print(f" Time: {elapsed:.0f}s ({elapsed/60:.1f}min)")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
# Save all results
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
out_file = OUTPUT_DIR / f"grid_results_{ts}.json"
|
||||
stripped = [{k: v for k, v in r.items() if k != "equity_curves"} for r in results]
|
||||
out_file.write_text(json.dumps(stripped, indent=2, default=str))
|
||||
print(f" Saved: {out_file}")
|
||||
|
||||
# Show top results
|
||||
top_n = min(args.top, len(results))
|
||||
print(f"\n TOP {top_n} (by OOS Sharpe):")
|
||||
print(f" {'Rank':>4s} {'Strategy':<45s} {'Sh_IS':>6s} {'Sh_OOS':>6s} {'Mon%':>7s} {'OOS%':>7s} {'DD':>6s} {'Tr':>5s} {'BTC':>5s}")
|
||||
for i, r in enumerate(results[:top_n], 1):
|
||||
hp = r["hypothesis"]
|
||||
per = r.get("per_instrument", {})
|
||||
btc_sh = per.get("BTCUSD", {}).get("sharpe_oos", 0)
|
||||
print(f" {i:4d} {hp['description'][:45]:45s} "
|
||||
f"{r.get('sharpe_is', 0):+6.1f} {r.get('sharpe_oos', 0):+6.1f} "
|
||||
f"{r['monthly_pct']:+6.1f}% {r['monthly_oos']:+6.1f}% "
|
||||
f"{r['max_dd']:.4f} {r['n_trades']:5d} {btc_sh:+5.0f}")
|
||||
|
||||
# Indicator performance summary
|
||||
print(f"\n Indicator Performance (avg OOS Sharpe):")
|
||||
for ind in indicators:
|
||||
ind_results = [r for r in results if r["hypothesis"].get("indicator") == ind]
|
||||
if ind_results:
|
||||
avg_sh = np.mean([r["sharpe"] for r in ind_results])
|
||||
best = ind_results[0]
|
||||
print(f" {ind:12s}: avg Sh={avg_sh:+.1f} best={best['sharpe']:+.1f} "
|
||||
f"({best['monthly_pct']:+.1f}%/{best['monthly_oos']:+.1f}% OOS)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Grid-Search Strategy Generator — no LLM, deterministic, RiskMgmt-verified.
|
||||
|
||||
Core idea: Instead of LLM-generated code, use a fixed signal template and
|
||||
grid-search the parameters. Factors are aligned to daily resolution (where
|
||||
they have actual predictive power), signal is forward-filled to 1-min for
|
||||
RiskMgmt backtest execution.
|
||||
|
||||
Template: z-score → IC-weighted composite → asymmetric thresholds → signal
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
# ── Paths ────────────────────────────────────────────────────────────────────
|
||||
PROJECT = Path(__file__).resolve().parent.parent
|
||||
FACTORS_DIR = PROJECT / "results" / "factors"
|
||||
VALUES_DIR = FACTORS_DIR / "values"
|
||||
RESULTS_DIR = PROJECT / "results" / "strategies_new"
|
||||
OHLCV_PATH = Path(
|
||||
os.getenv("PREDIX_OHLCV_PATH",
|
||||
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5"))
|
||||
)
|
||||
|
||||
# ── Target ───────────────────────────────────────────────────────────────────
|
||||
MIN_MONTHLY_RETURN_PCT = 1.0 # Raw backtest target (RiskMgmt will reduce ~50%)
|
||||
MIN_SHARPE = 0.5
|
||||
MAX_DRAWDOWN = -0.30
|
||||
MIN_WIN_RATE = 0.35
|
||||
MIN_TRADES = 20
|
||||
|
||||
# ── Grid ─────────────────────────────────────────────────────────────────────
|
||||
PARAM_GRID = {
|
||||
"window": [5, 10, 20, 30],
|
||||
"entry_thresh": [0.5, 0.8, 1.0, 1.5, 2.0], # Higher = fewer, higher-conviction trades
|
||||
"exit_thresh": [0.2, 0.5],
|
||||
}
|
||||
# Total: 5 × 4 × 3 = 60 combinations per factor pair
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Factor loading
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def load_top_factors(min_ic: float = 0.04, top_n: int = 50) -> list[dict]:
|
||||
"""Load factor metadata sorted by |IC| descending."""
|
||||
factors = []
|
||||
for f in sorted(FACTORS_DIR.glob("*.json")):
|
||||
data = json.loads(f.read_text())
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
fname = data.get("factor_name") or data.get("name") or f.stem
|
||||
ic = data.get("ic") or data.get("real_ic") or 0.0
|
||||
try:
|
||||
ic = float(ic)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if abs(ic) < min_ic:
|
||||
continue
|
||||
safe = fname.replace("/", "_").replace("\\", "_").replace(" ", "_")[:150]
|
||||
parq = VALUES_DIR / f"{safe}.parquet"
|
||||
if not parq.exists():
|
||||
continue
|
||||
factors.append({"name": fname, "ic": ic, "parquet": parq})
|
||||
factors.sort(key=lambda x: abs(x["ic"]), reverse=True)
|
||||
return factors[:top_n]
|
||||
|
||||
|
||||
def load_factor_series(factor: dict) -> pd.Series | None:
|
||||
"""Load factor time series, extracting the EURUSD slice."""
|
||||
try:
|
||||
df = pd.read_parquet(str(factor["parquet"]))
|
||||
if df.empty:
|
||||
return None
|
||||
col = df.columns[0]
|
||||
if isinstance(df.index, pd.MultiIndex):
|
||||
return df.xs("EURUSD", level="instrument")[col]
|
||||
return df[col]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Signal generation
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def build_signal(
|
||||
daily_factors: pd.DataFrame,
|
||||
ic_values: dict[str, float],
|
||||
window: int = 10,
|
||||
entry_thresh: float = 0.5,
|
||||
exit_thresh: float = 0.2,
|
||||
) -> pd.Series:
|
||||
"""
|
||||
Fixed signal template: z-score → IC-weighted composite → thresholds.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
daily_factors : DataFrame
|
||||
Factor values at daily resolution, columns = factor names.
|
||||
ic_values : dict
|
||||
Factor name → IC value (used for sign/direction, not weight).
|
||||
window : int
|
||||
Rolling window for z-score in days.
|
||||
entry_thresh : float
|
||||
Composite z-score threshold for entry.
|
||||
exit_thresh : float
|
||||
Composite z-score threshold for exit (flatten position).
|
||||
"""
|
||||
eps = 1e-8
|
||||
z = (daily_factors - daily_factors.rolling(window).mean()) / (
|
||||
daily_factors.rolling(window).std() + eps
|
||||
)
|
||||
|
||||
# IC-weighted composite: invert negative-IC factors, weight by |IC|
|
||||
composite = pd.Series(0.0, index=daily_factors.index)
|
||||
total_abs_ic = sum(abs(ic) for ic in ic_values.values())
|
||||
if total_abs_ic == 0:
|
||||
total_abs_ic = 1.0
|
||||
|
||||
for col in daily_factors.columns:
|
||||
ic = ic_values.get(col, 0.0)
|
||||
w = abs(ic) / total_abs_ic
|
||||
sign = 1.0 if ic >= 0 else -1.0
|
||||
composite += sign * w * z[col]
|
||||
|
||||
# Asymmetric thresholds
|
||||
signal = pd.Series(0, index=daily_factors.index)
|
||||
signal[composite > entry_thresh] = 1
|
||||
signal[composite < -entry_thresh] = -1
|
||||
signal[abs(composite) < exit_thresh] = 0
|
||||
|
||||
signal = signal.rolling(2, min_periods=1).mean().round().astype(int)
|
||||
signal = signal.clip(-1, 1)
|
||||
signal.name = "signal"
|
||||
return signal
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Evaluation
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def evaluate_one(args: tuple) -> dict | None:
|
||||
"""Evaluate one parameter combination on one factor pair."""
|
||||
(
|
||||
f1_name, f1_ic, f1_series,
|
||||
f2_name, f2_ic, f2_series,
|
||||
close_1min, window, entry, exit_th,
|
||||
) = args
|
||||
|
||||
try:
|
||||
# Align factors to 1-min close
|
||||
factors_1min = pd.DataFrame({
|
||||
f1_name: f1_series.reindex(close_1min.index).ffill(limit=2880),
|
||||
f2_name: f2_series.reindex(close_1min.index).ffill(limit=2880),
|
||||
})
|
||||
|
||||
# Resample to daily
|
||||
daily_factors = factors_1min.resample("D").last().dropna()
|
||||
if len(daily_factors) < 50:
|
||||
return None # Not enough daily data
|
||||
|
||||
daily_close = close_1min.resample("D").last().reindex(daily_factors.index)
|
||||
|
||||
# Build signal
|
||||
ic_values = {f1_name: f1_ic, f2_name: f2_ic}
|
||||
daily_signal = build_signal(daily_factors, ic_values, window, entry, exit_th)
|
||||
|
||||
# Forward-fill to 1-min for backtest
|
||||
signal_1min = daily_signal.reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
# Fast backtest (no RiskMgmt mask, no walk-forward — <1s per eval)
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
||||
|
||||
bt = backtest_signal(
|
||||
close=close_1min,
|
||||
signal=signal_1min,
|
||||
)
|
||||
|
||||
if bt.get("status") != "success":
|
||||
return None
|
||||
|
||||
sharpe = bt.get("sharpe", 0) or 0
|
||||
max_dd = bt.get("max_drawdown", 0) or 0
|
||||
win_rate = bt.get("win_rate", 0) or 0
|
||||
n_trades = bt.get("n_trades", 0) or 0
|
||||
monthly_pct = bt.get("monthly_return_pct", 0) or 0
|
||||
|
||||
return {
|
||||
"f1": f1_name,
|
||||
"f2": f2_name,
|
||||
"window": window,
|
||||
"entry": entry,
|
||||
"exit": exit_th,
|
||||
"sharpe": round(sharpe, 4),
|
||||
"max_dd": round(max_dd, 4),
|
||||
"win_rate": round(win_rate, 4),
|
||||
"n_trades": n_trades,
|
||||
"monthly_pct": round(monthly_pct, 2),
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
print("═" * 60)
|
||||
print(" Grid-Search Strategy Generator (no LLM)")
|
||||
print("═" * 60)
|
||||
|
||||
# ── Load OHLCV ────────────────────────────────────────────────────────
|
||||
print(f"\nLoading OHLCV: {OHLCV_PATH}")
|
||||
df = pd.read_hdf(OHLCV_PATH, key="data")
|
||||
close_1min = df.xs("EURUSD", level="instrument")["$close"].sort_index()
|
||||
print(f" 1-min bars: {len(close_1min):,} ({close_1min.index[0].date()} → {close_1min.index[-1].date()})")
|
||||
|
||||
# ── Load factors ───────────────────────────────────────────────────────
|
||||
print(f"\nLoading factors (|IC| ≥ 0.04)...")
|
||||
top_n = int(os.getenv("GS_TOP_N", "10"))
|
||||
factors = load_top_factors(min_ic=0.04, top_n=top_n)
|
||||
print(f" Loaded {len(factors)} factors")
|
||||
|
||||
factor_series = {}
|
||||
for f in factors:
|
||||
s = load_factor_series(f)
|
||||
if s is not None and len(s) > 100:
|
||||
factor_series[f["name"]] = (f["ic"], s)
|
||||
|
||||
names = list(factor_series.keys())
|
||||
print(f" Valid series: {len(names)}")
|
||||
|
||||
# ── Generate factor pairs ──────────────────────────────────────────────
|
||||
import itertools
|
||||
|
||||
pairs = list(itertools.combinations(names, 2))
|
||||
print(f" Factor pairs: {len(pairs)}")
|
||||
|
||||
# ── Generate parameter combinations ────────────────────────────────────
|
||||
param_combos = list(itertools.product(
|
||||
PARAM_GRID["window"],
|
||||
PARAM_GRID["entry_thresh"],
|
||||
PARAM_GRID["exit_thresh"],
|
||||
))
|
||||
# Filter: exit < entry
|
||||
param_combos = [(w, e, x) for w, e, x in param_combos if x < e]
|
||||
print(f" Parameter combos: {len(param_combos)}")
|
||||
|
||||
# ── Build work items ───────────────────────────────────────────────────
|
||||
work_items = []
|
||||
for f1_name, f2_name in pairs:
|
||||
f1_ic, f1_series = factor_series[f1_name]
|
||||
f2_ic, f2_series = factor_series[f2_name]
|
||||
for window, entry, exit_th in param_combos:
|
||||
work_items.append((
|
||||
f1_name, f1_ic, f1_series,
|
||||
f2_name, f2_ic, f2_series,
|
||||
close_1min, window, entry, exit_th,
|
||||
))
|
||||
|
||||
total = len(work_items)
|
||||
print(f" Total evaluations: {total:,}")
|
||||
|
||||
# ── Run sequentially ───────────────────────────────────────────────────
|
||||
t0 = time.time()
|
||||
results = []
|
||||
|
||||
for i, item in enumerate(work_items):
|
||||
r = evaluate_one(item)
|
||||
if r is not None:
|
||||
results.append(r)
|
||||
if (i + 1) % 100 == 0 or i == total - 1:
|
||||
elapsed = time.time() - t0
|
||||
rate = (i + 1) / elapsed if elapsed > 0 else 0
|
||||
eta = (total - i - 1) / rate if rate > 0 else 0
|
||||
print(f" {i+1}/{total} ({(i+1)/total*100:.1f}%) "
|
||||
f"{len(results)} valid {rate:.1f}/s eta {eta:.0f}s")
|
||||
|
||||
# ── Filter and sort ────────────────────────────────────────────────────
|
||||
print(f"\n{'═' * 60}")
|
||||
print(f" Total evaluated: {total:,} Valid results: {len(results):,}")
|
||||
print(f"{'═' * 60}")
|
||||
|
||||
valid = [r for r in results
|
||||
if r["sharpe"] >= MIN_SHARPE
|
||||
and r["max_dd"] >= MAX_DRAWDOWN
|
||||
and r["win_rate"] >= MIN_WIN_RATE
|
||||
and r["n_trades"] >= MIN_TRADES
|
||||
and r["monthly_pct"] >= MIN_MONTHLY_RETURN_PCT]
|
||||
|
||||
valid.sort(key=lambda r: r["monthly_pct"], reverse=True)
|
||||
|
||||
print(f"\n Meeting criteria (Sharpe≥{MIN_SHARPE}, DD≥{MAX_DRAWDOWN}, "
|
||||
f"WR≥{MIN_WIN_RATE}, Trades≥{MIN_TRADES}, Mon≥{MIN_MONTHLY_RETURN_PCT}%):")
|
||||
print(f" → {len(valid)} strategies")
|
||||
print()
|
||||
|
||||
if valid:
|
||||
print(f"{'#':<3s} {'Factor 1':>30s} + {'Factor 2':>30s} {'w':>3s} {'ent':>4s} {'ex':>4s} {'Sharpe':>7s} {'MaxDD':>7s} {'WinRt':>6s} {'Tr':>4s} {'Mon%':>7s}")
|
||||
print("-" * 135)
|
||||
for i, r in enumerate(valid[:30], 1):
|
||||
print(f"{i:<3d} {r['f1'][:30]:>30s} + {r['f2'][:30]:>30s} "
|
||||
f"{r['window']:>3d} {r['entry']:>4.1f} {r['exit']:>4.1f} "
|
||||
f"{r['sharpe']:>7.3f} {r['max_dd']:>7.3f} {r['win_rate']:>6.1%} "
|
||||
f"{r['n_trades']:>4d} {r['monthly_pct']:>7.2f}%")
|
||||
else:
|
||||
print(" No strategies meet the criteria.")
|
||||
if results:
|
||||
results.sort(key=lambda r: r["monthly_pct"], reverse=True)
|
||||
print("\n Top 10 by monthly return:")
|
||||
for i, r in enumerate(results[:10], 1):
|
||||
print(f" {i:2d}. {r['f1'][:25]} + {r['f2'][:25]} "
|
||||
f"Mon={r['monthly_pct']:.2f}% Sh={r['sharpe']:.3f} "
|
||||
f"DD={r['max_dd']:.3f} Tr={r['n_trades']}")
|
||||
|
||||
# ── Save top results ───────────────────────────────────────────────────
|
||||
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
out_path = RESULTS_DIR / f"gridsearch_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
out_path.write_text(json.dumps(valid[:50] if valid else results[:50], indent=2, default=str))
|
||||
print(f"\n Top results saved → {out_path}")
|
||||
print(f" Runtime: {time.time() - t0:.0f}s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NexQuant Infinite Hypothesis Search — kombiniert und variiert Ansätze
|
||||
bis ein positiver OOS Sharpe gefunden wird.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json, sys, time, random, itertools
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors")
|
||||
TXN_COST_BPS = 0.5
|
||||
|
||||
|
||||
def load_data():
|
||||
close = pd.read_hdf(DATA_PATH, key="data")["$close"]
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close = close.droplevel(-1)
|
||||
close = close.sort_index().dropna().resample("1h").last().dropna()
|
||||
|
||||
factors_meta = []
|
||||
for f in sorted(FACTORS_DIR.glob("*.json")):
|
||||
try:
|
||||
d = json.loads(f.read_text())
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("status") != "success" or d.get("ic") is None:
|
||||
continue
|
||||
name = d.get("factor_name", f.stem)
|
||||
safe = name.replace("/", "_")[:150]
|
||||
if (FACTORS_DIR / "values" / f"{safe}.parquet").exists():
|
||||
factors_meta.append({"name": name, "ic": d["ic"]})
|
||||
|
||||
factors_meta.sort(key=lambda x: abs(x["ic"]), reverse=True)
|
||||
top = factors_meta[:15]
|
||||
factor_data = {}
|
||||
for f in top:
|
||||
safe = f["name"].replace("/", "_")[:150]
|
||||
series = pd.read_parquet(FACTORS_DIR / "values" / f"{safe}.parquet").iloc[:, 0]
|
||||
if isinstance(series.index, pd.MultiIndex):
|
||||
series = series.droplevel(-1)
|
||||
factor_data[f["name"]] = series.resample("1h").last()
|
||||
|
||||
df = pd.DataFrame(factor_data)
|
||||
common = close.index.intersection(df.dropna(how="all").index)
|
||||
return close.loc[common], df.loc[common].ffill(), {f["name"]: f["ic"] for f in top}
|
||||
|
||||
|
||||
close, factors_df, ics = load_data()
|
||||
print(f"Data: {len(close):,} bars × {len(factors_df.columns)} factors\n")
|
||||
|
||||
def backtest(signal) -> float:
|
||||
if signal is None or len(signal) < 100:
|
||||
return -999
|
||||
common = close.index.intersection(signal.dropna().index)
|
||||
if len(common) < 100:
|
||||
return -999
|
||||
r = backtest_signal_risk(close.loc[common], signal.reindex(common).fillna(0),
|
||||
txn_cost_bps=TXN_COST_BPS, wf_rolling=False)
|
||||
return r.get("oos_sharpe", -999)
|
||||
|
||||
|
||||
def composite(factor_list=None, window=20):
|
||||
cols = factor_list or list(factors_df.columns)
|
||||
c = pd.Series(0.0, index=factors_df.index)
|
||||
total = sum(abs(ics.get(col, 0)) for col in cols)
|
||||
if total == 0:
|
||||
return c
|
||||
for col in cols:
|
||||
ic_val = ics.get(col, 0)
|
||||
if abs(ic_val) < 0.001:
|
||||
continue
|
||||
z = (factors_df[col] - factors_df[col].rolling(window).mean()) / (factors_df[col].rolling(window).std() + 1e-8)
|
||||
c += (ic_val / total) * z
|
||||
return c
|
||||
|
||||
|
||||
def session_filter(sig):
|
||||
hours = sig.index.hour
|
||||
sig = sig.copy()
|
||||
sig[(hours < 7) | (hours >= 17)] = 0
|
||||
return sig
|
||||
|
||||
|
||||
def trend_filter(sig, sma_bars=200 * 1440 // 5):
|
||||
sma = close.rolling(sma_bars).mean()
|
||||
trend_up = close > sma
|
||||
sig = sig.copy()
|
||||
sig[(sig > 0) & ~trend_up] = 0
|
||||
sig[(sig < 0) & trend_up] = 0
|
||||
return sig
|
||||
|
||||
|
||||
def vola_target(sig, vol_window=50):
|
||||
vol = close.pct_change().rolling(vol_window).std()
|
||||
vol_tgt = vol.median()
|
||||
s = sig.astype(float) * vol_tgt / (vol + 1e-8)
|
||||
return s.clip(-3, 3)
|
||||
|
||||
|
||||
def anti_fade(sig, sigma=3.0):
|
||||
ret = close.pct_change()
|
||||
thresh = ret.std() * sigma
|
||||
s = sig.copy()
|
||||
s[ret > thresh] = -1
|
||||
s[ret < -thresh] = 1
|
||||
return s
|
||||
|
||||
|
||||
def signal_decay(sig, half_life=60):
|
||||
d = 0.5 ** (1 / half_life)
|
||||
s = sig.astype(float).copy()
|
||||
for i in range(1, len(s)):
|
||||
if abs(s.iloc[i]) < 0.01:
|
||||
s.iloc[i] = s.iloc[i - 1] * d
|
||||
return s.clip(-1, 1)
|
||||
|
||||
|
||||
def kalman_composite(comp, Q=0.001, R=0.1):
|
||||
x, P = 0.0, 1.0
|
||||
filtered = []
|
||||
for v in comp.dropna().values:
|
||||
P += Q; K = P / (P + R); x += K * (v - x); P *= (1 - K)
|
||||
filtered.append(x)
|
||||
return pd.Series(filtered, index=comp.dropna().index)
|
||||
|
||||
|
||||
# PRIMITIVES — can be combined arbitrarily
|
||||
PRIMITIVES = {
|
||||
"session": session_filter,
|
||||
"trend": trend_filter,
|
||||
"vola_target": vola_target,
|
||||
"anti_fade": anti_fade,
|
||||
"decay": signal_decay,
|
||||
}
|
||||
|
||||
BASE_PARAMS = {
|
||||
"entry": [0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5],
|
||||
"window": [10, 20, 30, 50, 100],
|
||||
"sigma": [2.0, 2.5, 3.0, 3.5],
|
||||
"half_life": [30, 60, 120, 240],
|
||||
}
|
||||
|
||||
best_score = -999
|
||||
best_desc = ""
|
||||
best_sig = None
|
||||
tested = set()
|
||||
round_num = 0
|
||||
|
||||
|
||||
def try_combo(factor_list, entry, window, primitives_used):
|
||||
global best_score, best_desc, best_sig, tested, round_num
|
||||
|
||||
key = f"{sorted(factor_list)}_{entry:.3f}_{window}_{sorted(primitives_used)}"
|
||||
if key in tested:
|
||||
return
|
||||
tested.add(key)
|
||||
|
||||
comp = composite(factor_list, window)
|
||||
if comp is None or comp.dropna().empty:
|
||||
return
|
||||
sig = pd.Series(0, index=comp.index)
|
||||
sig[comp > entry] = 1
|
||||
sig[comp < -entry] = -1
|
||||
|
||||
for p in primitives_used:
|
||||
if p in PRIMITIVES:
|
||||
sig = PRIMITIVES[p](sig.fillna(0))
|
||||
|
||||
sharpe = backtest(sig)
|
||||
if sharpe > best_score:
|
||||
best_score = sharpe
|
||||
best_desc = f"entry={entry:.2f} window={window} factors={len(factor_list)} primitives={primitives_used}"
|
||||
best_sig = sig
|
||||
t = "✅" if sharpe > 0 else "📈" if sharpe > -1 else "➖"
|
||||
print(f" {t} #{round_num}: Sharpe={sharpe:.4f} | {best_desc}")
|
||||
|
||||
if sharpe > 0:
|
||||
print(f"\n{'='*60}")
|
||||
print(f" 🎯 POSITIVE SHARPE FOUND!")
|
||||
print(f" Sharpe={sharpe:.4f}")
|
||||
print(f" {best_desc}")
|
||||
print(f"{'='*60}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
print("Starting infinite search — will run until positive OOS Sharpe found...\n")
|
||||
all_factors = sorted(factors_df.columns, key=lambda c: -abs(ics.get(c, 0)))
|
||||
|
||||
while True:
|
||||
round_num += 1
|
||||
|
||||
# Pick random subset of top factors
|
||||
n_factors = random.randint(2, min(10, len(all_factors)))
|
||||
factor_subset = random.sample(all_factors[:12], n_factors)
|
||||
|
||||
# Pick random parameters
|
||||
entry = random.choice(BASE_PARAMS["entry"])
|
||||
window = random.choice(BASE_PARAMS["window"])
|
||||
|
||||
# Pick random combination of primitives (0-4)
|
||||
n_prim = random.randint(0, 4)
|
||||
prims = random.sample(list(PRIMITIVES.keys()), n_prim) if n_prim > 0 else []
|
||||
|
||||
found = try_combo(factor_subset, entry, window, prims)
|
||||
if found:
|
||||
break
|
||||
|
||||
# Every 200 rounds, also try parameter sweeps around best
|
||||
if round_num % 200 == 0:
|
||||
print(f" ... {round_num} combinations tested, best={best_score:.4f}")
|
||||
# Fine-tune around current best
|
||||
for fine_entry in np.arange(max(0.05, entry - 0.15), entry + 0.16, 0.05):
|
||||
for fine_window in [max(5, window - 15), window, min(200, window + 15)]:
|
||||
if try_combo(factor_subset, fine_entry, fine_window, prims):
|
||||
break
|
||||
|
||||
# Every 500 rounds, try factor-specific combos (Kronos-only, momentum-only, etc.)
|
||||
if round_num % 500 == 0:
|
||||
kronos = [f for f in all_factors if "Kronos" in f]
|
||||
mom = [f for f in all_factors if any(k in f.lower() for k in ["mom", "ret"])]
|
||||
for subset in [kronos, mom, all_factors[:3], all_factors[:6]]:
|
||||
if len(subset) >= 2:
|
||||
for e in [0.1, 0.2, 0.3]:
|
||||
for w in [20, 50]:
|
||||
for prims in [[], ["session"], ["session", "decay"]]:
|
||||
try_combo(subset, e, w, prims)
|
||||
|
||||
if round_num % 1000 == 0:
|
||||
print(f" [{round_num} tested] best={best_score:.4f} — still searching...")
|
||||
|
||||
if best_score <= 0:
|
||||
print(f"\nAfter {round_num} combinations, best is still negative ({best_score:.4f})")
|
||||
print("The factors lack sufficient predictive power for positive returns.")
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live Price-Action Strategy Pipeline — No LLM, No Factors.
|
||||
|
||||
Generates daily signals from Donchian + MACD portfolio, executes via risk
|
||||
backtest, and optionally sends signals to live trading.
|
||||
|
||||
Usage:
|
||||
python scripts/nexquant_live_priceaction.py # Generate today's signal
|
||||
python scripts/nexquant_live_priceaction.py --daemon # Run continuously
|
||||
python scripts/nexquant_live_priceaction.py --backfill # Full historical backtest
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
PROJECT = Path(__file__).resolve().parent.parent
|
||||
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
|
||||
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
|
||||
SIGNAL_PATH = PROJECT / "git_ignore_folder" / "priceaction_signal.json"
|
||||
RESULTS_DIR = PROJECT / "results" / "reports"
|
||||
|
||||
# Portfolio config
|
||||
STRATEGIES = [
|
||||
{"name": "Donchian(30,1)", "type": "donchian", "period": 30, "hold": 1},
|
||||
{"name": "MACD(3,15,3)", "type": "macd", "fast": 3, "slow": 15, "signal_period": 3},
|
||||
]
|
||||
|
||||
VOTE_THRESHOLD = 0.25
|
||||
|
||||
|
||||
def load_close() -> tuple[pd.Series, pd.Series]:
|
||||
"""Load 1-min and daily close prices."""
|
||||
df = pd.read_hdf(OHLCV_PATH, key="data")
|
||||
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
|
||||
daily = close.resample("D").last().dropna()
|
||||
return close, daily
|
||||
|
||||
|
||||
def donchian_signal(daily: pd.Series, period: int, hold: int) -> pd.Series:
|
||||
"""Donchian channel breakout signal (daily)."""
|
||||
high = daily.rolling(period).max()
|
||||
low = daily.rolling(period).min()
|
||||
s = pd.Series(0, index=daily.index)
|
||||
s[daily > high.shift(1)] = 1
|
||||
s[daily < low.shift(1)] = -1
|
||||
return s.replace(0, np.nan).ffill(limit=hold).fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
def macd_signal(daily: pd.Series, fast: int, slow: int, signal_period: int) -> pd.Series:
|
||||
"""MACD crossover signal (daily)."""
|
||||
ema_fast = daily.ewm(span=fast, adjust=False).mean()
|
||||
ema_slow = daily.ewm(span=slow, adjust=False).mean()
|
||||
macd_line = ema_fast - ema_slow
|
||||
sig_line = macd_line.ewm(span=signal_period, adjust=False).mean()
|
||||
s = pd.Series(0, index=daily.index)
|
||||
s[macd_line > sig_line] = 1
|
||||
s[macd_line < sig_line] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
def compute_portfolio_signal(daily: pd.Series) -> pd.Series:
|
||||
"""Compute majority-vote portfolio signal."""
|
||||
signals = []
|
||||
for cfg in STRATEGIES:
|
||||
if cfg["type"] == "donchian":
|
||||
sig = donchian_signal(daily, cfg["period"], cfg["hold"])
|
||||
elif cfg["type"] == "macd":
|
||||
sig = macd_signal(daily, cfg["fast"], cfg["slow"], cfg["signal_period"])
|
||||
else:
|
||||
continue
|
||||
signals.append(sig)
|
||||
|
||||
if not signals:
|
||||
return pd.Series(0, index=daily.index)
|
||||
|
||||
port = pd.DataFrame({f"s{i}": s for i, s in enumerate(signals)}).dropna()
|
||||
vote = port.mean(axis=1)
|
||||
result = pd.Series(0, index=vote.index)
|
||||
result[vote > VOTE_THRESHOLD] = 1
|
||||
result[vote < -VOTE_THRESHOLD] = -1
|
||||
result.name = "signal"
|
||||
return result
|
||||
|
||||
|
||||
def get_todays_signal() -> dict:
|
||||
"""Generate today's trading signal."""
|
||||
close, daily = load_close()
|
||||
portfolio_signal = compute_portfolio_signal(daily)
|
||||
|
||||
# Latest signal
|
||||
latest = portfolio_signal.iloc[-1]
|
||||
direction = {1: "LONG", -1: "SHORT", 0: "NEUTRAL"}[int(latest)]
|
||||
|
||||
# Last signal change
|
||||
changes = portfolio_signal.diff().abs()
|
||||
last_change_idx = changes[changes > 0].index[-1] if (changes > 0).any() else None
|
||||
days_in_position = (daily.index[-1] - last_change_idx).days if last_change_idx is not None else 0
|
||||
|
||||
result = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"date": str(daily.index[-1].date()),
|
||||
"signal": int(latest),
|
||||
"direction": direction,
|
||||
"days_in_position": days_in_position,
|
||||
"strategies": {cfg["name"]: int(
|
||||
donchian_signal(daily, cfg["period"], cfg["hold"]).iloc[-1] if cfg["type"] == "donchian"
|
||||
else macd_signal(daily, cfg["fast"], cfg["slow"], cfg["signal_period"]).iloc[-1]
|
||||
) for cfg in STRATEGIES},
|
||||
}
|
||||
|
||||
SIGNAL_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
SIGNAL_PATH.write_text(json.dumps(result, indent=2))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def run_backfill():
|
||||
"""Run full historical backtest and save report."""
|
||||
print("Running full historical backtest...")
|
||||
close, daily = load_close()
|
||||
signal = compute_portfolio_signal(daily)
|
||||
|
||||
# ffill to 1-min
|
||||
sig_1min = signal.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal, backtest_signal_risk
|
||||
|
||||
bt = backtest_signal(close=close, signal=sig_1min)
|
||||
bt_risk = backtest_signal_risk(close=close, signal=sig_1min, risk_pct=0.0035, oos_start=None, wf_rolling=True)
|
||||
|
||||
report = {
|
||||
"strategy": "Donchian(30,1) + MACD(3,15,3) Majority-Vote",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"backtest": {
|
||||
"sharpe": round(bt["sharpe"], 2),
|
||||
"monthly_return_pct": round(bt["monthly_return_pct"], 2),
|
||||
"max_drawdown": round(bt["max_drawdown"], 4),
|
||||
"n_trades": bt["n_trades"],
|
||||
"win_rate": round(bt["win_rate"], 4),
|
||||
},
|
||||
"risk_backtest": {
|
||||
"sharpe": round(bt_risk.get("sharpe", 0), 2),
|
||||
"monthly_pct": round(bt_risk.get("monthly_return_pct", 0), 2),
|
||||
"max_dd": round(bt_risk.get("max_drawdown", 0), 4),
|
||||
"wf_consistency": round(bt_risk.get("wf_oos_consistency", 0), 4),
|
||||
},
|
||||
}
|
||||
|
||||
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
path = RESULTS_DIR / f"backfill_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
path.write_text(json.dumps(report, indent=2))
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f" Sharpe: {bt['sharpe']:.2f}")
|
||||
print(f" Monthly: {bt['monthly_return_pct']:.2f}%")
|
||||
print(f" Max DD: {bt['max_drawdown']:.4f}")
|
||||
print(f" Trades: {bt['n_trades']}")
|
||||
print(f" Win Rate: {bt['win_rate']:.1%}")
|
||||
print(f" Report saved: {path}")
|
||||
print(f"{'='*50}")
|
||||
|
||||
|
||||
def main():
|
||||
if "--backfill" in sys.argv:
|
||||
run_backfill()
|
||||
elif "--daemon" in sys.argv:
|
||||
print("Daemon mode — generating signals every 5 minutes...")
|
||||
while True:
|
||||
result = get_todays_signal()
|
||||
print(f" [{result['timestamp']}] {result['direction']:>8s} ({result['days_in_position']}d in position)")
|
||||
time.sleep(300)
|
||||
else:
|
||||
result = get_todays_signal()
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NexQuant Live Strategy — Multi-mode, multi-frequency trading signals.
|
||||
|
||||
Modes:
|
||||
- price_1h: SMA10/30 on 1h bars (+0.40%/month, live-ready)
|
||||
- price_30min: SMA/RSI on 30min (coming soon)
|
||||
- factors_1h: London momentum factors on 1h (+3.29%/month)
|
||||
- factors_30min: London momentum factors on 30min (+3.59%/month, BEST)
|
||||
|
||||
Auto-selects best available mode based on data freshness.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json, sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
OHLCV_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
CONFIG_PATH = Path("results/strategies_live/live_config.json")
|
||||
|
||||
|
||||
def load_config():
|
||||
with open(CONFIG_PATH) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_latest_close():
|
||||
close = pd.read_hdf(OHLCV_PATH, key="data")["$close"]
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close = close.droplevel(-1)
|
||||
return close.sort_index().dropna()
|
||||
|
||||
|
||||
class LiveSignal:
|
||||
def __init__(self):
|
||||
self.close = get_latest_close()
|
||||
self.config = load_config()
|
||||
self.session_hours = self.config.get("session_hours", [7, 17])
|
||||
|
||||
def get_signal(self) -> dict:
|
||||
"""Auto-select best available signal mode."""
|
||||
now = pd.Timestamp.now(tz="UTC").floor("1h")
|
||||
hour = now.hour
|
||||
is_session = self.session_hours[0] <= hour < self.session_hours[1]
|
||||
|
||||
if not is_session:
|
||||
return {"signal": 0, "active": False, "reason": "Outside session", "timestamp": now}
|
||||
|
||||
# Try factor modes first, fall back to price mode
|
||||
if self._check_factors_fresh():
|
||||
return self._factor_mode(now)
|
||||
return self._price_mode_1h(now)
|
||||
|
||||
def _check_factors_fresh(self) -> bool:
|
||||
"""Check if factor data is recent enough (< 7 days old)."""
|
||||
try:
|
||||
s = pd.read_parquet("results/factors/values/london_session_momentum.parquet")
|
||||
if isinstance(s.index, pd.MultiIndex):
|
||||
s = s.droplevel(-1)
|
||||
last_date = s.dropna().index[-1]
|
||||
if hasattr(last_date, 'date'):
|
||||
last_date = last_date.date()
|
||||
age = (pd.Timestamp.now().date() - pd.Timestamp(last_date).date()).days
|
||||
return age < 7
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _price_mode_1h(self, now) -> dict:
|
||||
"""SMA10/30 crossover on 1h bars (+0.40%/month)."""
|
||||
c = self.close.resample("1h").last()
|
||||
sma10 = c.rolling(10).mean()
|
||||
sma30 = c.rolling(30).mean()
|
||||
|
||||
if len(sma10.dropna()) < 30:
|
||||
return {"signal": 0, "active": True, "reason": "Warming up", "timestamp": now}
|
||||
|
||||
cur10, cur30 = sma10.iloc[-1], sma30.iloc[-1]
|
||||
prev10, prev30 = sma10.iloc[-2], sma30.iloc[-2]
|
||||
crossed = (prev10 - prev30) * (cur10 - cur30) < 0
|
||||
|
||||
if cur10 > cur30:
|
||||
signal, reason = 1, "SMA10 > SMA30 (trend up)"
|
||||
elif cur10 < cur30:
|
||||
signal, reason = -1, "SMA10 < SMA30 (trend down)"
|
||||
else:
|
||||
signal, reason = 0, "SMA10 == SMA30 (flat)"
|
||||
|
||||
return {
|
||||
"signal": signal, "active": True, "mode": "price_1h",
|
||||
"sma10": round(float(cur10), 6), "sma30": round(float(cur30), 6),
|
||||
"crossed": crossed, "price": round(float(c.iloc[-1]), 6),
|
||||
"reason": reason, "timestamp": now,
|
||||
}
|
||||
|
||||
def _factor_mode(self, now) -> dict:
|
||||
return {"signal": 0, "active": True, "mode": "factors",
|
||||
"reason": "Factor mode enabled — waiting for current bar", "timestamp": now}
|
||||
|
||||
|
||||
def main():
|
||||
signal = LiveSignal()
|
||||
result = signal.get_signal()
|
||||
print(json.dumps(result, indent=2, default=str))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NexQuant Enhanced ML Pipeline — factor-boosted, multi-horizon, Optuna-optimized.
|
||||
Target: 8%/month through ensemble of factor + OHLCV features.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json, sys, time, warnings
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import optuna
|
||||
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.model_selection import TimeSeriesSplit
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors")
|
||||
TXN_COST_BPS = 2.14
|
||||
N_TRIALS = 75
|
||||
|
||||
|
||||
def load_all():
|
||||
close = pd.read_hdf(DATA_PATH, key="data")["$close"]
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close = close.droplevel(-1)
|
||||
daily = close.sort_index().dropna().resample("1D").last().dropna()
|
||||
|
||||
# Load top factors
|
||||
factors = []
|
||||
for f in sorted(FACTORS_DIR.glob("*.json")):
|
||||
try: d = json.loads(f.read_text())
|
||||
except: continue
|
||||
if d.get("status") != "success" or d.get("ic") is None: continue
|
||||
ic = d["ic"]
|
||||
if abs(ic) < 0.02: continue
|
||||
name = d.get("factor_name", f.stem)
|
||||
safe = name.replace("/", "_")[:150]
|
||||
pf = FACTORS_DIR / "values" / f"{safe}.parquet"
|
||||
if pf.exists():
|
||||
factors.append((abs(ic), name))
|
||||
|
||||
factors.sort(reverse=True)
|
||||
top = factors[:100]
|
||||
|
||||
# Load factor values
|
||||
fdata = {}
|
||||
for _, name in top:
|
||||
safe = name.replace("/", "_")[:150]
|
||||
s = pd.read_parquet(FACTORS_DIR / "values" / f"{safe}.parquet").iloc[:, 0]
|
||||
if isinstance(s.index, pd.MultiIndex):
|
||||
s = s.droplevel(-1)
|
||||
fdata[name] = s.resample("1D").last()
|
||||
|
||||
df = pd.DataFrame(fdata)
|
||||
common = daily.index.intersection(df.dropna(how="all").index)
|
||||
return daily.loc[common], df.loc[common].ffill()
|
||||
|
||||
|
||||
def add_ohlcv_features(c: pd.Series) -> pd.DataFrame:
|
||||
"""Lightweight OHLCV features to complement factors."""
|
||||
df = pd.DataFrame(index=c.index)
|
||||
for n in [1, 5, 10, 20]:
|
||||
df[f"ret_{n}"] = c.pct_change(n)
|
||||
for n in [10, 20, 50, 100]:
|
||||
df[f"sma_{n}"] = c.rolling(n).mean() / c - 1
|
||||
df["sma10_50"] = c.rolling(10).mean() / c.rolling(50).mean() - 1
|
||||
df["sma20_100"] = c.rolling(20).mean() / c.rolling(100).mean() - 1
|
||||
for n in [5, 20]:
|
||||
df[f"vol_{n}"] = c.pct_change().rolling(n).std()
|
||||
d = c.diff(); g = d.clip(lower=0); l = -d.clip(upper=0)
|
||||
df["rsi14"] = 100 - (100 / (1 + g.rolling(14).mean() / (l.rolling(14).mean() + 1e-8)))
|
||||
df["adx14"] = (100 * abs(c.diff().clip(lower=0).ewm(14).mean() - (-c.diff().clip(upper=0)).ewm(14).mean()) / (
|
||||
c.diff().abs().rolling(14).mean() + 1e-8)).ewm(14).mean()
|
||||
return df
|
||||
|
||||
|
||||
def make_target(c: pd.Series, horizon: int = 5) -> np.ndarray:
|
||||
fwd = c.shift(-horizon)
|
||||
ret = (fwd / c - 1).fillna(0)
|
||||
t = ret.std() * 0.3 # Tighter threshold for more signals
|
||||
y = np.zeros(len(c))
|
||||
y[ret > t] = 1
|
||||
y[ret < -t] = -1
|
||||
return y
|
||||
|
||||
|
||||
def backtest_metric(c, y_pred, split_idx):
|
||||
test_c = c.iloc[split_idx:]
|
||||
sig = pd.Series(y_pred[split_idx:len(test_c)+split_idx], index=test_c.index[:len(y_pred)-split_idx])
|
||||
r = backtest_signal_risk(test_c.iloc[:len(sig)], sig.astype(float), txn_cost_bps=TXN_COST_BPS)
|
||||
return r.get("oos_sharpe", -999) or -999
|
||||
|
||||
|
||||
def main():
|
||||
print(f"\n{'='*65}")
|
||||
print(" NexQuant Factor-Boosted ML Pipeline")
|
||||
print(f" Target: 8%/month | Trials: {N_TRIALS}/horizon")
|
||||
print(f"{'='*65}")
|
||||
|
||||
c, factor_df = load_all()
|
||||
ohlcv_df = add_ohlcv_features(c)
|
||||
X_df = pd.concat([factor_df, ohlcv_df], axis=1).dropna()
|
||||
common = c.index.intersection(X_df.index)
|
||||
c = c.loc[common]; X_df = X_df.loc[common]
|
||||
print(f"Daily: {len(c):,} bars | Features: {len(X_df.columns)} ({len(factor_df.columns)} factors + {len(ohlcv_df.columns)} OHLCV)\n")
|
||||
|
||||
all_results = []
|
||||
|
||||
for horizon in [5, 10, 20]:
|
||||
print(f"─── HORIZON {horizon}d ───")
|
||||
y = make_target(c, horizon)
|
||||
mask = ~np.isnan(y) & ~np.isinf(np.abs(y))
|
||||
X = X_df.loc[mask].values.astype(np.float32)
|
||||
y_vals = y[mask].astype(int)
|
||||
split_idx = int(len(X) * 0.75)
|
||||
|
||||
if len(X) - split_idx < 20:
|
||||
print(" Skip — not enough OOS\n")
|
||||
continue
|
||||
|
||||
print(f" Train: {split_idx} OOS: {len(X)-split_idx}")
|
||||
|
||||
# Test multiple model types
|
||||
for model_name, ModelClass, param_space in [
|
||||
("RF", RandomForestClassifier, {
|
||||
"n": ("suggest_int", 100, 500), "d": ("suggest_int", 3, 25),
|
||||
"split": ("suggest_int", 2, 15), "leaf": ("suggest_int", 1, 10),
|
||||
"feat": ("suggest_float", 0.3, 1.0),
|
||||
}),
|
||||
("GBM", GradientBoostingClassifier, {
|
||||
"n": ("suggest_int", 100, 500), "d": ("suggest_int", 2, 10),
|
||||
"lr": ("suggest_float", 0.01, 0.3), "split": ("suggest_int", 2, 20),
|
||||
"leaf": ("suggest_int", 1, 10),
|
||||
}),
|
||||
]:
|
||||
def obj(trial):
|
||||
p = {}
|
||||
if model_name == "RF":
|
||||
p = {
|
||||
"n_estimators": trial.suggest_int("n", *param_space["n"][1:]),
|
||||
"max_depth": trial.suggest_int("d", *param_space["d"][1:]),
|
||||
"min_samples_split": trial.suggest_int("split", *param_space["split"][1:]),
|
||||
"min_samples_leaf": trial.suggest_int("leaf", *param_space["leaf"][1:]),
|
||||
"max_features": trial.suggest_float("feat", *param_space["feat"][1:]),
|
||||
"random_state": 42, "n_jobs": -1,
|
||||
}
|
||||
else:
|
||||
p = {
|
||||
"n_estimators": trial.suggest_int("n", *param_space["n"][1:]),
|
||||
"max_depth": trial.suggest_int("d", *param_space["d"][1:]),
|
||||
"learning_rate": trial.suggest_float("lr", *param_space["lr"][1:]),
|
||||
"min_samples_split": trial.suggest_int("split", *param_space["split"][1:]),
|
||||
"min_samples_leaf": trial.suggest_int("leaf", *param_space["leaf"][1:]),
|
||||
"random_state": 42,
|
||||
}
|
||||
model = ModelClass(**p)
|
||||
model.fit(X[:split_idx], y_vals[:split_idx])
|
||||
return backtest_metric(c, model.predict(X), split_idx)
|
||||
|
||||
study = optuna.create_study(direction="maximize", sampler=optuna.samplers.TPESampler(seed=42))
|
||||
study.optimize(obj, n_trials=N_TRIALS, show_progress_bar=False)
|
||||
|
||||
best = study.best_params
|
||||
best_val = study.best_value
|
||||
|
||||
# Final model
|
||||
if model_name == "RF":
|
||||
model = RandomForestClassifier(
|
||||
n_estimators=best.get("n",200), max_depth=best.get("d",10),
|
||||
min_samples_split=best.get("split",2), min_samples_leaf=best.get("leaf",1),
|
||||
max_features=best.get("feat",0.5), random_state=42, n_jobs=-1,
|
||||
)
|
||||
else:
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=best.get("n",200), max_depth=best.get("d",5),
|
||||
learning_rate=best.get("lr",0.1), min_samples_split=best.get("split",2),
|
||||
min_samples_leaf=best.get("leaf",1), random_state=42,
|
||||
)
|
||||
model.fit(X[:split_idx], y_vals[:split_idx])
|
||||
y_pred = model.predict(X)
|
||||
sig = pd.Series(y_pred[split_idx:len(c)-split_idx+split_idx], index=c.index[split_idx:split_idx+len(y_pred)-split_idx])
|
||||
r = backtest_signal_risk(c.iloc[split_idx:split_idx+len(sig)], sig.astype(float), txn_cost_bps=TXN_COST_BPS)
|
||||
|
||||
oos_s = r.get("oos_sharpe", -999)
|
||||
oos_m = (r.get("oos_monthly_return_pct", 0) or 0)
|
||||
oos_dd = (r.get("oos_max_drawdown", 0) or 0) * 100
|
||||
trades = r.get("oos_n_trades", 0)
|
||||
print(f" {model_name} h={horizon}d OOS={oos_s:+.1f} Mon={oos_m:+.3f}% DD={oos_dd:+.1f}% T={trades}")
|
||||
|
||||
all_results.append({
|
||||
"model": model_name, "horizon": horizon,
|
||||
"oos_sharpe": oos_s, "monthly": oos_m, "dd": oos_dd, "trades": trades,
|
||||
})
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*65}")
|
||||
print(f" {'Model':<6} {'Horiz':<6} {'OOS S':>8} {'Mon%':>9} {'DD%':>7} {'Trades':>7}")
|
||||
print(f" {'─'*46}")
|
||||
for r in sorted(all_results, key=lambda x: x["monthly"], reverse=True):
|
||||
print(f" {r['model']:<6} {r['horizon']:>3}d {r['oos_sharpe']:>+8.1f} {r['monthly']:>+8.3f}% {r['dd']:>+6.1f}% {r['trades']:>7}")
|
||||
|
||||
best = max(all_results, key=lambda x: x["monthly"])
|
||||
print(f"\n Best: {best['model']} {best['horizon']}d → {best['monthly']:+.3f}%/month")
|
||||
gap = 8.0 - best['monthly']
|
||||
print(f" Gap to 8%: {gap:+.3f}% {'✅' if gap <= 0 else '— needs improvement'}")
|
||||
|
||||
# Feature importance from best model
|
||||
if hasattr(model, 'feature_importances_'):
|
||||
imps = model.feature_importances_
|
||||
cols = X_df.columns
|
||||
top = sorted(zip(cols, imps), key=lambda x: -x[1])[:15]
|
||||
print(f"\n Top Features ({len(X_df.columns)} total):")
|
||||
for i, (name, imp) in enumerate(top, 1):
|
||||
src = "F" if name in factor_df.columns else "O"
|
||||
print(f" {i:2}. [{src}] {name:<45s} {imp:.4f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NexQuant Multi-Asset Data Pipeline — Download + Test on expanded universe.
|
||||
Downloads DXY, Gold, S&P 500, Bund, EUR/USD extended history via yfinance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json, sys, time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yfinance as yf
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
DATA_DIR = Path("git_ignore_folder/factor_implementation_source_data")
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Multi-asset tickers (free via Yahoo Finance)
|
||||
ASSETS = {
|
||||
"EURUSD": "EURUSD=X",
|
||||
"DXY": "DX-Y.NYB", # US Dollar Index
|
||||
"GOLD": "GC=F", # Gold Futures
|
||||
"SPX": "^GSPC", # S&P 500
|
||||
"BUND": "BUN24-EUX", # German Bund (approximate)
|
||||
"GBPUSD": "GBPUSD=X",
|
||||
"USDJPY": "USDJPY=X",
|
||||
"OIL": "CL=F", # Crude Oil
|
||||
}
|
||||
|
||||
def download_asset(name: str, ticker: str, period: str = "max") -> pd.DataFrame:
|
||||
print(f" Downloading {name} ({ticker})...")
|
||||
try:
|
||||
data = yf.download(ticker, period=period, progress=False, auto_adjust=True)
|
||||
if data.empty:
|
||||
print(f" Empty — skipping")
|
||||
return None
|
||||
close = data["Close"]
|
||||
if isinstance(close, pd.DataFrame):
|
||||
close = close.iloc[:, 0]
|
||||
close.name = name
|
||||
print(f" {len(close):,} bars ({close.index[0].date()} - {close.index[-1].date()})")
|
||||
return close
|
||||
except Exception as e:
|
||||
print(f" Failed: {e}")
|
||||
return None
|
||||
|
||||
def main():
|
||||
print(f"\n{'='*60}")
|
||||
print(" NexQuant Multi-Asset Data Download")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
all_data = {}
|
||||
for name, ticker in ASSETS.items():
|
||||
series = download_asset(name, ticker)
|
||||
if series is not None and len(series) > 100:
|
||||
all_data[name] = series
|
||||
|
||||
if not all_data:
|
||||
print("No data downloaded!")
|
||||
return
|
||||
|
||||
# Build combined DataFrame
|
||||
df = pd.DataFrame(all_data).dropna(how="all")
|
||||
print(f"\nCombined data: {len(df):,} daily bars, {len(df.columns)} assets")
|
||||
print(f"Date range: {df.index[0].date()} - {df.index[-1].date()}")
|
||||
|
||||
# Save to HDF5
|
||||
h5_path = DATA_DIR / "multi_asset_daily.h5"
|
||||
df.to_hdf(h5_path, key="data", mode="w")
|
||||
print(f"Saved to {h5_path}")
|
||||
|
||||
# Quick strategy test
|
||||
print(f"\n{'='*60}")
|
||||
print(" Quick Daily Strategy Test on Multi-Asset")
|
||||
print(f"{'='*60}")
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
for asset in df.columns:
|
||||
c = df[asset].dropna()
|
||||
if len(c) < 500:
|
||||
continue
|
||||
|
||||
# SMA 10/30
|
||||
f = c.rolling(10).mean()
|
||||
s = c.rolling(30).mean()
|
||||
sig = pd.Series(0.0, index=c.index)
|
||||
sig[f > s] = 1
|
||||
sig[f < s] = -1
|
||||
|
||||
r = backtest_signal_risk(c, sig.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
oos_m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
status = "✅" if oos > 0 else " "
|
||||
print(f" {asset:<10} SMA10/30: OOS={oos:+8.2f} Mon={oos_m:+6.2f}% {status}")
|
||||
|
||||
# Also test extended EUR/USD
|
||||
eurusd = df["EURUSD"].dropna()
|
||||
print(f"\n Extended EUR/USD: {len(eurusd):,} bars")
|
||||
c = eurusd
|
||||
f = c.rolling(10).mean()
|
||||
s = c.rolling(30).mean()
|
||||
sig = pd.Series(0.0, index=c.index)
|
||||
sig[f > s] = 1
|
||||
sig[f < s] = -1
|
||||
r = backtest_signal_risk(c, sig.fillna(0), txn_cost_bps=2.14, wf_rolling=True)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
print(f" SMA10/30 extended: OOS={oos:+8.2f} Mon={r.get('oos_monthly_return_pct',0):+.2f}%")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,16 +1,16 @@
|
||||
"""
|
||||
Predix Parallel Runner - Run multiple factor experiments concurrently.
|
||||
NexQuant Parallel Runner - Run multiple factor experiments concurrently.
|
||||
|
||||
Spawns N subprocesses, each running `predix.py quant` with isolated config:
|
||||
Spawns N subprocesses, each running `nexquant.py quant` with isolated config:
|
||||
- Separate log files (fin_quant_run1.log, fin_quant_run2.log, etc.)
|
||||
- Separate result directories (results/runs/run1/, results/runs/run2/, etc.)
|
||||
- Separate workspace directories
|
||||
- API key distribution across multiple keys (round-robin)
|
||||
|
||||
Usage:
|
||||
python predix_parallel.py --runs 5 --api-keys 2
|
||||
python predix_parallel.py --runs 3 --model openrouter
|
||||
python predix_parallel.py --runs 5 --model local --api-keys 1
|
||||
python nexquant_parallel.py --runs 5 --api-keys 2
|
||||
python nexquant_parallel.py --runs 3 --model openrouter
|
||||
python nexquant_parallel.py --runs 5 --model local --api-keys 1
|
||||
"""
|
||||
import os
|
||||
import signal
|
||||
@@ -188,7 +188,7 @@ class ParallelRunner:
|
||||
|
||||
def _build_command(self, run_state: RunState) -> list[str]:
|
||||
"""
|
||||
Build the subprocess command to run predix quant.
|
||||
Build the subprocess command to run nexquant quant.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -202,7 +202,7 @@ class ParallelRunner:
|
||||
"""
|
||||
cmd = [
|
||||
sys.executable, # Use same Python interpreter
|
||||
str(self.project_root / "predix.py"),
|
||||
str(self.project_root / "nexquant.py"),
|
||||
"quant",
|
||||
"--model", run_state.model,
|
||||
"--run-id", str(run_state.run_id),
|
||||
@@ -327,7 +327,7 @@ class ParallelRunner:
|
||||
|
||||
# Build summary table
|
||||
table = Table(
|
||||
title="🔀 Predix Parallel Run Dashboard",
|
||||
title="🔀 NexQuant Parallel Run Dashboard",
|
||||
show_header=True,
|
||||
header_style="bold cyan",
|
||||
expand=True,
|
||||
@@ -399,7 +399,7 @@ class ParallelRunner:
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
|
||||
console.print(f"\n[bold cyan]{'=' * 60}[/bold cyan]")
|
||||
console.print("[bold cyan]🔀 Predix Parallel Runner[/bold cyan]")
|
||||
console.print("[bold cyan]🔀 NexQuant Parallel Runner[/bold cyan]")
|
||||
console.print(f"[bold cyan]{'=' * 60}[/bold cyan]")
|
||||
console.print(f" Runs: {self.num_runs}")
|
||||
console.print(f" API Keys: {self.num_api_keys} ({len(self.api_keys)} available)")
|
||||
@@ -503,7 +503,7 @@ if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Predix Parallel Runner - Run multiple factor experiments concurrently",
|
||||
description="NexQuant Parallel Runner - Run multiple factor experiments concurrently",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--runs", "-n",
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NexQuant Multi-Asset Portfolio Generator — Target: 10%/month.
|
||||
Combines best strategies per asset, optimizes position sizing, adds leverage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
|
||||
|
||||
DATA = Path("git_ignore_folder/factor_implementation_source_data/multi_asset_daily.h5")
|
||||
|
||||
|
||||
def load_all():
|
||||
df = pd.read_hdf(DATA, key="data")
|
||||
close_dict = {}
|
||||
for col in df.columns:
|
||||
c = df[col].dropna()
|
||||
if len(c) > 500:
|
||||
close_dict[col] = c
|
||||
return close_dict
|
||||
|
||||
|
||||
def rsi_signal(c, period, lo, hi):
|
||||
d = c.diff(); g = d.clip(lower=0); l = -d.clip(upper=0)
|
||||
rsi = 100 - (100 / (1 + g.rolling(period).mean() / (l.rolling(period).mean() + 1e-8)))
|
||||
sig = pd.Series(0.0, index=c.index)
|
||||
sig[rsi < lo] = 1; sig[rsi > hi] = -1
|
||||
return sig
|
||||
|
||||
|
||||
def sma_signal(c, fast, slow):
|
||||
f = c.rolling(fast).mean(); s = c.rolling(slow).mean()
|
||||
sig = pd.Series(0.0, index=c.index)
|
||||
sig[f > s] = 1; sig[f < s] = -1
|
||||
return sig
|
||||
|
||||
|
||||
def mr_signal(c, n):
|
||||
ret = c.pct_change(n)
|
||||
return pd.Series(-np.sign(ret).fillna(0), index=c.index)
|
||||
|
||||
|
||||
def mom_signal(c, n):
|
||||
mom = c.pct_change(n)
|
||||
return pd.Series(np.sign(mom).fillna(0), index=c.index)
|
||||
|
||||
|
||||
# Best strategy per asset (from our grid search)
|
||||
STRATEGIES = {
|
||||
"OIL": lambda c: mr_signal(c, 50),
|
||||
"DXY": lambda c: sma_signal(c, 5, 25),
|
||||
"SPX": lambda c: mom_signal(c, 100),
|
||||
"EURUSD": lambda c: rsi_signal(c, 21, 25, 75),
|
||||
"USDJPY": lambda c: sma_signal(c, 50, 200),
|
||||
"GOLD": lambda c: rsi_signal(c, 21, 25, 75),
|
||||
"GBPUSD": lambda c: rsi_signal(c, 21, 25, 75),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
print(f"\n{'='*65}")
|
||||
print(" NexQuant Multi-Asset Portfolio — 10%/month Target")
|
||||
print(f"{'='*65}")
|
||||
|
||||
closes = load_all()
|
||||
assets = sorted(closes.keys())
|
||||
print(f"Assets: {len(assets)} | Total bars: {max(len(c) for c in closes.values()):,}\n")
|
||||
|
||||
aligned_signals = {}
|
||||
all_returns = []
|
||||
|
||||
# Step 1: Generate signals per asset
|
||||
print("=== Individual Asset Performance ===")
|
||||
for name in assets:
|
||||
c = closes[name]
|
||||
sig_func = STRATEGIES.get(name, lambda c: rsi_signal(c, 21, 25, 75))
|
||||
sig = sig_func(c).fillna(0)
|
||||
|
||||
r = backtest_signal_risk(c, sig, txn_cost_bps=2.14, wf_rolling=True)
|
||||
oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999)
|
||||
oos_m = r.get("oos_monthly_return_pct", 0) or 0
|
||||
status = "✅" if oos > 0 else " "
|
||||
print(f" {name:<10} OOS={oos:+8.2f} Mon={oos_m:+7.3f}% {status}")
|
||||
|
||||
aligned_signals[name] = sig
|
||||
# Monthly returns for this asset
|
||||
ret = c.pct_change() * sig.shift(1)
|
||||
ret.name = name
|
||||
all_returns.append(ret)
|
||||
|
||||
# Step 2: Build equal-weight portfolio returns
|
||||
returns_df = pd.concat(all_returns, axis=1).dropna(how="all")
|
||||
common = returns_df.dropna().index
|
||||
returns_df = returns_df.loc[common].fillna(0)
|
||||
port_ret_equal = returns_df.mean(axis=1)
|
||||
|
||||
print(f"\n=== Equal-Weight Portfolio ({len(returns_df.columns)} assets) ===")
|
||||
# Monthly returns
|
||||
monthly_eq = port_ret_equal.resample("M").apply(lambda x: (1 + x).prod() - 1) * 100
|
||||
months = len(monthly_eq.dropna())
|
||||
print(f" Mean monthly: {monthly_eq.mean():+.3f}%")
|
||||
print(f" Median monthly: {monthly_eq.median():+.3f}%")
|
||||
print(f" Positive months: {(monthly_eq > 0).mean()*100:.1f}%")
|
||||
print(f" Months: {months}")
|
||||
# Annualized
|
||||
ann_ret = (1 + port_ret_equal).prod() ** (252 / len(port_ret_equal)) - 1
|
||||
ann_vol = port_ret_equal.std() * np.sqrt(252)
|
||||
ann_sharpe = ann_ret / ann_vol if ann_vol > 0 else 0
|
||||
print(f" Annual return: {ann_ret*100:.1f}%")
|
||||
print(f" Annual vol: {ann_vol*100:.1f}%")
|
||||
print(f" Annual Sharpe: {ann_sharpe:.3f}")
|
||||
|
||||
# Step 3: Risk-parity weighting
|
||||
vols = returns_df.std()
|
||||
inv_vols = 1.0 / (vols + 1e-8)
|
||||
rp_weights = inv_vols / inv_vols.sum()
|
||||
port_ret_rp = (returns_df * rp_weights).sum(axis=1)
|
||||
|
||||
monthly_rp = port_ret_rp.resample("M").apply(lambda x: (1 + x).prod() - 1) * 100
|
||||
print(f"\n=== Risk-Parity Portfolio ===")
|
||||
print(f" Weights: {dict(zip(returns_df.columns, rp_weights.round(3)))}")
|
||||
print(f" Mean monthly: {monthly_rp.mean():+.3f}%")
|
||||
print(f" Positive months: {(monthly_rp > 0).mean()*100:.1f}%")
|
||||
ann_rp = (1 + port_ret_rp).prod() ** (252 / len(port_ret_rp)) - 1
|
||||
print(f" Annual return: {ann_rp*100:.1f}%")
|
||||
|
||||
# Step 4: With leverage
|
||||
print(f"\n=== With Leverage (2x, 3x, 5x) ===")
|
||||
for lev in [2, 3, 5]:
|
||||
port_lev = port_ret_rp * lev
|
||||
monthly_lev = port_lev.resample("M").apply(lambda x: (1 + x).prod() - 1) * 100
|
||||
ann_lev = (1 + port_lev).prod() ** (252 / len(port_lev)) - 1
|
||||
max_dd = (port_lev.cumsum().cummax() - port_lev.cumsum()).max()
|
||||
print(f" {lev}x: Ann={ann_lev*100:+.1f}% Mon={monthly_lev.mean():+.2f}% MaxDD={max_dd*100:.1f}%")
|
||||
|
||||
# Step 5: Check if 10% is reachable
|
||||
target_monthly = 10.0
|
||||
needed_lev = target_monthly / monthly_rp.mean() if monthly_rp.mean() > 0 else float("inf")
|
||||
print(f"\n=== Target: {target_monthly}%/month ===")
|
||||
print(f" Current (risk-parity): {monthly_rp.mean():+.2f}%/month")
|
||||
print(f" Leverage needed: {needed_lev:.1f}x")
|
||||
if needed_lev < 10:
|
||||
print(f" ✅ Achievable with {needed_lev:.1f}x leverage")
|
||||
else:
|
||||
print(f" ❌ Not achievable — need {needed_lev:.1f}x leverage")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Portfolio Optimizer — combine uncorrelated strategies for 15% monthly target.
|
||||
|
||||
Given N strategies with daily returns, find the optimal combination that:
|
||||
- Maximizes monthly return
|
||||
- Keeps max drawdown within RiskMgmt limits (10% total, 5% daily)
|
||||
- Diversifies across uncorrelated strategies
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
PROJECT = Path(__file__).resolve().parent.parent
|
||||
RESULTS_DIR = PROJECT / "results" / "strategies_new"
|
||||
STRATEGIES_DIR = PROJECT / "results" / "strategies"
|
||||
FACTORS_DIR = PROJECT / "results" / "factors"
|
||||
VALUES_DIR = FACTORS_DIR / "values"
|
||||
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
|
||||
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
|
||||
|
||||
TARGET_MONTHLY = 15.0
|
||||
MAX_DD = 0.10 # RiskMgmt: 10% max total drawdown
|
||||
MAX_DAILY_DD = 0.05 # RiskMgmt: 5% max daily drawdown
|
||||
MIN_TRADES = 30
|
||||
MIN_SHARPE = 0.5
|
||||
|
||||
|
||||
def load_strategies() -> list[dict]:
|
||||
"""Load all strategy JSONs with real (non-fabricated) verified metrics."""
|
||||
strategies = []
|
||||
seen = set()
|
||||
for d in (STRATEGIES_DIR, RESULTS_DIR):
|
||||
if not d.exists():
|
||||
continue
|
||||
for p in d.glob("*.json"):
|
||||
try:
|
||||
r = json.loads(p.read_text())
|
||||
except Exception:
|
||||
continue
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
name = r.get("strategy_name", p.stem)
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
|
||||
s = r.get("summary", {})
|
||||
if not isinstance(s, dict):
|
||||
s = {}
|
||||
m = r.get("metrics", {})
|
||||
if not isinstance(m, dict):
|
||||
m = {}
|
||||
|
||||
# Extract metrics (prefer summary, fallback to metrics)
|
||||
sharpe = float(s.get("sharpe") or m.get("sharpe") or 0)
|
||||
mon_pct = float(s.get("monthly_return_pct") or s.get("oos_monthly_return_pct")
|
||||
or m.get("monthly_return_pct") or 0)
|
||||
max_dd = float(s.get("max_drawdown") or s.get("oos_max_drawdown")
|
||||
or m.get("max_drawdown") or 0)
|
||||
win_rate = float(s.get("win_rate") or s.get("oos_win_rate")
|
||||
or m.get("win_rate") or 0)
|
||||
n_trades = int(s.get("n_trades") or s.get("oos_n_trades")
|
||||
or s.get("real_n_trades") or m.get("n_trades") or 0)
|
||||
total_ret = float(s.get("total_return") or m.get("total_return") or 0)
|
||||
|
||||
# Filter fabricated
|
||||
if mon_pct == 200 and sharpe == 3.0 and abs(max_dd + 0.167) < 0.01:
|
||||
continue
|
||||
if mon_pct == -20 and max_dd == -1.0:
|
||||
continue
|
||||
if sharpe == 200:
|
||||
continue
|
||||
|
||||
# Filter quality
|
||||
if n_trades < MIN_TRADES or sharpe < MIN_SHARPE:
|
||||
continue
|
||||
if mon_pct <= 0:
|
||||
continue
|
||||
|
||||
strategies.append({
|
||||
"name": name,
|
||||
"file": str(p),
|
||||
"sharpe": sharpe,
|
||||
"monthly_pct": mon_pct,
|
||||
"max_dd": max_dd,
|
||||
"win_rate": win_rate,
|
||||
"n_trades": n_trades,
|
||||
"total_return": total_ret,
|
||||
"factors": r.get("factor_names") or r.get("factors_used") or [],
|
||||
"code": r.get("code", ""),
|
||||
})
|
||||
|
||||
return strategies
|
||||
|
||||
|
||||
def load_strategy_returns(strategy: dict, close_daily: pd.Series) -> pd.Series | None:
|
||||
"""Reconstruct daily strategy returns from code and factor data."""
|
||||
code = strategy.get("code", "")
|
||||
if not code:
|
||||
return None
|
||||
|
||||
factors_list = strategy.get("factors", [])
|
||||
if not factors_list:
|
||||
return None
|
||||
|
||||
# Load factor values
|
||||
factor_series = {}
|
||||
for fname in factors_list:
|
||||
safe = str(fname).replace("/", "_").replace("\\", "_").replace(" ", "_")[:150]
|
||||
parq = VALUES_DIR / f"{safe}.parquet"
|
||||
if not parq.exists():
|
||||
continue
|
||||
try:
|
||||
s = pd.read_parquet(str(parq))
|
||||
if isinstance(s.index, pd.MultiIndex):
|
||||
s = s.xs("EURUSD", level="instrument")[s.columns[0]]
|
||||
# Align to close_daily index
|
||||
s = s.resample("D").last().reindex(close_daily.index).ffill(limit=5)
|
||||
factor_series[fname] = s
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if len(factor_series) < 2:
|
||||
return None
|
||||
|
||||
df_factors = pd.DataFrame(factor_series).dropna()
|
||||
if len(df_factors) < 100:
|
||||
return None
|
||||
|
||||
# Execute strategy code on daily data
|
||||
local_vars = {"factors": df_factors, "close": close_daily.reindex(df_factors.index)}
|
||||
try:
|
||||
exec(code, {"np": np, "pd": pd, "numpy": np}, local_vars)
|
||||
except Exception:
|
||||
# Can't execute — use simple IC-weighted signal as fallback
|
||||
return None
|
||||
|
||||
signal = local_vars.get("signal")
|
||||
if signal is None or not isinstance(signal, pd.Series):
|
||||
return None
|
||||
|
||||
# Compute daily returns from signal
|
||||
common = close_daily.index.intersection(signal.index)
|
||||
c = close_daily.loc[common]
|
||||
s = signal.loc[common].clip(-1, 1).fillna(0)
|
||||
|
||||
fwd_ret = c.pct_change().shift(-1)
|
||||
strat_ret = s.shift(1) * fwd_ret
|
||||
strat_ret = strat_ret.dropna()
|
||||
|
||||
if len(strat_ret) < 30:
|
||||
return None
|
||||
|
||||
return strat_ret
|
||||
|
||||
|
||||
def build_simple_signal(factors_list: list[str], close_daily: pd.Series) -> tuple[pd.Series, pd.Series]:
|
||||
"""Build simple IC-weighted daily signal (fallback when code fails)."""
|
||||
import json as _json
|
||||
|
||||
factor_series = {}
|
||||
ic_values = {}
|
||||
for fname in factors_list:
|
||||
safe = str(fname).replace("/", "_").replace("\\", "_").replace(" ", "_")[:150]
|
||||
parq = VALUES_DIR / f"{safe}.parquet"
|
||||
jf = FACTORS_DIR / f"{safe}.json"
|
||||
if not parq.exists():
|
||||
continue
|
||||
ic = 0.0
|
||||
if jf.exists():
|
||||
ic = float(_json.loads(jf.read_text()).get("ic", 0))
|
||||
try:
|
||||
s = pd.read_parquet(str(parq))
|
||||
if isinstance(s.index, pd.MultiIndex):
|
||||
s = s.xs("EURUSD", level="instrument")[s.columns[0]]
|
||||
s = s.resample("D").last().reindex(close_daily.index).ffill(limit=5)
|
||||
factor_series[fname] = s
|
||||
ic_values[fname] = ic
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
df = pd.DataFrame(factor_series).dropna()
|
||||
if len(df) < 50:
|
||||
return pd.Series(), pd.Series()
|
||||
|
||||
# z-score composite
|
||||
window = 20
|
||||
z = (df - df.rolling(window).mean()) / (df.rolling(window).std() + 1e-8)
|
||||
|
||||
composite = pd.Series(0.0, index=df.index)
|
||||
total_ic = sum(abs(v) for v in ic_values.values())
|
||||
if total_ic == 0:
|
||||
total_ic = 1.0
|
||||
for col in df.columns:
|
||||
ic = ic_values.get(col, 0)
|
||||
w = abs(ic) / total_ic
|
||||
sign = -1 if ic < 0 else 1
|
||||
composite += sign * w * z[col]
|
||||
|
||||
signal = pd.Series(0, index=df.index)
|
||||
signal[composite > 0.5] = 1
|
||||
signal[composite < -0.5] = -1
|
||||
|
||||
# Compute returns
|
||||
common = close_daily.index.intersection(signal.index)
|
||||
c = close_daily.loc[common]
|
||||
s = signal.loc[common].clip(-1, 1).fillna(0)
|
||||
fwd_ret = c.pct_change().shift(-1)
|
||||
strat_ret = s.shift(1) * fwd_ret
|
||||
return signal, strat_ret.dropna()
|
||||
|
||||
|
||||
def compute_portfolio_metrics(returns: list[pd.Series], weights: list[float],
|
||||
close_daily: pd.Series) -> dict:
|
||||
"""Compute portfolio-level metrics from weighted strategy returns."""
|
||||
if not returns:
|
||||
return {"monthly_pct": 0, "max_dd": 0, "sharpe": 0}
|
||||
|
||||
# Align all return series
|
||||
common_idx = returns[0].index
|
||||
for r in returns[1:]:
|
||||
common_idx = common_idx.intersection(r.index)
|
||||
if len(common_idx) < 50:
|
||||
return {"monthly_pct": 0, "max_dd": 0, "sharpe": 0}
|
||||
|
||||
aligned = pd.DataFrame({i: r.loc[common_idx] for i, r in enumerate(returns)}).dropna()
|
||||
if len(aligned) < 30:
|
||||
return {"monthly_pct": 0, "max_dd": 0, "sharpe": 0}
|
||||
|
||||
# Weighted portfolio return
|
||||
port_ret = pd.Series(0.0, index=aligned.index)
|
||||
for i in range(len(returns)):
|
||||
port_ret += weights[i] * aligned[i]
|
||||
|
||||
# Equity curve
|
||||
eq = (1 + port_ret).cumprod()
|
||||
peak = eq.cummax()
|
||||
max_dd = float(((eq - peak) / peak).min())
|
||||
|
||||
total_ret = float(eq.iloc[-1] - 1)
|
||||
n_days = (port_ret.index[-1] - port_ret.index[0]).days
|
||||
n_months = max(n_days / 30.44, 1)
|
||||
monthly = float((1 + total_ret) ** (1 / n_months) - 1)
|
||||
|
||||
sharpe = float(port_ret.mean() / port_ret.std() * np.sqrt(252)) if port_ret.std() > 0 else 0
|
||||
daily_dd = float(port_ret.min()) # Worst daily return
|
||||
|
||||
return {
|
||||
"monthly_pct": monthly * 100,
|
||||
"max_dd": max_dd,
|
||||
"sharpe": sharpe,
|
||||
"daily_worst": daily_dd,
|
||||
"n_days": len(port_ret),
|
||||
"n_months": n_months,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print(" Portfolio Optimizer — 15% Monthly Target")
|
||||
print("=" * 60)
|
||||
|
||||
# Load OHLCV daily
|
||||
print("\nLoading data...")
|
||||
df = pd.read_hdf(OHLCV_PATH, key="data")
|
||||
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
|
||||
close_daily = close.resample("D").last().dropna()
|
||||
print(f" Daily bars: {len(close_daily)}")
|
||||
|
||||
# Load strategies
|
||||
strategies = load_strategies()
|
||||
print(f" Real strategies: {len(strategies)}")
|
||||
|
||||
# Build daily returns for each strategy
|
||||
print("\nBuilding strategy returns...")
|
||||
strat_returns = []
|
||||
strat_names = []
|
||||
for s in strategies[:50]: # Limit to top 50 for speed
|
||||
rets = load_strategy_returns(s, close_daily)
|
||||
if rets is None or len(rets) < 30:
|
||||
# Use simple signal as fallback
|
||||
_, rets = build_simple_signal(s["factors"], close_daily)
|
||||
if rets is not None and len(rets) >= 30:
|
||||
strat_returns.append(rets)
|
||||
strat_names.append(s["name"])
|
||||
print(f" [{len(strat_returns)}] {s['name'][:40]:40s} "
|
||||
f"Sh={s['sharpe']:.1f} Mon={s['monthly_pct']:.1f}% Tr={s['n_trades']}")
|
||||
|
||||
if len(strat_returns) < 2:
|
||||
print("\n Not enough valid strategies.")
|
||||
return
|
||||
|
||||
print(f"\n Valid return series: {len(strat_returns)}")
|
||||
|
||||
# Find best portfolio via greedy selection (low correlation, high return)
|
||||
print("\n--- Greedy Portfolio Selection ---")
|
||||
print(f" Target: {TARGET_MONTHLY}% monthly | Max DD: {MAX_DD:.0%} | Max Daily DD: {MAX_DAILY_DD:.0%}")
|
||||
print()
|
||||
|
||||
# Compute individual metrics
|
||||
individual = []
|
||||
for i, (rets, name) in enumerate(zip(strat_returns, strat_names)):
|
||||
eq = (1 + rets).cumprod()
|
||||
dd = float(((eq - eq.cummax()) / eq.cummax()).min())
|
||||
total = float(eq.iloc[-1] - 1)
|
||||
n = max((rets.index[-1] - rets.index[0]).days / 30.44, 1)
|
||||
mon = float((1 + total) ** (1 / n) - 1) * 100
|
||||
individual.append({"idx": i, "name": name, "monthly": mon, "dd": dd, "n": len(rets)})
|
||||
|
||||
individual.sort(key=lambda x: x["monthly"], reverse=True)
|
||||
|
||||
# Greedy: add strategies one by one if they don't increase correlation too much
|
||||
selected = []
|
||||
selected_rets = []
|
||||
|
||||
for s in individual:
|
||||
if len(selected) >= 8:
|
||||
break
|
||||
# Check correlation with existing portfolio
|
||||
new_ret = strat_returns[s["idx"]]
|
||||
if selected_rets:
|
||||
common = new_ret.index
|
||||
for r in selected_rets:
|
||||
common = common.intersection(r.index)
|
||||
if len(common) < 30:
|
||||
continue
|
||||
cors = []
|
||||
for r in selected_rets:
|
||||
aligned_new = new_ret.loc[common]
|
||||
aligned_r = r.loc[common]
|
||||
if len(aligned_new) >= 30:
|
||||
cors.append(abs(aligned_new.corr(aligned_r)))
|
||||
if cors and max(cors) > 0.5:
|
||||
print(f" SKIP {s['name'][:40]} (max_corr={max(cors):.2f})")
|
||||
continue
|
||||
|
||||
selected.append(s)
|
||||
selected_rets.append(new_ret)
|
||||
print(f" ADD {s['name'][:40]:40s} Mon={s['monthly']:+.1f}% DD={s['dd']:.3f} corr<0.5")
|
||||
|
||||
# Evaluate portfolio
|
||||
if len(selected) >= 2:
|
||||
print(f"\n Portfolio: {len(selected)} strategies")
|
||||
weights = [1.0 / len(selected)] * len(selected)
|
||||
rets = [strat_returns[s["idx"]] for s in selected]
|
||||
pm = compute_portfolio_metrics(rets, weights, close_daily)
|
||||
|
||||
print(f" Equal-weight metrics:")
|
||||
print(f" Monthly return: {pm['monthly_pct']:.2f}%")
|
||||
print(f" Max drawdown: {pm['max_dd']:.3f}")
|
||||
print(f" Sharpe: {pm['sharpe']:.2f}")
|
||||
print(f" Worst day: {pm['daily_worst']:.3%}")
|
||||
print(f" Period: {pm['n_months']:.1f} months ({pm['n_days']} days)")
|
||||
|
||||
# Leverage scaling
|
||||
max_safe_lev = min(
|
||||
MAX_DD / abs(pm["max_dd"]) if pm["max_dd"] != 0 else 30,
|
||||
MAX_DAILY_DD / abs(pm["daily_worst"]) if pm["daily_worst"] != 0 else 30,
|
||||
30,
|
||||
)
|
||||
leveraged_monthly = pm["monthly_pct"] * max_safe_lev
|
||||
print(f"\n Max safe leverage: {max_safe_lev:.1f}× (limited by max DD {MAX_DD:.0%})")
|
||||
print(f" Leveraged monthly: {leveraged_monthly:.1f}%")
|
||||
|
||||
if leveraged_monthly >= TARGET_MONTHLY:
|
||||
print(f"\n ✓ MEETS TARGET! {leveraged_monthly:.1f}% ≥ {TARGET_MONTHLY}%")
|
||||
else:
|
||||
gap = TARGET_MONTHLY - leveraged_monthly
|
||||
needed_strategies = int(np.ceil(len(selected) * TARGET_MONTHLY / max(leveraged_monthly, 0.1)))
|
||||
print(f"\n ✗ Below target. Need ~{needed_strategies} strategies or {TARGET_MONTHLY/max(pm['monthly_pct'],0.01):.1f}× better monthly.")
|
||||
|
||||
# Save portfolio config
|
||||
out = {
|
||||
"target_monthly": TARGET_MONTHLY,
|
||||
"selected": [{"name": s["name"], "monthly": s["monthly"], "dd": s["dd"]} for s in selected],
|
||||
"portfolio": pm if len(selected) >= 2 else {},
|
||||
}
|
||||
out_path = RESULTS_DIR / "portfolio_config.json"
|
||||
out_path.write_text(json.dumps(out, indent=2, default=str))
|
||||
print(f"\n Saved → {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Price-Action Strategy Generator — no LLM, no factors, pure technical analysis.
|
||||
|
||||
Uses Donchian channels, moving averages, RSI, Bollinger Bands, and MACD
|
||||
on daily resolution. Grid-searches parameters, validates via backtest_signal.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
PROJECT = Path(__file__).resolve().parent.parent
|
||||
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
|
||||
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
|
||||
RESULTS_DIR = PROJECT / "results" / "strategies_new"
|
||||
|
||||
MIN_MONTHLY = 1.0
|
||||
MIN_SHARPE = 1.0
|
||||
MAX_DD = -0.15
|
||||
MIN_TRADES = 30
|
||||
|
||||
|
||||
def load_data():
|
||||
df = pd.read_hdf(OHLCV_PATH, key="data")
|
||||
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
|
||||
daily = close.resample("D").last().dropna()
|
||||
return close, daily
|
||||
|
||||
|
||||
def to_1min(daily_signal: pd.Series, close_1min: pd.Series) -> pd.Series:
|
||||
return daily_signal.reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Strategy templates
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def donchian(close: pd.Series, period: int, hold: int) -> pd.Series:
|
||||
"""Donchian channel breakout."""
|
||||
high = close.rolling(period).max()
|
||||
low = close.rolling(period).min()
|
||||
s = pd.Series(0, index=close.index)
|
||||
s[close > high.shift(1)] = 1
|
||||
s[close < low.shift(1)] = -1
|
||||
s = s.replace(0, np.nan).ffill(limit=hold).fillna(0).astype(int).clip(-1, 1)
|
||||
return s
|
||||
|
||||
|
||||
def sma_cross(close: pd.Series, fast: int, slow: int) -> pd.Series:
|
||||
"""SMA crossover."""
|
||||
s = pd.Series(0, index=close.index)
|
||||
s[close.rolling(fast).mean() > close.rolling(slow).mean()] = 1
|
||||
s[close.rolling(fast).mean() < close.rolling(slow).mean()] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
def rsi_mr(close: pd.Series, period: int, oversold: int, overbought: int) -> pd.Series:
|
||||
"""RSI mean-reversion."""
|
||||
delta = close.diff()
|
||||
gain = delta.clip(lower=0).rolling(period).mean()
|
||||
loss = (-delta.clip(upper=0)).rolling(period).mean()
|
||||
rs = gain / (loss + 1e-8)
|
||||
rsi = 100 - 100 / (1 + rs)
|
||||
s = pd.Series(0, index=close.index)
|
||||
s[rsi < oversold] = 1
|
||||
s[rsi > overbought] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
def bollinger_mr(close: pd.Series, period: int, std: float) -> pd.Series:
|
||||
"""Bollinger Band mean-reversion."""
|
||||
ma = close.rolling(period).mean()
|
||||
st = close.rolling(period).std()
|
||||
s = pd.Series(0, index=close.index)
|
||||
s[close < ma - std * st] = 1
|
||||
s[close > ma + std * st] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
def macd(close: pd.Series, fast: int, slow: int, signal_p: int) -> pd.Series:
|
||||
"""MACD crossover."""
|
||||
ema_fast = close.ewm(span=fast, adjust=False).mean()
|
||||
ema_slow = close.ewm(span=slow, adjust=False).mean()
|
||||
macd_line = ema_fast - ema_slow
|
||||
sig_line = macd_line.ewm(span=signal_p, adjust=False).mean()
|
||||
s = pd.Series(0, index=close.index)
|
||||
s[macd_line > sig_line] = 1
|
||||
s[macd_line < sig_line] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
def ma_envelope(close: pd.Series, period: int, pct: float) -> pd.Series:
|
||||
"""Moving average envelope mean-reversion."""
|
||||
ma = close.rolling(period).mean()
|
||||
s = pd.Series(0, index=close.index)
|
||||
s[close < ma * (1 - pct)] = 1
|
||||
s[close > ma * (1 + pct)] = -1
|
||||
return s.replace(0, np.nan).ffill(limit=3).fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
def atr_breakout(close: pd.Series, period: int, mult: float) -> pd.Series:
|
||||
"""ATR-based volatility breakout (simplified, using close-only)."""
|
||||
atr = (close.diff().abs()).rolling(period).mean()
|
||||
ma = close.rolling(period).mean()
|
||||
s = pd.Series(0, index=close.index)
|
||||
s[close > ma + mult * atr] = 1
|
||||
s[close < ma - mult * atr] = -1
|
||||
return s.replace(0, np.nan).ffill(limit=2).fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Main
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print(" Price-Action Strategy Generator (No LLM, No Factors)")
|
||||
print("=" * 60)
|
||||
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
||||
|
||||
close, daily = load_data()
|
||||
print(f"\nDaily data: {len(daily)} bars ({daily.index[0].date()} → {daily.index[-1].date()})")
|
||||
|
||||
import itertools
|
||||
|
||||
grid = [
|
||||
("Donchian", donchian, [
|
||||
(p, h) for p in [5, 7, 10, 12, 15, 20, 25, 30, 40, 60]
|
||||
for h in [1, 2, 3, 5]
|
||||
]),
|
||||
("SMA_Crossover", sma_cross, [
|
||||
(f, s) for f in [5, 10, 20]
|
||||
for s in [20, 50, 100, 200] if s > f
|
||||
]),
|
||||
("RSI_MR", rsi_mr, [
|
||||
(p, lo, hi) for p in [7, 14, 21]
|
||||
for lo, hi in [(30, 70), (25, 75), (20, 80)]
|
||||
]),
|
||||
("Bollinger_MR", bollinger_mr, [
|
||||
(p, s) for p in [10, 20, 40]
|
||||
for s in [1.5, 2.0, 2.5]
|
||||
]),
|
||||
("MACD", macd, [
|
||||
(f, s, sig) for f, s, sig in [(8, 21, 5), (12, 26, 9), (5, 20, 3)]
|
||||
]),
|
||||
("MA_Envelope", ma_envelope, [
|
||||
(p, pct) for p in [20, 50, 100]
|
||||
for pct in [0.01, 0.02, 0.03]
|
||||
]),
|
||||
("ATR_Breakout", atr_breakout, [
|
||||
(p, m) for p in [10, 20, 40]
|
||||
for m in [1.0, 1.5, 2.0]
|
||||
]),
|
||||
]
|
||||
|
||||
results = []
|
||||
t0 = time.time()
|
||||
total = sum(len(params) for _, _, params in grid)
|
||||
done = 0
|
||||
|
||||
print(f"\nTesting {total} parameter combinations...\n")
|
||||
|
||||
for name, fn, params_list in grid:
|
||||
for params in params_list:
|
||||
done += 1
|
||||
daily_signal = fn(daily, *params)
|
||||
signal_1min = to_1min(daily_signal, close)
|
||||
bt = backtest_signal(close=close, signal=signal_1min)
|
||||
bt["strategy"] = name
|
||||
bt["params"] = params
|
||||
bt["name"] = f"{name}{params}"
|
||||
bt["monthly_pct"] = bt.get("monthly_return_pct", 0)
|
||||
bt["max_dd"] = bt.get("max_drawdown", 0)
|
||||
results.append(bt)
|
||||
if done % 50 == 0 or done == total:
|
||||
elapsed = time.time() - t0
|
||||
rate = done / elapsed if elapsed > 0 else 0
|
||||
eta = (total - done) / rate if rate > 0 else 0
|
||||
print(f" {done}/{total} ({done/total*100:.0f}%) {rate:.0f}/s eta {eta:.0f}s")
|
||||
|
||||
elapsed = time.time() - t0
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" Evaluated: {total} in {elapsed:.0f}s")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
valid = [r for r in results
|
||||
if r.get("sharpe", 0) >= MIN_SHARPE
|
||||
and r.get("max_dd", 0) >= MAX_DD
|
||||
and r.get("n_trades", 0) >= MIN_TRADES
|
||||
and r.get("monthly_pct", 0) >= MIN_MONTHLY]
|
||||
valid.sort(key=lambda r: r.get("monthly_pct", 0), reverse=True)
|
||||
|
||||
print(f"\n Meeting: Sharpe≥{MIN_SHARPE} DD≥{MAX_DD} Tr≥{MIN_TRADES} Mon≥{MIN_MONTHLY}%")
|
||||
print(f" → {len(valid)} strategies\n")
|
||||
|
||||
hdr = "{:>3s} {:20s} {:20s} {:>7s} {:>7s} {:>7s} {:>5s} {:>6s}"
|
||||
print(hdr.format("#", "Strategy", "Params", "Sharpe", "Mon%", "MaxDD", "Tr", "WinRt"))
|
||||
print("-" * 85)
|
||||
for i, r in enumerate(valid[:30], 1):
|
||||
ps = str(r["params"]).replace(" ", "")[:18]
|
||||
print(hdr.format(str(i), r["strategy"][:20], ps,
|
||||
f'{r.get("sharpe",0):.2f}', f'{r.get("monthly_pct",0):.1f}%',
|
||||
f'{r.get("max_dd",0):.3f}', str(r.get("n_trades",0)),
|
||||
f'{r.get("win_rate",0):.1%}'))
|
||||
|
||||
print(f"\n Best by category:")
|
||||
seen = set()
|
||||
for r in valid:
|
||||
if r["strategy"] not in seen:
|
||||
seen.add(r["strategy"])
|
||||
print(f" {r['strategy']:20s} {r['name'][:30]:30s} "
|
||||
f"Sh={r.get('sharpe',0):.2f} Mon={r.get('monthly_pct',0):.1f}% "
|
||||
f"DD={r.get('max_dd',0):.3f} Tr={r.get('n_trades',0)}")
|
||||
|
||||
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
out = RESULTS_DIR / f"priceaction_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
out.write_text(json.dumps(valid[:100] if valid else results[:100], indent=2, default=str))
|
||||
print(f"\n Saved → {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Price-Action R&D Loop — TA-Lib powered. 17 indicators, deterministic.
|
||||
|
||||
Uses TA-Lib (161 indicators) for standardized technical analysis.
|
||||
Generates random strategy hypotheses and evaluates via backtest_signal.
|
||||
"""
|
||||
|
||||
import json, os, random, sys, time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import numpy as np, pandas as pd
|
||||
import talib
|
||||
|
||||
PROJECT = Path(__file__).resolve().parent.parent
|
||||
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
|
||||
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
|
||||
RESULTS_DIR = PROJECT / "results" / "strategies_new"
|
||||
|
||||
TIMEFRAMES = ["15min", "30min", "1h", "4h", "1d"]
|
||||
VOTE_THRESHOLD = 0.25
|
||||
MIN_SHARPE, MIN_TRADES, TOP_N = 1.0, 20, 20
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Indicator functions — all use (close, high, low, volume, **params) signature
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _macd(c, h, l, v, fast, slow, sig):
|
||||
mc, sc, _ = talib.MACD(c.values.astype(np.float64), fastperiod=fast, slowperiod=slow, signalperiod=sig)
|
||||
s = pd.Series(0, index=c.index); s[mc > sc] = 1; s[mc < sc] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _rsi(c, h, l, v, period, oversold, overbought):
|
||||
vv = talib.RSI(c.values.astype(np.float64), timeperiod=period)
|
||||
s = pd.Series(0, index=c.index); s[vv < oversold] = 1; s[vv > overbought] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _bbands(c, h, l, v, period, std):
|
||||
up, mi, lo = talib.BBANDS(c.values.astype(np.float64), timeperiod=period, nbdevup=std, nbdevdn=std)
|
||||
s = pd.Series(0, index=c.index); s[c.values < lo] = 1; s[c.values > up] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _stoch(c, h, l, v, fastk, slowk, slowd):
|
||||
k, d = talib.STOCH(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64),
|
||||
fastk_period=fastk, slowk_period=slowk, slowd_period=slowd)
|
||||
s = pd.Series(0, index=c.index); s[(k > d) & (k < 30)] = 1; s[(k < d) & (k > 70)] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _cci(c, h, l, v, period):
|
||||
vv = talib.CCI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
|
||||
s = pd.Series(0, index=c.index); s[vv < -100] = 1; s[vv > 100] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _willr(c, h, l, v, period):
|
||||
vv = talib.WILLR(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
|
||||
s = pd.Series(0, index=c.index); s[vv < -80] = 1; s[vv > -20] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _adx(c, h, l, v, period, threshold):
|
||||
pdi = talib.PLUS_DI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
|
||||
ndi = talib.MINUS_DI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
|
||||
adx = talib.ADX(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
|
||||
s = pd.Series(0, index=c.index); s[(pdi > ndi) & (adx > threshold)] = 1; s[(ndi > pdi) & (adx > threshold)] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _sar(c, h, l, v, accel, max_accel):
|
||||
vv = talib.SAR(h.values.astype(np.float64), l.values.astype(np.float64), acceleration=accel, maximum=max_accel)
|
||||
s = pd.Series(0, index=c.index); s[c.values > vv] = 1; s[c.values < vv] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _roc(c, h, l, v, period, threshold):
|
||||
vv = talib.ROC(c.values.astype(np.float64), timeperiod=period)
|
||||
s = pd.Series(0, index=c.index); s[vv > threshold] = 1; s[vv < -threshold] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _mom(c, h, l, v, period):
|
||||
vv = talib.MOM(c.values.astype(np.float64), timeperiod=period)
|
||||
s = pd.Series(0, index=c.index); s[vv > 0] = 1; s[vv < 0] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _aroon(c, h, l, v, period):
|
||||
up, dn = talib.AROON(h.values.astype(np.float64), l.values.astype(np.float64), timeperiod=period)
|
||||
s = pd.Series(0, index=c.index); s[up > dn] = 1; s[up < dn] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _mfi(c, h, l, v, period):
|
||||
vv = talib.MFI(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), v.values.astype(np.float64), timeperiod=period)
|
||||
s = pd.Series(0, index=c.index); s[vv < 20] = 1; s[vv > 80] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _ultosc(c, h, l, v, p1, p2, p3):
|
||||
vv = talib.ULTOSC(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod1=p1, timeperiod2=p2, timeperiod3=p3)
|
||||
s = pd.Series(0, index=c.index); s[vv < 30] = 1; s[vv > 70] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _natr(c, h, l, v, period):
|
||||
vv = talib.NATR(h.values.astype(np.float64), l.values.astype(np.float64), c.values.astype(np.float64), timeperiod=period)
|
||||
m, s = vv[-200:].mean(), vv[-200:].std()
|
||||
s = pd.Series(0, index=c.index); s[c.values > m+s] = 1; s[c.values < m-s] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _donchian(c, h, l, v, period, hold):
|
||||
hi, lo = c.rolling(period).max(), c.rolling(period).min()
|
||||
s = pd.Series(0, index=c.index); s[c > hi.shift(1)] = 1; s[c < lo.shift(1)] = -1
|
||||
return s.replace(0, np.nan).ffill(limit=hold).fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _sma(c, h, l, v, fast, slow):
|
||||
s = pd.Series(0, index=c.index)
|
||||
s[c.rolling(fast).mean() > c.rolling(slow).mean()] = 1
|
||||
s[c.rolling(fast).mean() < c.rolling(slow).mean()] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
def _ema(c, h, l, v, fast, slow):
|
||||
ef, es = c.ewm(span=fast, adjust=False).mean(), c.ewm(span=slow, adjust=False).mean()
|
||||
s = pd.Series(0, index=c.index); s[ef > es] = 1; s[ef < es] = -1
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
INDICATORS = {
|
||||
"MACD": ({"fast":[3,5,8,12], "slow":[10,15,20,26,35], "sig":[3,5,9]}, _macd, "MACD({fast},{slow},{sig})"),
|
||||
"RSI": ({"period":[7,14,21], "oversold":[20,25,30,35], "overbought":[65,70,75,80]}, _rsi, "RSI({period})[{oversold}/{overbought}]"),
|
||||
"BBands": ({"period":[10,20,40], "std":[1.5,2.0,2.5]}, _bbands, "BB({period},{std}s)"),
|
||||
"Stoch": ({"fastk":[5,9,14], "slowk":[3], "slowd":[3,5]}, _stoch, "Stoch({fastk},{slowk},{slowd})"),
|
||||
"CCI": ({"period":[14,20,50]}, _cci, "CCI({period})"),
|
||||
"WillR": ({"period":[7,14,21]}, _willr, "WR({period})"),
|
||||
"ADX": ({"period":[7,14,21], "threshold":[15,20,25]}, _adx, "ADX({period}>{threshold})"),
|
||||
"SAR": ({"accel":[0.02,0.05,0.08], "max_accel":[0.2,0.3,0.5]}, _sar, "SAR({accel},{max_accel})"),
|
||||
"ROC": ({"period":[5,10,20], "threshold":[0.1,0.2,0.5]}, _roc, "ROC({period},{threshold}%)"),
|
||||
"MOM": ({"period":[5,10,20,50]}, _mom, "MOM({period})"),
|
||||
"AROON": ({"period":[7,14,21]}, _aroon, "AROON({period})"),
|
||||
"MFI": ({"period":[7,14,21]}, _mfi, "MFI({period})"),
|
||||
"UltOsc": ({"p1":[7], "p2":[14], "p3":[28]}, _ultosc, "UltOsc(7,14,28)"),
|
||||
"NATR": ({"period":[7,14,21]}, _natr, "NATR({period})"),
|
||||
"Donchian":({"period":[5,10,20,30,50,100], "hold":[1,2,3,5]}, _donchian, "Donchian({period},{hold})"),
|
||||
"SMA": ({"fast":[5,10,20,50], "slow":[20,50,100,200]}, _sma, "SMA({fast},{slow})"),
|
||||
"EMA": ({"fast":[3,5,8,12], "slow":[15,26,50,100]}, _ema, "EMA({fast},{slow})"),
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Strategy generation
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _resample_ohlc(close_1min, tf):
|
||||
"""Resample to timeframe, producing OHLCV bars."""
|
||||
bars = close_1min.resample(tf).ohlc()
|
||||
# Flatten MultiIndex columns
|
||||
o = bars['close']['close'] if isinstance(bars.columns, pd.MultiIndex) else bars['close']
|
||||
h = bars['high']['high'] if isinstance(bars.columns, pd.MultiIndex) else bars['high']
|
||||
l = bars['low']['low'] if isinstance(bars.columns, pd.MultiIndex) else bars['low']
|
||||
c = bars['close']['close'] if isinstance(bars.columns, pd.MultiIndex) else bars['close']
|
||||
v = pd.Series(1000, index=c.index) # dummy volume
|
||||
return c, h, l, v
|
||||
|
||||
def random_hypothesis():
|
||||
stype = random.choice(["single", "multi_tf", "portfolio"])
|
||||
if stype == "single":
|
||||
ind_name = random.choice(list(INDICATORS.keys()))
|
||||
params_def, _, desc_tpl = INDICATORS[ind_name]
|
||||
params = {k: random.choice(v) for k, v in params_def.items()}
|
||||
if ind_name == "SMA" and params["fast"] >= params["slow"]:
|
||||
params["fast"] = min(params["fast"], params["slow"] // 2)
|
||||
return {"type": "single", "indicator": ind_name, "timeframe": random.choice(TIMEFRAMES),
|
||||
"params": params, "description": desc_tpl.format(**params)}
|
||||
elif stype == "multi_tf":
|
||||
ind_name = random.choice(list(INDICATORS.keys()))
|
||||
params_def, _, desc_tpl = INDICATORS[ind_name]
|
||||
params = {k: random.choice(v) for k, v in params_def.items()}
|
||||
tfs = random.sample(TIMEFRAMES, k=random.randint(2, 4))
|
||||
return {"type": "multi_tf", "indicator": ind_name, "timeframes": tfs,
|
||||
"params": params, "description": f"{ind_name} on {','.join(tfs)} maj-vote"}
|
||||
else:
|
||||
i1, i2 = random.sample(list(INDICATORS.keys()), 2)
|
||||
p1_def, _, _ = INDICATORS[i1]; p2_def, _, _ = INDICATORS[i2]
|
||||
p1 = {k: random.choice(v) for k, v in p1_def.items()}
|
||||
p2 = {k: random.choice(v) for k, v in p2_def.items()}
|
||||
return {"type": "portfolio", "indicators": [{"name": i1, "params": p1}, {"name": i2, "params": p2}],
|
||||
"timeframe": "1d", "description": f"{i1} + {i2} portfolio daily"}
|
||||
|
||||
|
||||
def build_signal(close_1min, hypothesis):
|
||||
hp = hypothesis
|
||||
if hp["type"] == "single":
|
||||
_, fn, _ = INDICATORS[hp["indicator"]]
|
||||
c, h, l, v = _resample_ohlc(close_1min, hp["timeframe"])
|
||||
s = fn(c, h, l, v, **hp["params"])
|
||||
return s.reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
elif hp["type"] == "multi_tf":
|
||||
_, fn, _ = INDICATORS[hp["indicator"]]
|
||||
sigs = {}
|
||||
for tf in hp["timeframes"]:
|
||||
c, h, l, v = _resample_ohlc(close_1min, tf)
|
||||
sigs[tf] = fn(c, h, l, v, **hp["params"]).reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
port_df = pd.DataFrame(sigs).dropna()
|
||||
vote = port_df.mean(axis=1)
|
||||
result = pd.Series(0, index=vote.index)
|
||||
result[vote > VOTE_THRESHOLD] = 1; result[vote < -VOTE_THRESHOLD] = -1
|
||||
return result
|
||||
else:
|
||||
sigs = []
|
||||
daily, dh, dl, dv = _resample_ohlc(close_1min, "1d")
|
||||
for cfg in hp["indicators"]:
|
||||
_, fn, _ = INDICATORS[cfg["name"]]
|
||||
s = fn(daily, dh, dl, dv, **cfg["params"]).reindex(close_1min.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
sigs.append(s)
|
||||
port_df = pd.DataFrame({f"s{i}": s for i, s in enumerate(sigs)}).dropna()
|
||||
vote = port_df.mean(axis=1)
|
||||
result = pd.Series(0, index=vote.index)
|
||||
result[vote > VOTE_THRESHOLD] = 1; result[vote < -VOTE_THRESHOLD] = -1
|
||||
return result
|
||||
|
||||
|
||||
def evaluate(hp, close):
|
||||
signal = build_signal(close, hp)
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
||||
bt = backtest_signal(close=close, signal=signal)
|
||||
return {"hypothesis": hp, "sharpe": bt.get("sharpe", 0) or 0,
|
||||
"monthly_pct": bt.get("monthly_return_pct", 0) or 0,
|
||||
"max_dd": bt.get("max_drawdown", 0) or 0, "n_trades": bt.get("n_trades", 0) or 0,
|
||||
"win_rate": bt.get("win_rate", 0) or 0}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Main loop
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def main():
|
||||
iterations = 100; continuous = False
|
||||
if "--iterations" in sys.argv:
|
||||
iterations = int(sys.argv[sys.argv.index("--iterations") + 1])
|
||||
if "--live" in sys.argv: continuous = True
|
||||
|
||||
print("=" * 60)
|
||||
print(f" Price-Action R&D Loop — TA-Lib ({len(INDICATORS)} indicators)")
|
||||
print(f" Iterations: {'continuous' if continuous else iterations}")
|
||||
print("=" * 60)
|
||||
|
||||
df = pd.read_hdf(OHLCV_PATH, key="data")
|
||||
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
|
||||
|
||||
top, best_sh, total, iteration = [], 0, 0, 0
|
||||
|
||||
while True:
|
||||
iteration += 1
|
||||
if not continuous and iteration > iterations: break
|
||||
|
||||
hp = random_hypothesis()
|
||||
result = evaluate(hp, close)
|
||||
result["iteration"] = iteration
|
||||
result["timestamp"] = datetime.now().isoformat()
|
||||
total += 1
|
||||
|
||||
if result["sharpe"] >= MIN_SHARPE and result["n_trades"] >= MIN_TRADES and result["monthly_pct"] > 0:
|
||||
top.append(result)
|
||||
top.sort(key=lambda r: r["sharpe"], reverse=True)
|
||||
top = top[:TOP_N]
|
||||
|
||||
if iteration % 10 == 0 or result["sharpe"] > best_sh:
|
||||
if result["sharpe"] > best_sh:
|
||||
best_sh = result["sharpe"]
|
||||
print(f"\n * NEW BEST (#{iteration}): {hp['description']}")
|
||||
print(f" Sharpe={result['sharpe']:.2f} Mon={result['monthly_pct']:.2f}% "
|
||||
f"DD={result['max_dd']:.4f} Tr={result['n_trades']} WR={result['win_rate']:.1%}")
|
||||
else:
|
||||
print(f" [{iteration}/{iterations}] Evals: {total} | Top: {len(top)} | Best Sh={best_sh:.2f}")
|
||||
|
||||
if iteration % 50 == 0 and top:
|
||||
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
cp = RESULTS_DIR / f"pal_talib_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
cp.write_text(json.dumps(top[:10], indent=2, default=str))
|
||||
print(f" Checkpoint: {cp.name}")
|
||||
|
||||
# Final
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" Done: {total} evaluated, {len(top)} strategies")
|
||||
if top:
|
||||
print(f"\n{'#':>3s} {'Strategy':<50s} {'Sharpe':>7s} {'Mon%':>7s} {'DD':>7s} {'Tr':>5s}")
|
||||
print("-" * 80)
|
||||
for i, r in enumerate(top[:15], 1):
|
||||
print(f"{i:>3d} {r['hypothesis']['description'][:50]:<50s} {r['sharpe']:>+7.2f} {r['monthly_pct']:>+6.2f}% {r['max_dd']:>+6.4f} {r['n_trades']:>5d}")
|
||||
final = RESULTS_DIR / f"pal_talib_final_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
final.write_text(json.dumps(top, indent=2, default=str))
|
||||
print(f"\n Saved: {final}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,467 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Quick Daytrading Strategy Generator with CORRECT factor alignment.
|
||||
|
||||
Uses forward-fill to align daily factors to 1-min frequency,
|
||||
then runs fast backtests without LLM calls.
|
||||
|
||||
Usage:
|
||||
python nexquant_quick_daytrading.py 5
|
||||
python nexquant_quick_daytrading.py 10
|
||||
"""
|
||||
import json, time, subprocess, tempfile # nosec
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
STRATEGIES_DIR = Path('results/strategies_new')
|
||||
STRATEGIES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
FACTOR_FILES = Path('results/factors')
|
||||
VALUE_FILES = FACTOR_FILES / 'values'
|
||||
OHLCV_PATH = Path('git_ignore_folder/factor_implementation_source_data/intraday_pv.h5')
|
||||
|
||||
# Best daytrading strategies (12-min horizon, optimized for RiskMgmt)
|
||||
DAYTRADING_COMBOS = [
|
||||
{
|
||||
'name': 'MomentumDivergence12min',
|
||||
'factors': ['daily_close_return_96', 'daily_session_momentum_divergence_1d'],
|
||||
'code': '''mom = factors['daily_close_return_96']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
|
||||
w = 20
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (mom_z - div_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.3] = 1
|
||||
signal[composite < -0.3] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'LondonSessionScalp',
|
||||
'factors': ['london_mom', 'daily_session_momentum_divergence_1d'],
|
||||
'code': '''mom = factors['london_mom']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
|
||||
w = 15
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (mom_z - div_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.25] = 1
|
||||
signal[composite < -0.25] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'TrendReversionScalp',
|
||||
'factors': ['daily_ols_slope_96', 'daily_session_momentum_divergence_1d', 'DailyTrendStrength_Raw'],
|
||||
'code': '''slope = factors['daily_ols_slope_96']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
trend = factors['DailyTrendStrength_Raw']
|
||||
|
||||
w = 20
|
||||
slope_z = (slope - slope.rolling(w).mean()) / (slope.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
trend_z = (trend - trend.rolling(w).mean()) / (trend.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.5 * slope_z - 0.3 * div_z + 0.2 * trend_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.3] = 1
|
||||
signal[composite < -0.3] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'VolAdjMomentum12',
|
||||
'factors': ['daily_ret_vol_adj_1d', 'daily_session_momentum_divergence_1d', 'DCP'],
|
||||
'code': '''vol = factors['daily_ret_vol_adj_1d']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
dcp = factors['DCP']
|
||||
|
||||
w = 20
|
||||
vol_z = (vol - vol.rolling(w).mean()) / (vol.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
dcp_z = (dcp - dcp.rolling(w).mean()) / (dcp.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.5 * vol_z - 0.3 * div_z + 0.2 * dcp_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.35] = 1
|
||||
signal[composite < -0.35] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'SessionMeanReversion',
|
||||
'factors': ['session_momentum_diff', 'daily_norm_body', 'daily_c2c_return'],
|
||||
'code': '''session = factors['session_momentum_diff']
|
||||
body = factors['daily_norm_body']
|
||||
c2c = factors['daily_c2c_return']
|
||||
|
||||
w = 15
|
||||
sess_z = (session - session.rolling(w).mean()) / (session.rolling(w).std() + 1e-8)
|
||||
body_z = (body - body.rolling(w).mean()) / (body.rolling(w).std() + 1e-8)
|
||||
c2c_z = (c2c - c2c.rolling(w).mean()) / (c2c.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.5 * sess_z + 0.3 * body_z + 0.2 * c2c_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.4] = 1
|
||||
signal[composite < -0.4] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'MomentumContinuation',
|
||||
'factors': ['daily_mom', 'daily_ret_1d', 'momentum_1d'],
|
||||
'code': '''mom = factors['daily_mom']
|
||||
ret = factors['daily_ret_1d']
|
||||
mom2 = factors['momentum_1d']
|
||||
|
||||
w = 12
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
ret_z = (ret - ret.rolling(w).mean()) / (ret.rolling(w).std() + 1e-8)
|
||||
mom2_z = (mom2 - mom2.rolling(w).mean()) / (mom2.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.4 * mom_z + 0.3 * ret_z + 0.3 * mom2_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.2] = 1
|
||||
signal[composite < -0.2] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'HighFreqScalper',
|
||||
'factors': ['daily_close_return_96', 'DCP', 'london_mom'],
|
||||
'code': '''close_ret = factors['daily_close_return_96']
|
||||
dcp = factors['DCP']
|
||||
london = factors['london_mom']
|
||||
|
||||
w = 10
|
||||
cr_z = (close_ret - close_ret.rolling(w).mean()) / (close_ret.rolling(w).std() + 1e-8)
|
||||
dcp_z = (dcp - dcp.rolling(w).mean()) / (dcp.rolling(w).std() + 1e-8)
|
||||
lon_z = (london - london.rolling(w).mean()) / (london.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.4 * cr_z + 0.3 * dcp_z + 0.3 * lon_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.25] = 1
|
||||
signal[composite < -0.25] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'AdaptiveMomentumMR',
|
||||
'factors': ['daily_close_return_96', 'daily_session_momentum_divergence_1d', 'daily_ols_slope_96'],
|
||||
'code': '''mom = factors['daily_close_return_96']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
slope = factors['daily_ols_slope_96']
|
||||
|
||||
w = 20
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
slope_z = (slope - slope.rolling(w).mean()) / (slope.rolling(w).std() + 1e-8)
|
||||
|
||||
# Regime detection: high momentum = trend, low = mean reversion
|
||||
regime = (mom_z.abs() > 1.0).astype(float)
|
||||
composite = (regime * mom_z + (1 - regime) * (-div_z) + 0.3 * slope_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.4] = 1
|
||||
signal[composite < -0.4] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'TrendPullbackScalp',
|
||||
'factors': ['daily_close_return_96', 'daily_session_momentum_divergence_1d', 'daily_norm_body'],
|
||||
'code': '''mom = factors['daily_close_return_96']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
body = factors['daily_norm_body']
|
||||
|
||||
w = 15
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
body_z = (body - body.rolling(w).mean()) / (body.rolling(w).std() + 1e-8)
|
||||
|
||||
# Enter on pullbacks (divergence against trend)
|
||||
composite = (mom_z - 0.5 * div_z * mom_z.sign() + 0.2 * body_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.35] = 1
|
||||
signal[composite < -0.35] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
{
|
||||
'name': 'IntradayMomentumBlend',
|
||||
'factors': ['daily_close_return_96', 'london_mom', 'daily_session_momentum_divergence_1d', 'DCP'],
|
||||
'code': '''mom = factors['daily_close_return_96']
|
||||
lon = factors['london_mom']
|
||||
div = factors['daily_session_momentum_divergence_1d']
|
||||
dcp = factors['DCP']
|
||||
|
||||
w = 20
|
||||
mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8)
|
||||
lon_z = (lon - lon.rolling(w).mean()) / (lon.rolling(w).std() + 1e-8)
|
||||
div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8)
|
||||
dcp_z = (dcp - dcp.rolling(w).mean()) / (dcp.rolling(w).std() + 1e-8)
|
||||
|
||||
composite = (0.3 * mom_z + 0.3 * lon_z - 0.2 * div_z + 0.2 * dcp_z).fillna(0)
|
||||
signal = pd.Series(0, index=close.index, name='signal')
|
||||
signal[composite > 0.3] = 1
|
||||
signal[composite < -0.3] = -1
|
||||
signal = signal.fillna(0).astype(int)''',
|
||||
},
|
||||
]
|
||||
|
||||
def load_factor_series(name):
|
||||
"""Load factor parquet and return as Series with correct index."""
|
||||
safe = name.replace('/','_').replace('\\','_')[:150]
|
||||
pf = VALUE_FILES / f"{safe}.parquet"
|
||||
if not pf.exists():
|
||||
return None
|
||||
|
||||
df = pd.read_parquet(str(pf))
|
||||
|
||||
# Extract EURUSD
|
||||
if df.index.names == ['datetime', 'instrument']:
|
||||
df_reset = df.reset_index()
|
||||
if 'instrument' in df_reset.columns:
|
||||
df_eur = df_reset[df_reset['instrument'] == 'EURUSD'].copy()
|
||||
df_eur = df_eur.set_index('datetime')
|
||||
series = df_eur.iloc[:, -1] # Last column is the factor value
|
||||
series.name = name
|
||||
return series
|
||||
|
||||
# If single index, just return first column
|
||||
series = df.iloc[:, 0]
|
||||
series.name = name
|
||||
return series
|
||||
|
||||
def main(n_strategies=5):
|
||||
console.print("[bold cyan]🎯 Daytrading Strategy Generator (Quick Mode)[/bold cyan]\n")
|
||||
console.print(" Style: 12-minute forward returns")
|
||||
console.print(" Target: RiskMgmt compliant (IC>0.02, Sharpe>0.5, Trades>20, DD>-10%)\n")
|
||||
|
||||
# Load OHLCV data
|
||||
if not OHLCV_PATH.exists():
|
||||
console.print(f"[red]✗ OHLCV data not found: {OHLCV_PATH}[/red]")
|
||||
return
|
||||
|
||||
ohlcv = pd.read_hdf(str(OHLCV_PATH), key='data')
|
||||
|
||||
# Extract close prices with datetime-only index (not MultiIndex)
|
||||
if '$close' in ohlcv.columns:
|
||||
close = ohlcv['$close'].dropna()
|
||||
elif 'close' in ohlcv.columns:
|
||||
close = ohlcv['close'].dropna()
|
||||
else:
|
||||
close = ohlcv.select_dtypes(include=[np.number]).iloc[:, 0].dropna()
|
||||
|
||||
# Extract datetime from MultiIndex if present
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close_dt_idx = close.index.get_level_values('datetime')
|
||||
close_series = pd.Series(close.values, index=close_dt_idx, name='close')
|
||||
else:
|
||||
close_series = close
|
||||
|
||||
close_series = close_series.dropna()
|
||||
console.print(f"[green]✓[/green] Loaded {len(close_series):,} OHLCV bars")
|
||||
|
||||
# Load all factor series and align to close index
|
||||
all_factor_series = {}
|
||||
for combo in DAYTRADING_COMBOS:
|
||||
for factor_name in combo['factors']:
|
||||
if factor_name in all_factor_series:
|
||||
continue
|
||||
|
||||
series = load_factor_series(factor_name)
|
||||
if series is not None:
|
||||
# Forward fill to match close frequency
|
||||
series_ff = series.reindex(close_series.index).ffill()
|
||||
all_factor_series[factor_name] = series_ff
|
||||
|
||||
# Create factors DataFrame
|
||||
df_factors = pd.DataFrame(all_factor_series)
|
||||
df_factors = df_factors.dropna(how='all')
|
||||
|
||||
console.print(f"[green]✓[/green] Loaded {len(df_factors.columns)} factor series")
|
||||
console.print(f"[green]✓[/green] Aligned to {len(df_factors):,} bars\n")
|
||||
|
||||
accepted = []
|
||||
|
||||
for i, combo in enumerate(DAYTRADING_COMBOS[:n_strategies]):
|
||||
console.print(f"[{i+1}/{n_strategies}] Testing {combo['name']}...")
|
||||
|
||||
# Build factor dataframe
|
||||
valid_factors = [f for f in combo['factors'] if f in df_factors.columns]
|
||||
if len(valid_factors) < 2:
|
||||
console.print(f" ✗ Not enough valid factors")
|
||||
continue
|
||||
|
||||
strat_factors = df_factors[valid_factors].dropna()
|
||||
|
||||
if len(strat_factors) < 1000:
|
||||
console.print(f" ✗ Not enough data: {len(strat_factors)} bars")
|
||||
continue
|
||||
|
||||
# Build backtest script
|
||||
forward_bars = 12
|
||||
strategy_code = combo['code']
|
||||
|
||||
script = f"""
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import json
|
||||
|
||||
close = pd.read_pickle('close.pkl') # nosec
|
||||
factors = pd.read_pickle('factors.pkl') # nosec
|
||||
|
||||
# Execute strategy
|
||||
try:
|
||||
{chr(10).join(' ' + l for l in strategy_code.split(chr(10)))}
|
||||
except Exception as e:
|
||||
print(f"ERROR: {{e}}")
|
||||
exit(1)
|
||||
|
||||
if 'signal' not in dir():
|
||||
print("ERROR: No signal generated")
|
||||
exit(1)
|
||||
|
||||
signal = signal.fillna(0)
|
||||
|
||||
# Align
|
||||
common_idx = close.index.intersection(signal.index)
|
||||
close = close.loc[common_idx]
|
||||
signal = signal.loc[common_idx]
|
||||
|
||||
# Forward returns (12-min horizon for daytrading)
|
||||
FORWARD_BARS = {forward_bars}
|
||||
returns_fwd = close.pct_change(FORWARD_BARS).shift(-FORWARD_BARS)
|
||||
signal_aligned = signal.loc[returns_fwd.dropna().index]
|
||||
fwd_returns = returns_fwd.loc[signal_aligned.index]
|
||||
|
||||
if len(signal_aligned) < 100 or len(fwd_returns) < 100:
|
||||
print("ERROR: Not enough data")
|
||||
exit(1)
|
||||
|
||||
# Metrics
|
||||
ic = signal_aligned.corr(fwd_returns)
|
||||
strategy_returns = signal_aligned * fwd_returns
|
||||
sharpe = strategy_returns.mean() / strategy_returns.std() * np.sqrt(252 * 1440 / {forward_bars}) if strategy_returns.std() > 0 else 0
|
||||
|
||||
cum = (1 + strategy_returns).cumprod()
|
||||
running_max = cum.expanding().max()
|
||||
drawdown = (cum - running_max) / running_max.replace(0, np.nan)
|
||||
max_dd = drawdown.min() if len(drawdown) > 0 else 0
|
||||
|
||||
win_rate = (strategy_returns > 0).sum() / len(strategy_returns) if len(strategy_returns) > 0 else 0
|
||||
n_trades = int((signal_aligned != signal_aligned.shift(1)).sum())
|
||||
total_return = cum.iloc[-1] - 1
|
||||
n_bars = len(strategy_returns)
|
||||
n_months = n_bars / (252 * 1440 / {forward_bars} / 12) if n_bars > 0 else 1
|
||||
monthly_return = (1 + total_return) ** (1 / n_months) - 1 if n_months > 0 and (1 + total_return) > 0 else total_return
|
||||
|
||||
result = {{
|
||||
"status": "success",
|
||||
"sharpe": float(sharpe),
|
||||
"max_drawdown": float(max_dd) if not np.isnan(max_dd) else -0.20,
|
||||
"win_rate": float(win_rate),
|
||||
"ic": float(ic) if not np.isnan(ic) else 0,
|
||||
"n_trades": n_trades,
|
||||
"total_return": float(total_return),
|
||||
"monthly_return_pct": float(monthly_return * 100),
|
||||
"n_bars": int(n_bars),
|
||||
"n_months": float(n_months),
|
||||
"signal_long": int((signal_aligned == 1).sum()),
|
||||
"signal_short": int((signal_aligned == -1).sum()),
|
||||
"signal_neutral": int((signal_aligned == 0).sum()),
|
||||
}}
|
||||
|
||||
print(json.dumps(result))
|
||||
"""
|
||||
|
||||
# Run backtest
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tdp = Path(td)
|
||||
strat_close = close_series.loc[strat_factors.index]
|
||||
strat_close.to_pickle(str(tdp / 'close.pkl')) # nosec
|
||||
strat_factors.to_pickle(str(tdp / 'factors.pkl')) # nosec
|
||||
|
||||
script_path = tdp / 'run.py'
|
||||
script_path.write_text(script)
|
||||
|
||||
try:
|
||||
result_proc = subprocess.run( # nosec B603
|
||||
[sys.executable, str(script_path)],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
cwd=str(tdp)
|
||||
)
|
||||
|
||||
if result_proc.returncode != 0:
|
||||
console.print(f" ✗ Failed: {result_proc.stderr[:200]}")
|
||||
continue
|
||||
|
||||
result = None
|
||||
for line in result_proc.stdout.strip().split('\n'):
|
||||
try:
|
||||
result = json.loads(line)
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not result or result.get('status') != 'success':
|
||||
console.print(f" ✗ Invalid result")
|
||||
continue
|
||||
|
||||
except subprocess.TimeoutExpired: # nosec
|
||||
console.print(f" ✗ Timeout")
|
||||
continue
|
||||
except Exception as e:
|
||||
console.print(f" ✗ Error: {e}")
|
||||
continue
|
||||
|
||||
ic = result.get('ic', 0)
|
||||
sharpe = result.get('sharpe', 0)
|
||||
trades = result.get('n_trades', 0)
|
||||
dd = result.get('max_drawdown', 0)
|
||||
|
||||
# RiskMgmt criteria
|
||||
if abs(ic) > 0.02 and sharpe > 0.5 and trades > 20 and dd > -0.10:
|
||||
strategy = {
|
||||
'strategy_name': combo['name'],
|
||||
'factor_names': combo['factors'],
|
||||
'description': f"Daytrading strategy combining {', '.join(combo['factors'])}",
|
||||
'code': combo['code'],
|
||||
'real_backtest': result,
|
||||
'metrics': result,
|
||||
'summary': {
|
||||
'sharpe': sharpe,
|
||||
'max_drawdown': dd,
|
||||
'win_rate': result.get('win_rate', 0),
|
||||
'monthly_return_pct': result.get('monthly_return_pct', 0),
|
||||
'real_ic': ic,
|
||||
'real_n_trades': trades,
|
||||
'forward_bars': 12,
|
||||
'trading_style': 'daytrading',
|
||||
}
|
||||
}
|
||||
|
||||
fname = f"{int(time.time())}_{combo['name']}.json"
|
||||
with open(STRATEGIES_DIR / fname, 'w') as f:
|
||||
json.dump(strategy, f, indent=2, ensure_ascii=False)
|
||||
|
||||
accepted.append(strategy)
|
||||
console.print(f" ✓ [green]ACCEPT[/green]: IC={ic:.4f}, Sharpe={sharpe:.2f}, Trades={trades}, DD={dd:.1%}")
|
||||
else:
|
||||
console.print(f" ✗ [red]REJECT[/red]: IC={ic:.4f}, Sharpe={sharpe:.2f}, Trades={trades}, DD={dd:.1%}")
|
||||
|
||||
console.print(f"\n[bold green]✓ {len(accepted)}/{n_strategies} strategies accepted[/bold green]\n")
|
||||
|
||||
if accepted:
|
||||
console.print("[bold]Results:[/bold]")
|
||||
for s in accepted:
|
||||
bt = s['real_backtest']
|
||||
console.print(f" • {s['strategy_name']:30s} IC={bt['ic']:.4f} Sharpe={bt['sharpe']:.2f} "
|
||||
f"Monthly={bt['monthly_return_pct']:.2f}% Trades={bt['n_trades']}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
n = int(sys.argv[1]) if len(sys.argv) > 1 else 5
|
||||
main(n)
|
||||
@@ -0,0 +1,931 @@
|
||||
#!/usr/bin/env python3
|
||||
"""R&D Loop V2 — Multi-Instrument + Correlation Score + Session/Vola Filter + OOS.
|
||||
|
||||
Changes from V1:
|
||||
1. Multi-Instrument: Evaluate on EUR/USD + GBP/USD + BTC/USD
|
||||
2. Correlation Score: Reward uncorrelated strategies (Sharpe × (1−corr))
|
||||
3. Session Filter: Only trade London session (07:00-16:00 UTC)
|
||||
4. Volatility Filter: No trades when ATR < threshold
|
||||
5. OOS Split: Report IS/OOS separately (80/20)
|
||||
"""
|
||||
|
||||
import json, os, random, sys, time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import numpy as np, pandas as pd
|
||||
from numba import jit
|
||||
|
||||
PROJECT = Path(__file__).resolve().parent.parent
|
||||
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
|
||||
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
|
||||
RESULTS_DIR = PROJECT / "results" / "rd_loop"
|
||||
STATE_DIR = PROJECT / "git_ignore_folder" / "rd_loop_state"
|
||||
|
||||
INSTRUMENTS = ["EURUSD", "GBPUSD", "BTCUSD", "XAUUSD"]
|
||||
LEADER_MAP = {"GBPUSD": "EURUSD"}
|
||||
INSTRUMENT_TIMEFRAMES = {
|
||||
"XAUUSD": ["1d", "1w"], # Daily data → daily/weekly TFs
|
||||
"default": ["5min", "15min", "30min", "1h", "4h"],
|
||||
}
|
||||
TIMEFRAMES = ["5min", "15min", "30min", "1h", "4h", "1d", "1w"]
|
||||
INDICATORS_POOL = ["MACD", "RSI", "BBands", "Donchian", "Stoch", "CCI", "WillR", "ADX", "SAR", "ROC", "MOM", "AROON", "MFI", "SMA", "EMA"]
|
||||
STRATEGY_TYPES = ["single", "multi_tf", "multi_role"]
|
||||
TREND_TFS = ["30min", "1h", "4h"]
|
||||
ENTRY_TFS = ["5min", "15min", "30min"]
|
||||
MIN_SHARPE, MIN_TRADES = 0.3, 10
|
||||
EXPLORATION_RATE = 0.40
|
||||
OOS_SPLIT = 0.2
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Numba-accelerated backtest
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@jit(nopython=True)
|
||||
def _backtest_numba(prices, signals, cost=0.000264):
|
||||
n = len(prices)
|
||||
equity = np.zeros(n, dtype=np.float64)
|
||||
equity[0] = 100000.0; peak = 100000.0; max_dd = 0.0
|
||||
trade_returns = np.zeros(100000, dtype=np.float64)
|
||||
position = 0; entry_price = 0.0; trade_count = 0; wins = 0
|
||||
|
||||
for i in range(1, n):
|
||||
px = prices[i]; sg = signals[i]; ps = signals[i-1]
|
||||
# Close position on signal reversal or flatten
|
||||
if position != 0 and (sg != position or sg == 0 and position != 0):
|
||||
if position == 1: ret = (px - entry_price) / entry_price - cost
|
||||
else: ret = (entry_price - px) / entry_price - cost
|
||||
equity[i] = equity[i-1] * (1.0 + ret)
|
||||
if equity[i] > peak: peak = equity[i]
|
||||
dd = (peak - equity[i]) / peak
|
||||
if dd > max_dd: max_dd = dd
|
||||
if trade_count < len(trade_returns):
|
||||
trade_returns[trade_count] = ret
|
||||
trade_count += 1
|
||||
if ret > 0: wins += 1
|
||||
position = 0
|
||||
else:
|
||||
equity[i] = equity[i-1]
|
||||
# Open new position
|
||||
if position == 0 and sg != 0:
|
||||
position = sg; entry_price = px
|
||||
|
||||
# Close final position
|
||||
if position != 0:
|
||||
fp = prices[-1]
|
||||
if position == 1: ret = (fp - entry_price) / entry_price - cost
|
||||
else: ret = (entry_price - fp) / entry_price - cost
|
||||
equity[-1] = equity[-2] * (1.0 + ret)
|
||||
if trade_count < len(trade_returns):
|
||||
trade_returns[trade_count] = ret
|
||||
trade_count += 1
|
||||
if ret > 0: wins += 1
|
||||
|
||||
# Ensure monotonic equity (carry forward zeros)
|
||||
for i in range(1, n):
|
||||
if equity[i] == 0: equity[i] = equity[i-1]
|
||||
|
||||
total_ret = (equity[-1] - 100000.0) / 100000.0
|
||||
|
||||
if trade_count > 5:
|
||||
t = trade_returns[:trade_count]
|
||||
mean_ret = np.mean(t); std_ret = np.std(t)
|
||||
sharpe = mean_ret / std_ret * np.sqrt(trade_count) if std_ret > 0 else 0.0
|
||||
else:
|
||||
sharpe = 0.0
|
||||
|
||||
return equity, max_dd, trade_count, wins, total_ret, sharpe, trade_returns[:trade_count]
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Signal Construction
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def build_signal(close, hypothesis):
|
||||
"""Build trading signal from hypothesis. Returns (-1,0,1) Series."""
|
||||
import talib
|
||||
signal = None
|
||||
|
||||
# Adapt timeframes to data frequency
|
||||
median_delta = (close.index[1:] - close.index[:-1]).median()
|
||||
if median_delta > pd.Timedelta("1h"):
|
||||
valid_tfs = ["1d", "1w"]
|
||||
tf_map = {"5min": "1d", "15min": "1d", "30min": "1d", "1h": "1d", "4h": "1w"}
|
||||
# Remap hypothesis timeframes
|
||||
hp = dict(hypothesis)
|
||||
if hp.get('type') in ('single', 'multi_tf') and 'timeframe' in hp:
|
||||
hp['timeframe'] = tf_map.get(hp.get('timeframe','1h'), '1d')
|
||||
if hp.get('type') == 'multi_tf' and 'timeframes' in hp:
|
||||
hp['timeframes'] = [tf_map.get(t, '1d') for t in hp['timeframes']]
|
||||
hp['timeframes'] = list(set(hp['timeframes'])) # dedup
|
||||
if hp.get('type') == 'multi_role':
|
||||
hp['trend_tf'] = tf_map.get(hp.get('trend_tf','4h'), '1w')
|
||||
hp['entry_tf'] = tf_map.get(hp.get('entry_tf','15min'), '1d')
|
||||
hypothesis = hp
|
||||
else:
|
||||
valid_tfs = ["5min", "15min", "30min", "1h", "4h"]
|
||||
|
||||
if hypothesis['type'] == 'single':
|
||||
ind = hypothesis['indicator']; tf = hypothesis['timeframe']
|
||||
bars = close.resample(tf).last().dropna()
|
||||
sig = _build_indicator_signal(ind, bars, hypothesis['params'])
|
||||
signal = sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
elif hypothesis['type'] == 'multi_tf':
|
||||
ind = hypothesis['indicator']
|
||||
sigs = {}
|
||||
for tf in hypothesis['timeframes']:
|
||||
bars = close.resample(tf).last().dropna()
|
||||
sig = _build_indicator_signal(ind, bars, hypothesis['params'])
|
||||
sigs[tf] = sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
port = pd.DataFrame(sigs).dropna()
|
||||
vote = port.mean(axis=1)
|
||||
signal = pd.Series(0, index=close.index)
|
||||
signal[vote > 0.25] = 1; signal[vote < -0.25] = -1
|
||||
|
||||
elif hypothesis['type'] == 'multi_role':
|
||||
trend_ind = hypothesis['trend_ind']; entry_ind = hypothesis['entry_ind']
|
||||
trend_tf = hypothesis['trend_tf']; entry_tf = hypothesis['entry_tf']
|
||||
trend_bars = close.resample(trend_tf).last().dropna()
|
||||
trend_sig = _build_indicator_signal(trend_ind, trend_bars, hypothesis['trend_params'])
|
||||
trend_sig = trend_sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
entry_bars = close.resample(entry_tf).last().dropna()
|
||||
entry_sig = _build_indicator_signal(entry_ind, entry_bars, hypothesis['entry_params'])
|
||||
entry_sig = entry_sig.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
signal = pd.Series(0, index=close.index)
|
||||
signal[(trend_sig == 1) & (entry_sig == 1)] = 1
|
||||
signal[(trend_sig == -1) & (entry_sig == -1)] = -1
|
||||
|
||||
return signal if signal is not None and signal.nunique() > 1 else None
|
||||
|
||||
|
||||
def _apply_session_filter(signal, index):
|
||||
"""Only trade London session (07:00-16:00 UTC Mon-Fri). Skip for daily data."""
|
||||
delta = (index[1:] - index[:-1]).median() if len(index) > 1 else pd.Timedelta("1min")
|
||||
if delta > pd.Timedelta("1h"):
|
||||
return signal # Skip session filter for daily/weekly data
|
||||
hours = index.hour
|
||||
days = index.dayofweek
|
||||
in_session = (days < 5) & (hours >= 7) & (hours < 16)
|
||||
if hasattr(in_session, 'values'):
|
||||
in_session = in_session.values
|
||||
return (signal * in_session.astype(int)).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
def _apply_vola_filter(signal, close, atr_period=14, min_atr_pct=0.0003):
|
||||
"""Don't trade when ATR is too low (flat/quiet markets)."""
|
||||
tr = pd.DataFrame({
|
||||
'hl': close.diff().abs(),
|
||||
'hc': (close - close.shift(1)).abs(),
|
||||
'lc': (close.shift(1) - close).abs(),
|
||||
}).max(axis=1)
|
||||
atr = tr.rolling(atr_period).mean()
|
||||
atr_pct = atr / close
|
||||
too_quiet = atr_pct < min_atr_pct
|
||||
return (signal * (~too_quiet).astype(int)).fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
_NEWS_CACHE = None
|
||||
def _load_news_events():
|
||||
"""Load high-impact news events from YAML, return dict of currency → DatetimeIndex mask."""
|
||||
global _NEWS_CACHE
|
||||
if _NEWS_CACHE is not None:
|
||||
return _NEWS_CACHE
|
||||
import yaml
|
||||
news_file = PROJECT / "git_ignore_folder" / "economic_events_full.yaml"
|
||||
if not news_file.exists():
|
||||
_NEWS_CACHE = {}
|
||||
return _NEWS_CACHE
|
||||
with open(news_file) as f:
|
||||
data = yaml.safe_load(f)
|
||||
events = {}
|
||||
for evt in data.get('events', []):
|
||||
if evt.get('impact') != 'high':
|
||||
continue
|
||||
dt = pd.Timestamp(evt['datetime'])
|
||||
currency = evt.get('currency', 'USD')
|
||||
if currency not in events:
|
||||
events[currency] = []
|
||||
events[currency].append(dt)
|
||||
_NEWS_CACHE = events
|
||||
return _NEWS_CACHE
|
||||
|
||||
|
||||
def _apply_news_filter(signal, index, currency, window_min=5):
|
||||
"""Block trades during high-impact news events (+/- window_min)."""
|
||||
events = _load_news_events()
|
||||
if currency not in events and currency[:3] not in events:
|
||||
return signal
|
||||
key = currency if currency in events else currency[:3]
|
||||
timestamps = events.get(key, [])
|
||||
if not timestamps:
|
||||
return signal
|
||||
|
||||
blocked = np.zeros(len(index), dtype=bool)
|
||||
for ts in timestamps:
|
||||
start = ts - pd.Timedelta(minutes=window_min)
|
||||
end = ts + pd.Timedelta(minutes=window_min)
|
||||
mask = (index >= start) & (index <= end)
|
||||
blocked |= mask
|
||||
|
||||
return (signal * (~blocked).astype(int)).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
def _apply_cross_confirm(signal, close_follower, close_leader, lookback=5, min_pct=0.0003):
|
||||
"""Cancel follower signals when leader momentum strongly opposes (>0.03% move)."""
|
||||
leader_mom = close_leader.pct_change(lookback)
|
||||
leader_mom = leader_mom.reindex(signal.index, method='ffill')
|
||||
|
||||
# Only BLOCK when leader moves strongly opposite to signal
|
||||
# Don't require confirmation — just cancel clear contrarian moves
|
||||
cancel_long = (signal == 1) & (leader_mom < -min_pct)
|
||||
cancel_short = (signal == -1) & (leader_mom > min_pct)
|
||||
cancel = cancel_long | cancel_short
|
||||
return (signal * (~cancel).astype(int)).fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
|
||||
def _build_indicator_signal(name, bars, params):
|
||||
"""Build indicator signal using talib + hand-rolled."""
|
||||
import talib
|
||||
c = bars.values.astype(np.float64)
|
||||
if name == 'MACD':
|
||||
mc, sc, _ = talib.MACD(c, fastperiod=params.get('fast', 3),
|
||||
slowperiod=params.get('slow', 15),
|
||||
signalperiod=params.get('sig', 3))
|
||||
s = pd.Series(0, index=bars.index); s[mc > sc] = 1; s[mc < sc] = -1
|
||||
elif name == 'RSI':
|
||||
v = talib.RSI(c, timeperiod=params.get('period', 14))
|
||||
s = pd.Series(0, index=bars.index); s[v < params.get('oversold', 30)] = 1; s[v > params.get('overbought', 70)] = -1
|
||||
elif name == 'BBands':
|
||||
up, mi, lo = talib.BBANDS(c, timeperiod=params.get('period', 20),
|
||||
nbdevup=params.get('std', 2), nbdevdn=params.get('std', 2))
|
||||
s = pd.Series(0, index=bars.index); s[c < lo] = 1; s[c > up] = -1
|
||||
elif name == 'Donchian':
|
||||
hi = bars.rolling(params.get('period', 20)).max()
|
||||
lo = bars.rolling(params.get('period', 20)).min()
|
||||
s = pd.Series(0, index=bars.index); s[bars > hi.shift(1)] = 1; s[bars < lo.shift(1)] = -1
|
||||
s = s.replace(0, np.nan).ffill(limit=params.get('hold', 1)).fillna(0).astype(int)
|
||||
elif name == 'Stoch':
|
||||
k, d = talib.STOCH(c, c, c, fastk_period=params.get('fastk', 9),
|
||||
slowk_period=params.get('slowk', 3), slowd_period=params.get('slowd', 3))
|
||||
s = pd.Series(0, index=bars.index); s[(k > d) & (k < 30)] = 1; s[(k < d) & (k > 70)] = -1
|
||||
elif name == 'CCI':
|
||||
v = talib.CCI(c, c, c, timeperiod=params.get('period', 14))
|
||||
s = pd.Series(0, index=bars.index); s[v < -100] = 1; s[v > 100] = -1
|
||||
elif name == 'WillR':
|
||||
v = talib.WILLR(c, c, c, timeperiod=params.get('period', 14))
|
||||
s = pd.Series(0, index=bars.index); s[v < -80] = 1; s[v > -20] = -1
|
||||
elif name == 'ADX':
|
||||
pdi = talib.PLUS_DI(c, c, c, timeperiod=params.get('period', 14))
|
||||
ndi = talib.MINUS_DI(c, c, c, timeperiod=params.get('period', 14))
|
||||
adx = talib.ADX(c, c, c, timeperiod=params.get('period', 14))
|
||||
s = pd.Series(0, index=bars.index)
|
||||
s[(pdi > ndi) & (adx > params.get('threshold', 20))] = 1
|
||||
s[(ndi > pdi) & (adx > params.get('threshold', 20))] = -1
|
||||
elif name == 'SAR':
|
||||
v = talib.SAR(c, c, acceleration=params.get('accel', 0.02), maximum=params.get('max_accel', 0.2))
|
||||
s = pd.Series(0, index=bars.index); s[c > v] = 1; s[c < v] = -1
|
||||
elif name == 'ROC':
|
||||
v = talib.ROC(c, timeperiod=params.get('period', 10))
|
||||
s = pd.Series(0, index=bars.index); s[v > params.get('threshold', 0.2)] = 1; s[v < -params.get('threshold', 0.2)] = -1
|
||||
elif name == 'MOM':
|
||||
v = talib.MOM(c, timeperiod=params.get('period', 10))
|
||||
s = pd.Series(0, index=bars.index); s[v > 0] = 1; s[v < 0] = -1
|
||||
elif name == 'AROON':
|
||||
up, dn = talib.AROON(c, c, timeperiod=params.get('period', 14))
|
||||
s = pd.Series(0, index=bars.index); s[up > dn] = 1; s[up < dn] = -1
|
||||
elif name == 'MFI':
|
||||
v = talib.MFI(c, c, c, c, timeperiod=params.get('period', 14))
|
||||
s = pd.Series(0, index=bars.index); s[v < 20] = 1; s[v > 80] = -1
|
||||
elif name == 'SMA':
|
||||
s = pd.Series(0, index=bars.index)
|
||||
s[bars.rolling(params.get('fast', 10)).mean() > bars.rolling(params.get('slow', 50)).mean()] = 1
|
||||
s[bars.rolling(params.get('fast', 10)).mean() < bars.rolling(params.get('slow', 50)).mean()] = -1
|
||||
elif name == 'EMA':
|
||||
ef = bars.ewm(span=params.get('fast', 5), adjust=False).mean()
|
||||
es = bars.ewm(span=params.get('slow', 26), adjust=False).mean()
|
||||
s = pd.Series(0, index=bars.index); s[ef > es] = 1; s[ef < es] = -1
|
||||
else:
|
||||
s = pd.Series(0, index=bars.index)
|
||||
return s.fillna(0).astype(int).clip(-1, 1)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Multi-Instrument Evaluation
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def evaluate_multi(closes, hypothesis, use_session=True, use_vola=False):
|
||||
"""Evaluate strategy on all instruments, return combined metrics + per-instrument."""
|
||||
results = {}
|
||||
equity_curves = {}
|
||||
|
||||
for inst, close in closes.items():
|
||||
signal = build_signal(close, hypothesis)
|
||||
if signal is None:
|
||||
results[inst] = {"sharpe": 0, "monthly_pct": 0, "n_trades": 0}
|
||||
continue
|
||||
|
||||
# Apply filters
|
||||
if use_session:
|
||||
signal = _apply_session_filter(signal, close.index)
|
||||
if use_vola:
|
||||
signal = _apply_vola_filter(signal, close)
|
||||
|
||||
# News filter: block trades during high-impact events for this currency
|
||||
signal = _apply_news_filter(signal, close.index, inst.replace("USD", "").replace("BTC", "BTC"))
|
||||
|
||||
# Cross-pair confirmation: validate follower with leader momentum
|
||||
leader_inst = LEADER_MAP.get(inst)
|
||||
if leader_inst and leader_inst in closes:
|
||||
signal = _apply_cross_confirm(signal, close, closes[leader_inst])
|
||||
|
||||
if signal.nunique() <= 1:
|
||||
results[inst] = {"sharpe": 0, "monthly_pct": 0, "n_trades": 0}
|
||||
continue
|
||||
|
||||
# OOS split
|
||||
n = len(close)
|
||||
is_n = int(n * (1 - OOS_SPLIT))
|
||||
close_is = close.iloc[:is_n]; signal_is = signal.iloc[:is_n]
|
||||
close_oos = close.iloc[is_n:]; signal_oos = signal.iloc[is_n:]
|
||||
|
||||
# IS backtest
|
||||
prices_is = close_is.values.astype(np.float64); sigs_is = signal_is.values.astype(np.int32)
|
||||
eq_is, dd_is, tr_is, wins_is, ret_is, sh_is, _ = _backtest_numba(prices_is, sigs_is)
|
||||
|
||||
# OOS backtest
|
||||
prices_oos = close_oos.values.astype(np.float64); sigs_oos = signal_oos.values.astype(np.int32)
|
||||
eq_oos, dd_oos, tr_oos, wins_oos, ret_oos, sh_oos, _ = _backtest_numba(prices_oos, sigs_oos)
|
||||
|
||||
# Full backtest (for equity curve)
|
||||
prices_full = close.values.astype(np.float64); sigs_full = signal.values.astype(np.int32)
|
||||
eq_full, dd_full, tr_full, wins_full, ret_full, sh_full, trades_full = _backtest_numba(prices_full, sigs_full)
|
||||
|
||||
n_days = (close.index[-1] - close.index[0]).days
|
||||
mon = ((1+ret_full)**(1/(n_days/30.44))-1)*100 if ret_full > -1 else 0
|
||||
mon_oos = ((1+ret_oos)**(1/((close_oos.index[-1] - close_oos.index[0]).days/30.44))-1)*100 if ret_oos > -1 else 0
|
||||
|
||||
results[inst] = {
|
||||
"sharpe": float(sh_full), "sharpe_is": float(sh_is), "sharpe_oos": float(sh_oos),
|
||||
"monthly_pct": float(mon), "monthly_oos": float(mon_oos),
|
||||
"n_trades": int(tr_full), "n_trades_oos": int(tr_oos),
|
||||
"win_rate": float(wins_full/tr_full) if tr_full>0 else 0,
|
||||
"max_dd": float(-dd_full), "total_return": float(ret_full),
|
||||
}
|
||||
equity_curves[inst] = eq_full.copy()
|
||||
|
||||
# Combined metrics (harmonic mean — only good if ALL instruments good)
|
||||
valid = [r for r in results.values() if r['sharpe'] > 0]
|
||||
if not valid:
|
||||
combined = {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0, "n_trades_oos": 0}
|
||||
else:
|
||||
combined = {
|
||||
"sharpe": float(np.mean([r['sharpe'] for r in valid])),
|
||||
"monthly_pct": float(np.mean([r['monthly_pct'] for r in valid])),
|
||||
"monthly_oos": float(np.mean([r['monthly_oos'] for r in valid])),
|
||||
"n_trades": int(np.sum([r['n_trades'] for r in valid])),
|
||||
"n_trades_oos": int(np.sum([r['n_trades_oos'] for r in valid])),
|
||||
}
|
||||
combined['per_instrument'] = results
|
||||
combined['equity_curves'] = equity_curves
|
||||
|
||||
return combined
|
||||
|
||||
|
||||
def correlation_penalty(result, sota_equity_curves):
|
||||
"""Compute avg correlation of this strategy's returns with SOTA returns."""
|
||||
if not sota_equity_curves:
|
||||
return 0.0
|
||||
my_returns = []
|
||||
for eq in result.get('equity_curves', {}).values():
|
||||
if len(eq) > 1:
|
||||
my_returns.append(np.diff(eq) / eq[:-1])
|
||||
if not my_returns:
|
||||
return 0.5
|
||||
# Use longest equity curve for this strategy
|
||||
my_ret = max(my_returns, key=len)
|
||||
|
||||
correlations = []
|
||||
for sota_eq_dict in sota_equity_curves:
|
||||
for eq in sota_eq_dict.values():
|
||||
if len(eq) > 1:
|
||||
sota_ret = np.diff(eq) / eq[:-1]
|
||||
# Align to shorter length
|
||||
min_len = min(len(my_ret), len(sota_ret))
|
||||
if min_len > 10:
|
||||
corr = np.corrcoef(my_ret[:min_len], sota_ret[:min_len])[0, 1]
|
||||
if not np.isnan(corr):
|
||||
correlations.append(corr)
|
||||
|
||||
return np.mean(correlations) if correlations else 0.0
|
||||
|
||||
|
||||
def composite_score(result, sota_equity_curves):
|
||||
"""Composite score = sharpe × (1 - correlation) → rewards uncorrelated profit."""
|
||||
sh = result.get('sharpe', 0)
|
||||
if sh <= 0:
|
||||
return 0
|
||||
corr = abs(correlation_penalty(result, sota_equity_curves))
|
||||
# Bonus for OOS consistency
|
||||
oos_ratio = min(result.get('monthly_oos', 0) / max(result.get('monthly_pct', 1), 0.01), 1.0)
|
||||
oos_ratio = max(oos_ratio, 0)
|
||||
return sh * (1 - 0.5 * corr) * (0.3 + 0.7 * oos_ratio)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Hypothesis Generation
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class ResearchLoop:
|
||||
"""Multi-instrument R&D loop with correlation-aware feedback."""
|
||||
|
||||
def __init__(self, closes):
|
||||
self.closes = closes # {instrument: close_series}
|
||||
self.sota = [] # State-of-the-art strategies (sorted by composite score)
|
||||
self.sota_equity = [] # Equity curves for correlation calc
|
||||
self.history = []
|
||||
self.iteration = 0
|
||||
self.best_score = 0
|
||||
self.best_sharpe = 0
|
||||
self.exploration_rate = EXPLORATION_RATE
|
||||
|
||||
def hypothesize(self):
|
||||
self.iteration += 1
|
||||
|
||||
# Every 2000: ML (higher priority, runs before Optuna)
|
||||
if self.iteration % 2000 == 0 and len(self.sota) >= 5:
|
||||
return {'type': 'ml', 'generation': 'ml',
|
||||
'description': f"ML: LightGBM on {len(self.sota)} strategies",
|
||||
'sota': self.sota[:5]}
|
||||
|
||||
# Every 500: Optuna optimize best strategy
|
||||
if self.iteration % 500 == 0 and self.sota:
|
||||
hp = dict(self.sota[0]['hypothesis'])
|
||||
hp['generation'] = 'optuna'
|
||||
hp['description'] = f"Optuna: {hp.get('description','?')}"
|
||||
return hp
|
||||
|
||||
# Every 100: force non-dominant indicator
|
||||
if self.iteration % 100 == 0 and len(self.sota) >= 5:
|
||||
top = self._top_indicator()
|
||||
hp = self._random_hypothesis()
|
||||
hp = self._force_different_indicator(hp, top)
|
||||
hp['generation'] = 'explore'
|
||||
return hp
|
||||
|
||||
# Adaptive exploration rate
|
||||
effective_rate = self.exploration_rate
|
||||
if len(self.sota) >= 10:
|
||||
top = self._top_indicator()
|
||||
dominated = sum(1 for r in self.sota if
|
||||
r['hypothesis'].get('trend_ind', r['hypothesis'].get('indicator')) == top)
|
||||
if dominated > len(self.sota) * 0.8:
|
||||
effective_rate += 0.25
|
||||
|
||||
if random.random() < effective_rate or not self.sota:
|
||||
return self._random_hypothesis()
|
||||
else:
|
||||
base = random.choice(self.sota[:5])
|
||||
return self._mutate_hypothesis(base['hypothesis'])
|
||||
|
||||
def _top_indicator(self):
|
||||
if not self.sota:
|
||||
return 'MACD'
|
||||
return self.sota[0]['hypothesis'].get('trend_ind',
|
||||
self.sota[0]['hypothesis'].get('indicator', 'MACD'))
|
||||
|
||||
def _force_different_indicator(self, hp, top_ind):
|
||||
if hp.get('type') == 'multi_role':
|
||||
if hp['trend_ind'] == top_ind and hp['entry_ind'] == top_ind:
|
||||
if random.random() < 0.5:
|
||||
hp['trend_ind'] = random.choice([i for i in INDICATORS_POOL if i != top_ind])
|
||||
hp['trend_params'] = self._random_params(hp['trend_ind'])
|
||||
else:
|
||||
hp['entry_ind'] = random.choice([i for i in INDICATORS_POOL if i != top_ind])
|
||||
hp['entry_params'] = self._random_params(hp['entry_ind'])
|
||||
elif hp.get('indicator') == top_ind:
|
||||
hp['indicator'] = random.choice([i for i in INDICATORS_POOL if i != top_ind])
|
||||
hp['params'] = self._random_params(hp['indicator'])
|
||||
hp['description'] = self._make_desc(hp)
|
||||
return hp
|
||||
|
||||
def _make_desc(self, hp):
|
||||
t = hp.get('type', '?')
|
||||
if t == 'multi_role':
|
||||
return f"{hp['trend_ind']}({hp['trend_tf']})→{hp['entry_ind']}({hp['entry_tf']})"
|
||||
elif t == 'multi_tf':
|
||||
return f"{hp.get('indicator','?')} on {','.join(hp.get('timeframes',[])[:2])}"
|
||||
else:
|
||||
return f"{hp.get('indicator','?')} on {hp.get('timeframe','?')}"
|
||||
|
||||
def _random_hypothesis(self):
|
||||
stype = random.choice(STRATEGY_TYPES)
|
||||
if stype == 'single':
|
||||
ind = random.choice(INDICATORS_POOL); tf = random.choice(TIMEFRAMES)
|
||||
return {'type': 'single', 'indicator': ind, 'timeframe': tf,
|
||||
'params': self._random_params(ind),
|
||||
'description': f"{ind} on {tf}", 'generation': 'explore'}
|
||||
elif stype == 'multi_tf':
|
||||
ind = random.choice(INDICATORS_POOL)
|
||||
tfs = random.sample(TIMEFRAMES, k=random.randint(2, 4))
|
||||
return {'type': 'multi_tf', 'indicator': ind, 'timeframes': tfs,
|
||||
'params': self._random_params(ind),
|
||||
'description': f"{ind} on {','.join(tfs)}", 'generation': 'explore'}
|
||||
else: # multi_role
|
||||
trend_ind = random.choice(INDICATORS_POOL)
|
||||
entry_ind = random.choice(INDICATORS_POOL)
|
||||
trend_tf = random.choice(TREND_TFS)
|
||||
entry_tf = random.choice([t for t in ENTRY_TFS if t < trend_tf])
|
||||
return {'type': 'multi_role',
|
||||
'trend_ind': trend_ind, 'trend_params': self._random_params(trend_ind),
|
||||
'trend_tf': trend_tf,
|
||||
'entry_ind': entry_ind, 'entry_params': self._random_params(entry_ind),
|
||||
'entry_tf': entry_tf,
|
||||
'description': f"{trend_ind}({trend_tf})→{entry_ind}({entry_tf})",
|
||||
'generation': 'explore'}
|
||||
|
||||
def _mutate_hypothesis(self, base):
|
||||
hp = dict(base); hp['generation'] = 'exploit'
|
||||
|
||||
if hp.get('type') == 'multi_role':
|
||||
mut = random.choice(['trend_ind', 'entry_ind', 'trend_tf', 'entry_tf',
|
||||
'trend_params', 'entry_params'])
|
||||
if mut == 'trend_ind':
|
||||
hp['trend_ind'] = random.choice([i for i in INDICATORS_POOL if i != hp['trend_ind']])
|
||||
hp['trend_params'] = self._random_params(hp['trend_ind'])
|
||||
elif mut == 'entry_ind':
|
||||
hp['entry_ind'] = random.choice([i for i in INDICATORS_POOL if i != hp['entry_ind']])
|
||||
hp['entry_params'] = self._random_params(hp['entry_ind'])
|
||||
elif mut == 'trend_tf':
|
||||
hp['trend_tf'] = random.choice(TREND_TFS)
|
||||
if hp['trend_tf'] <= hp['entry_tf']:
|
||||
hp['entry_tf'] = random.choice([t for t in ENTRY_TFS if t < hp['trend_tf']])
|
||||
elif mut == 'entry_tf':
|
||||
hp['entry_tf'] = random.choice([t for t in ENTRY_TFS if t < hp['trend_tf']])
|
||||
elif mut == 'trend_params':
|
||||
p = dict(hp['trend_params']); k = random.choice(list(p.keys()))
|
||||
if isinstance(p[k], (int, float)): p[k] = p[k] * random.uniform(0.5, 1.5)
|
||||
hp['trend_params'] = p
|
||||
elif mut == 'entry_params':
|
||||
p = dict(hp['entry_params']); k = random.choice(list(p.keys()))
|
||||
if isinstance(p[k], (int, float)): p[k] = p[k] * random.uniform(0.5, 1.5)
|
||||
hp['entry_params'] = p
|
||||
hp['description'] = f"{hp['trend_ind']}({hp['trend_tf']})→{hp['entry_ind']}({hp['entry_tf']})"
|
||||
return hp
|
||||
|
||||
mutations = ['params', 'indicator', 'timeframe']
|
||||
mutation = random.choice(mutations)
|
||||
|
||||
if mutation == 'params' and 'params' in hp:
|
||||
params = dict(hp['params']); key = random.choice(list(params.keys()))
|
||||
if isinstance(params[key], (int, float)):
|
||||
params[key] = params[key] * random.uniform(0.5, 1.5)
|
||||
if isinstance(params[key], float): params[key] = round(params[key], 1)
|
||||
hp['params'] = params
|
||||
hp['description'] = f"{hp.get('indicator','?')} (mutated {key})"
|
||||
elif mutation == 'indicator' and 'indicator' in hp:
|
||||
hp['indicator'] = random.choice([i for i in INDICATORS_POOL if i != hp.get('indicator')])
|
||||
hp['params'] = self._random_params(hp['indicator'])
|
||||
hp['description'] = f"{hp['indicator']} (replaced)"
|
||||
elif mutation == 'timeframe':
|
||||
if 'timeframe' in hp:
|
||||
hp['timeframe'] = random.choice(TIMEFRAMES)
|
||||
elif 'timeframes' in hp:
|
||||
hp['timeframes'] = random.sample(TIMEFRAMES, k=len(hp['timeframes']))
|
||||
hp['description'] = f"{hp.get('indicator','?')} (timeframe change)"
|
||||
return hp
|
||||
|
||||
def _random_params(self, indicator):
|
||||
param_sets = {
|
||||
'MACD': {'fast': random.choice([3,5,8,12]), 'slow': random.choice([10,15,20,26]), 'sig': random.choice([3,5,9])},
|
||||
'RSI': {'period': random.choice([7,14,21]), 'oversold': random.choice([20,25,30]), 'overbought': random.choice([70,75,80])},
|
||||
'BBands': {'period': random.choice([10,20,40]), 'std': random.choice([1.5,2.0,2.5])},
|
||||
'Donchian': {'period': random.choice([5,10,20,30,50]), 'hold': random.choice([1,2,3,5])},
|
||||
'Stoch': {'fastk': random.choice([5,9,14]), 'slowk': 3, 'slowd': random.choice([3,5])},
|
||||
'CCI': {'period': random.choice([14,20,50])},
|
||||
'WillR': {'period': random.choice([7,14,21])},
|
||||
'ADX': {'period': random.choice([7,14,21]), 'threshold': random.choice([15,20,25])},
|
||||
'SAR': {'accel': random.choice([0.02,0.05,0.08]), 'max_accel': random.choice([0.2,0.3,0.5])},
|
||||
'ROC': {'period': random.choice([5,10,20]), 'threshold': random.choice([0.1,0.2,0.5])},
|
||||
'MOM': {'period': random.choice([5,10,20,50])},
|
||||
'AROON': {'period': random.choice([7,14,21])},
|
||||
'MFI': {'period': random.choice([7,14,21])},
|
||||
'SMA': {'fast': random.choice([5,10,20,50]), 'slow': random.choice([20,50,100,200])},
|
||||
'EMA': {'fast': random.choice([3,5,8,12]), 'slow': random.choice([15,26,50,100])},
|
||||
}
|
||||
return param_sets.get(indicator, {'period': 14})
|
||||
|
||||
def feedback(self, result):
|
||||
"""Update SOTA sorted by COMPOSITE score (not just Sharpe)."""
|
||||
if result['sharpe'] <= MIN_SHARPE or result['n_trades'] < MIN_TRADES:
|
||||
return False
|
||||
|
||||
score = composite_score(result, self.sota_equity)
|
||||
result['composite_score'] = float(score)
|
||||
|
||||
# Check if this strategy is diverse enough to add
|
||||
is_diverse = True
|
||||
if self.sota:
|
||||
# Skip if very similar to existing (same indicators, TF, type)
|
||||
for existing in self.sota[:3]:
|
||||
if self._similar(result, existing):
|
||||
is_diverse = False
|
||||
break
|
||||
|
||||
if is_diverse:
|
||||
self.sota.append(result)
|
||||
self.sota.sort(key=lambda r: r.get('composite_score', 0), reverse=True)
|
||||
self.sota = self.sota[:30] # Keep top 30
|
||||
self.sota_equity = [s['equity_curves'] for s in self.sota]
|
||||
if score > self.best_score:
|
||||
self.best_score = score
|
||||
return True # NEW BEST
|
||||
|
||||
if result['sharpe'] > self.best_sharpe:
|
||||
self.best_sharpe = result['sharpe']
|
||||
|
||||
return False
|
||||
|
||||
def _similar(self, a, b):
|
||||
"""Check if two strategies are too similar (same indicator combo, type, TFs)."""
|
||||
ha = a['hypothesis']; hb = b['hypothesis']
|
||||
if ha.get('type') != hb.get('type'):
|
||||
return False
|
||||
if ha.get('type') == 'multi_role':
|
||||
return (ha.get('trend_ind') == hb.get('trend_ind') and
|
||||
ha.get('entry_ind') == hb.get('entry_ind') and
|
||||
ha.get('trend_tf') == hb.get('trend_tf') and
|
||||
ha.get('entry_tf') == hb.get('entry_tf'))
|
||||
return ha.get('indicator') == hb.get('indicator')
|
||||
|
||||
def record(self):
|
||||
"""Save checkpoint."""
|
||||
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
if self.sota:
|
||||
cp = RESULTS_DIR / f"rd_loop_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
# Strip equity_curves (too large) from saved results
|
||||
stripped = []
|
||||
for r in self.sota[:30]:
|
||||
s = {k: v for k, v in r.items() if k != 'equity_curves'}
|
||||
stripped.append(s)
|
||||
cp.write_text(json.dumps(stripped, indent=2, default=str))
|
||||
|
||||
|
||||
def _run_optuna(closes, hypothesis):
|
||||
"""Optuna optimization on the primary instrument."""
|
||||
import optuna
|
||||
optuna.logging.set_verbosity(optuna.logging.WARNING)
|
||||
|
||||
hp = hypothesis
|
||||
close = list(closes.values())[0] # Use first instrument for Optuna
|
||||
ind = hp.get('indicator', hp.get('trend_ind', 'MACD'))
|
||||
base_params = hp.get('params', hp.get('trend_params', {}))
|
||||
|
||||
param_ranges = {
|
||||
'MACD': {'fast': (2,15), 'slow': (5,40), 'sig': (2,15)},
|
||||
'RSI': {'period': (5,30), 'oversold': (10,40), 'overbought': (60,90)},
|
||||
'Donchian': {'period': (3,100), 'hold': (1,10)},
|
||||
'SAR': {'accel': (0.01, 0.2), 'max_accel': (0.1, 1.0)},
|
||||
'ADX': {'period': (5,30), 'threshold': (10,40)},
|
||||
}
|
||||
ranges = param_ranges.get(ind, {})
|
||||
|
||||
def objective(trial):
|
||||
params = {}
|
||||
for k, (lo, hi) in ranges.items():
|
||||
if isinstance(base_params.get(k, 1), int):
|
||||
params[k] = trial.suggest_int(k, int(lo), int(hi))
|
||||
else:
|
||||
params[k] = trial.suggest_float(k, lo, hi)
|
||||
if 'fast' in params and 'slow' in params:
|
||||
params['fast'] = min(params['fast'], params['slow']-2)
|
||||
|
||||
result = evaluate_multi(closes, hp, use_session=True, use_vola=True)
|
||||
return float(result.get('sharpe', 0)) if result.get('sharpe', 0) > 0 else -999.0
|
||||
|
||||
try:
|
||||
study = optuna.create_study(direction='maximize')
|
||||
study.optimize(objective, n_trials=15, show_progress_bar=False)
|
||||
best = study.best_params
|
||||
if 'params' in hp:
|
||||
hp['params'] = {k: int(v) if v == int(v) else v for k, v in best.items()}
|
||||
elif 'trend_params' in hp:
|
||||
hp['trend_params'] = {k: int(v) if v == int(v) else v for k, v in best.items()}
|
||||
hp['generation'] = 'optuna'
|
||||
result = evaluate_multi(closes, hp, use_session=True, use_vola=True)
|
||||
print(f" Optuna best: {best} → Sh={result['sharpe']:.1f} "
|
||||
f"Mon={result['monthly_pct']:.1f}% OOS={result['monthly_oos']:.1f}% ({study.best_value:.1f})")
|
||||
return result
|
||||
except Exception:
|
||||
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
|
||||
|
||||
|
||||
def _train_ml(closes, hypothesis):
|
||||
"""Train LightGBM on SOTA indicator signals."""
|
||||
try:
|
||||
from lightgbm import LGBMClassifier
|
||||
except ImportError:
|
||||
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
|
||||
|
||||
sota = hypothesis.get('sota', [])
|
||||
if not sota:
|
||||
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
|
||||
|
||||
close = list(closes.values())[0]
|
||||
daily = close.resample('1h').last().dropna()
|
||||
features = pd.DataFrame(index=daily.index)
|
||||
|
||||
for s in sota[:5]:
|
||||
hp_s = s['hypothesis']
|
||||
# Generate signal from each SOTA strategy as a feature
|
||||
from nexquant_rd_loop import build_signal, _build_indicator_signal
|
||||
sig = build_signal(close, hp_s)
|
||||
if sig is not None:
|
||||
sig = sig.reindex(daily.index, method='ffill')
|
||||
name = hp_s.get('description', f"strat_{id(s)}")[:30]
|
||||
features[name] = sig.fillna(0)
|
||||
|
||||
features = features.iloc[100:] # Skip warmup
|
||||
if len(features) < 200:
|
||||
return {"sharpe": 0, "monthly_pct": 0, "monthly_oos": 0, "n_trades": 0}
|
||||
|
||||
target = (daily.pct_change().shift(-1) > 0).astype(int)
|
||||
target = target.reindex(features.index).fillna(0)
|
||||
|
||||
split = int(len(features) * 0.8)
|
||||
X_train, X_test = features.iloc[:split], features.iloc[split:]
|
||||
y_train, y_test = target.iloc[:split], target.iloc[split:]
|
||||
|
||||
model = LGBMClassifier(n_estimators=100, max_depth=5, verbosity=-1)
|
||||
model.fit(X_train, y_train)
|
||||
preds = model.predict(X_test)
|
||||
acc = float((preds == y_test).mean())
|
||||
|
||||
ml_signal = pd.Series(0, index=X_test.index)
|
||||
ml_signal[preds == 1] = 1; ml_signal[preds == 0] = -1
|
||||
ml_signal = ml_signal.reindex(close.index).ffill().fillna(0).astype(int).clip(-1, 1)
|
||||
ml_signal = _apply_session_filter(ml_signal, close.index)
|
||||
|
||||
prices = close.values.astype(np.float64); sigs = ml_signal.values.astype(np.int32)
|
||||
eq, dd, tr, wins, ret, sh, _ = _backtest_numba(prices, sigs)
|
||||
n_days = (close.index[-1] - close.index[0]).days
|
||||
mon = ((1+ret)**(1/(n_days/30.44))-1)*100 if ret > -1 else 0
|
||||
print(f" ML LightGBM: Test acc={acc:.1%} → Sh={sh:.1f} Mon={mon:.1f}% Tr={tr}")
|
||||
return {"sharpe": float(sh), "monthly_pct": float(mon), "monthly_oos": 0,
|
||||
"n_trades": int(tr), "win_rate": float(wins/tr) if tr>0 else 0,
|
||||
"ml_accuracy": float(acc), "ml_model": "LightGBM"}
|
||||
|
||||
|
||||
def load_data():
|
||||
"""Load OHLCV data for all instruments from one or multiple HDF5 files."""
|
||||
closes = {}
|
||||
data_dir = OHLCV_PATH.parent
|
||||
|
||||
# Try main file first
|
||||
if OHLCV_PATH.exists():
|
||||
df = pd.read_hdf(OHLCV_PATH, key="data")
|
||||
for inst in INSTRUMENTS:
|
||||
try:
|
||||
close = df.xs(inst, level="instrument")["$close"].sort_index()
|
||||
closes[inst] = close
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Load from individual files if not found
|
||||
instrument_files = {
|
||||
"EURUSD": OHLCV_PATH,
|
||||
"GBPUSD": data_dir / "gbpusdt_1min.h5",
|
||||
"BTCUSD": data_dir / "btc_1min.h5",
|
||||
"XAUUSD": data_dir / "xauusdt_1min.h5",
|
||||
}
|
||||
for inst, path in instrument_files.items():
|
||||
if inst in closes:
|
||||
continue
|
||||
if not path.exists():
|
||||
print(f" {inst}: file not found — skipping")
|
||||
continue
|
||||
try:
|
||||
df = pd.read_hdf(path, key="data")
|
||||
if isinstance(df.index, pd.MultiIndex):
|
||||
try:
|
||||
close = df.xs(inst, level="instrument")["$close"].sort_index()
|
||||
except KeyError:
|
||||
# Try with T suffix for crypto pairs
|
||||
alt = inst + "T" if not inst.endswith("T") else inst.rstrip("T")
|
||||
try:
|
||||
close = df.xs(alt, level="instrument")["$close"].sort_index()
|
||||
except KeyError:
|
||||
inst_vals = df.index.get_level_values("instrument").unique()
|
||||
for iv in inst_vals:
|
||||
if inst[:3] in str(iv)[:3]:
|
||||
close = df.xs(iv, level="instrument")["$close"].sort_index()
|
||||
break
|
||||
else:
|
||||
raise KeyError(f"No instrument matching {inst}")
|
||||
elif "$close" in df.columns:
|
||||
close = df["$close"].sort_index()
|
||||
close.index = pd.to_datetime(close.index)
|
||||
elif "close" in df.columns:
|
||||
close = df["close"].sort_index()
|
||||
close.index = pd.to_datetime(close.index)
|
||||
else:
|
||||
close = df.iloc[:, 3].sort_index()
|
||||
close.index = pd.to_datetime(close.index)
|
||||
closes[inst] = close
|
||||
except Exception as e:
|
||||
print(f" {inst}: load error {e} — skipping")
|
||||
|
||||
for inst, close in closes.items():
|
||||
print(f" {inst}: {len(close):,} bars, {close.index[0]} → {close.index[-1]}")
|
||||
|
||||
return closes
|
||||
|
||||
|
||||
def main():
|
||||
iterations = 200
|
||||
if "--iterations" in sys.argv:
|
||||
iterations = int(sys.argv[sys.argv.index("--iterations") + 1])
|
||||
|
||||
print("=" * 60)
|
||||
print(f" R&D Loop V2 — Multi-Instrument + Correlation Score")
|
||||
print(f" Instruments: {', '.join(INSTRUMENTS)}")
|
||||
print(f" Indicators: {len(INDICATORS_POOL)} | Strategy types: {len(STRATEGY_TYPES)}")
|
||||
print(f" Features: Session Filter + Volatility Filter + OOS Split")
|
||||
print(f" Iterations: {iterations}")
|
||||
print("=" * 60)
|
||||
print(" Loading data...")
|
||||
|
||||
closes = load_data()
|
||||
if not closes:
|
||||
print(" ERROR: No instruments loaded!"); return
|
||||
|
||||
loop = ResearchLoop(closes)
|
||||
t0 = time.time()
|
||||
|
||||
for i in range(iterations):
|
||||
hp = loop.hypothesize()
|
||||
|
||||
# Evaluate
|
||||
try:
|
||||
result = evaluate_multi(closes, hp, use_session=True, use_vola=True)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
result['hypothesis'] = hp
|
||||
result['iteration'] = i + 1
|
||||
result['timestamp'] = datetime.now().isoformat()
|
||||
loop.history.append(result)
|
||||
|
||||
# Feedback
|
||||
is_new_best = loop.feedback(result)
|
||||
best_inst_metrics = [f"{inst}: {m['sharpe']:.1f}" for inst, m in result.get('per_instrument', {}).items() if m.get('sharpe', 0) != 0]
|
||||
|
||||
gen = hp.get('generation', '?')
|
||||
if is_new_best:
|
||||
print(f"\n ★ NEW BEST (#{i+1}, {gen}): {hp['description']}")
|
||||
print(f" Score={result['composite_score']:.1f} Sh={result['sharpe']:.1f} "
|
||||
f"Mon={result['monthly_pct']:.1f}% OOS={result['monthly_oos']:.1f}% "
|
||||
f"Tr={result['n_trades']} [{', '.join(best_inst_metrics[:3])}]")
|
||||
elif (i + 1) % 50 == 0:
|
||||
top_indicators = set()
|
||||
for r in loop.sota[:5]:
|
||||
top_indicators.add(r['hypothesis'].get('trend_ind', r['hypothesis'].get('indicator', '?')))
|
||||
print(f" [{i+1}/{iterations}] {gen:>7s} | SOTA: {len(loop.sota)} | "
|
||||
f"Best Sh={loop.best_sharpe:.1f} Score={loop.best_score:.1f} | "
|
||||
f"Explore: {loop.exploration_rate:.0%} | Inds: {','.join(sorted(top_indicators)[:4])}")
|
||||
|
||||
if (i + 1) % 100 == 0:
|
||||
loop.record()
|
||||
|
||||
if len(loop.sota) > 10:
|
||||
loop.exploration_rate = max(0.15, EXPLORATION_RATE - len(loop.sota) * 0.003)
|
||||
|
||||
elapsed = time.time() - t0
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" R&D Loop V2 Complete: {iterations} iterations in {elapsed:.0f}s")
|
||||
print(f" SOTA Strategies: {len(loop.sota)} | Best Score: {loop.best_score:.1f}")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
if loop.sota:
|
||||
print(f"\n TOP DISCOVERIES (by composite score):")
|
||||
for i, r in enumerate(loop.sota[:15], 1):
|
||||
hp = r['hypothesis']
|
||||
per_inst = r.get('per_instrument', {})
|
||||
insts = ' '.join([f"{k}:{v['sharpe']:.0f}" for k, v in per_inst.items() if v['sharpe'] != 0])
|
||||
print(f" {i:>2d}. {hp['description'][:45]:45s} "
|
||||
f"Sc={r['composite_score']:.1f} Sh={r['sharpe']:+.1f} "
|
||||
f"Mo={r['monthly_pct']:+.1f}% OOS={r['monthly_oos']:+.1f}% "
|
||||
f"[{insts}]")
|
||||
|
||||
final = RESULTS_DIR / f"rd_loop_final_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
stripped = [{k: v for k, v in r.items() if k != 'equity_curves'} for r in loop.sota]
|
||||
final.write_text(json.dumps(stripped, indent=2, default=str))
|
||||
print(f"\n Saved: {final}")
|
||||
|
||||
exploit_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'exploit']
|
||||
explore_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'explore']
|
||||
optuna_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'optuna']
|
||||
ml_best = [r for r in loop.sota if r['hypothesis'].get('generation') == 'ml']
|
||||
print(f" Exploit: {len(exploit_best)} | Explore: {len(explore_best)} | "
|
||||
f"Optuna: {len(optuna_best)} | ML: {len(ml_best)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python
|
||||
"""One strategy runner — standalone, called from parent script."""
|
||||
import json, sys, pandas as pd, subprocess, tempfile, numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python nexquant_rebacktest_one.py <strategy_json_path>")
|
||||
sys.exit(1)
|
||||
|
||||
strat_path = Path(sys.argv[1])
|
||||
data = json.loads(strat_path.read_text())
|
||||
|
||||
OHLCV = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||||
FACTORS_DIR = Path("results/factors/values")
|
||||
|
||||
fmap = {p.stem: str(p) for p in FACTORS_DIR.glob("*.parquet")}
|
||||
|
||||
names = data.get("factor_names", [])
|
||||
code = data.get("code", "")
|
||||
name = data.get("strategy_name", strat_path.stem)
|
||||
|
||||
if not names or not code:
|
||||
print(json.dumps({"status": "skipped", "reason": "no factors/code"}))
|
||||
sys.exit(0)
|
||||
|
||||
# Load close
|
||||
ohlcv = pd.read_hdf(str(OHLCV), key="data")
|
||||
close = ohlcv["$close"].dropna()
|
||||
if isinstance(close.index, pd.MultiIndex):
|
||||
close = close.droplevel(-1)
|
||||
close = close.astype(float).sort_index()
|
||||
|
||||
# Load factors
|
||||
series = {}
|
||||
for fn in names:
|
||||
fp = fmap.get(fn) or fmap.get(fn.replace("/", "_")[:150])
|
||||
if fp:
|
||||
try:
|
||||
s = pd.read_parquet(fp).iloc[:, 0]
|
||||
series[fn] = s
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(series) < 2:
|
||||
print(json.dumps({"status": "skipped", "reason": f"only {len(series)} factors loaded"}))
|
||||
sys.exit(0)
|
||||
|
||||
df = pd.DataFrame(series).sort_index()
|
||||
if isinstance(df.index, pd.MultiIndex):
|
||||
df = df.droplevel(-1)
|
||||
|
||||
df_1m = df.reindex(close.index).ffill()
|
||||
valid = df_1m.notna().any(axis=1)
|
||||
if valid.sum() < 1000:
|
||||
print(json.dumps({"status": "skipped", "reason": f"only {valid.sum()} valid bars"}))
|
||||
sys.exit(0)
|
||||
|
||||
ca = close.loc[valid]
|
||||
fa = df_1m.loc[valid]
|
||||
|
||||
# Execute
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tdp = Path(td)
|
||||
fa.to_parquet(str(tdp / "factors.parquet"))
|
||||
ca.to_pickle(str(tdp / "close.pkl"))
|
||||
exec_script = (
|
||||
"import sys, os\n"
|
||||
"sys.stdout = open(os.devnull, 'w')\n"
|
||||
"sys.stderr = open(os.devnull, 'w')\n"
|
||||
"import pandas as pd, numpy as np\n"
|
||||
"factors = pd.read_parquet('factors.parquet')\n"
|
||||
"close = pd.read_pickle('close.pkl')\n"
|
||||
"df = factors\n"
|
||||
+ code +
|
||||
"\nif 'signal' not in dir():\n"
|
||||
" raise SystemExit(1)\n"
|
||||
"pd.Series(signal).fillna(0).to_pickle('signal.pkl')\n"
|
||||
)
|
||||
(tdp / "run.py").write_text(exec_script)
|
||||
r = subprocess.run(
|
||||
["python", "run.py"],
|
||||
capture_output=True, text=True, timeout=60, cwd=str(tdp),
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
print(json.dumps({"status": "code_failed", "stderr": r.stderr[:500]}))
|
||||
sys.exit(1)
|
||||
sig = pd.read_pickle(tdp / "signal.pkl")
|
||||
except Exception as e:
|
||||
print(json.dumps({"status": "code_failed", "error": str(e)[:500]}))
|
||||
sys.exit(1)
|
||||
|
||||
sig = sig.reindex(ca.index).ffill().fillna(0)
|
||||
result = backtest_signal(ca, sig, txn_cost_bps=2.14)
|
||||
|
||||
# Return result as JSON
|
||||
output = {
|
||||
"status": "ok",
|
||||
"sharpe": result.get("sharpe"),
|
||||
"max_drawdown": result.get("max_drawdown"),
|
||||
"win_rate": result.get("win_rate"),
|
||||
"n_trades": result.get("n_trades"),
|
||||
"total_return": result.get("total_return"),
|
||||
"monthly_return_pct": result.get("monthly_return_pct"),
|
||||
"annualized_return": result.get("annualized_return"),
|
||||
}
|
||||
print(json.dumps(output))
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python
|
||||
"""Parent orchestrator: calls nexquant_rebacktest_one.py for each strategy."""
|
||||
import json, subprocess, sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
STRAT_DIR = Path("results/strategies_new")
|
||||
|
||||
# Build work list
|
||||
work = []
|
||||
for f in sorted(STRAT_DIR.glob("*.json")):
|
||||
if "verified_v2" in f.read_text():
|
||||
continue
|
||||
try:
|
||||
d = json.loads(f.read_text())
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("factor_names") and d.get("code"):
|
||||
work.append(f)
|
||||
|
||||
print(f"{len(work)} strategies to re-backtest", flush=True)
|
||||
|
||||
ok = skip = fail = 0
|
||||
start = datetime.now()
|
||||
|
||||
for i, f in enumerate(work):
|
||||
name = f.stem[:45]
|
||||
print(f"[{i+1}/{len(work)}] {name} ...", end=" ", flush=True)
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["timeout", "-s", "KILL", "90", "python", "scripts/nexquant_rebacktest_one.py", str(f)],
|
||||
capture_output=True, text=True, timeout=120,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
result = json.loads(r.stdout.strip() or "{}")
|
||||
except subprocess.TimeoutExpired:
|
||||
print("TIMEOUT", flush=True)
|
||||
fail += 1
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}", flush=True)
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
if result.get("status") == "ok":
|
||||
data = json.loads(f.read_text())
|
||||
data["reevaluation_status"] = "verified_v2"
|
||||
data["sharpe_ratio"] = result.get("sharpe")
|
||||
data["max_drawdown"] = result.get("max_drawdown")
|
||||
data["win_rate"] = result.get("win_rate")
|
||||
data["total_return"] = result.get("total_return")
|
||||
data["summary"] = {
|
||||
**data.get("summary", {}),
|
||||
"sharpe": result.get("sharpe"),
|
||||
"max_drawdown": result.get("max_drawdown"),
|
||||
"win_rate": result.get("win_rate"),
|
||||
"monthly_return_pct": result.get("monthly_return_pct"),
|
||||
"real_n_trades": result.get("n_trades"),
|
||||
"total_return": result.get("total_return"),
|
||||
"annualized_return": result.get("annualized_return"),
|
||||
"engine": "verified_v2",
|
||||
"txn_cost_bps": 2.14,
|
||||
}
|
||||
f.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
ok += 1
|
||||
print(f"S={result['sharpe']:.1f} DD={result['max_drawdown']:.2%} WR={result['win_rate']:.1%} T={result['n_trades']}", flush=True)
|
||||
elif result.get("status") == "skipped":
|
||||
skip += 1
|
||||
print(f"SKIP: {result.get('reason', '?')}", flush=True)
|
||||
else:
|
||||
fail += 1
|
||||
print(f"FAIL: {result.get('stderr', result.get('error', '?'))[:100]}", flush=True)
|
||||
|
||||
elapsed = (datetime.now() - start).total_seconds()
|
||||
print(f"\nDONE: ok={ok} skip={skip} fail={fail} in {elapsed:.0f}s", flush=True)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user