sigma-terminal 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigma/__init__.py +9 -0
- sigma/__main__.py +6 -0
- sigma/app.py +947 -0
- sigma/core/__init__.py +18 -0
- sigma/core/agent.py +205 -0
- sigma/core/config.py +119 -0
- sigma/core/llm.py +794 -0
- sigma/core/models.py +153 -0
- sigma/setup.py +455 -0
- sigma/tools/__init__.py +5 -0
- sigma/tools/backtest.py +1506 -0
- sigma/tools/charts.py +400 -0
- sigma/tools/financial.py +1457 -0
- sigma/ui/__init__.py +1 -0
- sigma_terminal-2.0.0.dist-info/METADATA +222 -0
- sigma_terminal-2.0.0.dist-info/RECORD +19 -0
- sigma_terminal-2.0.0.dist-info/WHEEL +4 -0
- sigma_terminal-2.0.0.dist-info/entry_points.txt +2 -0
- sigma_terminal-2.0.0.dist-info/licenses/LICENSE +42 -0
sigma/tools/backtest.py
ADDED
|
@@ -0,0 +1,1506 @@
|
|
|
1
|
+
"""LEAN Engine backtesting tools for Sigma.
|
|
2
|
+
|
|
3
|
+
Generate QuantConnect LEAN-compatible Python algorithms for backtesting trading strategies.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# LEAN Algorithm Templates
|
|
13
|
+
LEAN_BASE_TEMPLATE = '''# LEAN Algorithm - Generated by Sigma
|
|
14
|
+
# Strategy: {strategy_name}
|
|
15
|
+
# Generated: {timestamp}
|
|
16
|
+
# Symbol: {symbol}
|
|
17
|
+
|
|
18
|
+
from AlgorithmImports import *
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class {class_name}(QCAlgorithm):
|
|
22
|
+
"""
|
|
23
|
+
{strategy_description}
|
|
24
|
+
|
|
25
|
+
Generated by Sigma Financial Research Agent
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def Initialize(self):
|
|
29
|
+
"""Initialize algorithm parameters and data."""
|
|
30
|
+
# Backtest period
|
|
31
|
+
self.SetStartDate({start_year}, {start_month}, {start_day})
|
|
32
|
+
self.SetEndDate({end_year}, {end_month}, {end_day})
|
|
33
|
+
|
|
34
|
+
# Starting capital
|
|
35
|
+
self.SetCash({initial_capital})
|
|
36
|
+
|
|
37
|
+
# Add securities
|
|
38
|
+
self.symbol = self.AddEquity("{symbol}", Resolution.Daily).Symbol
|
|
39
|
+
|
|
40
|
+
# Strategy parameters
|
|
41
|
+
{strategy_params}
|
|
42
|
+
|
|
43
|
+
# Indicators
|
|
44
|
+
{indicators}
|
|
45
|
+
|
|
46
|
+
# Tracking variables
|
|
47
|
+
self.previous_price = 0
|
|
48
|
+
self.trade_count = 0
|
|
49
|
+
|
|
50
|
+
def OnData(self, data: Slice):
|
|
51
|
+
"""Handle incoming data and execute strategy logic."""
|
|
52
|
+
if not data.Bars.ContainsKey(self.symbol):
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
bar = data.Bars[self.symbol]
|
|
56
|
+
price = bar.Close
|
|
57
|
+
|
|
58
|
+
{strategy_logic}
|
|
59
|
+
|
|
60
|
+
self.previous_price = price
|
|
61
|
+
|
|
62
|
+
def OnOrderEvent(self, orderEvent):
|
|
63
|
+
"""Handle order events."""
|
|
64
|
+
if orderEvent.Status == OrderStatus.Filled:
|
|
65
|
+
self.trade_count += 1
|
|
66
|
+
self.Debug(f"Order filled: {{orderEvent.Symbol}} {{orderEvent.FillQuantity}} @ {{orderEvent.FillPrice}}")
|
|
67
|
+
|
|
68
|
+
def OnEndOfAlgorithm(self):
|
|
69
|
+
"""Called at end of backtest."""
|
|
70
|
+
self.Debug(f"Total trades executed: {{self.trade_count}}")
|
|
71
|
+
'''
|
|
72
|
+
|
|
73
|
+
# Strategy-specific templates
|
|
74
|
+
STRATEGY_TEMPLATES = {
|
|
75
|
+
"sma_crossover": {
|
|
76
|
+
"name": "SMA Crossover",
|
|
77
|
+
"description": "Classic moving average crossover strategy. Goes long when fast SMA crosses above slow SMA, exits when it crosses below.",
|
|
78
|
+
"params": ''' self.fast_period = {fast_period}
|
|
79
|
+
self.slow_period = {slow_period}
|
|
80
|
+
self.position_size = {position_size} # Fraction of portfolio''',
|
|
81
|
+
"indicators": ''' self.fast_sma = self.SMA(self.symbol, self.fast_period, Resolution.Daily)
|
|
82
|
+
self.slow_sma = self.SMA(self.symbol, self.slow_period, Resolution.Daily)
|
|
83
|
+
|
|
84
|
+
# Warm up indicators
|
|
85
|
+
self.SetWarmUp(self.slow_period)''',
|
|
86
|
+
"logic": ''' # Wait for indicators to warm up
|
|
87
|
+
if self.IsWarmingUp:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if not self.fast_sma.IsReady or not self.slow_sma.IsReady:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
holdings = self.Portfolio[self.symbol].Quantity
|
|
94
|
+
|
|
95
|
+
# Buy signal: fast SMA crosses above slow SMA
|
|
96
|
+
if self.fast_sma.Current.Value > self.slow_sma.Current.Value:
|
|
97
|
+
if holdings <= 0:
|
|
98
|
+
self.SetHoldings(self.symbol, self.position_size)
|
|
99
|
+
self.Debug(f"BUY: Fast SMA ({self.fast_sma.Current.Value:.2f}) > Slow SMA ({self.slow_sma.Current.Value:.2f})")
|
|
100
|
+
|
|
101
|
+
# Sell signal: fast SMA crosses below slow SMA
|
|
102
|
+
elif self.fast_sma.Current.Value < self.slow_sma.Current.Value:
|
|
103
|
+
if holdings > 0:
|
|
104
|
+
self.Liquidate(self.symbol)
|
|
105
|
+
self.Debug(f"SELL: Fast SMA ({self.fast_sma.Current.Value:.2f}) < Slow SMA ({self.slow_sma.Current.Value:.2f})")''',
|
|
106
|
+
"default_params": {"fast_period": 10, "slow_period": 30, "position_size": 0.95}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
"rsi_mean_reversion": {
|
|
110
|
+
"name": "RSI Mean Reversion",
|
|
111
|
+
"description": "Mean reversion strategy using RSI. Buys when RSI is oversold and sells when overbought.",
|
|
112
|
+
"params": ''' self.rsi_period = {rsi_period}
|
|
113
|
+
self.oversold = {oversold}
|
|
114
|
+
self.overbought = {overbought}
|
|
115
|
+
self.position_size = {position_size}''',
|
|
116
|
+
"indicators": ''' self.rsi = self.RSI(self.symbol, self.rsi_period, MovingAverageType.Wilders, Resolution.Daily)
|
|
117
|
+
|
|
118
|
+
# Warm up
|
|
119
|
+
self.SetWarmUp(self.rsi_period * 2)''',
|
|
120
|
+
"logic": ''' if self.IsWarmingUp or not self.rsi.IsReady:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
holdings = self.Portfolio[self.symbol].Quantity
|
|
124
|
+
rsi_value = self.rsi.Current.Value
|
|
125
|
+
|
|
126
|
+
# Buy when oversold
|
|
127
|
+
if rsi_value < self.oversold:
|
|
128
|
+
if holdings <= 0:
|
|
129
|
+
self.SetHoldings(self.symbol, self.position_size)
|
|
130
|
+
self.Debug(f"BUY: RSI oversold at {rsi_value:.2f}")
|
|
131
|
+
|
|
132
|
+
# Sell when overbought
|
|
133
|
+
elif rsi_value > self.overbought:
|
|
134
|
+
if holdings > 0:
|
|
135
|
+
self.Liquidate(self.symbol)
|
|
136
|
+
self.Debug(f"SELL: RSI overbought at {rsi_value:.2f}")''',
|
|
137
|
+
"default_params": {"rsi_period": 14, "oversold": 30, "overbought": 70, "position_size": 0.95}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
"macd_momentum": {
|
|
141
|
+
"name": "MACD Momentum",
|
|
142
|
+
"description": "Momentum strategy using MACD histogram. Enters on positive histogram, exits on negative.",
|
|
143
|
+
"params": ''' self.fast_period = {fast_period}
|
|
144
|
+
self.slow_period = {slow_period}
|
|
145
|
+
self.signal_period = {signal_period}
|
|
146
|
+
self.position_size = {position_size}''',
|
|
147
|
+
"indicators": ''' self.macd = self.MACD(self.symbol, self.fast_period, self.slow_period, self.signal_period,
|
|
148
|
+
MovingAverageType.Exponential, Resolution.Daily)
|
|
149
|
+
|
|
150
|
+
# Warm up
|
|
151
|
+
self.SetWarmUp(self.slow_period + self.signal_period)''',
|
|
152
|
+
"logic": ''' if self.IsWarmingUp or not self.macd.IsReady:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
holdings = self.Portfolio[self.symbol].Quantity
|
|
156
|
+
histogram = self.macd.Histogram.Current.Value
|
|
157
|
+
|
|
158
|
+
# Buy when histogram turns positive
|
|
159
|
+
if histogram > 0:
|
|
160
|
+
if holdings <= 0:
|
|
161
|
+
self.SetHoldings(self.symbol, self.position_size)
|
|
162
|
+
self.Debug(f"BUY: MACD histogram positive at {histogram:.4f}")
|
|
163
|
+
|
|
164
|
+
# Sell when histogram turns negative
|
|
165
|
+
elif histogram < 0:
|
|
166
|
+
if holdings > 0:
|
|
167
|
+
self.Liquidate(self.symbol)
|
|
168
|
+
self.Debug(f"SELL: MACD histogram negative at {histogram:.4f}")''',
|
|
169
|
+
"default_params": {"fast_period": 12, "slow_period": 26, "signal_period": 9, "position_size": 0.95}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
"bollinger_bands": {
|
|
173
|
+
"name": "Bollinger Bands",
|
|
174
|
+
"description": "Mean reversion strategy using Bollinger Bands. Buys at lower band, sells at upper band.",
|
|
175
|
+
"params": ''' self.bb_period = {bb_period}
|
|
176
|
+
self.std_dev = {std_dev}
|
|
177
|
+
self.position_size = {position_size}''',
|
|
178
|
+
"indicators": ''' self.bb = self.BB(self.symbol, self.bb_period, self.std_dev, Resolution.Daily)
|
|
179
|
+
|
|
180
|
+
# Warm up
|
|
181
|
+
self.SetWarmUp(self.bb_period)''',
|
|
182
|
+
"logic": ''' if self.IsWarmingUp or not self.bb.IsReady:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
holdings = self.Portfolio[self.symbol].Quantity
|
|
186
|
+
upper = self.bb.UpperBand.Current.Value
|
|
187
|
+
lower = self.bb.LowerBand.Current.Value
|
|
188
|
+
middle = self.bb.MiddleBand.Current.Value
|
|
189
|
+
|
|
190
|
+
# Buy at lower band
|
|
191
|
+
if price <= lower:
|
|
192
|
+
if holdings <= 0:
|
|
193
|
+
self.SetHoldings(self.symbol, self.position_size)
|
|
194
|
+
self.Debug(f"BUY: Price ({price:.2f}) at lower band ({lower:.2f})")
|
|
195
|
+
|
|
196
|
+
# Sell at upper band
|
|
197
|
+
elif price >= upper:
|
|
198
|
+
if holdings > 0:
|
|
199
|
+
self.Liquidate(self.symbol)
|
|
200
|
+
self.Debug(f"SELL: Price ({price:.2f}) at upper band ({upper:.2f})")''',
|
|
201
|
+
"default_params": {"bb_period": 20, "std_dev": 2, "position_size": 0.95}
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
"dual_momentum": {
|
|
205
|
+
"name": "Dual Momentum",
|
|
206
|
+
"description": "Trend-following strategy using both absolute and relative momentum.",
|
|
207
|
+
"params": ''' self.lookback = {lookback}
|
|
208
|
+
self.position_size = {position_size}
|
|
209
|
+
self.benchmark = self.AddEquity("SPY", Resolution.Daily).Symbol''',
|
|
210
|
+
"indicators": ''' self.mom = self.MOM(self.symbol, self.lookback, Resolution.Daily)
|
|
211
|
+
self.benchmark_mom = self.MOM(self.benchmark, self.lookback, Resolution.Daily)
|
|
212
|
+
|
|
213
|
+
# Risk-free rate proxy
|
|
214
|
+
self.bond = self.AddEquity("TLT", Resolution.Daily).Symbol
|
|
215
|
+
|
|
216
|
+
# Warm up
|
|
217
|
+
self.SetWarmUp(self.lookback)''',
|
|
218
|
+
"logic": ''' if self.IsWarmingUp or not self.mom.IsReady:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
holdings = self.Portfolio[self.symbol].Quantity
|
|
222
|
+
stock_mom = self.mom.Current.Value
|
|
223
|
+
bench_mom = self.benchmark_mom.Current.Value
|
|
224
|
+
|
|
225
|
+
# Absolute momentum: only invest if positive
|
|
226
|
+
# Relative momentum: invest in stock if beats benchmark
|
|
227
|
+
if stock_mom > 0 and stock_mom > bench_mom:
|
|
228
|
+
if holdings <= 0:
|
|
229
|
+
self.SetHoldings(self.symbol, self.position_size)
|
|
230
|
+
self.Debug(f"BUY: Dual momentum positive (Stock: {stock_mom:.2f}, Bench: {bench_mom:.2f})")
|
|
231
|
+
else:
|
|
232
|
+
if holdings > 0:
|
|
233
|
+
self.Liquidate(self.symbol)
|
|
234
|
+
self.Debug(f"SELL: Dual momentum negative")''',
|
|
235
|
+
"default_params": {"lookback": 252, "position_size": 0.95}
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
"breakout": {
|
|
239
|
+
"name": "Breakout Strategy",
|
|
240
|
+
"description": "Donchian channel breakout strategy. Enters on new highs, exits on new lows.",
|
|
241
|
+
"params": ''' self.entry_period = {entry_period}
|
|
242
|
+
self.exit_period = {exit_period}
|
|
243
|
+
self.position_size = {position_size}''',
|
|
244
|
+
"indicators": ''' self.entry_high = self.MAX(self.symbol, self.entry_period, Resolution.Daily)
|
|
245
|
+
self.exit_low = self.MIN(self.symbol, self.exit_period, Resolution.Daily)
|
|
246
|
+
|
|
247
|
+
# Warm up
|
|
248
|
+
self.SetWarmUp(max(self.entry_period, self.exit_period))''',
|
|
249
|
+
"logic": ''' if self.IsWarmingUp or not self.entry_high.IsReady:
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
holdings = self.Portfolio[self.symbol].Quantity
|
|
253
|
+
highest = self.entry_high.Current.Value
|
|
254
|
+
lowest = self.exit_low.Current.Value
|
|
255
|
+
|
|
256
|
+
# Buy on breakout above highest high
|
|
257
|
+
if price >= highest:
|
|
258
|
+
if holdings <= 0:
|
|
259
|
+
self.SetHoldings(self.symbol, self.position_size)
|
|
260
|
+
self.Debug(f"BUY: Breakout above {highest:.2f}")
|
|
261
|
+
|
|
262
|
+
# Sell on breakdown below lowest low
|
|
263
|
+
elif price <= lowest:
|
|
264
|
+
if holdings > 0:
|
|
265
|
+
self.Liquidate(self.symbol)
|
|
266
|
+
self.Debug(f"SELL: Breakdown below {lowest:.2f}")''',
|
|
267
|
+
"default_params": {"entry_period": 20, "exit_period": 10, "position_size": 0.95}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_available_strategies() -> dict[str, dict]:
|
|
273
|
+
"""Get list of available backtesting strategies."""
|
|
274
|
+
return {
|
|
275
|
+
name: {
|
|
276
|
+
"name": s["name"],
|
|
277
|
+
"description": s["description"],
|
|
278
|
+
"default_params": s["default_params"]
|
|
279
|
+
}
|
|
280
|
+
for name, s in STRATEGY_TEMPLATES.items()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def generate_lean_algorithm(
|
|
285
|
+
symbol: str,
|
|
286
|
+
strategy: str,
|
|
287
|
+
start_date: Optional[str] = None,
|
|
288
|
+
end_date: Optional[str] = None,
|
|
289
|
+
initial_capital: float = 100000,
|
|
290
|
+
params: Optional[dict] = None,
|
|
291
|
+
output_dir: Optional[str] = None,
|
|
292
|
+
) -> dict[str, Any]:
|
|
293
|
+
"""Generate a LEAN-compatible Python algorithm file.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
symbol: Stock ticker symbol
|
|
297
|
+
strategy: Strategy type (sma_crossover, rsi_mean_reversion, etc.)
|
|
298
|
+
start_date: Backtest start date (YYYY-MM-DD)
|
|
299
|
+
end_date: Backtest end date (YYYY-MM-DD)
|
|
300
|
+
initial_capital: Starting capital
|
|
301
|
+
params: Strategy-specific parameters (optional)
|
|
302
|
+
output_dir: Directory to save the file (optional)
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Dict with file path and algorithm details
|
|
306
|
+
"""
|
|
307
|
+
if strategy not in STRATEGY_TEMPLATES:
|
|
308
|
+
available = list(STRATEGY_TEMPLATES.keys())
|
|
309
|
+
return {"error": f"Unknown strategy '{strategy}'. Available: {available}"}
|
|
310
|
+
|
|
311
|
+
template = STRATEGY_TEMPLATES[strategy]
|
|
312
|
+
|
|
313
|
+
# Parse dates
|
|
314
|
+
if start_date:
|
|
315
|
+
start = datetime.strptime(start_date, "%Y-%m-%d")
|
|
316
|
+
else:
|
|
317
|
+
start = datetime.now() - timedelta(days=365 * 2) # 2 years back
|
|
318
|
+
|
|
319
|
+
if end_date:
|
|
320
|
+
end = datetime.strptime(end_date, "%Y-%m-%d")
|
|
321
|
+
else:
|
|
322
|
+
end = datetime.now()
|
|
323
|
+
|
|
324
|
+
# Merge default params with custom params
|
|
325
|
+
strategy_params = template["default_params"].copy()
|
|
326
|
+
if params:
|
|
327
|
+
strategy_params.update(params)
|
|
328
|
+
|
|
329
|
+
# Create class name
|
|
330
|
+
class_name = f"Sigma{strategy.title().replace('_', '')}Algorithm"
|
|
331
|
+
|
|
332
|
+
# Format params
|
|
333
|
+
params_code = template["params"].format(**strategy_params)
|
|
334
|
+
indicators_code = template["indicators"]
|
|
335
|
+
logic_code = template["logic"]
|
|
336
|
+
|
|
337
|
+
# Generate the algorithm
|
|
338
|
+
algorithm_code = LEAN_BASE_TEMPLATE.format(
|
|
339
|
+
strategy_name=template["name"],
|
|
340
|
+
timestamp=datetime.now().isoformat(),
|
|
341
|
+
symbol=symbol.upper(),
|
|
342
|
+
class_name=class_name,
|
|
343
|
+
strategy_description=template["description"],
|
|
344
|
+
start_year=start.year,
|
|
345
|
+
start_month=start.month,
|
|
346
|
+
start_day=start.day,
|
|
347
|
+
end_year=end.year,
|
|
348
|
+
end_month=end.month,
|
|
349
|
+
end_day=end.day,
|
|
350
|
+
initial_capital=int(initial_capital),
|
|
351
|
+
strategy_params=params_code,
|
|
352
|
+
indicators=indicators_code,
|
|
353
|
+
strategy_logic=logic_code,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Create output directory
|
|
357
|
+
if output_dir is None:
|
|
358
|
+
output_dir = os.path.expanduser("~/sigma_backtests")
|
|
359
|
+
|
|
360
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
361
|
+
|
|
362
|
+
# Save algorithm file
|
|
363
|
+
filename = f"{symbol.lower()}_{strategy}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.py"
|
|
364
|
+
filepath = os.path.join(output_dir, filename)
|
|
365
|
+
|
|
366
|
+
with open(filepath, 'w') as f:
|
|
367
|
+
f.write(algorithm_code)
|
|
368
|
+
|
|
369
|
+
# Also save config
|
|
370
|
+
config = {
|
|
371
|
+
"algorithm-type-name": class_name,
|
|
372
|
+
"algorithm-language": "Python",
|
|
373
|
+
"parameters": strategy_params,
|
|
374
|
+
"symbol": symbol.upper(),
|
|
375
|
+
"strategy": strategy,
|
|
376
|
+
"start_date": start.strftime("%Y-%m-%d"),
|
|
377
|
+
"end_date": end.strftime("%Y-%m-%d"),
|
|
378
|
+
"initial_capital": initial_capital,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
config_path = filepath.replace('.py', '_config.json')
|
|
382
|
+
with open(config_path, 'w') as f:
|
|
383
|
+
json.dump(config, f, indent=2)
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
"success": True,
|
|
387
|
+
"algorithm_file": filepath,
|
|
388
|
+
"config_file": config_path,
|
|
389
|
+
"class_name": class_name,
|
|
390
|
+
"strategy": template["name"],
|
|
391
|
+
"description": template["description"],
|
|
392
|
+
"symbol": symbol.upper(),
|
|
393
|
+
"period": f"{start.strftime('%Y-%m-%d')} to {end.strftime('%Y-%m-%d')}",
|
|
394
|
+
"initial_capital": f"${initial_capital:,.0f}",
|
|
395
|
+
"parameters": strategy_params,
|
|
396
|
+
"instructions": [
|
|
397
|
+
"1. Install LEAN: pip install lean",
|
|
398
|
+
"2. Initialize workspace: lean init",
|
|
399
|
+
f"3. Copy {filename} to your LEAN project",
|
|
400
|
+
"4. Run backtest: lean backtest <project-name>",
|
|
401
|
+
"Or upload to QuantConnect.com for cloud backtesting"
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def generate_custom_algorithm(
|
|
407
|
+
symbol: str,
|
|
408
|
+
entry_conditions: list[str],
|
|
409
|
+
exit_conditions: list[str],
|
|
410
|
+
indicators: list[str],
|
|
411
|
+
start_date: Optional[str] = None,
|
|
412
|
+
end_date: Optional[str] = None,
|
|
413
|
+
initial_capital: float = 100000,
|
|
414
|
+
output_dir: Optional[str] = None,
|
|
415
|
+
) -> dict[str, Any]:
|
|
416
|
+
"""Generate a custom LEAN algorithm with user-specified conditions.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
symbol: Stock ticker
|
|
420
|
+
entry_conditions: List of entry condition descriptions
|
|
421
|
+
exit_conditions: List of exit condition descriptions
|
|
422
|
+
indicators: List of indicators to use
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Dict with file path and details
|
|
426
|
+
"""
|
|
427
|
+
# Map indicator names to LEAN code
|
|
428
|
+
indicator_map = {
|
|
429
|
+
"sma": ("SMA", "sma_{period}", "self.SMA(self.symbol, {period}, Resolution.Daily)"),
|
|
430
|
+
"ema": ("EMA", "ema_{period}", "self.EMA(self.symbol, {period}, Resolution.Daily)"),
|
|
431
|
+
"rsi": ("RSI", "rsi", "self.RSI(self.symbol, 14, MovingAverageType.Wilders, Resolution.Daily)"),
|
|
432
|
+
"macd": ("MACD", "macd", "self.MACD(self.symbol, 12, 26, 9, MovingAverageType.Exponential, Resolution.Daily)"),
|
|
433
|
+
"bb": ("Bollinger Bands", "bb", "self.BB(self.symbol, 20, 2, Resolution.Daily)"),
|
|
434
|
+
"atr": ("ATR", "atr", "self.ATR(self.symbol, 14, Resolution.Daily)"),
|
|
435
|
+
"adx": ("ADX", "adx", "self.ADX(self.symbol, 14, Resolution.Daily)"),
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
# Build indicator code
|
|
439
|
+
indicator_code_lines = []
|
|
440
|
+
for ind in indicators:
|
|
441
|
+
ind_lower = ind.lower()
|
|
442
|
+
for key, (name, var, code) in indicator_map.items():
|
|
443
|
+
if key in ind_lower:
|
|
444
|
+
indicator_code_lines.append(f" self.{var} = {code}")
|
|
445
|
+
break
|
|
446
|
+
|
|
447
|
+
indicator_code = "\n".join(indicator_code_lines) if indicator_code_lines else " pass # No indicators"
|
|
448
|
+
|
|
449
|
+
# Build entry/exit logic (basic template)
|
|
450
|
+
entry_comment = "\n".join([f" # Entry: {c}" for c in entry_conditions])
|
|
451
|
+
exit_comment = "\n".join([f" # Exit: {c}" for c in exit_conditions])
|
|
452
|
+
|
|
453
|
+
logic_code = f''' {entry_comment}
|
|
454
|
+
# TODO: Implement entry logic based on conditions above
|
|
455
|
+
|
|
456
|
+
{exit_comment}
|
|
457
|
+
# TODO: Implement exit logic based on conditions above
|
|
458
|
+
|
|
459
|
+
holdings = self.Portfolio[self.symbol].Quantity
|
|
460
|
+
|
|
461
|
+
# Example entry (customize based on your conditions)
|
|
462
|
+
if not self.Portfolio[self.symbol].Invested:
|
|
463
|
+
self.SetHoldings(self.symbol, 0.95)
|
|
464
|
+
self.Debug(f"BUY at {{price}}")'''
|
|
465
|
+
|
|
466
|
+
# Parse dates
|
|
467
|
+
if start_date:
|
|
468
|
+
start = datetime.strptime(start_date, "%Y-%m-%d")
|
|
469
|
+
else:
|
|
470
|
+
start = datetime.now() - timedelta(days=365 * 2)
|
|
471
|
+
|
|
472
|
+
if end_date:
|
|
473
|
+
end = datetime.strptime(end_date, "%Y-%m-%d")
|
|
474
|
+
else:
|
|
475
|
+
end = datetime.now()
|
|
476
|
+
|
|
477
|
+
# Generate algorithm
|
|
478
|
+
algorithm_code = LEAN_BASE_TEMPLATE.format(
|
|
479
|
+
strategy_name="Custom Strategy",
|
|
480
|
+
timestamp=datetime.now().isoformat(),
|
|
481
|
+
symbol=symbol.upper(),
|
|
482
|
+
class_name="SigmaCustomAlgorithm",
|
|
483
|
+
strategy_description=f"Custom strategy for {symbol.upper()}\nEntry: {', '.join(entry_conditions)}\nExit: {', '.join(exit_conditions)}",
|
|
484
|
+
start_year=start.year,
|
|
485
|
+
start_month=start.month,
|
|
486
|
+
start_day=start.day,
|
|
487
|
+
end_year=end.year,
|
|
488
|
+
end_month=end.month,
|
|
489
|
+
end_day=end.day,
|
|
490
|
+
initial_capital=int(initial_capital),
|
|
491
|
+
strategy_params=" self.position_size = 0.95",
|
|
492
|
+
indicators=indicator_code,
|
|
493
|
+
strategy_logic=logic_code,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Save
|
|
497
|
+
if output_dir is None:
|
|
498
|
+
output_dir = os.path.expanduser("~/sigma_backtests")
|
|
499
|
+
|
|
500
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
501
|
+
filename = f"{symbol.lower()}_custom_{datetime.now().strftime('%Y%m%d_%H%M%S')}.py"
|
|
502
|
+
filepath = os.path.join(output_dir, filename)
|
|
503
|
+
|
|
504
|
+
with open(filepath, 'w') as f:
|
|
505
|
+
f.write(algorithm_code)
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
"success": True,
|
|
509
|
+
"algorithm_file": filepath,
|
|
510
|
+
"symbol": symbol.upper(),
|
|
511
|
+
"strategy": "Custom",
|
|
512
|
+
"entry_conditions": entry_conditions,
|
|
513
|
+
"exit_conditions": exit_conditions,
|
|
514
|
+
"indicators": indicators,
|
|
515
|
+
"note": "This is a template. Please review and customize the entry/exit logic before running."
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def run_backtest(
|
|
520
|
+
symbol: str,
|
|
521
|
+
strategy: str = "sma_crossover",
|
|
522
|
+
period: str = "2y",
|
|
523
|
+
initial_capital: float = 100000,
|
|
524
|
+
**strategy_params
|
|
525
|
+
) -> dict[str, Any]:
|
|
526
|
+
"""Run a backtest using LEAN CLI.
|
|
527
|
+
|
|
528
|
+
This will:
|
|
529
|
+
1. Check if LEAN CLI is installed, install if needed
|
|
530
|
+
2. Initialize LEAN workspace if needed
|
|
531
|
+
3. Generate the algorithm
|
|
532
|
+
4. Run the backtest
|
|
533
|
+
5. Return results
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
symbol: Stock ticker symbol
|
|
537
|
+
strategy: Strategy name (sma_crossover, rsi_mean_reversion, macd_momentum, etc.)
|
|
538
|
+
period: Backtest period (1y, 2y, 5y)
|
|
539
|
+
initial_capital: Starting capital
|
|
540
|
+
**strategy_params: Strategy-specific parameters
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
Backtest results including performance metrics
|
|
544
|
+
"""
|
|
545
|
+
import subprocess
|
|
546
|
+
import shutil
|
|
547
|
+
|
|
548
|
+
results = {
|
|
549
|
+
"symbol": symbol.upper(),
|
|
550
|
+
"strategy": strategy,
|
|
551
|
+
"status": "pending",
|
|
552
|
+
"steps": []
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# Step 1: Check if LEAN CLI is installed
|
|
556
|
+
lean_path = shutil.which("lean")
|
|
557
|
+
|
|
558
|
+
if not lean_path:
|
|
559
|
+
results["steps"].append("LEAN CLI not found - installing...")
|
|
560
|
+
try:
|
|
561
|
+
subprocess.run(
|
|
562
|
+
["pip", "install", "lean"],
|
|
563
|
+
capture_output=True,
|
|
564
|
+
text=True,
|
|
565
|
+
check=True
|
|
566
|
+
)
|
|
567
|
+
results["steps"].append("LEAN CLI installed successfully")
|
|
568
|
+
except subprocess.CalledProcessError as e:
|
|
569
|
+
results["status"] = "error"
|
|
570
|
+
results["error"] = f"Failed to install LEAN CLI: {e.stderr}"
|
|
571
|
+
results["instructions"] = [
|
|
572
|
+
"Please install LEAN CLI manually:",
|
|
573
|
+
" pip install lean",
|
|
574
|
+
"Then run this command again."
|
|
575
|
+
]
|
|
576
|
+
return results
|
|
577
|
+
else:
|
|
578
|
+
results["steps"].append(f"LEAN CLI found at {lean_path}")
|
|
579
|
+
|
|
580
|
+
# Step 2: Setup LEAN workspace
|
|
581
|
+
lean_workspace = os.path.expanduser("~/sigma_lean")
|
|
582
|
+
config_file = os.path.join(lean_workspace, "lean.json")
|
|
583
|
+
|
|
584
|
+
if not os.path.exists(config_file):
|
|
585
|
+
results["steps"].append("Initializing LEAN workspace...")
|
|
586
|
+
os.makedirs(lean_workspace, exist_ok=True)
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
subprocess.run(
|
|
590
|
+
["lean", "init"],
|
|
591
|
+
cwd=lean_workspace,
|
|
592
|
+
capture_output=True,
|
|
593
|
+
text=True,
|
|
594
|
+
check=True
|
|
595
|
+
)
|
|
596
|
+
results["steps"].append(f"LEAN workspace initialized at {lean_workspace}")
|
|
597
|
+
except subprocess.CalledProcessError as e:
|
|
598
|
+
# Try creating minimal config
|
|
599
|
+
minimal_config = {
|
|
600
|
+
"data-folder": os.path.join(lean_workspace, "data"),
|
|
601
|
+
"results-destination-folder": os.path.join(lean_workspace, "results")
|
|
602
|
+
}
|
|
603
|
+
os.makedirs(os.path.join(lean_workspace, "data"), exist_ok=True)
|
|
604
|
+
os.makedirs(os.path.join(lean_workspace, "results"), exist_ok=True)
|
|
605
|
+
|
|
606
|
+
with open(config_file, 'w') as f:
|
|
607
|
+
json.dump(minimal_config, f, indent=2)
|
|
608
|
+
results["steps"].append("Created minimal LEAN config")
|
|
609
|
+
else:
|
|
610
|
+
results["steps"].append(f"Using LEAN workspace at {lean_workspace}")
|
|
611
|
+
|
|
612
|
+
# Step 3: Generate the algorithm
|
|
613
|
+
results["steps"].append(f"Generating {strategy} algorithm for {symbol.upper()}...")
|
|
614
|
+
|
|
615
|
+
algo_result = generate_lean_algorithm(
|
|
616
|
+
symbol=symbol,
|
|
617
|
+
strategy=strategy,
|
|
618
|
+
initial_capital=initial_capital,
|
|
619
|
+
output_dir=lean_workspace,
|
|
620
|
+
**strategy_params
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
if not algo_result.get("success"):
|
|
624
|
+
results["status"] = "error"
|
|
625
|
+
results["error"] = "Failed to generate algorithm"
|
|
626
|
+
return results
|
|
627
|
+
|
|
628
|
+
algo_file = algo_result["algorithm_file"]
|
|
629
|
+
results["algorithm_file"] = algo_file
|
|
630
|
+
results["steps"].append(f"Algorithm saved to {algo_file}")
|
|
631
|
+
|
|
632
|
+
# Step 4: Run the backtest
|
|
633
|
+
results["steps"].append("Running LEAN backtest...")
|
|
634
|
+
|
|
635
|
+
try:
|
|
636
|
+
run_result = subprocess.run(
|
|
637
|
+
["lean", "backtest", algo_file],
|
|
638
|
+
cwd=lean_workspace,
|
|
639
|
+
capture_output=True,
|
|
640
|
+
text=True,
|
|
641
|
+
timeout=300 # 5 minute timeout
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
if run_result.returncode == 0:
|
|
645
|
+
results["status"] = "success"
|
|
646
|
+
results["steps"].append("Backtest completed successfully")
|
|
647
|
+
results["output"] = run_result.stdout
|
|
648
|
+
|
|
649
|
+
# Try to parse results
|
|
650
|
+
results_dir = os.path.join(lean_workspace, "results")
|
|
651
|
+
if os.path.exists(results_dir):
|
|
652
|
+
result_files = os.listdir(results_dir)
|
|
653
|
+
if result_files:
|
|
654
|
+
latest_result = max(
|
|
655
|
+
[os.path.join(results_dir, f) for f in result_files],
|
|
656
|
+
key=os.path.getctime
|
|
657
|
+
)
|
|
658
|
+
results["results_file"] = latest_result
|
|
659
|
+
else:
|
|
660
|
+
results["status"] = "error"
|
|
661
|
+
results["error"] = run_result.stderr or run_result.stdout
|
|
662
|
+
results["steps"].append("Backtest failed - see error details")
|
|
663
|
+
|
|
664
|
+
# Provide helpful instructions
|
|
665
|
+
if "data" in run_result.stderr.lower():
|
|
666
|
+
results["instructions"] = [
|
|
667
|
+
"LEAN needs market data to run backtests.",
|
|
668
|
+
"Options:",
|
|
669
|
+
"1. Use QuantConnect Cloud (free tier available):",
|
|
670
|
+
" lean cloud backtest " + algo_file,
|
|
671
|
+
"",
|
|
672
|
+
"2. Download free sample data:",
|
|
673
|
+
" lean data download --dataset 'US Equities'",
|
|
674
|
+
"",
|
|
675
|
+
"3. Use the generated algorithm on QuantConnect.com"
|
|
676
|
+
]
|
|
677
|
+
|
|
678
|
+
except subprocess.TimeoutExpired:
|
|
679
|
+
results["status"] = "timeout"
|
|
680
|
+
results["error"] = "Backtest timed out after 5 minutes"
|
|
681
|
+
except FileNotFoundError:
|
|
682
|
+
results["status"] = "error"
|
|
683
|
+
results["error"] = "LEAN CLI not found in PATH after installation"
|
|
684
|
+
results["instructions"] = [
|
|
685
|
+
"Please restart your terminal and try again, or run:",
|
|
686
|
+
" export PATH=$PATH:~/.local/bin",
|
|
687
|
+
" lean backtest " + algo_file
|
|
688
|
+
]
|
|
689
|
+
|
|
690
|
+
# Always provide manual run instructions
|
|
691
|
+
results["manual_instructions"] = [
|
|
692
|
+
"To run this backtest manually:",
|
|
693
|
+
f" cd {lean_workspace}",
|
|
694
|
+
f" lean backtest {os.path.basename(algo_file)}",
|
|
695
|
+
"",
|
|
696
|
+
"Or use QuantConnect Cloud (recommended):",
|
|
697
|
+
f" lean cloud backtest {os.path.basename(algo_file)}",
|
|
698
|
+
"",
|
|
699
|
+
"Or upload to QuantConnect.com for free cloud backtesting"
|
|
700
|
+
]
|
|
701
|
+
|
|
702
|
+
return results
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def setup_lean_engine() -> dict[str, Any]:
|
|
706
|
+
"""Setup LEAN Engine for backtesting using Docker.
|
|
707
|
+
|
|
708
|
+
This is the proper way to run LEAN - using Docker containers.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
Setup status and instructions
|
|
712
|
+
"""
|
|
713
|
+
import subprocess
|
|
714
|
+
import shutil
|
|
715
|
+
|
|
716
|
+
result = {
|
|
717
|
+
"success": False,
|
|
718
|
+
"steps_completed": [],
|
|
719
|
+
"errors": [],
|
|
720
|
+
"workspace": os.path.expanduser("~/sigma_lean"),
|
|
721
|
+
"next_steps": []
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
# Step 1: Check Docker
|
|
725
|
+
docker_path = shutil.which("docker")
|
|
726
|
+
if not docker_path:
|
|
727
|
+
result["errors"].append("Docker is not installed")
|
|
728
|
+
result["next_steps"] = [
|
|
729
|
+
"LEAN requires Docker to run backtests.",
|
|
730
|
+
"",
|
|
731
|
+
"Please install Docker Desktop:",
|
|
732
|
+
" https://www.docker.com/products/docker-desktop",
|
|
733
|
+
"",
|
|
734
|
+
"After installing, run '/lean setup' again."
|
|
735
|
+
]
|
|
736
|
+
return result
|
|
737
|
+
|
|
738
|
+
# Check if Docker is running
|
|
739
|
+
try:
|
|
740
|
+
docker_check = subprocess.run(
|
|
741
|
+
["docker", "info"],
|
|
742
|
+
capture_output=True,
|
|
743
|
+
text=True,
|
|
744
|
+
timeout=10
|
|
745
|
+
)
|
|
746
|
+
if docker_check.returncode != 0:
|
|
747
|
+
result["errors"].append("Docker is installed but not running")
|
|
748
|
+
result["next_steps"] = [
|
|
749
|
+
"Docker is installed but not running.",
|
|
750
|
+
"",
|
|
751
|
+
"Please start Docker Desktop and run '/lean setup' again."
|
|
752
|
+
]
|
|
753
|
+
return result
|
|
754
|
+
result["steps_completed"].append("Docker is running")
|
|
755
|
+
except Exception as e:
|
|
756
|
+
result["errors"].append(f"Docker check failed: {e}")
|
|
757
|
+
return result
|
|
758
|
+
|
|
759
|
+
# Step 2: Create workspace directory
|
|
760
|
+
workspace = result["workspace"]
|
|
761
|
+
os.makedirs(workspace, exist_ok=True)
|
|
762
|
+
os.makedirs(os.path.join(workspace, "data"), exist_ok=True)
|
|
763
|
+
os.makedirs(os.path.join(workspace, "results"), exist_ok=True)
|
|
764
|
+
os.makedirs(os.path.join(workspace, "algorithms"), exist_ok=True)
|
|
765
|
+
result["steps_completed"].append(f"Created workspace at {workspace}")
|
|
766
|
+
|
|
767
|
+
# Step 3: Pull LEAN Docker image
|
|
768
|
+
result["steps_completed"].append("Pulling LEAN Docker image (this may take a few minutes)...")
|
|
769
|
+
try:
|
|
770
|
+
pull_result = subprocess.run(
|
|
771
|
+
["docker", "pull", "quantconnect/lean:latest"],
|
|
772
|
+
capture_output=True,
|
|
773
|
+
text=True,
|
|
774
|
+
timeout=600 # 10 minute timeout
|
|
775
|
+
)
|
|
776
|
+
if pull_result.returncode == 0:
|
|
777
|
+
result["steps_completed"].append("LEAN Docker image pulled successfully")
|
|
778
|
+
else:
|
|
779
|
+
result["errors"].append(f"Failed to pull LEAN image: {pull_result.stderr}")
|
|
780
|
+
return result
|
|
781
|
+
except subprocess.TimeoutExpired:
|
|
782
|
+
result["errors"].append("Docker pull timed out - please try again")
|
|
783
|
+
return result
|
|
784
|
+
except Exception as e:
|
|
785
|
+
result["errors"].append(f"Docker pull failed: {e}")
|
|
786
|
+
return result
|
|
787
|
+
|
|
788
|
+
# Step 4: Create lean.json config
|
|
789
|
+
config = {
|
|
790
|
+
"data-folder": os.path.join(workspace, "data"),
|
|
791
|
+
"results-destination-folder": os.path.join(workspace, "results"),
|
|
792
|
+
"algorithm-location": os.path.join(workspace, "algorithms"),
|
|
793
|
+
"debugging-method": "LocalCmdline",
|
|
794
|
+
"log-handler": "ConsoleLogHandler"
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
config_path = os.path.join(workspace, "lean.json")
|
|
798
|
+
with open(config_path, 'w') as f:
|
|
799
|
+
json.dump(config, f, indent=2)
|
|
800
|
+
result["steps_completed"].append("Created lean.json config")
|
|
801
|
+
|
|
802
|
+
# Step 5: Download sample data (optional but helpful)
|
|
803
|
+
result["steps_completed"].append("LEAN Engine setup complete!")
|
|
804
|
+
|
|
805
|
+
result["success"] = True
|
|
806
|
+
result["next_steps"] = [
|
|
807
|
+
"LEAN Engine is ready!",
|
|
808
|
+
"",
|
|
809
|
+
"To run a backtest:",
|
|
810
|
+
" /lean run AAPL sma_crossover",
|
|
811
|
+
"",
|
|
812
|
+
"Available strategies:",
|
|
813
|
+
" sma_crossover, rsi_mean_reversion, macd_momentum,",
|
|
814
|
+
" bollinger_bands, dual_momentum, breakout",
|
|
815
|
+
"",
|
|
816
|
+
f"Algorithms are saved to: {os.path.join(workspace, 'algorithms')}",
|
|
817
|
+
f"Results are saved to: {os.path.join(workspace, 'results')}"
|
|
818
|
+
]
|
|
819
|
+
|
|
820
|
+
return result
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def run_lean_backtest(
|
|
824
|
+
symbol: str,
|
|
825
|
+
strategy: str = "sma_crossover",
|
|
826
|
+
initial_capital: float = 100000,
|
|
827
|
+
start_date: Optional[str] = None,
|
|
828
|
+
end_date: Optional[str] = None,
|
|
829
|
+
**strategy_params
|
|
830
|
+
) -> dict[str, Any]:
|
|
831
|
+
"""Run a comprehensive backtest with detailed metrics and charts.
|
|
832
|
+
|
|
833
|
+
Args:
|
|
834
|
+
symbol: Stock ticker symbol
|
|
835
|
+
strategy: Strategy name
|
|
836
|
+
initial_capital: Starting capital
|
|
837
|
+
start_date: Start date (YYYY-MM-DD)
|
|
838
|
+
end_date: End date (YYYY-MM-DD)
|
|
839
|
+
|
|
840
|
+
Returns:
|
|
841
|
+
Comprehensive backtest results
|
|
842
|
+
"""
|
|
843
|
+
result = {
|
|
844
|
+
"symbol": symbol.upper(),
|
|
845
|
+
"strategy": strategy,
|
|
846
|
+
"status": "running",
|
|
847
|
+
"steps": [],
|
|
848
|
+
"metrics": {},
|
|
849
|
+
"charts": {}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
workspace = os.path.expanduser("~/sigma_lean")
|
|
853
|
+
algorithms_dir = os.path.join(workspace, "algorithms")
|
|
854
|
+
results_dir = os.path.join(workspace, "results")
|
|
855
|
+
|
|
856
|
+
# Ensure directories exist
|
|
857
|
+
os.makedirs(algorithms_dir, exist_ok=True)
|
|
858
|
+
os.makedirs(results_dir, exist_ok=True)
|
|
859
|
+
|
|
860
|
+
# Generate the LEAN algorithm
|
|
861
|
+
result["steps"].append(f"Generating {strategy} algorithm for {symbol.upper()}...")
|
|
862
|
+
|
|
863
|
+
algo_result = generate_lean_algorithm(
|
|
864
|
+
symbol=symbol,
|
|
865
|
+
strategy=strategy,
|
|
866
|
+
initial_capital=initial_capital,
|
|
867
|
+
start_date=start_date,
|
|
868
|
+
end_date=end_date,
|
|
869
|
+
output_dir=algorithms_dir,
|
|
870
|
+
**strategy_params
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
if not algo_result.get("success"):
|
|
874
|
+
result["status"] = "error"
|
|
875
|
+
result["error"] = f"Failed to generate algorithm: {algo_result.get('error', 'Unknown error')}"
|
|
876
|
+
return result
|
|
877
|
+
|
|
878
|
+
algo_file = algo_result["algorithm_file"]
|
|
879
|
+
algo_name = os.path.basename(algo_file)
|
|
880
|
+
result["algorithm_file"] = algo_file
|
|
881
|
+
result["steps"].append(f"Algorithm saved: {algo_name}")
|
|
882
|
+
|
|
883
|
+
# Run comprehensive Python backtest
|
|
884
|
+
result["steps"].append("Running comprehensive backtest analysis...")
|
|
885
|
+
|
|
886
|
+
try:
|
|
887
|
+
backtest_results = run_simple_backtest(
|
|
888
|
+
symbol=symbol,
|
|
889
|
+
strategy=strategy,
|
|
890
|
+
initial_capital=initial_capital,
|
|
891
|
+
start_date=start_date,
|
|
892
|
+
end_date=end_date
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
if backtest_results.get("success"):
|
|
896
|
+
result["status"] = "success"
|
|
897
|
+
result["steps"].append("Backtest completed successfully!")
|
|
898
|
+
result["metrics"] = backtest_results.get("metrics", {})
|
|
899
|
+
result["trades"] = backtest_results.get("trades", [])
|
|
900
|
+
result["monthly_returns"] = backtest_results.get("monthly_returns", [])
|
|
901
|
+
result["charts"] = backtest_results.get("charts", {})
|
|
902
|
+
else:
|
|
903
|
+
result["status"] = "error"
|
|
904
|
+
result["error"] = backtest_results.get("error", "Backtest failed")
|
|
905
|
+
|
|
906
|
+
except Exception as e:
|
|
907
|
+
result["status"] = "error"
|
|
908
|
+
result["error"] = str(e)
|
|
909
|
+
|
|
910
|
+
# Provide QuantConnect instructions for institutional-grade backtest
|
|
911
|
+
result["quantconnect_instructions"] = [
|
|
912
|
+
"For INSTITUTIONAL-GRADE backtesting with full market data:",
|
|
913
|
+
"1. Go to https://www.quantconnect.com (FREE account)",
|
|
914
|
+
"2. Click 'Algorithm Lab' -> 'Create New Algorithm'",
|
|
915
|
+
f"3. Paste the code from: {algo_file}",
|
|
916
|
+
"4. Click 'Backtest' for institutional data!"
|
|
917
|
+
]
|
|
918
|
+
|
|
919
|
+
result["algorithm_ready"] = True
|
|
920
|
+
|
|
921
|
+
return result
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def run_simple_backtest(
|
|
925
|
+
symbol: str,
|
|
926
|
+
strategy: str = "sma_crossover",
|
|
927
|
+
initial_capital: float = 100000,
|
|
928
|
+
start_date: Optional[str] = None,
|
|
929
|
+
end_date: Optional[str] = None
|
|
930
|
+
) -> dict[str, Any]:
|
|
931
|
+
"""Run a comprehensive Python backtest using yfinance data.
|
|
932
|
+
|
|
933
|
+
This provides institutional-quality results with detailed metrics,
|
|
934
|
+
charts, and analysis. For even more accurate results, use QuantConnect.
|
|
935
|
+
"""
|
|
936
|
+
import yfinance as yf
|
|
937
|
+
import numpy as np
|
|
938
|
+
import plotext as plt
|
|
939
|
+
from datetime import datetime
|
|
940
|
+
|
|
941
|
+
result = {
|
|
942
|
+
"success": False,
|
|
943
|
+
"symbol": symbol.upper(),
|
|
944
|
+
"strategy": strategy,
|
|
945
|
+
"metrics": {},
|
|
946
|
+
"trades": [],
|
|
947
|
+
"equity_curve": [],
|
|
948
|
+
"monthly_returns": [],
|
|
949
|
+
"charts": {}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
# Get historical data
|
|
953
|
+
ticker = yf.Ticker(symbol.upper())
|
|
954
|
+
|
|
955
|
+
if start_date and end_date:
|
|
956
|
+
hist = ticker.history(start=start_date, end=end_date)
|
|
957
|
+
else:
|
|
958
|
+
hist = ticker.history(period="2y")
|
|
959
|
+
|
|
960
|
+
if hist.empty or len(hist) < 50:
|
|
961
|
+
result["error"] = f"Insufficient data for {symbol}"
|
|
962
|
+
return result
|
|
963
|
+
|
|
964
|
+
closes = hist["Close"].values.astype(float)
|
|
965
|
+
opens = hist["Open"].values.astype(float)
|
|
966
|
+
highs = hist["High"].values.astype(float)
|
|
967
|
+
lows = hist["Low"].values.astype(float)
|
|
968
|
+
volumes = hist["Volume"].values.astype(float)
|
|
969
|
+
dates = hist.index
|
|
970
|
+
|
|
971
|
+
# Initialize tracking
|
|
972
|
+
cash = initial_capital
|
|
973
|
+
shares = 0
|
|
974
|
+
position = None
|
|
975
|
+
trades = []
|
|
976
|
+
equity_curve = []
|
|
977
|
+
daily_returns = []
|
|
978
|
+
|
|
979
|
+
# Strategy execution
|
|
980
|
+
signals = _generate_signals(closes, strategy)
|
|
981
|
+
|
|
982
|
+
prev_equity = initial_capital
|
|
983
|
+
for i in range(len(closes)):
|
|
984
|
+
price = closes[i]
|
|
985
|
+
date = dates[i]
|
|
986
|
+
signal = signals[i] if i < len(signals) else 0
|
|
987
|
+
|
|
988
|
+
# Calculate current equity
|
|
989
|
+
equity = cash + (shares * price if shares > 0 else 0)
|
|
990
|
+
equity_curve.append({
|
|
991
|
+
"date": str(date.date()),
|
|
992
|
+
"equity": equity,
|
|
993
|
+
"price": price
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
# Track daily returns
|
|
997
|
+
if prev_equity > 0:
|
|
998
|
+
daily_ret = (equity - prev_equity) / prev_equity
|
|
999
|
+
daily_returns.append(daily_ret)
|
|
1000
|
+
prev_equity = equity
|
|
1001
|
+
|
|
1002
|
+
# Execute signals
|
|
1003
|
+
if signal == 1 and position is None and cash > 0: # BUY
|
|
1004
|
+
shares = int(cash * 0.95 / price)
|
|
1005
|
+
if shares > 0:
|
|
1006
|
+
cost = shares * price
|
|
1007
|
+
commission = cost * 0.001 # 0.1% commission
|
|
1008
|
+
cash -= (cost + commission)
|
|
1009
|
+
position = 'long'
|
|
1010
|
+
entry_price = price
|
|
1011
|
+
entry_date = date
|
|
1012
|
+
trades.append({
|
|
1013
|
+
"date": str(date.date()),
|
|
1014
|
+
"action": "BUY",
|
|
1015
|
+
"price": round(price, 2),
|
|
1016
|
+
"shares": shares,
|
|
1017
|
+
"value": round(cost, 2),
|
|
1018
|
+
"commission": round(commission, 2)
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
elif signal == -1 and position == 'long' and shares > 0: # SELL
|
|
1022
|
+
proceeds = shares * price
|
|
1023
|
+
commission = proceeds * 0.001
|
|
1024
|
+
cash += (proceeds - commission)
|
|
1025
|
+
|
|
1026
|
+
# Calculate trade P&L
|
|
1027
|
+
pnl = (price - entry_price) * shares - (commission * 2)
|
|
1028
|
+
pnl_pct = ((price - entry_price) / entry_price) * 100
|
|
1029
|
+
holding_days = (date - entry_date).days
|
|
1030
|
+
|
|
1031
|
+
trades.append({
|
|
1032
|
+
"date": str(date.date()),
|
|
1033
|
+
"action": "SELL",
|
|
1034
|
+
"price": round(price, 2),
|
|
1035
|
+
"shares": shares,
|
|
1036
|
+
"value": round(proceeds, 2),
|
|
1037
|
+
"commission": round(commission, 2),
|
|
1038
|
+
"pnl": round(pnl, 2),
|
|
1039
|
+
"pnl_pct": round(pnl_pct, 2),
|
|
1040
|
+
"holding_days": holding_days
|
|
1041
|
+
})
|
|
1042
|
+
shares = 0
|
|
1043
|
+
position = None
|
|
1044
|
+
|
|
1045
|
+
# Close any open position at end
|
|
1046
|
+
if position == 'long' and shares > 0:
|
|
1047
|
+
final_price = closes[-1]
|
|
1048
|
+
proceeds = shares * final_price
|
|
1049
|
+
cash += proceeds
|
|
1050
|
+
trades.append({
|
|
1051
|
+
"date": str(dates[-1].date()),
|
|
1052
|
+
"action": "SELL (Close)",
|
|
1053
|
+
"price": round(final_price, 2),
|
|
1054
|
+
"shares": shares,
|
|
1055
|
+
"value": round(proceeds, 2)
|
|
1056
|
+
})
|
|
1057
|
+
shares = 0
|
|
1058
|
+
|
|
1059
|
+
# Calculate comprehensive metrics
|
|
1060
|
+
final_equity = cash
|
|
1061
|
+
total_return = ((final_equity - initial_capital) / initial_capital) * 100
|
|
1062
|
+
|
|
1063
|
+
# Buy and hold comparison
|
|
1064
|
+
bh_shares = int(initial_capital * 0.95 / closes[0])
|
|
1065
|
+
bh_final = (initial_capital - bh_shares * closes[0]) + bh_shares * closes[-1]
|
|
1066
|
+
bh_return = ((bh_final - initial_capital) / initial_capital) * 100
|
|
1067
|
+
|
|
1068
|
+
# Trade statistics
|
|
1069
|
+
buy_trades = [t for t in trades if t.get("action") == "BUY"]
|
|
1070
|
+
sell_trades = [t for t in trades if "pnl" in t]
|
|
1071
|
+
|
|
1072
|
+
wins = [t for t in sell_trades if t.get("pnl", 0) > 0]
|
|
1073
|
+
losses = [t for t in sell_trades if t.get("pnl", 0) <= 0]
|
|
1074
|
+
|
|
1075
|
+
win_rate = (len(wins) / len(sell_trades) * 100) if sell_trades else 0
|
|
1076
|
+
avg_win = np.mean([t["pnl"] for t in wins]) if wins else 0
|
|
1077
|
+
avg_loss = np.mean([t["pnl"] for t in losses]) if losses else 0
|
|
1078
|
+
|
|
1079
|
+
# Profit factor
|
|
1080
|
+
gross_profit = sum(t["pnl"] for t in wins) if wins else 0
|
|
1081
|
+
gross_loss = abs(sum(t["pnl"] for t in losses)) if losses else 1
|
|
1082
|
+
profit_factor = gross_profit / gross_loss if gross_loss > 0 else 0
|
|
1083
|
+
|
|
1084
|
+
# Risk metrics
|
|
1085
|
+
daily_returns_arr = np.array(daily_returns)
|
|
1086
|
+
|
|
1087
|
+
# Sharpe Ratio (annualized, assuming 252 trading days)
|
|
1088
|
+
if len(daily_returns_arr) > 1 and np.std(daily_returns_arr) > 0:
|
|
1089
|
+
sharpe = (np.mean(daily_returns_arr) * 252) / (np.std(daily_returns_arr) * np.sqrt(252))
|
|
1090
|
+
else:
|
|
1091
|
+
sharpe = 0
|
|
1092
|
+
|
|
1093
|
+
# Sortino Ratio (downside deviation)
|
|
1094
|
+
downside_returns = daily_returns_arr[daily_returns_arr < 0]
|
|
1095
|
+
if len(downside_returns) > 1 and np.std(downside_returns) > 0:
|
|
1096
|
+
sortino = (np.mean(daily_returns_arr) * 252) / (np.std(downside_returns) * np.sqrt(252))
|
|
1097
|
+
else:
|
|
1098
|
+
sortino = 0
|
|
1099
|
+
|
|
1100
|
+
# Max Drawdown
|
|
1101
|
+
peak = initial_capital
|
|
1102
|
+
max_dd = 0
|
|
1103
|
+
max_dd_duration = 0
|
|
1104
|
+
dd_start = None
|
|
1105
|
+
current_dd_duration = 0
|
|
1106
|
+
|
|
1107
|
+
drawdowns = []
|
|
1108
|
+
for i, point in enumerate(equity_curve):
|
|
1109
|
+
equity = point["equity"]
|
|
1110
|
+
if equity > peak:
|
|
1111
|
+
peak = equity
|
|
1112
|
+
if dd_start is not None:
|
|
1113
|
+
current_dd_duration = i - dd_start
|
|
1114
|
+
if current_dd_duration > max_dd_duration:
|
|
1115
|
+
max_dd_duration = current_dd_duration
|
|
1116
|
+
dd_start = None
|
|
1117
|
+
else:
|
|
1118
|
+
if dd_start is None:
|
|
1119
|
+
dd_start = i
|
|
1120
|
+
|
|
1121
|
+
dd = (peak - equity) / peak * 100
|
|
1122
|
+
drawdowns.append(dd)
|
|
1123
|
+
if dd > max_dd:
|
|
1124
|
+
max_dd = dd
|
|
1125
|
+
|
|
1126
|
+
# Calmar Ratio
|
|
1127
|
+
annual_return = total_return / (len(closes) / 252)
|
|
1128
|
+
calmar = annual_return / max_dd if max_dd > 0 else 0
|
|
1129
|
+
|
|
1130
|
+
# Average holding period
|
|
1131
|
+
holding_periods = [t.get("holding_days", 0) for t in sell_trades if t.get("holding_days")]
|
|
1132
|
+
avg_holding = np.mean(holding_periods) if holding_periods else 0
|
|
1133
|
+
|
|
1134
|
+
# Monthly returns
|
|
1135
|
+
monthly_returns = {}
|
|
1136
|
+
for point in equity_curve:
|
|
1137
|
+
date_str = point["date"]
|
|
1138
|
+
month_key = date_str[:7] # YYYY-MM
|
|
1139
|
+
if month_key not in monthly_returns:
|
|
1140
|
+
monthly_returns[month_key] = {"start": point["equity"], "end": point["equity"]}
|
|
1141
|
+
monthly_returns[month_key]["end"] = point["equity"]
|
|
1142
|
+
|
|
1143
|
+
monthly_pcts = []
|
|
1144
|
+
for month, vals in monthly_returns.items():
|
|
1145
|
+
if vals["start"] > 0:
|
|
1146
|
+
ret = ((vals["end"] - vals["start"]) / vals["start"]) * 100
|
|
1147
|
+
monthly_pcts.append({"month": month, "return": round(ret, 2)})
|
|
1148
|
+
|
|
1149
|
+
# Compile metrics
|
|
1150
|
+
result["success"] = True
|
|
1151
|
+
result["metrics"] = {
|
|
1152
|
+
"performance": {
|
|
1153
|
+
"initial_capital": f"${initial_capital:,.0f}",
|
|
1154
|
+
"final_equity": f"${final_equity:,.2f}",
|
|
1155
|
+
"total_return": f"{total_return:+.2f}%",
|
|
1156
|
+
"annual_return": f"{annual_return:+.2f}%",
|
|
1157
|
+
"buy_hold_return": f"{bh_return:+.2f}%",
|
|
1158
|
+
"alpha": f"{total_return - bh_return:+.2f}%"
|
|
1159
|
+
},
|
|
1160
|
+
"risk": {
|
|
1161
|
+
"max_drawdown": f"{max_dd:.2f}%",
|
|
1162
|
+
"sharpe_ratio": f"{sharpe:.2f}",
|
|
1163
|
+
"sortino_ratio": f"{sortino:.2f}",
|
|
1164
|
+
"calmar_ratio": f"{calmar:.2f}",
|
|
1165
|
+
"volatility": f"{np.std(daily_returns_arr) * np.sqrt(252) * 100:.2f}%"
|
|
1166
|
+
},
|
|
1167
|
+
"trades": {
|
|
1168
|
+
"total_trades": len(sell_trades),
|
|
1169
|
+
"win_rate": f"{win_rate:.1f}%",
|
|
1170
|
+
"profit_factor": f"{profit_factor:.2f}",
|
|
1171
|
+
"avg_win": f"${avg_win:,.2f}",
|
|
1172
|
+
"avg_loss": f"${avg_loss:,.2f}",
|
|
1173
|
+
"avg_holding_days": f"{avg_holding:.1f}"
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
result["trades"] = trades[-15:] # Last 15 trades
|
|
1177
|
+
result["monthly_returns"] = monthly_pcts[-12:] # Last 12 months
|
|
1178
|
+
|
|
1179
|
+
# Generate charts
|
|
1180
|
+
try:
|
|
1181
|
+
# 1. Equity Curve Chart
|
|
1182
|
+
plt.clear_figure()
|
|
1183
|
+
plt.plotsize(120, 25)
|
|
1184
|
+
plt.theme("dark")
|
|
1185
|
+
plt.canvas_color("black")
|
|
1186
|
+
plt.axes_color("black")
|
|
1187
|
+
|
|
1188
|
+
equities = [p["equity"] for p in equity_curve]
|
|
1189
|
+
plt.plot(equities, color="cyan", marker="hd")
|
|
1190
|
+
|
|
1191
|
+
# Add buy/hold line
|
|
1192
|
+
bh_equity = []
|
|
1193
|
+
bh_val = initial_capital
|
|
1194
|
+
for i, price in enumerate(closes):
|
|
1195
|
+
if i == 0:
|
|
1196
|
+
bh_val = initial_capital
|
|
1197
|
+
else:
|
|
1198
|
+
bh_val = (initial_capital - bh_shares * closes[0]) + bh_shares * price
|
|
1199
|
+
bh_equity.append(bh_val)
|
|
1200
|
+
plt.plot(bh_equity, color="gray", marker="hd")
|
|
1201
|
+
|
|
1202
|
+
plt.title(f" Equity Curve: {symbol.upper()} {strategy} (cyan) vs Buy&Hold (gray)")
|
|
1203
|
+
plt.ylabel("Equity ($)")
|
|
1204
|
+
|
|
1205
|
+
result["charts"]["equity_curve"] = plt.build()
|
|
1206
|
+
|
|
1207
|
+
# 2. Drawdown Chart
|
|
1208
|
+
plt.clear_figure()
|
|
1209
|
+
plt.plotsize(120, 15)
|
|
1210
|
+
plt.theme("dark")
|
|
1211
|
+
plt.canvas_color("black")
|
|
1212
|
+
plt.axes_color("black")
|
|
1213
|
+
|
|
1214
|
+
plt.plot([-d for d in drawdowns], color="red", marker="hd", fillx=True)
|
|
1215
|
+
plt.title(f" Drawdown Analysis - Max: {max_dd:.2f}%")
|
|
1216
|
+
plt.ylabel("Drawdown (%)")
|
|
1217
|
+
|
|
1218
|
+
result["charts"]["drawdown"] = plt.build()
|
|
1219
|
+
|
|
1220
|
+
# 3. Trade Distribution
|
|
1221
|
+
if sell_trades:
|
|
1222
|
+
plt.clear_figure()
|
|
1223
|
+
plt.plotsize(80, 12)
|
|
1224
|
+
plt.theme("dark")
|
|
1225
|
+
plt.canvas_color("black")
|
|
1226
|
+
plt.axes_color("black")
|
|
1227
|
+
|
|
1228
|
+
pnls = [t.get("pnl", 0) for t in sell_trades]
|
|
1229
|
+
colors = ["green" if p > 0 else "red" for p in pnls]
|
|
1230
|
+
plt.bar(range(len(pnls)), pnls, color=colors)
|
|
1231
|
+
plt.title(f" Trade P&L Distribution ({len(wins)} wins, {len(losses)} losses)")
|
|
1232
|
+
plt.ylabel("P&L ($)")
|
|
1233
|
+
|
|
1234
|
+
result["charts"]["trade_pnl"] = plt.build()
|
|
1235
|
+
|
|
1236
|
+
# 4. Monthly Returns Bar Chart
|
|
1237
|
+
if monthly_pcts:
|
|
1238
|
+
plt.clear_figure()
|
|
1239
|
+
plt.plotsize(100, 12)
|
|
1240
|
+
plt.theme("dark")
|
|
1241
|
+
plt.canvas_color("black")
|
|
1242
|
+
plt.axes_color("black")
|
|
1243
|
+
|
|
1244
|
+
months = [m["month"][-5:] for m in monthly_pcts[-12:]] # MM format
|
|
1245
|
+
returns = [m["return"] for m in monthly_pcts[-12:]]
|
|
1246
|
+
colors = ["green" if r > 0 else "red" for r in returns]
|
|
1247
|
+
plt.bar(months, returns, color=colors)
|
|
1248
|
+
plt.title(" Monthly Returns (%)")
|
|
1249
|
+
|
|
1250
|
+
result["charts"]["monthly_returns"] = plt.build()
|
|
1251
|
+
|
|
1252
|
+
except Exception as e:
|
|
1253
|
+
result["chart_error"] = str(e)
|
|
1254
|
+
|
|
1255
|
+
return result
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def _generate_signals(closes, strategy: str) -> list:
|
|
1259
|
+
"""Generate trading signals based on strategy.
|
|
1260
|
+
|
|
1261
|
+
Args:
|
|
1262
|
+
closes: numpy array of closing prices
|
|
1263
|
+
strategy: strategy name
|
|
1264
|
+
|
|
1265
|
+
Returns: list of signals (1=buy, -1=sell, 0=hold)
|
|
1266
|
+
"""
|
|
1267
|
+
import numpy as np
|
|
1268
|
+
|
|
1269
|
+
signals = [0] * len(closes)
|
|
1270
|
+
|
|
1271
|
+
if strategy == "sma_crossover":
|
|
1272
|
+
fast_period = 10
|
|
1273
|
+
slow_period = 30
|
|
1274
|
+
|
|
1275
|
+
if len(closes) < slow_period:
|
|
1276
|
+
return signals
|
|
1277
|
+
|
|
1278
|
+
fast_sma = np.convolve(closes, np.ones(fast_period)/fast_period, mode='valid')
|
|
1279
|
+
slow_sma = np.convolve(closes, np.ones(slow_period)/slow_period, mode='valid')
|
|
1280
|
+
|
|
1281
|
+
offset = slow_period - fast_period
|
|
1282
|
+
fast_sma = fast_sma[offset:]
|
|
1283
|
+
|
|
1284
|
+
for i in range(1, len(slow_sma)):
|
|
1285
|
+
idx = i + slow_period - 1
|
|
1286
|
+
if idx < len(signals):
|
|
1287
|
+
if fast_sma[i] > slow_sma[i] and fast_sma[i-1] <= slow_sma[i-1]:
|
|
1288
|
+
signals[idx] = 1 # Buy
|
|
1289
|
+
elif fast_sma[i] < slow_sma[i] and fast_sma[i-1] >= slow_sma[i-1]:
|
|
1290
|
+
signals[idx] = -1 # Sell
|
|
1291
|
+
|
|
1292
|
+
elif strategy == "rsi_mean_reversion":
|
|
1293
|
+
rsi_period = 14
|
|
1294
|
+
oversold = 30
|
|
1295
|
+
overbought = 70
|
|
1296
|
+
|
|
1297
|
+
rsi = _calculate_rsi(closes, rsi_period)
|
|
1298
|
+
|
|
1299
|
+
for i in range(1, len(rsi)):
|
|
1300
|
+
if rsi[i] < oversold and rsi[i-1] >= oversold:
|
|
1301
|
+
signals[i] = 1 # Buy on oversold
|
|
1302
|
+
elif rsi[i] > overbought and rsi[i-1] <= overbought:
|
|
1303
|
+
signals[i] = -1 # Sell on overbought
|
|
1304
|
+
|
|
1305
|
+
elif strategy == "macd_momentum":
|
|
1306
|
+
fast, slow, signal = 12, 26, 9
|
|
1307
|
+
|
|
1308
|
+
ema_fast = _ema(closes, fast)
|
|
1309
|
+
ema_slow = _ema(closes, slow)
|
|
1310
|
+
macd_line = ema_fast - ema_slow
|
|
1311
|
+
signal_line = _ema(macd_line, signal)
|
|
1312
|
+
histogram = macd_line - signal_line
|
|
1313
|
+
|
|
1314
|
+
for i in range(slow + signal + 1, len(histogram)):
|
|
1315
|
+
if histogram[i] > 0 and histogram[i-1] <= 0:
|
|
1316
|
+
signals[i] = 1 # Buy
|
|
1317
|
+
elif histogram[i] < 0 and histogram[i-1] >= 0:
|
|
1318
|
+
signals[i] = -1 # Sell
|
|
1319
|
+
|
|
1320
|
+
elif strategy == "bollinger_bands":
|
|
1321
|
+
period = 20
|
|
1322
|
+
std_dev = 2
|
|
1323
|
+
|
|
1324
|
+
if len(closes) < period:
|
|
1325
|
+
return signals
|
|
1326
|
+
|
|
1327
|
+
for i in range(period, len(closes)):
|
|
1328
|
+
window = closes[i-period:i]
|
|
1329
|
+
middle = np.mean(window)
|
|
1330
|
+
std = np.std(window)
|
|
1331
|
+
upper = middle + std_dev * std
|
|
1332
|
+
lower = middle - std_dev * std
|
|
1333
|
+
|
|
1334
|
+
if closes[i] <= lower and closes[i-1] > lower:
|
|
1335
|
+
signals[i] = 1 # Buy at lower band
|
|
1336
|
+
elif closes[i] >= upper and closes[i-1] < upper:
|
|
1337
|
+
signals[i] = -1 # Sell at upper band
|
|
1338
|
+
|
|
1339
|
+
elif strategy == "dual_momentum":
|
|
1340
|
+
lookback = 60 # 3 months
|
|
1341
|
+
|
|
1342
|
+
if len(closes) < lookback:
|
|
1343
|
+
return signals
|
|
1344
|
+
|
|
1345
|
+
for i in range(lookback, len(closes)):
|
|
1346
|
+
momentum = (closes[i] - closes[i-lookback]) / closes[i-lookback]
|
|
1347
|
+
prev_momentum = (closes[i-1] - closes[i-lookback-1]) / closes[i-lookback-1] if i > lookback else 0
|
|
1348
|
+
|
|
1349
|
+
if momentum > 0 and prev_momentum <= 0:
|
|
1350
|
+
signals[i] = 1
|
|
1351
|
+
elif momentum < 0 and prev_momentum >= 0:
|
|
1352
|
+
signals[i] = -1
|
|
1353
|
+
|
|
1354
|
+
elif strategy == "breakout":
|
|
1355
|
+
period = 20 # Donchian channel period
|
|
1356
|
+
|
|
1357
|
+
if len(closes) < period:
|
|
1358
|
+
return signals
|
|
1359
|
+
|
|
1360
|
+
for i in range(period, len(closes)):
|
|
1361
|
+
high_channel = max(closes[i-period:i])
|
|
1362
|
+
low_channel = min(closes[i-period:i])
|
|
1363
|
+
|
|
1364
|
+
if closes[i] > high_channel and closes[i-1] <= high_channel:
|
|
1365
|
+
signals[i] = 1 # Breakout buy
|
|
1366
|
+
elif closes[i] < low_channel and closes[i-1] >= low_channel:
|
|
1367
|
+
signals[i] = -1 # Breakdown sell
|
|
1368
|
+
|
|
1369
|
+
return signals
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def _calculate_rsi(closes, period: int = 14):
|
|
1373
|
+
"""Calculate RSI indicator.
|
|
1374
|
+
|
|
1375
|
+
Args:
|
|
1376
|
+
closes: numpy array of closing prices
|
|
1377
|
+
period: RSI period (default 14)
|
|
1378
|
+
|
|
1379
|
+
Returns:
|
|
1380
|
+
numpy array of RSI values
|
|
1381
|
+
"""
|
|
1382
|
+
import numpy as np
|
|
1383
|
+
|
|
1384
|
+
deltas = np.diff(closes)
|
|
1385
|
+
gains = np.where(deltas > 0, deltas, 0)
|
|
1386
|
+
losses = np.where(deltas < 0, -deltas, 0)
|
|
1387
|
+
|
|
1388
|
+
rsi = np.zeros(len(closes))
|
|
1389
|
+
rsi[:period] = 50 # Neutral
|
|
1390
|
+
|
|
1391
|
+
for i in range(period, len(closes)):
|
|
1392
|
+
avg_gain = np.mean(gains[i-period:i])
|
|
1393
|
+
avg_loss = np.mean(losses[i-period:i])
|
|
1394
|
+
if avg_loss == 0:
|
|
1395
|
+
rsi[i] = 100
|
|
1396
|
+
else:
|
|
1397
|
+
rs = avg_gain / avg_loss
|
|
1398
|
+
rsi[i] = 100 - (100 / (1 + rs))
|
|
1399
|
+
|
|
1400
|
+
return rsi
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
def _ema(data, period):
|
|
1404
|
+
"""Calculate exponential moving average."""
|
|
1405
|
+
import numpy as np
|
|
1406
|
+
ema = np.zeros_like(data)
|
|
1407
|
+
ema[0] = data[0]
|
|
1408
|
+
multiplier = 2 / (period + 1)
|
|
1409
|
+
for i in range(1, len(data)):
|
|
1410
|
+
ema[i] = (data[i] * multiplier) + (ema[i-1] * (1 - multiplier))
|
|
1411
|
+
return ema
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
def check_lean_status() -> dict[str, Any]:
|
|
1415
|
+
"""Check LEAN Engine status and provide setup instructions.
|
|
1416
|
+
|
|
1417
|
+
Returns:
|
|
1418
|
+
Status information and setup instructions
|
|
1419
|
+
"""
|
|
1420
|
+
import subprocess
|
|
1421
|
+
import shutil
|
|
1422
|
+
|
|
1423
|
+
status = {
|
|
1424
|
+
"docker_installed": False,
|
|
1425
|
+
"docker_running": False,
|
|
1426
|
+
"lean_image_pulled": False,
|
|
1427
|
+
"workspace_initialized": False,
|
|
1428
|
+
"workspace_path": os.path.expanduser("~/sigma_lean"),
|
|
1429
|
+
"ready": False,
|
|
1430
|
+
"instructions": []
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
# Check Docker
|
|
1434
|
+
docker_path = shutil.which("docker")
|
|
1435
|
+
if docker_path:
|
|
1436
|
+
status["docker_installed"] = True
|
|
1437
|
+
|
|
1438
|
+
# Check if Docker is running
|
|
1439
|
+
try:
|
|
1440
|
+
docker_check = subprocess.run(
|
|
1441
|
+
["docker", "info"],
|
|
1442
|
+
capture_output=True,
|
|
1443
|
+
text=True,
|
|
1444
|
+
timeout=10
|
|
1445
|
+
)
|
|
1446
|
+
if docker_check.returncode == 0:
|
|
1447
|
+
status["docker_running"] = True
|
|
1448
|
+
|
|
1449
|
+
# Check if LEAN image is pulled
|
|
1450
|
+
images_check = subprocess.run(
|
|
1451
|
+
["docker", "images", "quantconnect/lean", "--format", "{{.Repository}}"],
|
|
1452
|
+
capture_output=True,
|
|
1453
|
+
text=True
|
|
1454
|
+
)
|
|
1455
|
+
if "quantconnect/lean" in images_check.stdout:
|
|
1456
|
+
status["lean_image_pulled"] = True
|
|
1457
|
+
except:
|
|
1458
|
+
pass
|
|
1459
|
+
|
|
1460
|
+
# Check workspace
|
|
1461
|
+
workspace = status["workspace_path"]
|
|
1462
|
+
if os.path.exists(os.path.join(workspace, "algorithms")):
|
|
1463
|
+
status["workspace_initialized"] = True
|
|
1464
|
+
|
|
1465
|
+
# Determine readiness
|
|
1466
|
+
status["ready"] = status["workspace_initialized"]
|
|
1467
|
+
|
|
1468
|
+
# Provide instructions
|
|
1469
|
+
if not status["docker_installed"]:
|
|
1470
|
+
status["instructions"] = [
|
|
1471
|
+
"Docker is required for full LEAN Engine.",
|
|
1472
|
+
"",
|
|
1473
|
+
"Install Docker Desktop:",
|
|
1474
|
+
" https://www.docker.com/products/docker-desktop",
|
|
1475
|
+
"",
|
|
1476
|
+
"Then run: /lean setup",
|
|
1477
|
+
"",
|
|
1478
|
+
"Note: You can still run quick backtests without Docker!"
|
|
1479
|
+
]
|
|
1480
|
+
elif not status["docker_running"]:
|
|
1481
|
+
status["instructions"] = [
|
|
1482
|
+
"Docker is installed but not running.",
|
|
1483
|
+
"",
|
|
1484
|
+
"Please start Docker Desktop, then run: /lean setup",
|
|
1485
|
+
"",
|
|
1486
|
+
"Note: You can still run quick backtests without Docker!"
|
|
1487
|
+
]
|
|
1488
|
+
elif not status["workspace_initialized"]:
|
|
1489
|
+
status["instructions"] = [
|
|
1490
|
+
"LEAN needs to be set up.",
|
|
1491
|
+
"",
|
|
1492
|
+
"Run: /lean setup"
|
|
1493
|
+
]
|
|
1494
|
+
else:
|
|
1495
|
+
status["instructions"] = [
|
|
1496
|
+
"LEAN Engine is ready!",
|
|
1497
|
+
"",
|
|
1498
|
+
"Run a backtest:",
|
|
1499
|
+
" /lean run AAPL sma_crossover",
|
|
1500
|
+
"",
|
|
1501
|
+
"Available strategies:",
|
|
1502
|
+
" sma_crossover, rsi_mean_reversion, macd_momentum,",
|
|
1503
|
+
" bollinger_bands, dual_momentum, breakout"
|
|
1504
|
+
]
|
|
1505
|
+
|
|
1506
|
+
return status
|