Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse files- .gitignore +193 -0
- .python-version +1 -0
- README.md +240 -7
- StrategyGeneratorJupyter.ipynb +616 -0
- app.py +298 -0
- bt_strategies.py +463 -0
- bt_strategy.py +49 -0
- bt_testing.py +86 -0
- bt_utils.py +261 -0
- config/var_dev.yaml +62 -0
- data_utils.py +281 -0
- requirements.txt +13 -0
- strategy_generator.py +189 -0
- utils.py +214 -0
- version.txt +1 -0
.gitignore
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
data_cache/*
|
| 6 |
+
data_upload/*
|
| 7 |
+
# C extensions
|
| 8 |
+
*.so
|
| 9 |
+
|
| 10 |
+
# other files
|
| 11 |
+
fmp_historical_chart_5min_QQQ_*
|
| 12 |
+
test_replay.py
|
| 13 |
+
test_download*.py
|
| 14 |
+
todo.md
|
| 15 |
+
|
| 16 |
+
# Claude
|
| 17 |
+
.claude/
|
| 18 |
+
|
| 19 |
+
# Distribution / packaging
|
| 20 |
+
.Python
|
| 21 |
+
build/
|
| 22 |
+
develop-eggs/
|
| 23 |
+
dist/
|
| 24 |
+
downloads/
|
| 25 |
+
eggs/
|
| 26 |
+
.eggs/
|
| 27 |
+
lib/
|
| 28 |
+
lib64/
|
| 29 |
+
parts/
|
| 30 |
+
sdist/
|
| 31 |
+
var/
|
| 32 |
+
wheels/
|
| 33 |
+
share/python-wheels/
|
| 34 |
+
*.egg-info/
|
| 35 |
+
.installed.cfg
|
| 36 |
+
*.egg
|
| 37 |
+
MANIFEST
|
| 38 |
+
|
| 39 |
+
# PyInstaller
|
| 40 |
+
# Usually these files are written by a python script from a template
|
| 41 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 42 |
+
*.manifest
|
| 43 |
+
*.spec
|
| 44 |
+
|
| 45 |
+
# Installer logs
|
| 46 |
+
pip-log.txt
|
| 47 |
+
pip-delete-this-directory.txt
|
| 48 |
+
|
| 49 |
+
# Unit test / coverage reports
|
| 50 |
+
htmlcov/
|
| 51 |
+
.tox/
|
| 52 |
+
.nox/
|
| 53 |
+
.coverage
|
| 54 |
+
.coverage.*
|
| 55 |
+
.cache
|
| 56 |
+
nosetests.xml
|
| 57 |
+
coverage.xml
|
| 58 |
+
*.cover
|
| 59 |
+
*.py,cover
|
| 60 |
+
.hypothesis/
|
| 61 |
+
.pytest_cache/
|
| 62 |
+
cover/
|
| 63 |
+
|
| 64 |
+
# Translations
|
| 65 |
+
*.mo
|
| 66 |
+
*.pot
|
| 67 |
+
|
| 68 |
+
# Django stuff:
|
| 69 |
+
*.log
|
| 70 |
+
local_settings.py
|
| 71 |
+
db.sqlite3
|
| 72 |
+
db.sqlite3-journal
|
| 73 |
+
|
| 74 |
+
# Flask stuff:
|
| 75 |
+
instance/
|
| 76 |
+
.webassets-cache
|
| 77 |
+
|
| 78 |
+
# Scrapy stuff:
|
| 79 |
+
.scrapy
|
| 80 |
+
|
| 81 |
+
# Sphinx documentation
|
| 82 |
+
docs/_build/
|
| 83 |
+
|
| 84 |
+
# PyBuilder
|
| 85 |
+
.pybuilder/
|
| 86 |
+
target/
|
| 87 |
+
|
| 88 |
+
# Jupyter Notebook
|
| 89 |
+
.ipynb_checkpoints
|
| 90 |
+
|
| 91 |
+
# IPython
|
| 92 |
+
profile_default/
|
| 93 |
+
ipython_config.py
|
| 94 |
+
|
| 95 |
+
# pyenv
|
| 96 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 97 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 98 |
+
# .python-version
|
| 99 |
+
|
| 100 |
+
# pipenv
|
| 101 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 102 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 103 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 104 |
+
# install all needed dependencies.
|
| 105 |
+
#Pipfile.lock
|
| 106 |
+
|
| 107 |
+
# UV
|
| 108 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 109 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 110 |
+
# commonly ignored for libraries.
|
| 111 |
+
#uv.lock
|
| 112 |
+
|
| 113 |
+
# poetry
|
| 114 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 115 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 116 |
+
# commonly ignored for libraries.
|
| 117 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 118 |
+
#poetry.lock
|
| 119 |
+
|
| 120 |
+
# pdm
|
| 121 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 122 |
+
#pdm.lock
|
| 123 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 124 |
+
# in version control.
|
| 125 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
| 126 |
+
.pdm.toml
|
| 127 |
+
.pdm-python
|
| 128 |
+
.pdm-build/
|
| 129 |
+
|
| 130 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 131 |
+
__pypackages__/
|
| 132 |
+
|
| 133 |
+
# Celery stuff
|
| 134 |
+
celerybeat-schedule
|
| 135 |
+
celerybeat.pid
|
| 136 |
+
|
| 137 |
+
# SageMath parsed files
|
| 138 |
+
*.sage.py
|
| 139 |
+
|
| 140 |
+
# Environments
|
| 141 |
+
.env
|
| 142 |
+
.venv
|
| 143 |
+
env/
|
| 144 |
+
venv/
|
| 145 |
+
ENV/
|
| 146 |
+
env.bak/
|
| 147 |
+
venv.bak/
|
| 148 |
+
|
| 149 |
+
# Spyder project settings
|
| 150 |
+
.spyderproject
|
| 151 |
+
.spyproject
|
| 152 |
+
|
| 153 |
+
# Rope project settings
|
| 154 |
+
.ropeproject
|
| 155 |
+
|
| 156 |
+
# mkdocs documentation
|
| 157 |
+
/site
|
| 158 |
+
|
| 159 |
+
# mypy
|
| 160 |
+
.mypy_cache/
|
| 161 |
+
.dmypy.json
|
| 162 |
+
dmypy.json
|
| 163 |
+
|
| 164 |
+
# Pyre type checker
|
| 165 |
+
.pyre/
|
| 166 |
+
|
| 167 |
+
# pytype static type analyzer
|
| 168 |
+
.pytype/
|
| 169 |
+
|
| 170 |
+
# Cython debug symbols
|
| 171 |
+
cython_debug/
|
| 172 |
+
|
| 173 |
+
# PyCharm
|
| 174 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 175 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 176 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 177 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 178 |
+
#.idea/
|
| 179 |
+
|
| 180 |
+
# Ruff stuff:
|
| 181 |
+
.ruff_cache/
|
| 182 |
+
|
| 183 |
+
# PyPI configuration file
|
| 184 |
+
.pypirc
|
| 185 |
+
|
| 186 |
+
.DS_Store
|
| 187 |
+
|
| 188 |
+
# Ignore Crew engineering team output
|
| 189 |
+
3_crew/engineering_team/output/
|
| 190 |
+
|
| 191 |
+
# Ignore Accounts database in capstone project
|
| 192 |
+
6_mcp/accounts.db
|
| 193 |
+
6_mcp/memory/*.db
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12
|
README.md
CHANGED
|
@@ -1,12 +1,245 @@
|
|
| 1 |
---
|
| 2 |
title: StrategyGenerator
|
| 3 |
-
emoji: 💻
|
| 4 |
-
colorFrom: yellow
|
| 5 |
-
colorTo: blue
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 6.2.0
|
| 8 |
app_file: app.py
|
| 9 |
-
|
|
|
|
| 10 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
title: StrategyGenerator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
app_file: app.py
|
| 4 |
+
sdk: gradio
|
| 5 |
+
sdk_version: 5.47.2
|
| 6 |
---
|
| 7 |
+
# Financial Strategy Generator
|
| 8 |
+
|
| 9 |
+
A Gradio web application that generates Python trading strategies from natural language descriptions using Large Language Models (LLMs). The generated strategies can be instantly backtested using the Backtrader library with real market data from Yahoo Finance.
|
| 10 |
+
|
| 11 |
+
## Overview
|
| 12 |
+
|
| 13 |
+
This application allows you to:
|
| 14 |
+
- **Describe trading strategies in plain English** (e.g., "Buy when 10-day SMA crosses above 100-day SMA")
|
| 15 |
+
- **Generate Python code automatically** using state-of-the-art LLMs
|
| 16 |
+
- **Backtest strategies immediately** in the GUI with configurable parameters
|
| 17 |
+
- **Visualize results** with interactive charts and detailed transaction logs
|
| 18 |
+
- **Export data** for further analysis
|
| 19 |
+
|
| 20 |
+
## Key Features
|
| 21 |
+
|
| 22 |
+
- 🤖 **Multi-LLM Support**: Choose from GPT, Claude, Deepseek, Gemini, or Grok4
|
| 23 |
+
- 📊 **Financial Indicators**: Supports SMA, MACD, RSI, ATR, and more via Backtrader
|
| 24 |
+
- 📈 **Interactive Backtesting**: Real-time strategy testing with Yahoo Finance data
|
| 25 |
+
- 💾 **Data Caching**: Automatic caching to avoid redundant downloads
|
| 26 |
+
- 📉 **Visual Analytics**: Charts, transaction tables, and trade performance metrics
|
| 27 |
+
- ⚙️ **Configurable Parameters**: Customize capital, commission, slippage, and more
|
| 28 |
+
- 🎯 **Multiple Timeframes**: Support for 1m, 5m, 15m, 1h, 1d intervals
|
| 29 |
+
- 📥 **Export Capabilities**: Download generated code and CSV reports
|
| 30 |
+
|
| 31 |
+
## Requirements
|
| 32 |
+
|
| 33 |
+
- **Python 3.12**
|
| 34 |
+
- Dependencies listed in `requirements.txt`.
|
| 35 |
+
|
| 36 |
+
## Installation & Setup
|
| 37 |
+
|
| 38 |
+
### 1. Clone the Repository
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
git clone <repository-url>
|
| 42 |
+
cd LLM-fintech
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### 2. Install Dependencies
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
pip install -r requirements.txt
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### 3. Environment Variables
|
| 52 |
+
|
| 53 |
+
Create a `.env` file in the project root with your LLM API keys:
|
| 54 |
+
|
| 55 |
+
```env
|
| 56 |
+
OPENAI_API_KEY=your_openai_key_here
|
| 57 |
+
ANTHROPIC_API_KEY=your_anthropic_key_here
|
| 58 |
+
DEEPSEEK_API_KEY=your_deepseek_key_here
|
| 59 |
+
GOOGLE_API_KEY=your_google_key_here
|
| 60 |
+
XAI_API_KEY=your_grok_key_here
|
| 61 |
+
|
| 62 |
+
# Optional: For HuggingFace local mode
|
| 63 |
+
HF_TOKEN=your_huggingface_token_here
|
| 64 |
+
|
| 65 |
+
# Optional: Override default models (see config/var_dev.yaml for defaults)
|
| 66 |
+
OPENAI_MODEL=gpt-4
|
| 67 |
+
CLAUDE_MODEL=claude-sonnet-4-20250514
|
| 68 |
+
DEEPSEEK_MODEL=deepseek-reasoner
|
| 69 |
+
GEMINI_MODEL=gemini-2.5-flash
|
| 70 |
+
GROK4_MODEL=grok-4-fast-reasoning
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### 4. Configuration
|
| 74 |
+
|
| 75 |
+
The application uses `config/var_dev.yaml` for default settings:
|
| 76 |
+
- Market/ticker mappings (e.g., "S&P 500 ETF" → "SPY")
|
| 77 |
+
- Default LLM model names
|
| 78 |
+
- Backtesting parameters (initial capital, commission, slippage)
|
| 79 |
+
- Data intervals and periods
|
| 80 |
+
|
| 81 |
+
You can modify this file to customize defaults without changing code.
|
| 82 |
+
|
| 83 |
+
## Running Locally
|
| 84 |
+
|
| 85 |
+
### Start the Application
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
python app.py
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
The Gradio interface will automatically open in your default browser. The app runs on `http://127.0.0.1:7860` by default.
|
| 92 |
+
|
| 93 |
+
### Using the Interface
|
| 94 |
+
|
| 95 |
+
1. **Generate Strategy**:
|
| 96 |
+
- Select your preferred LLM model from the dropdown
|
| 97 |
+
- Enter a natural language description of your trading strategy
|
| 98 |
+
- Click "Generate Strategy" to create Python code
|
| 99 |
+
|
| 100 |
+
2. **Configure Backtesting**:
|
| 101 |
+
- Choose a stock/ETF or enter a custom ticker symbol
|
| 102 |
+
- Set initial capital, commission, and slippage
|
| 103 |
+
- Select date range and data interval
|
| 104 |
+
- Optionally enable adjusted prices (for dividends/splits)
|
| 105 |
+
|
| 106 |
+
3. **Run Backtest**:
|
| 107 |
+
- Click "Run Python Code" to execute the strategy
|
| 108 |
+
- View results in the Python result panel
|
| 109 |
+
- Check the "Charts" tab for visualizations
|
| 110 |
+
- Review "Transactions" and "Trades" tabs for detailed logs
|
| 111 |
+
|
| 112 |
+
4. **Export Results**:
|
| 113 |
+
- Download generated strategy code as a `.py` file
|
| 114 |
+
- Export transaction and trade data as CSV files
|
| 115 |
+
|
| 116 |
+
## Gradio Deployment
|
| 117 |
+
|
| 118 |
+
To deploy this application to Gradio's hosting service:
|
| 119 |
+
|
| 120 |
+
### 1. Install `uv`
|
| 121 |
+
|
| 122 |
+
```bash
|
| 123 |
+
# On macOS/Linux
|
| 124 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 125 |
+
|
| 126 |
+
# On Windows
|
| 127 |
+
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
| 128 |
+
|
| 129 |
+
# Or via pip
|
| 130 |
+
pip install uv
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### 2. Deploy
|
| 134 |
+
|
| 135 |
+
```bash
|
| 136 |
+
uv gradio deploy
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
This command will:
|
| 140 |
+
- Package your application
|
| 141 |
+
- Upload it to Gradio's servers
|
| 142 |
+
- Provide a public URL for your deployed app
|
| 143 |
+
|
| 144 |
+
**Note**: Make sure your `.env` file is configured properly, or set environment variables in your deployment environment for API keys.
|
| 145 |
+
|
| 146 |
+
For more deployment options and configurations, refer to [Gradio Deployment Documentation](https://www.gradio.app/guides/deploying-your-app).
|
| 147 |
+
|
| 148 |
+
## Project Structure
|
| 149 |
+
|
| 150 |
+
```
|
| 151 |
+
LLM-fintech/
|
| 152 |
+
├── app.py # Main Gradio application interface
|
| 153 |
+
├── strategy_generator.py # LLM integration and strategy code generation
|
| 154 |
+
├── bt_utils.py # Backtesting engine, data fetching, and plotting
|
| 155 |
+
├── bt_strategies.py # Example strategy classes (reference implementations)
|
| 156 |
+
├── bt_testing.py # Standalone backtesting script (for testing)
|
| 157 |
+
├── utils.py # Helper functions (validation, file operations)
|
| 158 |
+
├── config/
|
| 159 |
+
│ └── var_dev.yaml # Configuration file (markets, models, defaults)
|
| 160 |
+
├── data_cache/ # Cached Yahoo Finance data (auto-generated)
|
| 161 |
+
├── requirements.txt # Python dependencies
|
| 162 |
+
└── README.md # This file
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
## Configuration Details
|
| 166 |
+
|
| 167 |
+
### Market/Ticker Mappings
|
| 168 |
+
|
| 169 |
+
The `config/var_dev.yaml` file contains mappings between friendly names and ticker symbols. You can:
|
| 170 |
+
- Use predefined markets from the dropdown (e.g., "S&P 500 ETF")
|
| 171 |
+
- Enter any valid Yahoo Finance ticker symbol directly
|
| 172 |
+
- Add custom mappings by editing the YAML file
|
| 173 |
+
|
| 174 |
+
### Supported Data Intervals
|
| 175 |
+
|
| 176 |
+
- **Intraday**: `1m`, `2m`, `5m`, `15m`, `30m`, `1h`
|
| 177 |
+
- **Daily**: `1d`
|
| 178 |
+
|
| 179 |
+
Note: Intraday data has limitations on historical range (Yahoo Finance restrictions).
|
| 180 |
+
|
| 181 |
+
### Backtesting Parameters
|
| 182 |
+
|
| 183 |
+
- **Initial Capital**: Starting portfolio value (default: $10,000)
|
| 184 |
+
- **Commission**: Per-share trading commission (default: $0.005)
|
| 185 |
+
- **Slippage**: Percentage of price applied as slippage (default: 0.01%)
|
| 186 |
+
- **Adjusted Prices**: Use dividend/split-adjusted prices (recommended: enabled)
|
| 187 |
+
|
| 188 |
+
## Supported Financial Indicators
|
| 189 |
+
|
| 190 |
+
The generated strategies can use any Backtrader indicator, including:
|
| 191 |
+
- **Moving Averages**: SMA, EMA, WMA
|
| 192 |
+
- **Momentum**: RSI, Stochastic, MACD
|
| 193 |
+
- **Volatility**: ATR, Bollinger Bands
|
| 194 |
+
- **Volume**: Volume indicators
|
| 195 |
+
- **Custom Indicators**: Any Backtrader-compatible indicator
|
| 196 |
+
|
| 197 |
+
Example strategy descriptions:
|
| 198 |
+
- "Go long when RSI crosses above 30 and 10-day SMA is above 50-day SMA"
|
| 199 |
+
- "Buy when MACD line crosses above signal line, exit when it crosses below"
|
| 200 |
+
- "Use ATR for position sizing, enter on golden cross with RSI confirmation"
|
| 201 |
+
|
| 202 |
+
## Data Caching
|
| 203 |
+
|
| 204 |
+
The application automatically caches downloaded market data in the `data_cache/` directory. This:
|
| 205 |
+
- Speeds up subsequent backtests
|
| 206 |
+
- Reduces API calls to Yahoo Finance
|
| 207 |
+
- Caches are keyed by ticker, interval, date range, and adjustment settings
|
| 208 |
+
|
| 209 |
+
Cache files are automatically managed and will be re-downloaded if parameters change.
|
| 210 |
+
|
| 211 |
+
## Troubleshooting
|
| 212 |
+
|
| 213 |
+
### Common Issues
|
| 214 |
+
|
| 215 |
+
1. **API Key Errors**: Ensure all required API keys are set in your `.env` file
|
| 216 |
+
2. **No Data Available**: Check that your ticker symbol is valid and date range is appropriate
|
| 217 |
+
3. **Code Generation Fails**: Try a different LLM model or refine your strategy description
|
| 218 |
+
4. **Chart Not Displaying**: Ensure matplotlib backend is set correctly (handled automatically)
|
| 219 |
+
|
| 220 |
+
### Browser Compatibility
|
| 221 |
+
|
| 222 |
+
The Gradio interface works best on modern browsers (Chrome, Firefox, Safari, Edge). If charts don't display, try:
|
| 223 |
+
- Clearing browser cache
|
| 224 |
+
- Using a different browser
|
| 225 |
+
- Checking browser console for errors
|
| 226 |
+
|
| 227 |
+
## Notes
|
| 228 |
+
|
| 229 |
+
- Generated code follows Backtrader's strategy pattern and best practices
|
| 230 |
+
- Strategies must inherit from `bt.Strategy` and implement `__init__` and `next()` methods
|
| 231 |
+
- The app automatically wraps generated code with backtesting execution logic
|
| 232 |
+
- Data is fetched from Yahoo Finance with automatic handling of market hours and holidays
|
| 233 |
+
|
| 234 |
+
## Version
|
| 235 |
+
0.0.2: added FMP extraction, replay mode and database reading for intraday
|
| 236 |
+
|
| 237 |
+
0.0.1: First working version
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
## License
|
| 241 |
+
Needs to be done
|
| 242 |
+
|
| 243 |
+
## Contributing
|
| 244 |
+
Next Steps
|
| 245 |
|
|
|
StrategyGeneratorJupyter.ipynb
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"id": "fa14ed7f",
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"source": [
|
| 8 |
+
"# Strategy Generator"
|
| 9 |
+
]
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"cell_type": "markdown",
|
| 13 |
+
"id": "88b42b38",
|
| 14 |
+
"metadata": {},
|
| 15 |
+
"source": [
|
| 16 |
+
"## Imports"
|
| 17 |
+
]
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"cell_type": "markdown",
|
| 21 |
+
"id": "80806d52",
|
| 22 |
+
"metadata": {},
|
| 23 |
+
"source": [
|
| 24 |
+
" Using as API `backtrader`"
|
| 25 |
+
]
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"cell_type": "code",
|
| 29 |
+
"execution_count": 1,
|
| 30 |
+
"id": "02a959b3",
|
| 31 |
+
"metadata": {},
|
| 32 |
+
"outputs": [],
|
| 33 |
+
"source": [
|
| 34 |
+
"%matplotlib inline\n",
|
| 35 |
+
"import os\n",
|
| 36 |
+
"import io\n",
|
| 37 |
+
"import sys\n",
|
| 38 |
+
"import matplotlib.pyplot as plt\n",
|
| 39 |
+
"import matplotlib.dates as mdates\n",
|
| 40 |
+
"from datetime import datetime\n",
|
| 41 |
+
"import backtrader as bt\n",
|
| 42 |
+
"import pandas as pd\n",
|
| 43 |
+
"import yfinance as yf\n",
|
| 44 |
+
"import matplotlib.pyplot as plt\n",
|
| 45 |
+
"from matplotlib.ticker import FuncFormatter\n",
|
| 46 |
+
"from dotenv import load_dotenv\n",
|
| 47 |
+
"from openai import OpenAI\n",
|
| 48 |
+
"import anthropic\n",
|
| 49 |
+
"import huggingface_hub \n",
|
| 50 |
+
"from huggingface_hub import InferenceClient\n",
|
| 51 |
+
"from IPython.display import Markdown, display, update_display\n",
|
| 52 |
+
"import gradio as gr\n",
|
| 53 |
+
"import subprocess\n",
|
| 54 |
+
"\n",
|
| 55 |
+
"# Environment\n",
|
| 56 |
+
"load_dotenv(override=True)\n",
|
| 57 |
+
"os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY')\n",
|
| 58 |
+
"os.environ['ANTHROPIC_API_KEY'] = os.getenv('ANTHROPIC_API_KEY')\n",
|
| 59 |
+
"google_api_key = os.getenv('GOOGLE_API_KEY')\n",
|
| 60 |
+
"deepseek_api_key = os.getenv('DEEPSEEK_API_KEY')\n",
|
| 61 |
+
"grok_api_key = os.getenv(\"XAI_API_KEY\")\n",
|
| 62 |
+
"\n",
|
| 63 |
+
"\n"
|
| 64 |
+
]
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"cell_type": "code",
|
| 68 |
+
"execution_count": 2,
|
| 69 |
+
"id": "810ba72f",
|
| 70 |
+
"metadata": {},
|
| 71 |
+
"outputs": [],
|
| 72 |
+
"source": [
|
| 73 |
+
"# Init variables\n",
|
| 74 |
+
"# Market name mapping\n",
|
| 75 |
+
"TICKER_TO_MARKET = {\n",
|
| 76 |
+
" 'AAPL': 'Apple Inc.',\n",
|
| 77 |
+
" 'GOOGL': 'Alphabet/Google',\n",
|
| 78 |
+
" 'AMZN': 'Amazon.com',\n",
|
| 79 |
+
" 'TSLA': 'Tesla Inc.',\n",
|
| 80 |
+
" 'JPM': 'JPMorgan Chase & Co.',\n",
|
| 81 |
+
" 'V': 'Visa Inc.',\n",
|
| 82 |
+
" 'SPY': 'S&P 500 ETF',\n",
|
| 83 |
+
" 'QQQ': 'Nasdaq 100 ETF',\n",
|
| 84 |
+
" 'MSFT': 'Microsoft Corp.',\n",
|
| 85 |
+
" 'NVDA': 'NVIDIA Corp.',\n",
|
| 86 |
+
" 'META': 'Meta Platforms',\n",
|
| 87 |
+
" 'BRK-B': 'Berkshire Hathaway',\n",
|
| 88 |
+
" 'UNH': 'UnitedHealth Group',\n",
|
| 89 |
+
" 'XOM': 'Exxon Mobil Corp.'\n",
|
| 90 |
+
"}\n",
|
| 91 |
+
"MARKET_TO_TICKER = {\n",
|
| 92 |
+
" 'Apple Inc.': 'AAPL',\n",
|
| 93 |
+
" 'Alphabet/Google': 'GOOGL',\n",
|
| 94 |
+
" 'Amazon.com': 'AMZN',\n",
|
| 95 |
+
" 'Tesla Inc.': 'TSLA',\n",
|
| 96 |
+
" 'JPMorgan Chase & Co.': 'JPM',\n",
|
| 97 |
+
" 'Visa Inc.': 'V',\n",
|
| 98 |
+
" 'S&P 500 ETF': 'SPY',\n",
|
| 99 |
+
" 'Nasdaq 100 ETF': 'QQQ',\n",
|
| 100 |
+
" 'Microsoft Corp.': 'MSFT',\n",
|
| 101 |
+
" 'NVIDIA Corp.': 'NVDA',\n",
|
| 102 |
+
" 'Meta Platforms': 'META',\n",
|
| 103 |
+
" 'Berkshire Hathaway': 'BRK-B',\n",
|
| 104 |
+
" 'UnitedHealth Group': 'UNH',\n",
|
| 105 |
+
" 'Exxon Mobil Corp.': 'XOM'\n",
|
| 106 |
+
"}\n",
|
| 107 |
+
"# Variable that will get later with Gradio\n",
|
| 108 |
+
"current_date = datetime.now().strftime('%Y-%m-%d')\n",
|
| 109 |
+
"MARKET = 'AAPL'\n",
|
| 110 |
+
"API_FIN = 'backtrader'\n",
|
| 111 |
+
"DATE={'start':'1990-01-01', 'end':current_date}\n",
|
| 112 |
+
"INTERVAL='2m'\n",
|
| 113 |
+
"AUTO_PERIOD= True\n",
|
| 114 |
+
"PERIOD='60d'\n",
|
| 115 |
+
"# LLMsP\n",
|
| 116 |
+
"OPENAI_MODEL = \"gpt-5-nano\"\n",
|
| 117 |
+
"CLAUDE_MODEL = \"claude-sonnet-4-20250514\"\n",
|
| 118 |
+
"GIMINI_MODEL = \"gemini-2.5-flash\"\n",
|
| 119 |
+
"DEEPSEEK_MODEL = \"deepseek-reasoner\"\n",
|
| 120 |
+
"QWEN3_MODEL = \"Qwen/Qwen3-Coder-480B-A35B-Instruct\"\n",
|
| 121 |
+
"QWEN2_MODEL = \"Qwen/Qwen2.5-Coder-32B-Instruct\"\n",
|
| 122 |
+
"SAVE_PLT= True\n",
|
| 123 |
+
"GROK4_MODEL = \"grok-4-fast-reasoning\"\n",
|
| 124 |
+
"LOCAL=True\n",
|
| 125 |
+
"\n"
|
| 126 |
+
]
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"cell_type": "code",
|
| 130 |
+
"execution_count": null,
|
| 131 |
+
"id": "4a0713ed",
|
| 132 |
+
"metadata": {},
|
| 133 |
+
"outputs": [],
|
| 134 |
+
"source": [
|
| 135 |
+
"\n",
|
| 136 |
+
"# Initialize clients\n",
|
| 137 |
+
"openai = OpenAI()\n",
|
| 138 |
+
"deepseek_api= OpenAI(\n",
|
| 139 |
+
" api_key=deepseek_api_key, \n",
|
| 140 |
+
" base_url=\"https://api.deepseek.com\"\n",
|
| 141 |
+
" )\n",
|
| 142 |
+
"gemini_api = OpenAI(\n",
|
| 143 |
+
" api_key=google_api_key, \n",
|
| 144 |
+
" base_url=\"https://generativelanguage.googleapis.com/v1beta/openai/\"\n",
|
| 145 |
+
" )\n",
|
| 146 |
+
"grok_api = OpenAI(api_key=grok_api_key, base_url=\"https://api.x.ai/v1\")\n",
|
| 147 |
+
"claude = anthropic.Anthropic()\n",
|
| 148 |
+
"client = InferenceClient() # For HuggingFace Inference API"
|
| 149 |
+
]
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"cell_type": "markdown",
|
| 153 |
+
"id": "3bbe4f90",
|
| 154 |
+
"metadata": {},
|
| 155 |
+
"source": [
|
| 156 |
+
"## Prompt "
|
| 157 |
+
]
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"cell_type": "code",
|
| 161 |
+
"execution_count": null,
|
| 162 |
+
"id": "ea6649c5",
|
| 163 |
+
"metadata": {},
|
| 164 |
+
"outputs": [],
|
| 165 |
+
"source": [
|
| 166 |
+
"import inspect\n",
|
| 167 |
+
"from bt_strategies import SmaCross\n",
|
| 168 |
+
"import re\n",
|
| 169 |
+
"# VERSION not working\n",
|
| 170 |
+
"def format_class(strategy):\n",
|
| 171 |
+
" def replace_docstring(match):\n",
|
| 172 |
+
" # Replace with cleaned docstring\n",
|
| 173 |
+
" if remaining_doc:\n",
|
| 174 |
+
" return f'\"\"\"\\n{remaining_doc}\\n \"\"\"'\n",
|
| 175 |
+
" else:\n",
|
| 176 |
+
" return '\"\"\"\"\"\"' # Empty docstring\n",
|
| 177 |
+
" source_code = inspect.getsource(strategy)\n",
|
| 178 |
+
" #print(source_code)\n",
|
| 179 |
+
" docstring = SmaCross.__doc__\n",
|
| 180 |
+
" user_prompt_match = re.search(r'User prompt:\\s*\"([^\"]+)\"', docstring)\n",
|
| 181 |
+
" user_prompt = user_prompt_match.group(1) if user_prompt_match else \"No user prompt found.\"\n",
|
| 182 |
+
"\n",
|
| 183 |
+
" call_match = re.search(r'Call:\\s*(.+)', docstring)\n",
|
| 184 |
+
" call_line = call_match.group(1) if call_match else \"cerebro.addstrategy(NotFoundStrategy)\"\n",
|
| 185 |
+
" \n",
|
| 186 |
+
" # Clean docstring\n",
|
| 187 |
+
" remaining_doc = re.sub(r'User prompt:\\s*\"[^\"]+\"\\s*\\n?', '', docstring)\n",
|
| 188 |
+
" remaining_doc = re.sub(r'Call:\\s*.+\\s*\\n?', '', remaining_doc)\n",
|
| 189 |
+
" remaining_doc = remaining_doc.strip()\n",
|
| 190 |
+
" \n",
|
| 191 |
+
" pattern = r'(\"\"\".*?\"\"\"|\\'\\'\\'.*?\\'\\'\\')'\n",
|
| 192 |
+
" clean_source = re.sub(pattern, replace_docstring, source_code, count=1, flags=re.DOTALL)\n",
|
| 193 |
+
"\n",
|
| 194 |
+
"\n",
|
| 195 |
+
" # ✅ Final formatted output\n",
|
| 196 |
+
" final_output = f'''\n",
|
| 197 |
+
"# User prompt:\n",
|
| 198 |
+
"# \"{user_prompt}\"\n",
|
| 199 |
+
"\n",
|
| 200 |
+
"# Generated Python code:\n",
|
| 201 |
+
"import backtrader as bt\n",
|
| 202 |
+
"{clean_source}\n",
|
| 203 |
+
"# Initialize Cerebro\n",
|
| 204 |
+
"cerebro = bt.Cerebro()\n",
|
| 205 |
+
"{call_line}\n",
|
| 206 |
+
"'''\n",
|
| 207 |
+
" return final_output\n",
|
| 208 |
+
"\n",
|
| 209 |
+
"print (format_class(SmaCross))"
|
| 210 |
+
]
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"cell_type": "code",
|
| 214 |
+
"execution_count": 18,
|
| 215 |
+
"id": "104360db",
|
| 216 |
+
"metadata": {},
|
| 217 |
+
"outputs": [],
|
| 218 |
+
"source": [
|
| 219 |
+
"example_1= ''' \n",
|
| 220 |
+
"# User prompt:\n",
|
| 221 |
+
"# \"Go long when the 10-period SMA crosses above the 100-period SMA,\n",
|
| 222 |
+
"# and exit when the 10-period SMA crosses below the 100-period SMA.\"\n",
|
| 223 |
+
"\n",
|
| 224 |
+
"# Generated Python code:\n",
|
| 225 |
+
"import backtrader as bt\n",
|
| 226 |
+
"class SmaCross(bt.Strategy):\n",
|
| 227 |
+
" \"\"\"\n",
|
| 228 |
+
" Simple moving average crossover strategy.\n",
|
| 229 |
+
" Buy when fast SMA crosses above slow SMA.\n",
|
| 230 |
+
" Sell when fast SMA crosses below slow SMA.\n",
|
| 231 |
+
" \"\"\"\n",
|
| 232 |
+
" params = dict(pfast=10, pslow=100)\n",
|
| 233 |
+
"\n",
|
| 234 |
+
" def __init__(self):\n",
|
| 235 |
+
" self.sma_fast = bt.ind.SMA(period=self.p.pfast)\n",
|
| 236 |
+
" self.sma_slow = bt.ind.SMA(period=self.p.pslow)\n",
|
| 237 |
+
" self.crossover = bt.ind.CrossOver(self.sma_fast, self.sma_slow)\n",
|
| 238 |
+
"\n",
|
| 239 |
+
" def next(self):\n",
|
| 240 |
+
" if not self.position:\n",
|
| 241 |
+
" if self.crossover > 0: # Golden cross\n",
|
| 242 |
+
" self.buy()\n",
|
| 243 |
+
" elif self.crossover < 0: # Death cross\n",
|
| 244 |
+
" self.close()\n",
|
| 245 |
+
"\n",
|
| 246 |
+
"# Initialize Cerebro\n",
|
| 247 |
+
"cerebro = bt.Cerebro()\n",
|
| 248 |
+
"cerebro.addstrategy(SmaCross, pfast=10, pslow=100)\n",
|
| 249 |
+
"'''\n",
|
| 250 |
+
"list_of_pyclasses = [example_1]\n",
|
| 251 |
+
"system_message = f'''\n",
|
| 252 |
+
"You are a financial assistant specialized in transforming natural language descriptions of trading strategies into clean, production-ready Python code.\n",
|
| 253 |
+
"\n",
|
| 254 |
+
"Guidelines:\n",
|
| 255 |
+
"- Use only the library {API_FIN}.\n",
|
| 256 |
+
"- Always create a class with the abreviation of the strategy with the form `NameOfStrategy(bt.Strategy)`.\n",
|
| 257 |
+
"- Implement strategy logic in `__init__` (indicators/signals) and `next()` (trade execution).\n",
|
| 258 |
+
"- Implement the strategy for this intervall of time {INTERVAL} \n",
|
| 259 |
+
"- Finish with initializing the strategy in Cerebro:\n",
|
| 260 |
+
" cerebro = bt.Cerebro()\n",
|
| 261 |
+
" cerebro.addstrategy(MyStrategy, param1=value, param2=value)\n",
|
| 262 |
+
"- Keep code minimal, clear, and follow Python best practices (PEP8, clear naming, modularity).\n",
|
| 263 |
+
"- If a strategy cannot be implemented with {API_FIN}, respond with: \"Unable to implement with {API_FIN}.\"\n",
|
| 264 |
+
"- If used any addional libraries, add it in the code: import MyUsedLibrary\n",
|
| 265 |
+
"- If you don't know the answer, just say that you don't know, don't try to make up an answer.\n",
|
| 266 |
+
"\n",
|
| 267 |
+
"Example(s) of transformation from user prompt to Python code: \\n\n",
|
| 268 |
+
"'''\n",
|
| 269 |
+
"for pyclass in list_of_pyclasses:\n",
|
| 270 |
+
" system_message += pyclass"
|
| 271 |
+
]
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"cell_type": "code",
|
| 275 |
+
"execution_count": 19,
|
| 276 |
+
"id": "f868548e",
|
| 277 |
+
"metadata": {},
|
| 278 |
+
"outputs": [],
|
| 279 |
+
"source": [
|
| 280 |
+
"\n",
|
| 281 |
+
"def user_prompt_for(user_msg):\n",
|
| 282 |
+
" return f\"\"\"\n",
|
| 283 |
+
"Trading strategy description:\n",
|
| 284 |
+
"\\\"\\\"\\\"{user_msg}\\\"\\\"\\\"\n",
|
| 285 |
+
"\n",
|
| 286 |
+
"Task:\n",
|
| 287 |
+
"- Convert the description into executable Python code.\n",
|
| 288 |
+
"- Use only the library {API_FIN}.\n",
|
| 289 |
+
"- Respond only with valid Python code, following Python best practices\n",
|
| 290 |
+
"\"\"\""
|
| 291 |
+
]
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
"cell_type": "code",
|
| 295 |
+
"execution_count": 20,
|
| 296 |
+
"id": "1589df2f",
|
| 297 |
+
"metadata": {},
|
| 298 |
+
"outputs": [],
|
| 299 |
+
"source": [
|
| 300 |
+
"# Messages in Openai format \n",
|
| 301 |
+
"def messages_for(user_msg):\n",
|
| 302 |
+
" return [\n",
|
| 303 |
+
" {\"role\": \"system\", \"content\": system_message},\n",
|
| 304 |
+
" {\"role\": \"user\", \"content\": user_prompt_for(user_msg)}\n",
|
| 305 |
+
" ]"
|
| 306 |
+
]
|
| 307 |
+
},
|
| 308 |
+
{
|
| 309 |
+
"cell_type": "markdown",
|
| 310 |
+
"id": "2ac6d71d",
|
| 311 |
+
"metadata": {},
|
| 312 |
+
"source": [
|
| 313 |
+
"## LLMS executors"
|
| 314 |
+
]
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
"cell_type": "code",
|
| 318 |
+
"execution_count": 21,
|
| 319 |
+
"id": "f522a0de",
|
| 320 |
+
"metadata": {},
|
| 321 |
+
"outputs": [],
|
| 322 |
+
"source": [
|
| 323 |
+
"def stream_llms(user_msg, typ_llm=\"gpt\"): \n",
|
| 324 |
+
" messages = messages_for(user_msg)\n",
|
| 325 |
+
" if typ_llm.lower() == \"deepseek\": \n",
|
| 326 |
+
" stream = deepseek_api.chat.completions.create(\n",
|
| 327 |
+
" model=\"deepseek-chat\",\n",
|
| 328 |
+
" messages=messages,\n",
|
| 329 |
+
" stream=True\n",
|
| 330 |
+
" )\n",
|
| 331 |
+
" elif typ_llm.lower() == \"gimini\":\n",
|
| 332 |
+
" stream = gemini_api.chat.completions.create(\n",
|
| 333 |
+
" model=\"gemini-2.5-flash\",\n",
|
| 334 |
+
" messages=messages,\n",
|
| 335 |
+
" stream=True\n",
|
| 336 |
+
" )\n",
|
| 337 |
+
" elif typ_llm.lower() == \"qween2\":\n",
|
| 338 |
+
" stream = client.chat.completions.create(\n",
|
| 339 |
+
" model=QWEN2_MODEL,\n",
|
| 340 |
+
" messages=messages,\n",
|
| 341 |
+
" stream= True\n",
|
| 342 |
+
" )\n",
|
| 343 |
+
" elif typ_llm.lower() == \"qween3\":\n",
|
| 344 |
+
" stream = client.chat.completions.create(\n",
|
| 345 |
+
" model=QWEN3_MODEL,\n",
|
| 346 |
+
" messages=messages,\n",
|
| 347 |
+
" stream= True\n",
|
| 348 |
+
" )\n",
|
| 349 |
+
" elif typ_llm.lower() == \"grok4\":\n",
|
| 350 |
+
" stream = grok_api.chat.completions.create(\n",
|
| 351 |
+
" model=GROK4_MODEL,\n",
|
| 352 |
+
" messages=messages,\n",
|
| 353 |
+
" stream= True\n",
|
| 354 |
+
" )\n",
|
| 355 |
+
" elif typ_llm.lower() == \"claude\":\n",
|
| 356 |
+
" stream = claude.messages.stream(\n",
|
| 357 |
+
" model=CLAUDE_MODEL,\n",
|
| 358 |
+
" max_tokens=2000,\n",
|
| 359 |
+
" system=messages[0]['content'],\n",
|
| 360 |
+
" messages=[messages[1]],\n",
|
| 361 |
+
" )\n",
|
| 362 |
+
" elif typ_llm.lower() == \"gpt\": \n",
|
| 363 |
+
" stream = openai.chat.completions.create(model=OPENAI_MODEL, messages=messages, stream=True)\n",
|
| 364 |
+
" else:\n",
|
| 365 |
+
" raise ValueError(\"Unknown model\")\n",
|
| 366 |
+
" \n",
|
| 367 |
+
" reply = \"\"\n",
|
| 368 |
+
" \n",
|
| 369 |
+
" if typ_llm.lower() == \"claude\":\n",
|
| 370 |
+
" with stream as stream_clde:\n",
|
| 371 |
+
" for fragment in stream_clde.text_stream:\n",
|
| 372 |
+
" reply += fragment\n",
|
| 373 |
+
" #print(fragment, end='', flush=True)\n",
|
| 374 |
+
" yield reply.replace(\"```python\\n\",\"\").replace(\"```\",\"\") \n",
|
| 375 |
+
" else:\n",
|
| 376 |
+
" for chunk in stream:\n",
|
| 377 |
+
" if chunk and chunk.choices:\n",
|
| 378 |
+
" fragment = chunk.choices[0].delta.content or \"\"\n",
|
| 379 |
+
" reply += fragment\n",
|
| 380 |
+
" #print(fragment, end='', flush=True)\n",
|
| 381 |
+
" yield reply.replace(\"```python\\n\",\"\").replace(\"```\",\"\") \n",
|
| 382 |
+
" #return reply"
|
| 383 |
+
]
|
| 384 |
+
},
|
| 385 |
+
{
|
| 386 |
+
"cell_type": "code",
|
| 387 |
+
"execution_count": 22,
|
| 388 |
+
"id": "c0089d2b",
|
| 389 |
+
"metadata": {},
|
| 390 |
+
"outputs": [],
|
| 391 |
+
"source": [
|
| 392 |
+
"def stream_manager(user_msg, model):\n",
|
| 393 |
+
" result = stream_llms(user_msg, model)\n",
|
| 394 |
+
" for stream_so_far in result:\n",
|
| 395 |
+
" yield stream_so_far"
|
| 396 |
+
]
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"cell_type": "code",
|
| 400 |
+
"execution_count": 23,
|
| 401 |
+
"id": "7792fa04",
|
| 402 |
+
"metadata": {},
|
| 403 |
+
"outputs": [],
|
| 404 |
+
"source": [
|
| 405 |
+
"# Write to python file\n",
|
| 406 |
+
"def write_output(code):\n",
|
| 407 |
+
" with open(\"code.py\", \"w\") as f:\n",
|
| 408 |
+
" f.write(code)\n",
|
| 409 |
+
" \n",
|
| 410 |
+
"def execute_python(code, market_name, interval):\n",
|
| 411 |
+
" tckr_symbl = MARKET_TO_TICKER[market_name] \n",
|
| 412 |
+
" # For the moment executing this values here in order to put less complexity to users\n",
|
| 413 |
+
" interval = interval\n",
|
| 414 |
+
" period=PERIOD\n",
|
| 415 |
+
" \n",
|
| 416 |
+
" code = code.replace(\"```python\",\"\").replace(\"```\",\"\")\n",
|
| 417 |
+
" output_code = f'''\n",
|
| 418 |
+
"from utils import run_bt\n",
|
| 419 |
+
"import backtrader as bt\n",
|
| 420 |
+
"{code}\n",
|
| 421 |
+
"\n",
|
| 422 |
+
"\n",
|
| 423 |
+
"_, _, tmp_img =run_bt(cerebro=cerebro, date={DATE}, market_name='{market_name}', save_img={SAVE_PLT}, tckr_symbl='{tckr_symbl}', interval='{interval}', auto_period='{AUTO_PERIOD}', period='{period}')\n",
|
| 424 |
+
"''' \n",
|
| 425 |
+
" tmp_img = \"\"\n",
|
| 426 |
+
" write_output(code)\n",
|
| 427 |
+
" output = io.StringIO()\n",
|
| 428 |
+
" sys_stdout = sys.stdout\n",
|
| 429 |
+
" sys.stdout = output\n",
|
| 430 |
+
" try:\n",
|
| 431 |
+
" # Execute the code into its own namespace\n",
|
| 432 |
+
" namespace = {}\n",
|
| 433 |
+
" exec(output_code, namespace)\n",
|
| 434 |
+
" tmp_img = namespace.get(\"tmp_img\", None) # ✅ retrieve from namespace\n",
|
| 435 |
+
" finally:\n",
|
| 436 |
+
" sys.stdout = sys_stdout\n",
|
| 437 |
+
"\n",
|
| 438 |
+
" return output.getvalue(), tmp_img"
|
| 439 |
+
]
|
| 440 |
+
},
|
| 441 |
+
{
|
| 442 |
+
"cell_type": "markdown",
|
| 443 |
+
"id": "e94c2b6b",
|
| 444 |
+
"metadata": {},
|
| 445 |
+
"source": [
|
| 446 |
+
"## Gradio interface"
|
| 447 |
+
]
|
| 448 |
+
},
|
| 449 |
+
{
|
| 450 |
+
"cell_type": "code",
|
| 451 |
+
"execution_count": null,
|
| 452 |
+
"id": "94dde64e",
|
| 453 |
+
"metadata": {},
|
| 454 |
+
"outputs": [],
|
| 455 |
+
"source": [
|
| 456 |
+
"%matplotlib inline\n",
|
| 457 |
+
"market_list = list(MARKET_TO_TICKER.keys())\n",
|
| 458 |
+
"with gr.Blocks() as ui:\n",
|
| 459 |
+
" gr.Markdown(\"## Convert Written Strategies into Python Code\")\n",
|
| 460 |
+
" with gr.Row():\n",
|
| 461 |
+
" strategy_msg = gr.Textbox( value=\"\", label=\"Enter the description of your strategy. \", lines=10)\n",
|
| 462 |
+
" code = gr.Textbox(label=\"Python code:\", lines=10)\n",
|
| 463 |
+
" with gr.Row():\n",
|
| 464 |
+
" gen_strategy = gr.Button(\"Generate Strategy\") \n",
|
| 465 |
+
" run_py = gr.Button(\"Run Python Code \", visible=True)\n",
|
| 466 |
+
" with gr.Row():\n",
|
| 467 |
+
" with gr.Column():\n",
|
| 468 |
+
" model = gr.Dropdown([\"GPT\", \"Claude\", \"Deepseek\", \"Gimini\",\"Qween2\", \"Qween3\", \"Grok4\"], label=\"Select model\", value=\"Deepseek\")\n",
|
| 469 |
+
" market = gr.Dropdown(market_list, label=\"Stock Name\", value=\"S&P 500 ETF\")\n",
|
| 470 |
+
" #date = gr.Textbox( value=\"2025-10-10\", label=\"End Date (yyyy-mm-dd)\", placeholder=\"yyyy-mm-dd\")\n",
|
| 471 |
+
" interval = gr.Dropdown([\"1m\",\"2m\", \"5m\", \"15m\", \"30m\", \"1h\",\"1d\"], value=\"1d\", label=\"Interval\")\n",
|
| 472 |
+
" #period = gr.Dropdown([\"30d\", \"10d\", \"60d\"], value=\"60d\", label=\"Period\")\n",
|
| 473 |
+
" with gr.Row():\n",
|
| 474 |
+
" py_out = gr.TextArea(label=\"Python result:\", elem_classes=[\"python\"])\n",
|
| 475 |
+
" with gr.Column(): \n",
|
| 476 |
+
" #image_output = gr.Image(type=\"numpy\", label=\"Chart\")\n",
|
| 477 |
+
" image_output = gr.Gallery(\n",
|
| 478 |
+
" label=\"Charts\", \n",
|
| 479 |
+
" show_label=True, \n",
|
| 480 |
+
" elem_id=\"gallery\",\n",
|
| 481 |
+
" columns=2, \n",
|
| 482 |
+
" height=\"auto\" # Height parameter\n",
|
| 483 |
+
" )\n",
|
| 484 |
+
"\n",
|
| 485 |
+
" # Select and send msg to create python outpu\n",
|
| 486 |
+
" gen_strategy.click(stream_manager, inputs=[strategy_msg, model], outputs=[code])\n",
|
| 487 |
+
" run_py.click(execute_python, inputs=[code, market, interval] , outputs=[py_out, image_output])\n",
|
| 488 |
+
" \n",
|
| 489 |
+
"ui.launch(inbrowser=True, share=True, debug=True)"
|
| 490 |
+
]
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
"cell_type": "markdown",
|
| 494 |
+
"id": "c3951dbd",
|
| 495 |
+
"metadata": {},
|
| 496 |
+
"source": [
|
| 497 |
+
"## On work\n",
|
| 498 |
+
"\n",
|
| 499 |
+
"Format backtrading strategies for doing multi prompt learning."
|
| 500 |
+
]
|
| 501 |
+
},
|
| 502 |
+
{
|
| 503 |
+
"cell_type": "code",
|
| 504 |
+
"execution_count": null,
|
| 505 |
+
"id": "e82f32e8",
|
| 506 |
+
"metadata": {},
|
| 507 |
+
"outputs": [],
|
| 508 |
+
"source": [
|
| 509 |
+
"import inspect\n",
|
| 510 |
+
"from bt_strategies import SmaCross\n",
|
| 511 |
+
"import re\n",
|
| 512 |
+
"# VERSION not working\n",
|
| 513 |
+
"def format_class(strategy):\n",
|
| 514 |
+
" def replace_docstring(match):\n",
|
| 515 |
+
" # Replace with cleaned docstring\n",
|
| 516 |
+
" if remaining_doc:\n",
|
| 517 |
+
" return f'\"\"\"\\n{remaining_doc}\\n \"\"\"'\n",
|
| 518 |
+
" else:\n",
|
| 519 |
+
" return '\"\"\"\"\"\"' # Empty docstring\n",
|
| 520 |
+
" source_code = inspect.getsource(strategy)\n",
|
| 521 |
+
" #print(source_code)\n",
|
| 522 |
+
" docstring = SmaCross.__doc__\n",
|
| 523 |
+
" user_prompt_match = re.search(r'User prompt:\\s*\"([^\"]+)\"', docstring)\n",
|
| 524 |
+
" user_prompt = user_prompt_match.group(1) if user_prompt_match else \"No user prompt found.\"\n",
|
| 525 |
+
"\n",
|
| 526 |
+
" call_match = re.search(r'Call:\\s*(.+)', docstring)\n",
|
| 527 |
+
" call_line = call_match.group(1) if call_match else \"cerebro.addstrategy(NotFoundStrategy)\"\n",
|
| 528 |
+
" \n",
|
| 529 |
+
" # Clean docstring\n",
|
| 530 |
+
" remaining_doc = re.sub(r'User prompt:\\s*\"[^\"]+\"\\s*\\n?', '', docstring)\n",
|
| 531 |
+
" remaining_doc = re.sub(r'Call:\\s*.+\\s*\\n?', '', remaining_doc)\n",
|
| 532 |
+
" remaining_doc = remaining_doc.strip()\n",
|
| 533 |
+
" \n",
|
| 534 |
+
" pattern = r'(\"\"\".*?\"\"\"|\\'\\'\\'.*?\\'\\'\\')'\n",
|
| 535 |
+
" clean_source = re.sub(pattern, replace_docstring, source_code, count=1, flags=re.DOTALL)\n",
|
| 536 |
+
"\n",
|
| 537 |
+
"\n",
|
| 538 |
+
" # ✅ Final formatted output\n",
|
| 539 |
+
" final_output = f'''\n",
|
| 540 |
+
"# User prompt:\n",
|
| 541 |
+
"# \"{user_prompt}\"\n",
|
| 542 |
+
"\n",
|
| 543 |
+
"# Generated Python code:\n",
|
| 544 |
+
"import backtrader as bt\n",
|
| 545 |
+
"{clean_source}\n",
|
| 546 |
+
"# Initialize Cerebro\n",
|
| 547 |
+
"cerebro = bt.Cerebro()\n",
|
| 548 |
+
"{call_line}\n",
|
| 549 |
+
"'''\n",
|
| 550 |
+
" return final_output\n",
|
| 551 |
+
"\n",
|
| 552 |
+
"print (format_class(SmaCross))"
|
| 553 |
+
]
|
| 554 |
+
},
|
| 555 |
+
{
|
| 556 |
+
"cell_type": "code",
|
| 557 |
+
"execution_count": null,
|
| 558 |
+
"id": "f2b9ef6e",
|
| 559 |
+
"metadata": {},
|
| 560 |
+
"outputs": [],
|
| 561 |
+
"source": [
|
| 562 |
+
"import inspect\n",
|
| 563 |
+
"from bt_strategies import SmaCross\n",
|
| 564 |
+
"import re\n",
|
| 565 |
+
"\n",
|
| 566 |
+
"def format_class(strategy):\n",
|
| 567 |
+
" source_code = inspect.getsource(strategy)\n",
|
| 568 |
+
" #print(source_code)\n",
|
| 569 |
+
" docstring = SmaCross.__doc__\n",
|
| 570 |
+
" user_prompt_match = re.search(r'User prompt:\\s*\"([^\"]+)\"', docstring)\n",
|
| 571 |
+
" user_prompt = user_prompt_match.group(1) if user_prompt_match else \"No user prompt found.\"\n",
|
| 572 |
+
"\n",
|
| 573 |
+
" call_match = re.search(r'Call:\\s*(.+)', docstring)\n",
|
| 574 |
+
" call_line = call_match.group(1) if call_match else \"cerebro.addstrategy(NotFoundStrategy)\"\n",
|
| 575 |
+
"\n",
|
| 576 |
+
"\n",
|
| 577 |
+
" # ✅ Final formatted output\n",
|
| 578 |
+
" final_output = f'''\n",
|
| 579 |
+
" # User prompt:\n",
|
| 580 |
+
" # \"{user_prompt}\"\n",
|
| 581 |
+
"\n",
|
| 582 |
+
" # Generated Python code:\n",
|
| 583 |
+
" import backtrader as bt\n",
|
| 584 |
+
" {source_code}\n",
|
| 585 |
+
" # Initialize Cerebro\n",
|
| 586 |
+
" cerebro = bt.Cerebro()\n",
|
| 587 |
+
" {call_line}\n",
|
| 588 |
+
" '''\n",
|
| 589 |
+
" return final_output\n",
|
| 590 |
+
"\n",
|
| 591 |
+
"print (format_class(SmaCross))"
|
| 592 |
+
]
|
| 593 |
+
}
|
| 594 |
+
],
|
| 595 |
+
"metadata": {
|
| 596 |
+
"kernelspec": {
|
| 597 |
+
"display_name": "llms",
|
| 598 |
+
"language": "python",
|
| 599 |
+
"name": "python3"
|
| 600 |
+
},
|
| 601 |
+
"language_info": {
|
| 602 |
+
"codemirror_mode": {
|
| 603 |
+
"name": "ipython",
|
| 604 |
+
"version": 3
|
| 605 |
+
},
|
| 606 |
+
"file_extension": ".py",
|
| 607 |
+
"mimetype": "text/x-python",
|
| 608 |
+
"name": "python",
|
| 609 |
+
"nbconvert_exporter": "python",
|
| 610 |
+
"pygments_lexer": "ipython3",
|
| 611 |
+
"version": "3.11.13"
|
| 612 |
+
}
|
| 613 |
+
},
|
| 614 |
+
"nbformat": 4,
|
| 615 |
+
"nbformat_minor": 5
|
| 616 |
+
}
|
app.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
#%matplotlib inline
|
| 3 |
+
import os
|
| 4 |
+
os.environ.setdefault("MPLBACKEND", "Agg")
|
| 5 |
+
import io
|
| 6 |
+
import sys
|
| 7 |
+
import tempfile
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import yfinance as yf
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
import gradio as gr
|
| 13 |
+
import yaml
|
| 14 |
+
import traceback
|
| 15 |
+
from strategy_generator import stream_manager
|
| 16 |
+
from utils import (
|
| 17 |
+
write_output,
|
| 18 |
+
_to_float_or_default,
|
| 19 |
+
save_df_to_csv,
|
| 20 |
+
resolve_market_to_ticker,
|
| 21 |
+
validate_date_range,
|
| 22 |
+
validate_ticker_symbol,
|
| 23 |
+
save_strategy_to_file,
|
| 24 |
+
yf_interval_info,
|
| 25 |
+
YF_INTERVALS,
|
| 26 |
+
YF_TO_FMP_MAP,
|
| 27 |
+
REPLAY_MAP,
|
| 28 |
+
update_replay_intervals,
|
| 29 |
+
)
|
| 30 |
+
from data_utils import get_data
|
| 31 |
+
|
| 32 |
+
# Environment
|
| 33 |
+
load_dotenv(override=True)
|
| 34 |
+
|
| 35 |
+
with open('config/var_dev.yaml', 'r') as f:
|
| 36 |
+
config = yaml.safe_load(f)
|
| 37 |
+
|
| 38 |
+
current_date = datetime.now().strftime('%Y-%m-%d')
|
| 39 |
+
DATE = {'start': '1990-01-01', 'end': current_date}
|
| 40 |
+
|
| 41 |
+
market_to_ticker = config['market_to_ticker']
|
| 42 |
+
|
| 43 |
+
default_data_source = (config.get("data_source") or "fmp").lower()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def execute_python(code, market_name, data_source, interval, start_date, end_date, initial_capital, commission, slippage_percent, adjust_prices, replay_enabled, progress=gr.Progress(track_tqdm=True)):
|
| 47 |
+
"""
|
| 48 |
+
Execute user-provided Python code for backtesting a trading strategy.
|
| 49 |
+
"""
|
| 50 |
+
# Determine if input is a market name or ticker symbol
|
| 51 |
+
tckr_symbl = resolve_market_to_ticker(market_name, market_to_ticker)
|
| 52 |
+
# For the moment executing this values here in order to put less complexity to users
|
| 53 |
+
interval = interval
|
| 54 |
+
period = config["period"]
|
| 55 |
+
selected_start = (start_date or "").strip() or DATE["start"]
|
| 56 |
+
selected_end = (end_date or "").strip() or current_date
|
| 57 |
+
date_range = {"start": selected_start, "end": selected_end}
|
| 58 |
+
# Validate dates
|
| 59 |
+
try:
|
| 60 |
+
validated_start, validated_end = validate_date_range(selected_start, selected_end, current_date)
|
| 61 |
+
date_range = {"start": validated_start, "end": validated_end}
|
| 62 |
+
except ValueError as e:
|
| 63 |
+
err = str(e)
|
| 64 |
+
return err, [], None, None, err
|
| 65 |
+
|
| 66 |
+
# Validate ticker symbol
|
| 67 |
+
try:
|
| 68 |
+
market_name = validate_ticker_symbol(tckr_symbl, data_source)
|
| 69 |
+
except ValueError as e:
|
| 70 |
+
err = str(e)
|
| 71 |
+
return err, [], None, None, err
|
| 72 |
+
|
| 73 |
+
capital_value = _to_float_or_default(initial_capital, config["initial_capital"])
|
| 74 |
+
commission_value = _to_float_or_default(commission, config["commission"])
|
| 75 |
+
slippage_percent_value = _to_float_or_default(slippage_percent, config["slippage_percent"])
|
| 76 |
+
adjust_prices_value = bool(adjust_prices) if adjust_prices is not None else bool(config.get("adjust_prices", True))
|
| 77 |
+
|
| 78 |
+
# Extract replay parameters and map intervals
|
| 79 |
+
replay_interval = interval
|
| 80 |
+
replay_compression = 1
|
| 81 |
+
source_lower = data_source.lower()
|
| 82 |
+
|
| 83 |
+
if replay_enabled and interval in REPLAY_MAP:
|
| 84 |
+
# Replay mode: use REPLAY_MAP
|
| 85 |
+
fmp_interval, yf_interval, compression = REPLAY_MAP[interval]
|
| 86 |
+
replay_interval = fmp_interval if source_lower == "fmp" else yf_interval
|
| 87 |
+
replay_compression = compression
|
| 88 |
+
elif source_lower == "fmp":
|
| 89 |
+
# FMP mode: map YF interval to FMP interval
|
| 90 |
+
fmp_interval = YF_TO_FMP_MAP.get(interval)
|
| 91 |
+
if fmp_interval is None:
|
| 92 |
+
err = f"❌ Interval '{interval}' not supported by FMP. Please switch to Yahoo data source."
|
| 93 |
+
return err, [], None, None
|
| 94 |
+
replay_interval = fmp_interval
|
| 95 |
+
|
| 96 |
+
# Fetch data once and pass to run_bt
|
| 97 |
+
status_msg = ""
|
| 98 |
+
progress(0, desc="Fetching data")
|
| 99 |
+
try:
|
| 100 |
+
print(f"Replay enabled: {replay_enabled}, interval: {interval}, mapped interval: {replay_interval}, compression: {replay_compression}")
|
| 101 |
+
df = get_data(
|
| 102 |
+
data_source=data_source,
|
| 103 |
+
tckr_symbl=tckr_symbl,
|
| 104 |
+
interval=replay_interval,
|
| 105 |
+
date=date_range,
|
| 106 |
+
adjust_prices=adjust_prices_value,
|
| 107 |
+
auto_period=config["auto_period"],
|
| 108 |
+
period=period,
|
| 109 |
+
upload_data=config.get("upload_data", False),
|
| 110 |
+
upload_data_path=config.get("upload_data_path"),
|
| 111 |
+
progress=progress
|
| 112 |
+
)
|
| 113 |
+
source_lower = data_source.lower()
|
| 114 |
+
show_range = (source_lower == "fmp") or (source_lower in ["yahoofinance", "yf", "yahoo"] and interval not in ["1m", "2m", "5m", "15m", "30m", "60m", "1h"])
|
| 115 |
+
status_msg = f"Data loaded: {len(df)} rows via {data_source} @ interval {interval}."
|
| 116 |
+
if show_range:
|
| 117 |
+
status_msg = f"{status_msg} Date range: {date_range['start']} → {date_range['end']}."
|
| 118 |
+
progress(0.6, desc="Data loaded")
|
| 119 |
+
if source_lower in ["yahoofinance", "yf", "yahoo"]:
|
| 120 |
+
extra = yf_interval_info(date_range, interval, config["auto_period"])
|
| 121 |
+
if extra:
|
| 122 |
+
status_msg = f"{status_msg}\n{extra}"
|
| 123 |
+
except Exception as e:
|
| 124 |
+
err = f"❌ Error loading data: {e}"
|
| 125 |
+
return err, [], None, None, err
|
| 126 |
+
|
| 127 |
+
code = code.replace("```python","").replace("```","")
|
| 128 |
+
# Extract dataframes from run_bt return values
|
| 129 |
+
progress(0.75, desc="Running strategy")
|
| 130 |
+
output_code = f'''
|
| 131 |
+
from bt_utils import run_bt
|
| 132 |
+
import backtrader as bt
|
| 133 |
+
{code}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
final_value, total_return, tmp_img, df_trades, df_transactions = run_bt(
|
| 137 |
+
cerebro=cerebro,
|
| 138 |
+
market_name='{market_name}',
|
| 139 |
+
save_img={config["save_plt"]},
|
| 140 |
+
tckr_symbl='{tckr_symbl}',
|
| 141 |
+
initial_capital={capital_value},
|
| 142 |
+
commission={commission_value},
|
| 143 |
+
slippage_percent={slippage_percent_value},
|
| 144 |
+
df=df,
|
| 145 |
+
replay={replay_enabled},
|
| 146 |
+
replay_compression={replay_compression},
|
| 147 |
+
interval='{replay_interval}'
|
| 148 |
+
)
|
| 149 |
+
'''
|
| 150 |
+
tmp_img = ""
|
| 151 |
+
df_trades = None
|
| 152 |
+
df_transactions = None
|
| 153 |
+
write_output(code)
|
| 154 |
+
output = io.StringIO()
|
| 155 |
+
sys_stdout = sys.stdout
|
| 156 |
+
sys.stdout = output
|
| 157 |
+
error_msg = None
|
| 158 |
+
try:
|
| 159 |
+
# Execute the code into its own namespace
|
| 160 |
+
namespace = {"df": df}
|
| 161 |
+
exec(output_code, namespace)
|
| 162 |
+
tmp_img = namespace.get("tmp_img", None)
|
| 163 |
+
df_trades = namespace.get("df_trades", None)
|
| 164 |
+
df_transactions = namespace.get("df_transactions", None)
|
| 165 |
+
except Exception:
|
| 166 |
+
error_msg = "❌ Error executing strategy:\n" + traceback.format_exc()
|
| 167 |
+
finally:
|
| 168 |
+
sys.stdout = sys_stdout
|
| 169 |
+
|
| 170 |
+
if error_msg:
|
| 171 |
+
combined = (status_msg or "") + ("\n" if status_msg else "") + error_msg
|
| 172 |
+
return combined, [], None, None, combined
|
| 173 |
+
progress(1.0, desc="Done")
|
| 174 |
+
ui_status = status_msg or "Data fetched."
|
| 175 |
+
return ui_status + "\n" + output.getvalue(), tmp_img, df_trades, df_transactions, ui_status
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def run_gradio_app():
|
| 181 |
+
""" Run the Gradio app for strategy generation and backtesting. """
|
| 182 |
+
market_list = list[market_to_ticker](market_to_ticker.keys())
|
| 183 |
+
with gr.Blocks(title="StrategyGenerator", theme=gr.themes.Default(primary_hue="emerald")) as ui:
|
| 184 |
+
gr.Markdown("# Financial Strategy Generator for Python ")
|
| 185 |
+
with gr.Tab("Strategy Generator"):
|
| 186 |
+
with gr.Row():
|
| 187 |
+
strategy_msg = gr.Textbox( value="", label="Enter the description of your strategy: ", lines=10)
|
| 188 |
+
code = gr.Textbox(label="Python code:", lines=10)
|
| 189 |
+
with gr.Row():
|
| 190 |
+
gen_strategy = gr.Button("Generate Strategy", variant="primary")
|
| 191 |
+
run_py = gr.Button("Run Python Code ", visible=True, variant="primary")
|
| 192 |
+
with gr.Row():
|
| 193 |
+
with gr.Row():
|
| 194 |
+
with gr.Group("General Config"):
|
| 195 |
+
with gr.Tab("Model Options"):
|
| 196 |
+
with gr.Column():
|
| 197 |
+
model = gr.Dropdown(["GPT", "Claude", "Deepseek", "Gemini", "Grok4"], label="Select model", value="Deepseek")
|
| 198 |
+
with gr.Tab("Replay Config"):
|
| 199 |
+
with gr.Column():
|
| 200 |
+
replay_enabled = gr.Checkbox(label="Enable Replay Mode", value=False)
|
| 201 |
+
initial_capital_in = gr.Number(label="Initial Capital ($)", value=config.get("initial_capital", 100000.0), precision=2)
|
| 202 |
+
data_source = gr.Dropdown(["fmp", "yahoo"], value=default_data_source, label="Data Source")
|
| 203 |
+
interval = gr.Dropdown(YF_INTERVALS, value="1d", label="Interval")
|
| 204 |
+
start_date = gr.Textbox(value="2020-01-01", label="Start Date (YYYY-MM-DD)")
|
| 205 |
+
end_date = gr.Textbox(value=current_date, label="End Date (YYYY-MM-DD, defaults to today)")
|
| 206 |
+
with gr.Column():
|
| 207 |
+
with gr.Group():
|
| 208 |
+
with gr.Tab("ETFS/Stock Selection"):
|
| 209 |
+
market = gr.Dropdown(market_list, label="Stock/ETFS (select or type ticker)", value="S&P 500 ETF", allow_custom_value=True)
|
| 210 |
+
commission_in = gr.Number(label="Commission per share ($)", value=config.get("commission", 0.005), precision=6)
|
| 211 |
+
slippage_percent_in = gr.Number(label="Slippage (% of price, e.g., 0.01 for 0.01%)", value=config.get("slippage_percent", 0.01), precision=6)
|
| 212 |
+
adjust_prices_in = gr.Checkbox(label="Use adjusted (dividend/split) prices", value=config.get("adjust_prices", True))
|
| 213 |
+
#period = gr.Dropdown(["30d", "10d", "60d"], value="60d", label="Period")
|
| 214 |
+
with gr.Row():
|
| 215 |
+
with gr.Column(scale=6):
|
| 216 |
+
py_out = gr.TextArea(label="Python result:", elem_classes=["python"])
|
| 217 |
+
with gr.Column(scale=1):
|
| 218 |
+
with gr.Group():
|
| 219 |
+
download_strategy_btn = gr.DownloadButton("Download Strategy Code", variant="primary")
|
| 220 |
+
strategy_file = gr.File(label="Strategy File", visible=True, interactive=False)
|
| 221 |
+
with gr.Tab("Charts"):
|
| 222 |
+
image_output = gr.Gallery(
|
| 223 |
+
label="Charts",
|
| 224 |
+
show_label=True,
|
| 225 |
+
elem_id="gallery",
|
| 226 |
+
columns=2,
|
| 227 |
+
height="auto"
|
| 228 |
+
)
|
| 229 |
+
with gr.Tab("Transactions"):
|
| 230 |
+
gr.Markdown("### Transaction Records (Buy/Sell Orders)")
|
| 231 |
+
transactions_df = gr.Dataframe(
|
| 232 |
+
label="All Transactions",
|
| 233 |
+
interactive=False,
|
| 234 |
+
wrap=True
|
| 235 |
+
)
|
| 236 |
+
with gr.Group():
|
| 237 |
+
download_transactions_btn = gr.Button("Generate CSV", variant="primary")
|
| 238 |
+
transactions_csv = gr.File(label="Download Transactions CSV", visible=True)
|
| 239 |
+
|
| 240 |
+
with gr.Tab("Trades"):
|
| 241 |
+
gr.Markdown("### Trade Records (Entry/Exit)")
|
| 242 |
+
trades_df = gr.Dataframe(
|
| 243 |
+
label="All Trades",
|
| 244 |
+
interactive=False,
|
| 245 |
+
wrap=True
|
| 246 |
+
)
|
| 247 |
+
with gr.Group():
|
| 248 |
+
download_trades_btn = gr.Button("Generate CSV", variant="primary")
|
| 249 |
+
trades_csv = gr.File(label="Download Trades CSV", visible=True)
|
| 250 |
+
# Connect generate strategy button
|
| 251 |
+
gen_strategy.click(stream_manager, inputs=[strategy_msg, model], outputs=[code])
|
| 252 |
+
|
| 253 |
+
replay_enabled.change(
|
| 254 |
+
fn=update_replay_intervals,
|
| 255 |
+
inputs=[replay_enabled],
|
| 256 |
+
outputs=[interval],
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
# Connect run button to execute strategy and update all outputs
|
| 260 |
+
run_py.click(
|
| 261 |
+
execute_python,
|
| 262 |
+
inputs=[
|
| 263 |
+
code,
|
| 264 |
+
market,
|
| 265 |
+
data_source,
|
| 266 |
+
interval,
|
| 267 |
+
start_date,
|
| 268 |
+
end_date,
|
| 269 |
+
initial_capital_in,
|
| 270 |
+
commission_in,
|
| 271 |
+
slippage_percent_in,
|
| 272 |
+
adjust_prices_in,
|
| 273 |
+
replay_enabled,
|
| 274 |
+
],
|
| 275 |
+
outputs=[py_out, image_output, trades_df, transactions_df],
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
# Connect CSV download buttons
|
| 279 |
+
download_transactions_btn.click(
|
| 280 |
+
lambda df: save_df_to_csv(df, "transactions"),
|
| 281 |
+
inputs=[transactions_df],
|
| 282 |
+
outputs=[transactions_csv]
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
download_trades_btn.click(
|
| 286 |
+
lambda df: save_df_to_csv(df, "trades"),
|
| 287 |
+
inputs=[trades_df],
|
| 288 |
+
outputs=[trades_csv]
|
| 289 |
+
)
|
| 290 |
+
download_strategy_btn.click(
|
| 291 |
+
save_strategy_to_file,
|
| 292 |
+
inputs=[code],
|
| 293 |
+
outputs=[strategy_file]
|
| 294 |
+
)
|
| 295 |
+
ui.launch(inbrowser=True, share=False, debug=True)
|
| 296 |
+
|
| 297 |
+
if __name__ == "__main__":
|
| 298 |
+
run_gradio_app()
|
bt_strategies.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import backtrader as bt
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class SmaCross(bt.Strategy):
|
| 6 |
+
"""
|
| 7 |
+
Simple moving average crossover strategy.
|
| 8 |
+
Buy when fast SMA crosses above slow SMA.
|
| 9 |
+
Sell when fast SMA crosses below slow SMA.
|
| 10 |
+
|
| 11 |
+
User prompt: "Go long when the 10-period SMA crosses above the 100-period SMA,
|
| 12 |
+
and exit when the 10-period SMA crosses below the 100-period SMA."
|
| 13 |
+
|
| 14 |
+
Call: cerebro.addstrategy(SmaCross, pfast=10, pslow=100)
|
| 15 |
+
"""
|
| 16 |
+
params = dict(pfast=10, pslow=100)
|
| 17 |
+
|
| 18 |
+
def __init__(self):
|
| 19 |
+
self.sma_fast = bt.ind.SMA(period=self.p.pfast)
|
| 20 |
+
self.sma_slow = bt.ind.SMA(period=self.p.pslow)
|
| 21 |
+
self.crossover = bt.ind.CrossOver(self.sma_fast, self.sma_slow)
|
| 22 |
+
|
| 23 |
+
def next(self):
|
| 24 |
+
if not self.position:
|
| 25 |
+
if self.crossover > 0: # Golden cross
|
| 26 |
+
self.buy()
|
| 27 |
+
elif self.crossover < 0: # Death cross
|
| 28 |
+
self.close()
|
| 29 |
+
|
| 30 |
+
class TrendMomentumLongStrategy(bt.Strategy):
|
| 31 |
+
"""
|
| 32 |
+
Multi-indicator strategy with trend following and momentum
|
| 33 |
+
Long entries
|
| 34 |
+
"""
|
| 35 |
+
params = dict(
|
| 36 |
+
sma_fast=5,# 20
|
| 37 |
+
sma_slow=30,# 50
|
| 38 |
+
rsi_period=14,
|
| 39 |
+
rsi_upper=90, # 70
|
| 40 |
+
rsi_lower=30,
|
| 41 |
+
atr_period=14,
|
| 42 |
+
atr_multiplier=2.0
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
def __init__(self):
|
| 46 |
+
# Trend indicators
|
| 47 |
+
self.sma_fast = bt.ind.SMA(period=self.p.sma_fast)
|
| 48 |
+
self.sma_slow = bt.ind.SMA(period=self.p.sma_slow)
|
| 49 |
+
self.trend = self.sma_fast - self.sma_slow
|
| 50 |
+
|
| 51 |
+
# Momentum indicator
|
| 52 |
+
self.rsi = bt.ind.RSI(period=self.p.rsi_period)
|
| 53 |
+
|
| 54 |
+
# Volatility for position sizing
|
| 55 |
+
self.atr = bt.ind.ATR(period=self.p.atr_period)
|
| 56 |
+
|
| 57 |
+
# Crossovers focall_liner entry signals
|
| 58 |
+
self.crossover = bt.ind.CrossOver(self.sma_fast, self.sma_slow)
|
| 59 |
+
|
| 60 |
+
def next(self):
|
| 61 |
+
# Calculate position size based on volatility (1% risk per trade)
|
| 62 |
+
if self.atr[0] > 0:
|
| 63 |
+
risk_amount = self.broker.getvalue() * 0.01
|
| 64 |
+
size = risk_amount / (self.atr[0] * self.p.atr_multiplier)
|
| 65 |
+
size = int(size)
|
| 66 |
+
else:
|
| 67 |
+
size = 100 # Default size
|
| 68 |
+
|
| 69 |
+
# Entry conditions: Golden cross + RSI not overbought
|
| 70 |
+
if not self.position:
|
| 71 |
+
if (self.crossover > 0 and
|
| 72 |
+
self.rsi < self.p.rsi_upper and
|
| 73 |
+
self.trend > 0):
|
| 74 |
+
self.buy(size=size)
|
| 75 |
+
|
| 76 |
+
# Exit conditions: Death cross OR RSI overbought
|
| 77 |
+
elif self.position:
|
| 78 |
+
if (self.crossover < 0 or
|
| 79 |
+
self.rsi > self.p.rsi_upper or
|
| 80 |
+
self.trend < 0):
|
| 81 |
+
self.close()
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class TrendMomentumShortStrategy(bt.Strategy):
|
| 85 |
+
"""
|
| 86 |
+
Multi-indicator strategy with trend following and momentum
|
| 87 |
+
Short entries only
|
| 88 |
+
"""
|
| 89 |
+
params = dict(
|
| 90 |
+
sma_fast=5, # Fast SMA period
|
| 91 |
+
sma_slow=30, # Slow SMA period
|
| 92 |
+
rsi_period=14, # RSI period
|
| 93 |
+
rsi_upper=70, # Overbought threshold
|
| 94 |
+
rsi_lower=10, # Oversold threshold
|
| 95 |
+
atr_period=14, # ATR period for volatility-based sizing
|
| 96 |
+
atr_multiplier=2.0
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
def __init__(self):
|
| 100 |
+
# Trend indicators
|
| 101 |
+
self.sma_fast = bt.ind.SMA(period=self.p.sma_fast)
|
| 102 |
+
self.sma_slow = bt.ind.SMA(period=self.p.sma_slow)
|
| 103 |
+
self.trend = self.sma_fast - self.sma_slow
|
| 104 |
+
|
| 105 |
+
# Momentum indicator
|
| 106 |
+
self.rsi = bt.ind.RSI(period=self.p.rsi_period)
|
| 107 |
+
|
| 108 |
+
# Volatility indicator
|
| 109 |
+
self.atr = bt.ind.ATR(period=self.p.atr_period)
|
| 110 |
+
|
| 111 |
+
# Crossover signal
|
| 112 |
+
self.crossover = bt.ind.CrossOver(self.sma_fast, self.sma_slow)
|
| 113 |
+
|
| 114 |
+
def next(self):
|
| 115 |
+
# Volatility-based position sizing (1% risk per trade)
|
| 116 |
+
if self.atr[0] > 0:
|
| 117 |
+
risk_amount = self.broker.getvalue() * 0.01
|
| 118 |
+
size = risk_amount / (self.atr[0] * self.p.atr_multiplier)
|
| 119 |
+
size = int(size)
|
| 120 |
+
else:
|
| 121 |
+
size = 100 # Fallback default
|
| 122 |
+
|
| 123 |
+
# Entry conditions: Death cross + RSI not oversold
|
| 124 |
+
if not self.position:
|
| 125 |
+
if (self.crossover < 0 and
|
| 126 |
+
self.rsi > self.p.rsi_lower and
|
| 127 |
+
self.trend < 0):
|
| 128 |
+
self.sell(size=size)
|
| 129 |
+
|
| 130 |
+
# Exit conditions: Golden cross OR RSI oversold
|
| 131 |
+
elif self.position:
|
| 132 |
+
if (self.crossover > 0 or
|
| 133 |
+
self.rsi < self.p.rsi_lower or
|
| 134 |
+
self.trend > 0):
|
| 135 |
+
self.close()
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# Go long when the 10-period SMA crosses above the 100-period SMA, and exit when the 10-period SMA crosses below the 100-period SMA.
|
| 139 |
+
# With logging
|
| 140 |
+
class SmaCrossExtended(bt.Strategy):
|
| 141 |
+
'''
|
| 142 |
+
Follow two moving average lines on a stock chart to decide when to buy and sell.
|
| 143 |
+
'''
|
| 144 |
+
params = dict(pfast=10, pslow=100)
|
| 145 |
+
|
| 146 |
+
def __init__(self):
|
| 147 |
+
self.sma1 = bt.ind.SMA(period=self.p.pfast)
|
| 148 |
+
self.sma2 = bt.ind.SMA(period=self.p.pslow)
|
| 149 |
+
self.crossover = bt.ind.CrossOver(self.sma1, self.sma2)
|
| 150 |
+
|
| 151 |
+
# Initialize order tracking
|
| 152 |
+
self.order = None
|
| 153 |
+
|
| 154 |
+
def log(self, txt, dt=None):
|
| 155 |
+
"""Logging function for this strategy"""
|
| 156 |
+
dt = dt or self.datas[0].datetime.date(0)
|
| 157 |
+
print(f'{dt.isoformat()}: {txt}')
|
| 158 |
+
|
| 159 |
+
def notify_order(self, order):
|
| 160 |
+
"""Called when order status changes"""
|
| 161 |
+
if order.status in [order.Submitted, order.Accepted]:
|
| 162 |
+
# Order submitted/accepted - nothing to do
|
| 163 |
+
return
|
| 164 |
+
|
| 165 |
+
# Order completed
|
| 166 |
+
if order.status in [order.Completed]:
|
| 167 |
+
if order.isbuy():
|
| 168 |
+
self.log(f'BUY EXECUTED - Price: {order.executed.price:.2f}, '
|
| 169 |
+
f'Cost: {order.executed.value:.2f}, '
|
| 170 |
+
f'Comm: {order.executed.comm:.2f}, '
|
| 171 |
+
f'Size: {order.executed.size}')
|
| 172 |
+
else:
|
| 173 |
+
self.log(f'SELL EXECUTED - Price: {order.executed.price:.2f}, '
|
| 174 |
+
f'Cost: {order.executed.value:.2f}, '
|
| 175 |
+
f'Comm: {order.executed.comm:.2f}, '
|
| 176 |
+
f'Size: {order.executed.size}')
|
| 177 |
+
|
| 178 |
+
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
|
| 179 |
+
self.log('Order Canceled/Margin/Rejected')
|
| 180 |
+
|
| 181 |
+
# Reset order
|
| 182 |
+
self.order = None
|
| 183 |
+
|
| 184 |
+
def notify_trade(self, trade):
|
| 185 |
+
"""Called when a trade is closed"""
|
| 186 |
+
if not trade.isclosed:
|
| 187 |
+
return
|
| 188 |
+
|
| 189 |
+
self.log(f'TRADE CLOSED - PnL: {trade.pnl:.2f}, PnL Net: {trade.pnlcomm:.2f}')
|
| 190 |
+
|
| 191 |
+
def next(self):
|
| 192 |
+
# Check if we have a pending order
|
| 193 |
+
if self.order:
|
| 194 |
+
return
|
| 195 |
+
|
| 196 |
+
if not self.position: # not in market
|
| 197 |
+
if self.crossover > 0: # Golden cross
|
| 198 |
+
self.log('BUY SIGNAL DETECTED')
|
| 199 |
+
self.order = self.buy()
|
| 200 |
+
|
| 201 |
+
else: # in market
|
| 202 |
+
if self.crossover < 0: # Death cross
|
| 203 |
+
self.log('SELL SIGNAL DETECTED')
|
| 204 |
+
self.order = self.close()
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
import backtrader as bt
|
| 208 |
+
|
| 209 |
+
class TrendMomentumLongStrategyTS(bt.Strategy):
|
| 210 |
+
"""
|
| 211 |
+
Multi-indicator strategy with trend following and momentum
|
| 212 |
+
Long entries with take profit and stop loss
|
| 213 |
+
"""
|
| 214 |
+
params = dict(
|
| 215 |
+
sma_fast=5,
|
| 216 |
+
sma_slow=30,
|
| 217 |
+
rsi_period=14,
|
| 218 |
+
rsi_upper=90,
|
| 219 |
+
rsi_lower=30,
|
| 220 |
+
atr_period=14,
|
| 221 |
+
atr_multiplier=2.0,
|
| 222 |
+
stop_loss_pct=0.05, # 5% stop loss
|
| 223 |
+
take_profit_pct=0.10 # 10% take profit
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
def __init__(self):
|
| 227 |
+
# Trend indicators
|
| 228 |
+
self.sma_fast = bt.ind.SMA(period=self.p.sma_fast)
|
| 229 |
+
self.sma_slow = bt.ind.SMA(period=self.p.sma_slow)
|
| 230 |
+
self.trend = self.sma_fast - self.sma_slow
|
| 231 |
+
|
| 232 |
+
# Momentum indicator
|
| 233 |
+
self.rsi = bt.ind.RSI(period=self.p.rsi_period)
|
| 234 |
+
|
| 235 |
+
# Volatility for position sizing
|
| 236 |
+
self.atr = bt.ind.ATR(period=self.p.atr_period)
|
| 237 |
+
|
| 238 |
+
# Crossovers for entry signals
|
| 239 |
+
self.crossover = bt.ind.CrossOver(self.sma_fast, self.sma_slow)
|
| 240 |
+
|
| 241 |
+
# Track entry price for stop loss and take profit
|
| 242 |
+
self.entry_price = None
|
| 243 |
+
|
| 244 |
+
def next(self):
|
| 245 |
+
# Calculate position size based on volatility (1% risk per trade)
|
| 246 |
+
if self.atr[0] > 0:
|
| 247 |
+
risk_amount = self.broker.getvalue() * 0.01
|
| 248 |
+
size = risk_amount / (self.atr[0] * self.p.atr_multiplier)
|
| 249 |
+
size = int(size)
|
| 250 |
+
else:
|
| 251 |
+
size = 100 # Default size
|
| 252 |
+
|
| 253 |
+
# Entry conditions: Golden cross + RSI not overbought
|
| 254 |
+
if not self.position:
|
| 255 |
+
if (self.crossover > 0 and
|
| 256 |
+
self.rsi < self.p.rsi_upper and
|
| 257 |
+
self.trend > 0):
|
| 258 |
+
self.buy(size=size)
|
| 259 |
+
self.entry_price = self.data.close[0] # Track entry price
|
| 260 |
+
|
| 261 |
+
# Exit conditions: Death cross OR RSI overbought OR stop loss/take profit
|
| 262 |
+
elif self.position:
|
| 263 |
+
current_price = self.data.close[0]
|
| 264 |
+
|
| 265 |
+
# Calculate stop loss and take profit levels
|
| 266 |
+
stop_loss_price = self.entry_price * (1 - self.p.stop_loss_pct)
|
| 267 |
+
take_profit_price = self.entry_price * (1 + self.p.take_profit_pct)
|
| 268 |
+
|
| 269 |
+
# Check exit conditions
|
| 270 |
+
if (self.crossover < 0 or
|
| 271 |
+
self.rsi > self.p.rsi_upper or
|
| 272 |
+
self.trend < 0 or
|
| 273 |
+
current_price <= stop_loss_price or
|
| 274 |
+
current_price >= take_profit_price):
|
| 275 |
+
self.close()
|
| 276 |
+
self.entry_price = None # Reset entry price
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
class TrendMomentumShortStrategyTS(bt.Strategy):
|
| 281 |
+
"""
|
| 282 |
+
Multi-indicator strategy with trend following and momentum
|
| 283 |
+
Short entries only with take profit and stop loss
|
| 284 |
+
"""
|
| 285 |
+
params = dict(
|
| 286 |
+
sma_fast=5, # Fast SMA period
|
| 287 |
+
sma_slow=30, # Slow SMA period
|
| 288 |
+
rsi_period=14, # RSI period
|
| 289 |
+
rsi_upper=70, # Overbought threshold
|
| 290 |
+
rsi_lower=10, # Oversold threshold
|
| 291 |
+
atr_period=14, # ATR period for volatility-based sizing
|
| 292 |
+
atr_multiplier=2.0,
|
| 293 |
+
stop_loss_pct=0.05, # 5% stop loss
|
| 294 |
+
take_profit_pct=0.10 # 10% take profit
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
def __init__(self):
|
| 298 |
+
# Trend indicators
|
| 299 |
+
self.sma_fast = bt.ind.SMA(period=self.p.sma_fast)
|
| 300 |
+
self.sma_slow = bt.ind.SMA(period=self.p.sma_slow)
|
| 301 |
+
self.trend = self.sma_fast - self.sma_slow
|
| 302 |
+
|
| 303 |
+
# Momentum indicator
|
| 304 |
+
self.rsi = bt.ind.RSI(period=self.p.rsi_period)
|
| 305 |
+
|
| 306 |
+
# Volatility indicator
|
| 307 |
+
self.atr = bt.ind.ATR(period=self.p.atr_period)
|
| 308 |
+
|
| 309 |
+
# Crossover signal
|
| 310 |
+
self.crossover = bt.ind.CrossOver(self.sma_fast, self.sma_slow)
|
| 311 |
+
# Entry price for stop loss and take profit
|
| 312 |
+
self.entry_price = None
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def next(self):
|
| 316 |
+
# Volatility-based position sizing (1% risk per trade)
|
| 317 |
+
if self.atr[0] > 0:
|
| 318 |
+
risk_amount = self.broker.getvalue() * 0.01
|
| 319 |
+
size = risk_amount / (self.atr[0] * self.p.atr_multiplier)
|
| 320 |
+
size = int(size)
|
| 321 |
+
else:
|
| 322 |
+
size = 100 # Fallback default
|
| 323 |
+
|
| 324 |
+
# Entry conditions: Death cross + RSI not oversold
|
| 325 |
+
if not self.position:
|
| 326 |
+
if (self.crossover < 0 and
|
| 327 |
+
self.rsi > self.p.rsi_lower and
|
| 328 |
+
self.trend < 0):
|
| 329 |
+
self.sell(size=size)
|
| 330 |
+
self.entry_price = self.data.close[0] # Track entry price
|
| 331 |
+
|
| 332 |
+
# Exit conditions: Golden cross OR RSI oversold OR stop loss/take profit
|
| 333 |
+
elif self.position:
|
| 334 |
+
current_price = self.data.close[0]
|
| 335 |
+
|
| 336 |
+
# Calculate stop loss and take profit levels
|
| 337 |
+
# For short positions:
|
| 338 |
+
# - Stop loss triggers when price goes UP (price > entry * (1 + stop_loss_pct))
|
| 339 |
+
# - Take profit triggers when price goes DOWN (price < entry * (1 - take_profit_pct))
|
| 340 |
+
stop_loss_price = self.entry_price * (1 + self.p.stop_loss_pct) # Stop loss above entry
|
| 341 |
+
take_profit_price = self.entry_price * (1 - self.p.take_profit_pct) # Take profit below entry
|
| 342 |
+
|
| 343 |
+
if (self.crossover > 0 or # Golden cross
|
| 344 |
+
self.rsi < self.p.rsi_lower or # RSI oversold
|
| 345 |
+
self.trend > 0 or # Trend turned positive
|
| 346 |
+
current_price >= stop_loss_price or # Stop loss hit (price went up)
|
| 347 |
+
current_price <= take_profit_price): # Take profit hit (price went down)
|
| 348 |
+
self.close()
|
| 349 |
+
self.entry_price = None # Reset entry price
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
class ScalpingBB(bt.Strategy):
|
| 353 |
+
"""
|
| 354 |
+
Scalping strategy using Bollinger Bands with RSI during high volatility periods.
|
| 355 |
+
Only trades during specific time windows for 1-minute.
|
| 356 |
+
"""
|
| 357 |
+
params = dict(
|
| 358 |
+
bb_period=20,
|
| 359 |
+
bb_dev=2.0,
|
| 360 |
+
rsi_period=14,
|
| 361 |
+
rsi_oversold=30,
|
| 362 |
+
rsi_overbought=70,
|
| 363 |
+
start_hour=9, # 9:00 AM
|
| 364 |
+
end_hour=16, # 4:00 PM
|
| 365 |
+
position_size=100
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
def __init__(self):
|
| 369 |
+
# Bollinger Bands indicator
|
| 370 |
+
self.bb = bt.ind.BollingerBands(
|
| 371 |
+
period=self.p.bb_period,
|
| 372 |
+
devfactor=self.p.bb_dev
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
# RSI indicator for confirmation
|
| 376 |
+
self.rsi = bt.ind.RSI(
|
| 377 |
+
period=self.p.rsi_period
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
# Track current time
|
| 381 |
+
self.current_time = None
|
| 382 |
+
|
| 383 |
+
def is_trading_hours(self):
|
| 384 |
+
"""Check if current time is within trading hours"""
|
| 385 |
+
if self.current_time is None:
|
| 386 |
+
return False
|
| 387 |
+
|
| 388 |
+
hour = self.current_time.hour
|
| 389 |
+
minute = self.current_time.minute
|
| 390 |
+
|
| 391 |
+
# Check if within 9:00 AM to 4:00 PM
|
| 392 |
+
if hour < self.p.start_hour or hour >= self.p.end_hour:
|
| 393 |
+
return False
|
| 394 |
+
|
| 395 |
+
return True
|
| 396 |
+
|
| 397 |
+
def next(self):
|
| 398 |
+
# Get current datetime
|
| 399 |
+
self.current_time = self.data.datetime.datetime()
|
| 400 |
+
|
| 401 |
+
# Only trade during specified hours
|
| 402 |
+
if not self.is_trading_hours():
|
| 403 |
+
if self.position:
|
| 404 |
+
self.close()
|
| 405 |
+
return
|
| 406 |
+
|
| 407 |
+
# Check for buy signal (price touches lower band, RSI oversold)
|
| 408 |
+
if self.data.close[0] <= self.bb.lines.bot[0] and self.rsi[0] <= self.p.rsi_oversold:
|
| 409 |
+
if not self.position:
|
| 410 |
+
self.buy(size=self.p.position_size)
|
| 411 |
+
|
| 412 |
+
# Check for sell signal (price touches upper band, RSI overbought)
|
| 413 |
+
elif self.data.close[0] >= self.bb.lines.top[0] and self.rsi[0] >= self.p.rsi_overbought:
|
| 414 |
+
if self.position:
|
| 415 |
+
self.sell(size=self.p.position_size)
|
| 416 |
+
|
| 417 |
+
# Exit if price returns to middle band
|
| 418 |
+
elif self.position:
|
| 419 |
+
if abs(self.data.close[0] - self.bb.lines.mid[0]) < (self.bb.lines.top[0] - self.bb.lines.mid[0]) * 0.3:
|
| 420 |
+
self.close()
|
| 421 |
+
|
| 422 |
+
class TripleMACross(bt.Strategy):
|
| 423 |
+
"""
|
| 424 |
+
Triple moving average crossover strategy with 10% position sizing.
|
| 425 |
+
- Entry: Fast SMA (5) crosses above Medium SMA (10) and Medium SMA is above Slow SMA (20)
|
| 426 |
+
- Exit: Fast SMA crosses below Medium SMA OR Medium SMA crosses below Slow SMA
|
| 427 |
+
- Position sizing: 10% of portfolio per trade
|
| 428 |
+
"""
|
| 429 |
+
params = (
|
| 430 |
+
('fast_period', 5),
|
| 431 |
+
('medium_period', 10),
|
| 432 |
+
('slow_period', 20),
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
def __init__(self):
|
| 436 |
+
# Three moving averages
|
| 437 |
+
self.sma_fast = bt.indicators.SMA(period=self.p.fast_period)
|
| 438 |
+
self.sma_medium = bt.indicators.SMA(period=self.p.medium_period)
|
| 439 |
+
self.sma_slow = bt.indicators.SMA(period=self.p.slow_period)
|
| 440 |
+
|
| 441 |
+
# Crossover indicators
|
| 442 |
+
self.cross_fast_medium = bt.indicators.CrossOver(self.sma_fast, self.sma_medium)
|
| 443 |
+
self.cross_medium_slow = bt.indicators.CrossOver(self.sma_medium, self.sma_slow)
|
| 444 |
+
|
| 445 |
+
# Track position for conditional logic
|
| 446 |
+
self.position_open = False
|
| 447 |
+
|
| 448 |
+
def next(self):
|
| 449 |
+
# Entry condition: Fast crosses above Medium AND Medium > Slow (no existing position)
|
| 450 |
+
if not self.position:
|
| 451 |
+
if self.cross_fast_medium > 0 and self.sma_medium[0] > self.sma_slow[0]:
|
| 452 |
+
self.buy(size=self.get_target_size()) # Use dynamic sizing
|
| 453 |
+
self.position_open = True
|
| 454 |
+
|
| 455 |
+
# Exit conditions: Fast crosses below Medium OR Medium crosses below Slow
|
| 456 |
+
elif self.position_open:
|
| 457 |
+
if self.cross_fast_medium < 0 or self.cross_medium_slow < 0:
|
| 458 |
+
self.close()
|
| 459 |
+
self.position_open = False
|
| 460 |
+
|
| 461 |
+
def get_target_size(self):
|
| 462 |
+
"""Calculate 10% of current portfolio value"""
|
| 463 |
+
return int((self.broker.getvalue() * 0.90) / self.data.close[0])
|
bt_strategy.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Model deepseek-reasoner
|
| 2 |
+
import backtrader as bt
|
| 3 |
+
|
| 4 |
+
class TripleMACross(bt.Strategy):
|
| 5 |
+
"""
|
| 6 |
+
Triple moving average crossover strategy with 10% position sizing.
|
| 7 |
+
- Entry: Fast SMA (5) crosses above Medium SMA (10) and Medium SMA is above Slow SMA (20)
|
| 8 |
+
- Exit: Fast SMA crosses below Medium SMA OR Medium SMA crosses below Slow SMA
|
| 9 |
+
- Position sizing: 10% of portfolio per trade
|
| 10 |
+
"""
|
| 11 |
+
params = (
|
| 12 |
+
('fast_period', 5),
|
| 13 |
+
('medium_period', 10),
|
| 14 |
+
('slow_period', 20),
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
def __init__(self):
|
| 18 |
+
# Three moving averages
|
| 19 |
+
self.sma_fast = bt.indicators.SMA(period=self.p.fast_period)
|
| 20 |
+
self.sma_medium = bt.indicators.SMA(period=self.p.medium_period)
|
| 21 |
+
self.sma_slow = bt.indicators.SMA(period=self.p.slow_period)
|
| 22 |
+
|
| 23 |
+
# Crossover indicators
|
| 24 |
+
self.cross_fast_medium = bt.indicators.CrossOver(self.sma_fast, self.sma_medium)
|
| 25 |
+
self.cross_medium_slow = bt.indicators.CrossOver(self.sma_medium, self.sma_slow)
|
| 26 |
+
|
| 27 |
+
# Track position for conditional logic
|
| 28 |
+
self.position_open = False
|
| 29 |
+
|
| 30 |
+
def next(self):
|
| 31 |
+
# Entry condition: Fast crosses above Medium AND Medium > Slow (no existing position)
|
| 32 |
+
if not self.position:
|
| 33 |
+
if self.cross_fast_medium > 0 and self.sma_medium[0] > self.sma_slow[0]:
|
| 34 |
+
self.buy(size=self.get_target_size()) # Use dynamic sizing
|
| 35 |
+
self.position_open = True
|
| 36 |
+
|
| 37 |
+
# Exit conditions: Fast crosses below Medium OR Medium crosses below Slow
|
| 38 |
+
elif self.position_open:
|
| 39 |
+
if self.cross_fast_medium < 0 or self.cross_medium_slow < 0:
|
| 40 |
+
self.close()
|
| 41 |
+
self.position_open = False
|
| 42 |
+
|
| 43 |
+
def get_target_size(self):
|
| 44 |
+
"""Calculate 10% of current portfolio value"""
|
| 45 |
+
return int((self.broker.getvalue() * 0.90) / self.data.close[0])
|
| 46 |
+
|
| 47 |
+
# Initialize Cerebro with strategy and sizer
|
| 48 |
+
cerebro = bt.Cerebro()
|
| 49 |
+
cerebro.addstrategy(TripleMACross, fast_period=5, medium_period=10, slow_period=20)
|
bt_testing.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fsspec.core import conf
|
| 2 |
+
import yaml
|
| 3 |
+
from bt_strategies import *
|
| 4 |
+
from bt_utils import run_bt
|
| 5 |
+
from data_utils import get_data
|
| 6 |
+
import backtrader as bt
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import matplotlib.dates as mdates
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
# Environment
|
| 13 |
+
load_dotenv(override=True)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
with open('config/var_dev.yaml', 'r') as f:
|
| 18 |
+
config = yaml.safe_load(f)
|
| 19 |
+
if config["end_date"]=="current":
|
| 20 |
+
end = datetime.now().strftime('%Y-%m-%d')
|
| 21 |
+
else:
|
| 22 |
+
end = config["end_date"]
|
| 23 |
+
DATE= {'start': config["start_date"], 'end': end}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
cerebro = bt.Cerebro()
|
| 27 |
+
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
|
| 28 |
+
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
|
| 29 |
+
cerebro.addstrategy(TripleMACross, fast_period=5, medium_period=10, slow_period=20)
|
| 30 |
+
"""
|
| 31 |
+
cerebro.addstrategy(
|
| 32 |
+
ScalpingBB,
|
| 33 |
+
bb_period=20,
|
| 34 |
+
bb_dev=2.0,
|
| 35 |
+
rsi_period=14,
|
| 36 |
+
rsi_oversold=30,
|
| 37 |
+
rsi_overbought=70,
|
| 38 |
+
start_hour=14,
|
| 39 |
+
end_hour=21,
|
| 40 |
+
position_size=10
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
cerebro.addstrategy(SmaCross, pfast=5, pslow=50)
|
| 44 |
+
cerebro.addstrategy(TrendMomentumLongStrategyTS,
|
| 45 |
+
sma_fast=2,# 20
|
| 46 |
+
sma_slow=30,# 50
|
| 47 |
+
rsi_period=14,
|
| 48 |
+
rsi_upper=80, # 70
|
| 49 |
+
rsi_lower=30,
|
| 50 |
+
atr_period=14,
|
| 51 |
+
atr_multiplier=1.0,
|
| 52 |
+
stop_loss_pct=0.5, # 5% stop loss
|
| 53 |
+
take_profit_pct=0.5 ) # 10% take profit
|
| 54 |
+
|
| 55 |
+
cerebro.addstrategy(TrendMomentumShortStrategyTS, sma_fast=10, sma_slow=50, rsi_period=24, rsi_upper=65, rsi_lower=40, atr_period=14, atr_multiplier=2.0, stop_loss_pct=0.5, take_profit_pct=0.5)
|
| 56 |
+
"""
|
| 57 |
+
ticker_to_market = config['ticker_to_market']
|
| 58 |
+
tckr_symbl = config["market"]
|
| 59 |
+
market_name = ticker_to_market.get(tckr_symbl, tckr_symbl)
|
| 60 |
+
|
| 61 |
+
df = get_data(
|
| 62 |
+
data_source=config.get("data_source", "yahoofinance"),
|
| 63 |
+
tckr_symbl=tckr_symbl,
|
| 64 |
+
interval=config["interval"],
|
| 65 |
+
date=DATE,
|
| 66 |
+
adjust_prices=config.get("adjust_prices", True),
|
| 67 |
+
auto_period=config.get("auto_period", True),
|
| 68 |
+
period=config.get("period", "60d"),
|
| 69 |
+
upload_data=config.get("upload_data", False),
|
| 70 |
+
upload_data_path=config.get("upload_data_path"),
|
| 71 |
+
)
|
| 72 |
+
run_bt(
|
| 73 |
+
cerebro=cerebro,
|
| 74 |
+
market_name=market_name,
|
| 75 |
+
replay=config["replay"],
|
| 76 |
+
replay_compression=config["replay_compression"],
|
| 77 |
+
save_img=config["save_plt"],
|
| 78 |
+
interval=config["interval"],
|
| 79 |
+
tckr_symbl=tckr_symbl,
|
| 80 |
+
initial_capital=config["initial_capital"],
|
| 81 |
+
commission=config["commission"],
|
| 82 |
+
slippage_percent=config["slippage_percent"],
|
| 83 |
+
df=df,
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
|
bt_utils.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
os.environ.setdefault("MPLBACKEND", "Agg")
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from io import BytesIO
|
| 5 |
+
|
| 6 |
+
import backtrader as bt
|
| 7 |
+
import matplotlib
|
| 8 |
+
import matplotlib.pyplot as plt
|
| 9 |
+
import pandas as pd
|
| 10 |
+
|
| 11 |
+
matplotlib.use("Agg") # Set the backend to non-interactive
|
| 12 |
+
|
| 13 |
+
fmp_intervals_to_int = {"1min":1, "5min":5, "15min":15, "1hour":60, "4hour":240}
|
| 14 |
+
class TransactionRecorder(bt.Analyzer):
|
| 15 |
+
""" Records all buy/sell transactions with details."""
|
| 16 |
+
def __init__(self):
|
| 17 |
+
self.records = []
|
| 18 |
+
|
| 19 |
+
def notify_order(self, order):
|
| 20 |
+
if order.status != order.Completed:
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
self.records.append({
|
| 24 |
+
'datetime': self.strategy.data.datetime.datetime(),
|
| 25 |
+
'type': 'BUY' if order.isbuy() else 'SELL',
|
| 26 |
+
'price': order.executed.price,
|
| 27 |
+
'size': order.executed.size,
|
| 28 |
+
'value': order.executed.value,
|
| 29 |
+
'commission': order.executed.comm,
|
| 30 |
+
})
|
| 31 |
+
|
| 32 |
+
def get_analysis(self):
|
| 33 |
+
return pd.DataFrame(self.records)
|
| 34 |
+
|
| 35 |
+
class TradeRecorder(bt.Analyzer):
|
| 36 |
+
""" Records detailed trade information including entry/exit prices and PnL."""
|
| 37 |
+
def __init__(self):
|
| 38 |
+
self.records = []
|
| 39 |
+
self.trade_id = 0
|
| 40 |
+
|
| 41 |
+
def notify_trade(self, trade):
|
| 42 |
+
# RECORD ENTRY
|
| 43 |
+
if trade.isopen:
|
| 44 |
+
self.current_entry_price = trade.price
|
| 45 |
+
self.current_size = trade.size
|
| 46 |
+
return
|
| 47 |
+
|
| 48 |
+
# RECORD EXIT
|
| 49 |
+
if trade.isclosed:
|
| 50 |
+
self.trade_id += 1
|
| 51 |
+
|
| 52 |
+
# Backtrader clears trade.size to 0 on close, so restore saved size
|
| 53 |
+
size = self.current_size
|
| 54 |
+
|
| 55 |
+
# Compute exit price from PnL formula:
|
| 56 |
+
# pnl = (exit - entry) * size
|
| 57 |
+
if size is not None and size != 0:
|
| 58 |
+
exit_price = self.current_entry_price + (trade.pnl / size)
|
| 59 |
+
else:
|
| 60 |
+
exit_price = None
|
| 61 |
+
|
| 62 |
+
self.records.append({
|
| 63 |
+
'trade_id': self.trade_id,
|
| 64 |
+
'size': size,
|
| 65 |
+
'entry_price': self.current_entry_price,
|
| 66 |
+
'exit_price': exit_price,
|
| 67 |
+
'bruto_profitloss': trade.pnl,
|
| 68 |
+
'neto_profitloss': trade.pnlcomm,
|
| 69 |
+
'date_open': self._format_dt(trade.dtopen),
|
| 70 |
+
'date_close': self._format_dt(trade.dtclose),
|
| 71 |
+
})
|
| 72 |
+
|
| 73 |
+
# Reset after close
|
| 74 |
+
self.current_entry_price = None
|
| 75 |
+
self.current_size = None
|
| 76 |
+
|
| 77 |
+
def get_analysis(self):
|
| 78 |
+
return pd.DataFrame(self.records)
|
| 79 |
+
|
| 80 |
+
def _format_dt(self, val):
|
| 81 |
+
"""Convert a backtrader/matplotlib numeric datetime to a readable string."""
|
| 82 |
+
if val is None:
|
| 83 |
+
return None
|
| 84 |
+
try:
|
| 85 |
+
ord_day = int(val)
|
| 86 |
+
frac = val - ord_day
|
| 87 |
+
dt = datetime.fromordinal(ord_day) + timedelta(days=frac)
|
| 88 |
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
| 89 |
+
except Exception as e:
|
| 90 |
+
return "Error formatting dt: " + str(e)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _fig_to_numpy(fig, dpi=150):
|
| 94 |
+
buf = BytesIO()
|
| 95 |
+
fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
|
| 96 |
+
buf.seek(0)
|
| 97 |
+
img = plt.imread(buf, format="png")
|
| 98 |
+
buf.close()
|
| 99 |
+
return img
|
| 100 |
+
|
| 101 |
+
#TODO: Dates plotting not working
|
| 102 |
+
def plot_bt(figs, symbol, market_name, save_img=True):
|
| 103 |
+
"""
|
| 104 |
+
Plot backtrader results with market name in title
|
| 105 |
+
and readable date ticks on all x-axes.
|
| 106 |
+
"""
|
| 107 |
+
images = []
|
| 108 |
+
|
| 109 |
+
plt.ioff() # Turn off interactive mode
|
| 110 |
+
for i, fig_list in enumerate(figs):
|
| 111 |
+
for j, fig in enumerate(fig_list):
|
| 112 |
+
# Decorate titles
|
| 113 |
+
if fig.axes:
|
| 114 |
+
main_ax = fig.axes[0]
|
| 115 |
+
current_title = main_ax.get_title()
|
| 116 |
+
new_title = f"{market_name} ({symbol.upper()}) - {current_title or 'Price Chart'}"
|
| 117 |
+
main_ax.set_title(new_title, fontsize=8, fontweight="bold")
|
| 118 |
+
fig.suptitle(
|
| 119 |
+
f"Trading Strategy Analysis: {market_name}",
|
| 120 |
+
fontsize=10,
|
| 121 |
+
fontweight="bold",
|
| 122 |
+
y=0.98,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
fig.set_size_inches(12, 6)
|
| 126 |
+
fig.autofmt_xdate()
|
| 127 |
+
fig.tight_layout(rect=[0, 0, 1, 0.95])
|
| 128 |
+
# Convert to numpy
|
| 129 |
+
img_data = _fig_to_numpy(fig)
|
| 130 |
+
images.append(img_data)
|
| 131 |
+
|
| 132 |
+
if save_img:
|
| 133 |
+
filename = f"plot_{symbol}_{i}_{j}.png"
|
| 134 |
+
fig.savefig(filename, dpi=300, bbox_inches="tight")
|
| 135 |
+
print(f"Chart saved as: {filename}")
|
| 136 |
+
|
| 137 |
+
plt.close(fig) # Close the figure to free memory
|
| 138 |
+
return images
|
| 139 |
+
|
| 140 |
+
def run_bt(cerebro,
|
| 141 |
+
tckr_symbl="SPY",
|
| 142 |
+
replay=False,
|
| 143 |
+
replay_compression=15,
|
| 144 |
+
save_img=False,
|
| 145 |
+
interval=None,
|
| 146 |
+
market_name = "Complete Market Name here",
|
| 147 |
+
initial_capital=10000.0,
|
| 148 |
+
commission=0.001,
|
| 149 |
+
slippage_percent=0.01,
|
| 150 |
+
df=None):
|
| 151 |
+
"""
|
| 152 |
+
Run backtrader strategy with enhanced plotting
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
strategy: Backtrader strategy class already init
|
| 156 |
+
tckr_symbl: Stock ticker symbol
|
| 157 |
+
save_img: Whether to save plot images
|
| 158 |
+
initial_capital: Starting capital for the broker
|
| 159 |
+
commission: Commission per share
|
| 160 |
+
slippage_percent: Percent (e.g., 0.01 for 0.01%) applied as slippage
|
| 161 |
+
df: pandas DataFrame with datetime index.
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
print(f"Running strategy on: {market_name} ({tckr_symbl.upper()})")
|
| 165 |
+
print("-" * 50)
|
| 166 |
+
print(f"Initial capital: {initial_capital}, Commission: {commission}, Slippage%: {slippage_percent}")
|
| 167 |
+
|
| 168 |
+
if df is None:
|
| 169 |
+
raise ValueError("df is required. Load data with get_data before calling run_bt.")
|
| 170 |
+
|
| 171 |
+
if replay:
|
| 172 |
+
print(f"Replaying data on: {market_name} ({tckr_symbl.upper()}) at interval {interval} with compression {replay_compression}")
|
| 173 |
+
data = bt.feeds.PandasData(dataname=df, timeframe=bt.TimeFrame.Minutes, compression=fmp_intervals_to_int[interval])
|
| 174 |
+
cerebro.replaydata(data, timeframe=bt.TimeFrame.Minutes, compression=replay_compression)
|
| 175 |
+
else:
|
| 176 |
+
data = bt.feeds.PandasData(dataname=df)
|
| 177 |
+
cerebro.adddata(data)
|
| 178 |
+
|
| 179 |
+
# Set initial cash and commission
|
| 180 |
+
initial_cash = float(initial_capital)
|
| 181 |
+
cerebro.broker.setcash(initial_cash)
|
| 182 |
+
cerebro.broker.setcommission(commission=float(commission))
|
| 183 |
+
slippage_decimal = float(slippage_percent) / 100.0
|
| 184 |
+
cerebro.broker.set_slippage_perc(slippage_decimal)
|
| 185 |
+
|
| 186 |
+
# Add analyzers for better performance metrics
|
| 187 |
+
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
|
| 188 |
+
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
|
| 189 |
+
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
|
| 190 |
+
cerebro.addanalyzer(TransactionRecorder, _name='transactions')
|
| 191 |
+
cerebro.addanalyzer(TradeRecorder, _name='trades')
|
| 192 |
+
|
| 193 |
+
# Print starting conditions
|
| 194 |
+
print(f'Starting Portfolio Value: ${cerebro.broker.getvalue():,.2f}')
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# Run strategy
|
| 198 |
+
results = cerebro.run()
|
| 199 |
+
|
| 200 |
+
# Calculate and display results
|
| 201 |
+
final_value = cerebro.broker.getvalue()
|
| 202 |
+
total_return = (final_value - initial_cash) / initial_cash * 100
|
| 203 |
+
|
| 204 |
+
print(f'Final Portfolio Value: ${final_value:,.2f}')
|
| 205 |
+
print(f'Total Return: {total_return:.2f}%')
|
| 206 |
+
|
| 207 |
+
# Print analyzer results
|
| 208 |
+
strat = results[0]
|
| 209 |
+
|
| 210 |
+
try:
|
| 211 |
+
sharpe = strat.analyzers.sharpe.get_analysis().get('sharperatio', 'N/A')
|
| 212 |
+
if sharpe != 'N/A':
|
| 213 |
+
print(f'Sharpe Ratio: {sharpe:.3f}')
|
| 214 |
+
else:
|
| 215 |
+
print('Sharpe Ratio: N/A')
|
| 216 |
+
except:
|
| 217 |
+
print('Sharpe Ratio: N/A')
|
| 218 |
+
|
| 219 |
+
try:
|
| 220 |
+
max_dd = strat.analyzers.drawdown.get_analysis()['max']['drawdown']
|
| 221 |
+
print(f'Max Drawdown: {max_dd:.2f}%')
|
| 222 |
+
except:
|
| 223 |
+
print('Max Drawdown: N/A')
|
| 224 |
+
|
| 225 |
+
# Trades and transaction tables
|
| 226 |
+
df_transactions = strat.analyzers.transactions.get_analysis()
|
| 227 |
+
df_trades = strat.analyzers.trades.get_analysis()
|
| 228 |
+
print(f"Number of Trades: {len(df_trades)}")
|
| 229 |
+
print("-" * 50)
|
| 230 |
+
if not df_transactions.empty:
|
| 231 |
+
print("Transactions logs generated")
|
| 232 |
+
else:
|
| 233 |
+
print("No transactions recorded.")
|
| 234 |
+
if not df_trades.empty:
|
| 235 |
+
print("Trades logs generated")
|
| 236 |
+
else:
|
| 237 |
+
print("No trades recorded.")
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
# Generate plot with market name (disable interactive plotting to avoid GUI in threads)
|
| 243 |
+
fig = []
|
| 244 |
+
try:
|
| 245 |
+
figs = cerebro.plot(
|
| 246 |
+
style='candlestick',
|
| 247 |
+
barstyle='candlestick',
|
| 248 |
+
subplot=True,
|
| 249 |
+
plotabove=False,
|
| 250 |
+
downsample=False,
|
| 251 |
+
plotstyle='multiple',
|
| 252 |
+
iplot=False,
|
| 253 |
+
show=False,
|
| 254 |
+
dpi=120,
|
| 255 |
+
)
|
| 256 |
+
print("How many charts where created: ", len(figs))
|
| 257 |
+
fig = plot_bt(figs, market_name, tckr_symbl, save_img)
|
| 258 |
+
except Exception as e:
|
| 259 |
+
print(f"Plot skipped due to error: {e}")
|
| 260 |
+
|
| 261 |
+
return final_value, total_return, fig, df_trades, df_transactions
|
config/var_dev.yaml
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Init variables for backtrader testing
|
| 2 |
+
# Market name mapping
|
| 3 |
+
market: 'SPY'
|
| 4 |
+
api_fin: 'backtrader'
|
| 5 |
+
|
| 6 |
+
# Upload data to the database
|
| 7 |
+
upload_data: false
|
| 8 |
+
upload_data_path: 'data_upload/QQQ.csv'
|
| 9 |
+
|
| 10 |
+
data_source: "fmp"
|
| 11 |
+
|
| 12 |
+
interval: '15min'
|
| 13 |
+
auto_period: true
|
| 14 |
+
period: '60d'
|
| 15 |
+
replay: false
|
| 16 |
+
replay_compression: 5 # in minutes
|
| 17 |
+
initial_capital: 10000.0
|
| 18 |
+
commission: 0.000
|
| 19 |
+
slippage_percent: 0.00 # percent value (0.01% -> 0.01)
|
| 20 |
+
adjust_prices: true
|
| 21 |
+
start_date: "2025-05-01"
|
| 22 |
+
end_date: "2025-12-31" # not used in bt_testing
|
| 23 |
+
# llms
|
| 24 |
+
openai_model: "gpt-5-nano"
|
| 25 |
+
claude_model: "claude-sonnet-4-20250514"
|
| 26 |
+
gemini_model: "gemini-2.5-flash"
|
| 27 |
+
deepseek_model: "deepseek-reasoner"
|
| 28 |
+
save_plt: false
|
| 29 |
+
grok4_model: "grok-4-fast-reasoning"
|
| 30 |
+
local: false
|
| 31 |
+
|
| 32 |
+
ticker_to_market:
|
| 33 |
+
AAPL: Apple Inc.
|
| 34 |
+
GOOGL: Alphabet/Google
|
| 35 |
+
AMZN: Amazon.com
|
| 36 |
+
TSLA: Tesla Inc.
|
| 37 |
+
JPM: JPMorgan Chase & Co.
|
| 38 |
+
V: Visa Inc.
|
| 39 |
+
SPY: S&P 500 ETF
|
| 40 |
+
QQQ: Nasdaq 100 ETF
|
| 41 |
+
MSFT: Microsoft Corp.
|
| 42 |
+
NVDA: NVIDIA Corp.
|
| 43 |
+
META: Meta Platforms
|
| 44 |
+
BRK-B: Berkshire Hathaway
|
| 45 |
+
UNH: UnitedHealth Group
|
| 46 |
+
XOM: Exxon Mobil Corp.
|
| 47 |
+
|
| 48 |
+
market_to_ticker:
|
| 49 |
+
Apple Inc.: AAPL
|
| 50 |
+
Alphabet/Google: GOOGL
|
| 51 |
+
Amazon.com: AMZN
|
| 52 |
+
Tesla Inc.: TSLA
|
| 53 |
+
JPMorgan Chase & Co.: JPM
|
| 54 |
+
Visa Inc.: V
|
| 55 |
+
S&P 500 ETF: SPY
|
| 56 |
+
Nasdaq 100 ETF: QQQ
|
| 57 |
+
Microsoft Corp.: MSFT
|
| 58 |
+
NVIDIA Corp.: NVDA
|
| 59 |
+
Meta Platforms: META
|
| 60 |
+
Berkshire Hathaway: BRK-B
|
| 61 |
+
UnitedHealth Group: UNH
|
| 62 |
+
Exxon Mobil Corp.: XOM
|
data_utils.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
os.environ.setdefault("MPLBACKEND", "Agg")
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import yfinance as yf
|
| 8 |
+
from fmp_python.fmp import FMP
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
CACHE_DIR = "data_cache"
|
| 13 |
+
os.makedirs(CACHE_DIR, exist_ok=True)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _get_cache_filename_yf(tckr_symbl, interval, start_date, end_date, adjust_prices):
|
| 17 |
+
start_str = start_date.replace("-", "_")
|
| 18 |
+
end_str = end_date.replace("-", "_")
|
| 19 |
+
adjust_str = "adj" if adjust_prices else "raw"
|
| 20 |
+
return os.path.join(CACHE_DIR, f"{tckr_symbl}_{interval}_{start_str}_to_{end_str}_{adjust_str}.csv")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _get_cache_filename_fmp(tckr_symbl: str, interval: str) -> str:
|
| 24 |
+
return os.path.join(CACHE_DIR, f"{tckr_symbl}_{interval}_fmp.csv")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _is_valid_cache(df: pd.DataFrame) -> bool:
|
| 28 |
+
if df.empty or len(df) < 2:
|
| 29 |
+
return False
|
| 30 |
+
if not isinstance(df.index, pd.DatetimeIndex):
|
| 31 |
+
return False
|
| 32 |
+
required_upper = ["Open", "High", "Low", "Close", "Volume"]
|
| 33 |
+
required_lower = ["open", "high", "low", "close", "volume"]
|
| 34 |
+
has_upper = all(col in df.columns for col in required_upper)
|
| 35 |
+
has_lower = all(col in df.columns for col in required_lower)
|
| 36 |
+
if not (has_upper or has_lower):
|
| 37 |
+
return False
|
| 38 |
+
return True
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _load_cached_data(cache_file: str) -> Optional[pd.DataFrame]:
|
| 42 |
+
if os.path.exists(cache_file):
|
| 43 |
+
try:
|
| 44 |
+
df = pd.read_csv(cache_file, index_col=0, parse_dates=True, header=0)
|
| 45 |
+
if isinstance(df.columns, pd.MultiIndex):
|
| 46 |
+
df.columns = df.columns.get_level_values(0)
|
| 47 |
+
if not isinstance(df.index, pd.DatetimeIndex):
|
| 48 |
+
try:
|
| 49 |
+
df.index = pd.to_datetime(df.index)
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f"Could not parse dates in cache: {e}, will re-download")
|
| 52 |
+
return None
|
| 53 |
+
expected_upper = ["Open", "High", "Low", "Close", "Volume"]
|
| 54 |
+
expected_lower = ["open", "high", "low", "close", "volume"]
|
| 55 |
+
actual_cols = list(df.columns)
|
| 56 |
+
has_upper = any(col in actual_cols for col in expected_upper)
|
| 57 |
+
has_lower = any(col in actual_cols for col in expected_lower)
|
| 58 |
+
if not (has_upper or has_lower):
|
| 59 |
+
print(f"Unexpected columns in cache: {actual_cols}, will re-download")
|
| 60 |
+
return None
|
| 61 |
+
print(f"Loaded cached data: {len(df)} rows, columns: {list(df.columns)} from {cache_file}")
|
| 62 |
+
return df
|
| 63 |
+
except Exception as e:
|
| 64 |
+
print(f"Error loading cache: {e}, will re-download")
|
| 65 |
+
import traceback
|
| 66 |
+
traceback.print_exc()
|
| 67 |
+
return None
|
| 68 |
+
return None
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _save_cached_data(df: pd.DataFrame, cache_file: str):
|
| 72 |
+
try:
|
| 73 |
+
df.to_csv(cache_file)
|
| 74 |
+
print(f"Cached data saved: {cache_file}")
|
| 75 |
+
except Exception as e:
|
| 76 |
+
print(f"Error saving cache: {e}")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# FMP interval -> max days per API chunk
|
| 80 |
+
FMP_INTERVAL_DAYS = {"1min": 2, "5min": 7, "15min": 38, "1hour": 70, "4hour": 160}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _fetch_fmp_range(fmp, tckr_symbl, interval, start_dt, end_dt, progress=None):
|
| 84 |
+
"""Download FMP data for a date range. Returns DataFrame with 'date' index."""
|
| 85 |
+
chunk_span = timedelta(days=FMP_INTERVAL_DAYS[interval])
|
| 86 |
+
frames = []
|
| 87 |
+
|
| 88 |
+
# Build chunk list
|
| 89 |
+
chunks = []
|
| 90 |
+
temp = start_dt
|
| 91 |
+
while temp <= end_dt:
|
| 92 |
+
chunks.append(temp)
|
| 93 |
+
temp = min(temp + chunk_span, end_dt) + timedelta(days=1)
|
| 94 |
+
|
| 95 |
+
# Download with progress
|
| 96 |
+
if progress:
|
| 97 |
+
chunk_iter = progress.tqdm(chunks, desc=f"FMP {interval}")
|
| 98 |
+
else:
|
| 99 |
+
chunk_iter = tqdm(chunks, desc=f"FMP {interval}")
|
| 100 |
+
|
| 101 |
+
for chunk_start in chunk_iter:
|
| 102 |
+
chunk_end = min(chunk_start + chunk_span, end_dt)
|
| 103 |
+
try:
|
| 104 |
+
chunk = fmp.get_historical_chart(
|
| 105 |
+
interval, tckr_symbl,
|
| 106 |
+
_from=chunk_start.strftime("%Y-%m-%d"),
|
| 107 |
+
_to=chunk_end.strftime("%Y-%m-%d")
|
| 108 |
+
)
|
| 109 |
+
except Exception as e:
|
| 110 |
+
raise ValueError(f"FMP download failed ({chunk_start.date()} to {chunk_end.date()}): {e}")
|
| 111 |
+
|
| 112 |
+
if chunk is not None and not chunk.empty:
|
| 113 |
+
chunk["date"] = pd.to_datetime(chunk["date"], errors="coerce")
|
| 114 |
+
chunk = chunk.dropna(subset=["date"])
|
| 115 |
+
if not chunk.empty:
|
| 116 |
+
frames.append(chunk)
|
| 117 |
+
|
| 118 |
+
if not frames:
|
| 119 |
+
return pd.DataFrame()
|
| 120 |
+
|
| 121 |
+
df = pd.concat(frames, ignore_index=True)
|
| 122 |
+
df["date"] = pd.to_datetime(df["date"], errors="coerce")
|
| 123 |
+
df = df.dropna(subset=["date"])
|
| 124 |
+
|
| 125 |
+
# Timezone handling
|
| 126 |
+
if not df.empty and df["date"].dt.tz is None:
|
| 127 |
+
df["date"] = df["date"].dt.tz_localize("America/New_York", nonexistent="shift_forward", ambiguous="NaT")
|
| 128 |
+
df["date"] = df["date"].dt.tz_convert("UTC").dt.tz_localize(None)
|
| 129 |
+
|
| 130 |
+
return df.sort_values("date").set_index("date")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _download_data_fmp(tckr_symbl, interval, date, progress=None, replay=False, compression=None):
|
| 134 |
+
if interval not in FMP_INTERVAL_DAYS:
|
| 135 |
+
raise ValueError(f"Unsupported FMP interval '{interval}'")
|
| 136 |
+
|
| 137 |
+
end_dt = datetime.strptime(date["end"], "%Y-%m-%d")
|
| 138 |
+
start_dt = datetime.strptime(date["start"], "%Y-%m-%d")
|
| 139 |
+
|
| 140 |
+
# Clamp to 15-year limit
|
| 141 |
+
max_lookback = end_dt - timedelta(days=15 * 365)
|
| 142 |
+
if start_dt < max_lookback:
|
| 143 |
+
print(f"Warning: start date clamped to {max_lookback.date()} (15y limit).")
|
| 144 |
+
start_dt = max_lookback
|
| 145 |
+
|
| 146 |
+
fmp = FMP(output_format="pandas", write_to_file=False)
|
| 147 |
+
cache_file = _get_cache_filename_fmp(tckr_symbl, interval)
|
| 148 |
+
cached_df = _load_cached_data(cache_file)
|
| 149 |
+
|
| 150 |
+
if cached_df is not None and _is_valid_cache(cached_df):
|
| 151 |
+
cache_start = cached_df.index.min().date()
|
| 152 |
+
cache_end = cached_df.index.max().date()
|
| 153 |
+
frames = []
|
| 154 |
+
|
| 155 |
+
# Download past data if needed
|
| 156 |
+
if start_dt.date() < cache_start:
|
| 157 |
+
past_end = datetime.combine(cache_start - timedelta(days=1), datetime.min.time())
|
| 158 |
+
print(f"Fetching past data: {start_dt.date()} to {past_end.date()}")
|
| 159 |
+
chunk_past = _fetch_fmp_range(fmp, tckr_symbl, interval, start_dt, past_end, progress)
|
| 160 |
+
if not chunk_past.empty:
|
| 161 |
+
frames.append(chunk_past)
|
| 162 |
+
|
| 163 |
+
frames.append(cached_df)
|
| 164 |
+
|
| 165 |
+
# Download current data if needed
|
| 166 |
+
if end_dt.date() > cache_end:
|
| 167 |
+
current_start = datetime.combine(cache_end + timedelta(days=1), datetime.min.time())
|
| 168 |
+
print(f"Fetching current data: {current_start.date()} to {end_dt.date()}")
|
| 169 |
+
chunk_current = _fetch_fmp_range(fmp, tckr_symbl, interval, current_start, end_dt, progress)
|
| 170 |
+
if not chunk_current.empty:
|
| 171 |
+
frames.append(chunk_current)
|
| 172 |
+
|
| 173 |
+
# Merge, dedupe, sort, save
|
| 174 |
+
if len(frames) > 1:
|
| 175 |
+
df = pd.concat(frames).sort_index()
|
| 176 |
+
df = df[~df.index.duplicated(keep='last')]
|
| 177 |
+
_save_cached_data(df, cache_file)
|
| 178 |
+
else:
|
| 179 |
+
df = cached_df
|
| 180 |
+
print(f"Using cached data: {len(df)} rows (no download needed)")
|
| 181 |
+
else:
|
| 182 |
+
# No cache - download full range
|
| 183 |
+
print(f"No cache, downloading: {start_dt.date()} to {end_dt.date()}")
|
| 184 |
+
df = _fetch_fmp_range(fmp, tckr_symbl, interval, start_dt, end_dt, progress)
|
| 185 |
+
if not df.empty:
|
| 186 |
+
_save_cached_data(df, cache_file)
|
| 187 |
+
|
| 188 |
+
if df.empty:
|
| 189 |
+
return df
|
| 190 |
+
|
| 191 |
+
# Filter to user's requested range
|
| 192 |
+
df = df[(df.index >= pd.Timestamp(start_dt)) & (df.index <= pd.Timestamp(end_dt))]
|
| 193 |
+
return df
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def _download_data_yf(tckr_symbl, interval, date, adjust_prices, auto_period=True, period="60d"):
|
| 197 |
+
try:
|
| 198 |
+
print("Interval: ", interval)
|
| 199 |
+
start_dt = datetime.strptime(date["start"], "%Y-%m-%d")
|
| 200 |
+
end_dt = datetime.strptime(date["end"], "%Y-%m-%d")
|
| 201 |
+
|
| 202 |
+
cache_file = _get_cache_filename_yf(tckr_symbl, interval, date["start"], date["end"], adjust_prices)
|
| 203 |
+
cached_df = _load_cached_data(cache_file)
|
| 204 |
+
if cached_df is not None and _is_valid_cache(cached_df):
|
| 205 |
+
df = cached_df
|
| 206 |
+
print(f"Using cached data: {len(df)} rows (no download needed)")
|
| 207 |
+
else:
|
| 208 |
+
print("No valid cache, downloading...")
|
| 209 |
+
if interval in ["1m", "2m", "5m", "15m", "30m", "60m", "1h"] and auto_period:
|
| 210 |
+
if interval in ["1m"]:
|
| 211 |
+
max_days = 7
|
| 212 |
+
elif interval in ["2m", "5m", "15m", "30m"]:
|
| 213 |
+
max_days = 60
|
| 214 |
+
else:
|
| 215 |
+
max_days = 730
|
| 216 |
+
desired_days = max(1, (end_dt - start_dt).days or 1)
|
| 217 |
+
clamped_days = min(desired_days, max_days)
|
| 218 |
+
period = f"{clamped_days}d"
|
| 219 |
+
df = yf.download(tckr_symbl, period=period, interval=interval, auto_adjust=adjust_prices)
|
| 220 |
+
print(f"Downloaded {interval} data for {period}")
|
| 221 |
+
else:
|
| 222 |
+
df = yf.download(tckr_symbl, start=date["start"], end=date["end"], interval=interval, auto_adjust=adjust_prices)
|
| 223 |
+
print(f"Downloaded data from {date['start']} to {date['end']} with {interval} interval")
|
| 224 |
+
|
| 225 |
+
if isinstance(df.columns, pd.MultiIndex):
|
| 226 |
+
df.columns = df.columns.get_level_values(0)
|
| 227 |
+
_save_cached_data(df, cache_file)
|
| 228 |
+
if isinstance(df.columns, pd.MultiIndex):
|
| 229 |
+
df.columns = df.columns.get_level_values(0)
|
| 230 |
+
if df.empty:
|
| 231 |
+
raise ValueError("No data available for the specified parameters!")
|
| 232 |
+
if df.index.tz is not None:
|
| 233 |
+
df.index = df.index.tz_localize(None)
|
| 234 |
+
print(f"Data points: {len(df)}")
|
| 235 |
+
return df
|
| 236 |
+
except Exception as e:
|
| 237 |
+
raise ValueError(f"Error downloading data: {e}")
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def get_data(
|
| 241 |
+
data_source: str,
|
| 242 |
+
tckr_symbl: str,
|
| 243 |
+
interval: str,
|
| 244 |
+
date: dict,
|
| 245 |
+
adjust_prices: bool = True,
|
| 246 |
+
auto_period: bool = True,
|
| 247 |
+
period: str = "60d",
|
| 248 |
+
upload_data: bool = False,
|
| 249 |
+
upload_data_path: str = None,
|
| 250 |
+
progress=None,
|
| 251 |
+
):
|
| 252 |
+
if upload_data:
|
| 253 |
+
if not upload_data_path:
|
| 254 |
+
raise ValueError("upload_data_path is required when upload_data is True.")
|
| 255 |
+
df = pd.read_csv(upload_data_path)
|
| 256 |
+
if df.shape[1] >= 2:
|
| 257 |
+
dt = pd.to_datetime(df.iloc[:, 0].astype(str) + " " + df.iloc[:, 1].astype(str), errors="coerce")
|
| 258 |
+
df = df.drop(columns=df.columns[:2])
|
| 259 |
+
else:
|
| 260 |
+
dt = pd.to_datetime(df.iloc[:, 0], errors="coerce")
|
| 261 |
+
df = df.drop(columns=df.columns[:1])
|
| 262 |
+
df.insert(0, "datetime", dt)
|
| 263 |
+
df = df.dropna(subset=["datetime"]).set_index("datetime")
|
| 264 |
+
expected_cols = ["open", "high", "low", "close", "volume"]
|
| 265 |
+
if len(df.columns) >= 5:
|
| 266 |
+
df.columns = list(expected_cols) + list(df.columns[len(expected_cols) :])
|
| 267 |
+
df.columns = [c.capitalize() for c in df.columns]
|
| 268 |
+
df.index = df.index.tz_localize(None)
|
| 269 |
+
return df
|
| 270 |
+
|
| 271 |
+
source = (data_source or "").lower()
|
| 272 |
+
loaders = {
|
| 273 |
+
"yahoofinance": lambda: _download_data_yf(tckr_symbl, interval, date, adjust_prices, auto_period, period),
|
| 274 |
+
"yf": lambda: _download_data_yf(tckr_symbl, interval, date, adjust_prices, auto_period, period),
|
| 275 |
+
"yahoo": lambda: _download_data_yf(tckr_symbl, interval, date, adjust_prices, auto_period, period),
|
| 276 |
+
"fmp": lambda: _download_data_fmp(tckr_symbl, interval, date, progress),
|
| 277 |
+
}
|
| 278 |
+
if source not in loaders:
|
| 279 |
+
raise ValueError(f"Invalid data source: {data_source}")
|
| 280 |
+
return loaders[source]()
|
| 281 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
backtrader
|
| 2 |
+
yfinance
|
| 3 |
+
pandas
|
| 4 |
+
matplotlib
|
| 5 |
+
python-dotenv
|
| 6 |
+
gradio
|
| 7 |
+
huggingface_hub
|
| 8 |
+
openai
|
| 9 |
+
anthropic
|
| 10 |
+
ipython
|
| 11 |
+
opencv-python
|
| 12 |
+
openpyxl
|
| 13 |
+
fmp_python
|
strategy_generator.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from uu import Error
|
| 2 |
+
import anthropic
|
| 3 |
+
import os
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
import yaml
|
| 6 |
+
from openai import OpenAI
|
| 7 |
+
from huggingface_hub import InferenceClient
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
load_dotenv(override=True)
|
| 11 |
+
os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY')
|
| 12 |
+
os.environ['ANTHROPIC_API_KEY'] = os.getenv('ANTHROPIC_API_KEY')
|
| 13 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 14 |
+
deepseek_api_key = os.getenv('DEEPSEEK_API_KEY')
|
| 15 |
+
grok_api_key = os.getenv("XAI_API_KEY")
|
| 16 |
+
|
| 17 |
+
with open('config/var_dev.yaml', 'r') as f:
|
| 18 |
+
config = yaml.safe_load(f)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# Logging stuff
|
| 22 |
+
if config["local"]:
|
| 23 |
+
hf_token = os.getenv('HF_TOKEN') # Sign in to HuggingFace Hub
|
| 24 |
+
else:
|
| 25 |
+
hf_token = None
|
| 26 |
+
#huggingface_hub.login(hf_token)
|
| 27 |
+
# Initialize clients
|
| 28 |
+
openai = OpenAI()
|
| 29 |
+
deepseek_api= OpenAI(
|
| 30 |
+
api_key=deepseek_api_key,
|
| 31 |
+
base_url="https://api.deepseek.com"
|
| 32 |
+
)
|
| 33 |
+
gemini_api = OpenAI(
|
| 34 |
+
api_key=google_api_key,
|
| 35 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 36 |
+
)
|
| 37 |
+
grok_api = OpenAI(api_key=grok_api_key, base_url="https://api.x.ai/v1")
|
| 38 |
+
claude = anthropic.Anthropic()
|
| 39 |
+
client = InferenceClient() # For HuggingFace Inference API
|
| 40 |
+
|
| 41 |
+
LLM_MODEL = None
|
| 42 |
+
|
| 43 |
+
def user_prompt_for(user_msg):
|
| 44 |
+
return f"""
|
| 45 |
+
Trading strategy description:
|
| 46 |
+
\"\"\"{user_msg}\"\"\"
|
| 47 |
+
|
| 48 |
+
Task:
|
| 49 |
+
- Convert the description into executable Python code.
|
| 50 |
+
- Use only the library {config["api_fin"]}.
|
| 51 |
+
- Respond only with valid Python code, following Python best practices
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
example_1= f'''
|
| 55 |
+
# User prompt:
|
| 56 |
+
# "Go long when the 10-period SMA crosses above the 100-period SMA,
|
| 57 |
+
# and exit when the 10-period SMA crosses below the 100-period SMA."
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# Generated Python code:
|
| 61 |
+
|
| 62 |
+
import backtrader as bt
|
| 63 |
+
class SmaCross(bt.Strategy):
|
| 64 |
+
"""
|
| 65 |
+
Simple moving average crossover strategy.
|
| 66 |
+
Buy when fast SMA crosses above slow SMA.
|
| 67 |
+
Sell when fast SMA crosses below slow SMA.
|
| 68 |
+
"""
|
| 69 |
+
params = dict(pfast=10, pslow=100)
|
| 70 |
+
|
| 71 |
+
def __init__(self):
|
| 72 |
+
self.sma_fast = bt.ind.SMA(period=self.p.pfast)
|
| 73 |
+
self.sma_slow = bt.ind.SMA(period=self.p.pslow)
|
| 74 |
+
self.crossover = bt.ind.CrossOver(self.sma_fast, self.sma_slow)
|
| 75 |
+
|
| 76 |
+
def next(self):
|
| 77 |
+
if not self.position:
|
| 78 |
+
if self.crossover > 0: # Golden cross
|
| 79 |
+
self.buy()
|
| 80 |
+
elif self.crossover < 0: # Death cross
|
| 81 |
+
self.close()
|
| 82 |
+
|
| 83 |
+
# Initialize Cerebro
|
| 84 |
+
cerebro = bt.Cerebro()
|
| 85 |
+
cerebro.addstrategy(SmaCross, pfast=10, pslow=100)
|
| 86 |
+
'''
|
| 87 |
+
list_of_pyclasses = [example_1]
|
| 88 |
+
system_message = f'''
|
| 89 |
+
You are a financial assistant specialized in transforming natural language descriptions of trading strategies into clean, production-ready Python code.
|
| 90 |
+
|
| 91 |
+
Guidelines:
|
| 92 |
+
- Use only the library {config["api_fin"]}.
|
| 93 |
+
- Always create a class with the abreviation of the strategy with the form `NameOfStrategy(bt.Strategy)`.
|
| 94 |
+
- Implement strategy logic in `__init__` (indicators/signals) and `next()` (trade execution).
|
| 95 |
+
- Implement the strategy for this intervall of time {config["interval"]}
|
| 96 |
+
- Finish with initializing the strategy in Cerebro:
|
| 97 |
+
cerebro = bt.Cerebro()
|
| 98 |
+
cerebro.addstrategy(MyStrategy, param1=value, param2=value)
|
| 99 |
+
- Keep code minimal, clear, and follow Java best practices (PEP8, clear naming, modularity).
|
| 100 |
+
- If a strategy cannot be implemented with {config["api_fin"]}, respond with: "Unable to implement with {config["api_fin"]}."
|
| 101 |
+
- If used any addional libraries, add it in the code: import MyUsedLibrary
|
| 102 |
+
- If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
Example(s) of transformation from user prompt of Python code: \n
|
| 106 |
+
'''
|
| 107 |
+
for pyclass in list_of_pyclasses:
|
| 108 |
+
system_message += pyclass
|
| 109 |
+
# Messages in Openai format
|
| 110 |
+
def messages_for(user_msg):
|
| 111 |
+
return [
|
| 112 |
+
{"role": "system", "content": system_message},
|
| 113 |
+
{"role": "user", "content": user_prompt_for(user_msg)}
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def stream_llms(user_msg, typ_llm="gpt"):
|
| 118 |
+
""" Stream responses from different LLMs based on user message and selected model. """
|
| 119 |
+
global LLM_MODEL
|
| 120 |
+
llm_key = typ_llm.lower()
|
| 121 |
+
model_overrides = {
|
| 122 |
+
"deepseek": ("DEEPSEEK_MODEL", config.get("deepseek_model")),
|
| 123 |
+
"gemini": ("GEMINI_MODEL", config.get("gemini_model")),
|
| 124 |
+
"grok4": ("GROK4_MODEL", config.get("grok4_model")),
|
| 125 |
+
"claude": ("CLAUDE_MODEL", config.get("claude_model")),
|
| 126 |
+
"gpt": ("OPENAI_MODEL", config.get("openai_model")),
|
| 127 |
+
}
|
| 128 |
+
if llm_key not in model_overrides:
|
| 129 |
+
raise ValueError(f"Name of model {llm_key} not in provided list")
|
| 130 |
+
env_var, default_model = model_overrides[llm_key]
|
| 131 |
+
if not default_model:
|
| 132 |
+
raise KeyError(f"Missing default model configuration for '{llm_key}'")
|
| 133 |
+
selected_model = os.getenv(env_var) or default_model
|
| 134 |
+
LLM_MODEL = selected_model
|
| 135 |
+
messages = messages_for(user_msg)
|
| 136 |
+
try:
|
| 137 |
+
if llm_key == "deepseek":
|
| 138 |
+
stream = deepseek_api.chat.completions.create(
|
| 139 |
+
model=selected_model,
|
| 140 |
+
messages=messages,
|
| 141 |
+
stream=True
|
| 142 |
+
)
|
| 143 |
+
elif llm_key == "gemini":
|
| 144 |
+
stream = gemini_api.chat.completions.create(
|
| 145 |
+
model=selected_model,
|
| 146 |
+
messages=messages,
|
| 147 |
+
stream=True
|
| 148 |
+
)
|
| 149 |
+
elif llm_key == "grok4":
|
| 150 |
+
stream = grok_api.chat.completions.create(
|
| 151 |
+
model=selected_model,
|
| 152 |
+
messages=messages,
|
| 153 |
+
stream= True
|
| 154 |
+
)
|
| 155 |
+
elif llm_key == "claude":
|
| 156 |
+
stream = claude.messages.stream(
|
| 157 |
+
model=selected_model,
|
| 158 |
+
max_tokens=2000,
|
| 159 |
+
system=messages[0]['content'],
|
| 160 |
+
messages=[messages[1]],
|
| 161 |
+
)
|
| 162 |
+
elif llm_key == "gpt":
|
| 163 |
+
stream = openai.chat.completions.create(model=selected_model, messages=messages, stream=True)
|
| 164 |
+
except Exception as e:
|
| 165 |
+
raise ValueError(f"Unknown model with error {e}")
|
| 166 |
+
|
| 167 |
+
reply = f"# Model {LLM_MODEL}\n"
|
| 168 |
+
|
| 169 |
+
if typ_llm.lower() == "claude":
|
| 170 |
+
with stream as stream_clde:
|
| 171 |
+
for fragment in stream_clde.text_stream:
|
| 172 |
+
reply += fragment
|
| 173 |
+
yield reply.replace("```python\n","").replace("```","")
|
| 174 |
+
|
| 175 |
+
else:
|
| 176 |
+
for chunk in stream:
|
| 177 |
+
if chunk and chunk.choices:
|
| 178 |
+
fragment = chunk.choices[0].delta.content or ""
|
| 179 |
+
reply += fragment
|
| 180 |
+
yield reply.replace("```python\n","").replace("```","")
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def stream_manager(user_msg, model):
|
| 185 |
+
""" Streaming manager for different LLMs based on user message and selected model. """
|
| 186 |
+
result = stream_llms(user_msg, model)
|
| 187 |
+
for stream_so_far in result:
|
| 188 |
+
yield stream_so_far
|
| 189 |
+
|
utils.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility helper functions used across the project.
|
| 2 |
+
|
| 3 |
+
This module centralizes small helpers such as writing generated code to file,
|
| 4 |
+
converting values to floats with defaults, and saving dataframe objects to CSV.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import tempfile
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
from typing import Any, Optional, Tuple
|
| 12 |
+
|
| 13 |
+
import gradio as gr
|
| 14 |
+
|
| 15 |
+
from fmp_python.fmp import FMP
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
"write_output",
|
| 19 |
+
"_to_float_or_default",
|
| 20 |
+
"save_df_to_csv",
|
| 21 |
+
"resolve_market_to_ticker",
|
| 22 |
+
"validate_date_range",
|
| 23 |
+
"validate_ticker_symbol",
|
| 24 |
+
"save_strategy_to_file",
|
| 25 |
+
"yf_interval_info",
|
| 26 |
+
"YF_INTERVALS",
|
| 27 |
+
"YF_TO_FMP_MAP",
|
| 28 |
+
"REPLAY_MAP",
|
| 29 |
+
"REPLAY_INTERVALS",
|
| 30 |
+
"update_replay_intervals",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
def save_strategy_to_file(code_text: str):
|
| 34 |
+
"""Persist generated strategy code into a temp file Gradio can expose."""
|
| 35 |
+
if not code_text:
|
| 36 |
+
return None
|
| 37 |
+
tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".py", prefix="strategy_")
|
| 38 |
+
tmp_file.write(code_text.encode("utf-8"))
|
| 39 |
+
tmp_file.flush()
|
| 40 |
+
tmp_file.close()
|
| 41 |
+
return tmp_file.name
|
| 42 |
+
|
| 43 |
+
def write_output(code: str, filename: str = "bt_strategy.py") -> None:
|
| 44 |
+
"""Write a string of code to a file.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
code: The string content to write to disk.
|
| 48 |
+
filename: The filename to use (defaults to `code.py`).
|
| 49 |
+
"""
|
| 50 |
+
with open(filename, "w") as f:
|
| 51 |
+
f.write(code)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _to_float_or_default(value: Any, default: Any) -> float:
|
| 55 |
+
"""Convert a possibly-empty value to float or return a default.
|
| 56 |
+
|
| 57 |
+
This mirrors behavior in the application code: if the input is None or an
|
| 58 |
+
empty string it returns the provided default cast to float.
|
| 59 |
+
"""
|
| 60 |
+
try:
|
| 61 |
+
if value is None or value == "":
|
| 62 |
+
raise ValueError
|
| 63 |
+
return float(value)
|
| 64 |
+
except (ValueError, TypeError):
|
| 65 |
+
return float(default)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def resolve_market_to_ticker(market_name: str, market_to_ticker: dict) -> str:
|
| 69 |
+
"""Return a ticker symbol for a market name or ticker string.
|
| 70 |
+
|
| 71 |
+
If market_name is a friendly name (in market_to_ticker), returns the mapped
|
| 72 |
+
ticker symbol; otherwise assumes the input is a ticker symbol and returns
|
| 73 |
+
the uppercased stripped version.
|
| 74 |
+
"""
|
| 75 |
+
if market_name in list(market_to_ticker.keys()):
|
| 76 |
+
return market_to_ticker[market_name]
|
| 77 |
+
return market_name.upper().strip()
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def validate_date_range(selected_start: str, selected_end: str, current_date_str: str) -> Tuple[str, str]:
|
| 81 |
+
"""Validate date strings in YYYY-MM-DD format and ensure start <= end <= today.
|
| 82 |
+
|
| 83 |
+
Returns a tuple (start, end) as strings. Raises ValueError with a user-facing
|
| 84 |
+
message on invalid input.
|
| 85 |
+
"""
|
| 86 |
+
try:
|
| 87 |
+
start_dt = datetime.strptime(selected_start, "%Y-%m-%d")
|
| 88 |
+
except ValueError:
|
| 89 |
+
raise ValueError(f"❌ Error: Start Date must be in format YYYY-MM-DD. Got '{selected_start}'.")
|
| 90 |
+
try:
|
| 91 |
+
end_dt = datetime.strptime(selected_end, "%Y-%m-%d")
|
| 92 |
+
except ValueError:
|
| 93 |
+
raise ValueError(f"❌Error: End Date must be in format YYYY-MM-DD. Got '{selected_end}'.")
|
| 94 |
+
today_dt = datetime.strptime(current_date_str, "%Y-%m-%d")
|
| 95 |
+
if start_dt > end_dt:
|
| 96 |
+
raise ValueError("❌ Error: Start Date must be earlier than or equal to End Date.")
|
| 97 |
+
if end_dt > today_dt:
|
| 98 |
+
raise ValueError(f"❌ Error: End Date cannot be in the future. Today is {current_date_str}.")
|
| 99 |
+
return selected_start, selected_end
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def validate_ticker_symbol(tckr_symbl: str, data_source: str = "yahoofinance") -> str:
|
| 103 |
+
"""Validate a ticker symbol using either FMP or yfinance based on the data source.
|
| 104 |
+
|
| 105 |
+
Raises ValueError on invalid tickers or if the provider fails to respond.
|
| 106 |
+
"""
|
| 107 |
+
source = (data_source or "").lower()
|
| 108 |
+
if source == "fmp":
|
| 109 |
+
try:
|
| 110 |
+
fmp = FMP()
|
| 111 |
+
result = fmp.get_quote(tckr_symbl)
|
| 112 |
+
if not result:
|
| 113 |
+
raise ValueError(f"❌ Error: Ticker {tckr_symbl} not found.")
|
| 114 |
+
else:
|
| 115 |
+
# If there's a valid quote, the ticker exists
|
| 116 |
+
print(f"{tckr_symbl} exists! Data:", result)
|
| 117 |
+
except Exception as e:
|
| 118 |
+
raise ValueError(f"❌ Error: Invalid ticker symbol '{tckr_symbl}' for FMP. {e}")
|
| 119 |
+
try:
|
| 120 |
+
import yfinance as yf
|
| 121 |
+
|
| 122 |
+
ticker_data = yf.Ticker(tckr_symbl)
|
| 123 |
+
info = ticker_data.info
|
| 124 |
+
if not info or "symbol" not in info:
|
| 125 |
+
raise ValueError(f"❌ Error: Invalid ticker symbol '{tckr_symbl}'.")
|
| 126 |
+
return info.get("longName") or info.get("shortName") or tckr_symbl
|
| 127 |
+
except ValueError:
|
| 128 |
+
raise
|
| 129 |
+
except Exception:
|
| 130 |
+
raise ValueError(f"❌ Error: Unable to validate ticker symbol '{tckr_symbl}'.")
|
| 131 |
+
|
| 132 |
+
YF_INTERVALS = ["1m", "5m", "15m", "30m", "1h", "1d"]
|
| 133 |
+
|
| 134 |
+
# Map YF intervals to FMP intervals (None = not supported)
|
| 135 |
+
YF_TO_FMP_MAP = {
|
| 136 |
+
"1m": "1min",
|
| 137 |
+
"5m": "5min",
|
| 138 |
+
"15m": "15min",
|
| 139 |
+
"30m": None,
|
| 140 |
+
"1h": "1hour",
|
| 141 |
+
"1d": None,
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
# Replay mapping: key -> (fmp_interval, yf_interval, compression)
|
| 145 |
+
REPLAY_MAP = {
|
| 146 |
+
"1min -> 5min": ("1min", "1m", 5),
|
| 147 |
+
"1min -> 15min": ("1min", "1m", 15),
|
| 148 |
+
"1min -> 30min": ("1min", "1m", 30),
|
| 149 |
+
"5min -> 15min": ("5min", "5m", 15),
|
| 150 |
+
"5min -> 30min": ("5min", "5m", 30),
|
| 151 |
+
}
|
| 152 |
+
REPLAY_INTERVALS = list(REPLAY_MAP.keys())
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def update_replay_intervals(replay_enabled: bool):
|
| 156 |
+
"""Update interval dropdown based on replay checkbox."""
|
| 157 |
+
if replay_enabled:
|
| 158 |
+
return gr.update(choices=REPLAY_INTERVALS, value=REPLAY_INTERVALS[0])
|
| 159 |
+
else:
|
| 160 |
+
return gr.update(choices=YF_INTERVALS, value="1d")
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def yf_interval_info(date_range: dict, interval: str, auto_period: bool) -> str:
|
| 164 |
+
"""Describe the effective yfinance download window for intraday intervals."""
|
| 165 |
+
intraday = ["1m", "2m", "5m", "15m", "30m", "60m", "1h"]
|
| 166 |
+
if not auto_period or interval not in intraday:
|
| 167 |
+
return ""
|
| 168 |
+
end_dt = datetime.strptime(date_range["end"], "%Y-%m-%d")
|
| 169 |
+
start_dt = datetime.strptime(date_range["start"], "%Y-%m-%d")
|
| 170 |
+
if interval == "1m":
|
| 171 |
+
max_days = 7
|
| 172 |
+
elif interval in ["2m", "5m", "15m", "30m"]:
|
| 173 |
+
max_days = 60
|
| 174 |
+
else: # 60m/1h
|
| 175 |
+
max_days = 730
|
| 176 |
+
desired_days = max(1, (end_dt - start_dt).days or 1)
|
| 177 |
+
used_days = min(desired_days, max_days)
|
| 178 |
+
effective_start = (end_dt - timedelta(days=used_days - 1)).strftime("%Y-%m-%d")
|
| 179 |
+
return f"YFinance download window: {effective_start} → {date_range['end']} ({used_days}d) @ {interval}"
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def save_df_to_csv(df: Optional[Any], filename: str) -> Optional[str]:
|
| 183 |
+
"""Save a pandas DataFrame (or df-like object) to CSV in the system temp dir.
|
| 184 |
+
|
| 185 |
+
Returns the full path to the saved file or None when the input could not be
|
| 186 |
+
converted into a non-empty DataFrame.
|
| 187 |
+
"""
|
| 188 |
+
if df is None:
|
| 189 |
+
return None
|
| 190 |
+
|
| 191 |
+
try:
|
| 192 |
+
import pandas as pd
|
| 193 |
+
except Exception:
|
| 194 |
+
# pandas is required for this helper; if it's not installed return None to
|
| 195 |
+
# fail gracefully so callers can handle the absence (e.g. in limited
|
| 196 |
+
# environments where CSV exports aren't available).
|
| 197 |
+
return None
|
| 198 |
+
|
| 199 |
+
if isinstance(df, pd.DataFrame):
|
| 200 |
+
df_to_save = df
|
| 201 |
+
else:
|
| 202 |
+
try:
|
| 203 |
+
df_to_save = pd.DataFrame(df)
|
| 204 |
+
except Exception:
|
| 205 |
+
return None
|
| 206 |
+
|
| 207 |
+
if df_to_save.empty:
|
| 208 |
+
return None
|
| 209 |
+
|
| 210 |
+
out_dir = tempfile.gettempdir()
|
| 211 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 212 |
+
filepath = os.path.join(out_dir, f"{filename}.csv")
|
| 213 |
+
df_to_save.to_csv(filepath, index=False)
|
| 214 |
+
return filepath
|
version.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
0.0.1
|