JuanFuriaz commited on
Commit
63bad2b
·
verified ·
1 Parent(s): ff1f271

Upload folder using huggingface_hub

Browse files
Files changed (15) hide show
  1. .gitignore +193 -0
  2. .python-version +1 -0
  3. README.md +240 -7
  4. StrategyGeneratorJupyter.ipynb +616 -0
  5. app.py +298 -0
  6. bt_strategies.py +463 -0
  7. bt_strategy.py +49 -0
  8. bt_testing.py +86 -0
  9. bt_utils.py +261 -0
  10. config/var_dev.yaml +62 -0
  11. data_utils.py +281 -0
  12. requirements.txt +13 -0
  13. strategy_generator.py +189 -0
  14. utils.py +214 -0
  15. 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
- pinned: false
 
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