sigma-terminal 2.0.2__py3-none-any.whl → 3.3.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 +182 -6
- sigma/__main__.py +2 -2
- sigma/analytics/__init__.py +636 -0
- sigma/app.py +801 -892
- sigma/backtest.py +372 -0
- sigma/charts.py +407 -0
- sigma/cli.py +465 -0
- sigma/comparison.py +611 -0
- sigma/config.py +366 -0
- sigma/core/__init__.py +4 -17
- sigma/core/engine.py +493 -0
- sigma/core/intent.py +595 -0
- sigma/core/models.py +516 -125
- sigma/data/__init__.py +681 -0
- sigma/data/models.py +130 -0
- sigma/llm.py +639 -0
- sigma/monitoring.py +666 -0
- sigma/portfolio.py +697 -0
- sigma/reporting.py +658 -0
- sigma/robustness.py +675 -0
- sigma/setup.py +374 -403
- sigma/strategy.py +753 -0
- sigma/tools/backtest.py +23 -5
- sigma/tools.py +617 -0
- sigma/visualization.py +766 -0
- sigma_terminal-3.3.0.dist-info/METADATA +583 -0
- sigma_terminal-3.3.0.dist-info/RECORD +30 -0
- sigma_terminal-3.3.0.dist-info/entry_points.txt +6 -0
- sigma_terminal-3.3.0.dist-info/licenses/LICENSE +25 -0
- sigma/core/agent.py +0 -205
- sigma/core/config.py +0 -119
- sigma/core/llm.py +0 -794
- sigma/tools/__init__.py +0 -5
- sigma/tools/charts.py +0 -400
- sigma/tools/financial.py +0 -1457
- sigma/ui/__init__.py +0 -1
- sigma_terminal-2.0.2.dist-info/METADATA +0 -222
- sigma_terminal-2.0.2.dist-info/RECORD +0 -19
- sigma_terminal-2.0.2.dist-info/entry_points.txt +0 -2
- sigma_terminal-2.0.2.dist-info/licenses/LICENSE +0 -42
- {sigma_terminal-2.0.2.dist-info → sigma_terminal-3.3.0.dist-info}/WHEEL +0 -0
sigma/backtest.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Backtesting engine for Sigma."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import yfinance as yf
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ============================================================================
|
|
13
|
+
# BACKTEST STRATEGIES
|
|
14
|
+
# ============================================================================
|
|
15
|
+
|
|
16
|
+
STRATEGIES = {
|
|
17
|
+
"sma_crossover": {
|
|
18
|
+
"name": "SMA Crossover",
|
|
19
|
+
"description": "Buy when short SMA crosses above long SMA, sell when crosses below",
|
|
20
|
+
"params": {"short_window": 20, "long_window": 50},
|
|
21
|
+
},
|
|
22
|
+
"rsi_mean_reversion": {
|
|
23
|
+
"name": "RSI Mean Reversion",
|
|
24
|
+
"description": "Buy when RSI < 30 (oversold), sell when RSI > 70 (overbought)",
|
|
25
|
+
"params": {"rsi_period": 14, "oversold": 30, "overbought": 70},
|
|
26
|
+
},
|
|
27
|
+
"macd_momentum": {
|
|
28
|
+
"name": "MACD Momentum",
|
|
29
|
+
"description": "Buy on MACD bullish crossover, sell on bearish crossover",
|
|
30
|
+
"params": {"fast": 12, "slow": 26, "signal": 9},
|
|
31
|
+
},
|
|
32
|
+
"bollinger_bands": {
|
|
33
|
+
"name": "Bollinger Bands",
|
|
34
|
+
"description": "Buy when price touches lower band, sell at upper band",
|
|
35
|
+
"params": {"window": 20, "num_std": 2},
|
|
36
|
+
},
|
|
37
|
+
"dual_momentum": {
|
|
38
|
+
"name": "Dual Momentum",
|
|
39
|
+
"description": "Combines absolute and relative momentum",
|
|
40
|
+
"params": {"lookback": 12},
|
|
41
|
+
},
|
|
42
|
+
"breakout": {
|
|
43
|
+
"name": "Breakout",
|
|
44
|
+
"description": "Buy on new highs, sell on new lows",
|
|
45
|
+
"params": {"lookback": 20},
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_backtest(
|
|
51
|
+
symbol: str,
|
|
52
|
+
strategy: str,
|
|
53
|
+
period: str = "2y",
|
|
54
|
+
initial_capital: float = 100000,
|
|
55
|
+
params: Optional[dict] = None,
|
|
56
|
+
) -> dict:
|
|
57
|
+
"""Run a backtest for a given strategy."""
|
|
58
|
+
|
|
59
|
+
if strategy not in STRATEGIES:
|
|
60
|
+
return {"error": f"Unknown strategy: {strategy}", "available": list(STRATEGIES.keys())}
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# Get historical data
|
|
64
|
+
ticker = yf.Ticker(symbol.upper())
|
|
65
|
+
hist = ticker.history(period=period)
|
|
66
|
+
|
|
67
|
+
if hist.empty or len(hist) < 50:
|
|
68
|
+
return {"error": "Insufficient data for backtest", "symbol": symbol}
|
|
69
|
+
|
|
70
|
+
# Get strategy params
|
|
71
|
+
strat_info = STRATEGIES[strategy]
|
|
72
|
+
strat_params = params or strat_info["params"]
|
|
73
|
+
|
|
74
|
+
# Generate signals based on strategy
|
|
75
|
+
if strategy == "sma_crossover":
|
|
76
|
+
signals = _sma_crossover_signals(hist, **strat_params)
|
|
77
|
+
elif strategy == "rsi_mean_reversion":
|
|
78
|
+
signals = _rsi_signals(hist, **strat_params)
|
|
79
|
+
elif strategy == "macd_momentum":
|
|
80
|
+
signals = _macd_signals(hist, **strat_params)
|
|
81
|
+
elif strategy == "bollinger_bands":
|
|
82
|
+
signals = _bollinger_signals(hist, **strat_params)
|
|
83
|
+
elif strategy == "dual_momentum":
|
|
84
|
+
signals = _momentum_signals(hist, **strat_params)
|
|
85
|
+
elif strategy == "breakout":
|
|
86
|
+
signals = _breakout_signals(hist, **strat_params)
|
|
87
|
+
else:
|
|
88
|
+
signals = pd.Series(0, index=hist.index)
|
|
89
|
+
|
|
90
|
+
# Run simulation
|
|
91
|
+
results = _simulate_trades(hist, signals, initial_capital)
|
|
92
|
+
|
|
93
|
+
# Calculate metrics
|
|
94
|
+
metrics = _calculate_metrics(results, hist, initial_capital)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"symbol": symbol.upper(),
|
|
98
|
+
"strategy": strat_info["name"],
|
|
99
|
+
"strategy_description": strat_info["description"],
|
|
100
|
+
"period": period,
|
|
101
|
+
"initial_capital": initial_capital,
|
|
102
|
+
"parameters": strat_params,
|
|
103
|
+
"performance": metrics["performance"],
|
|
104
|
+
"risk": metrics["risk"],
|
|
105
|
+
"trades": metrics["trades"],
|
|
106
|
+
"monthly_returns": metrics["monthly_returns"],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return {"error": str(e), "symbol": symbol}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _sma_crossover_signals(hist: pd.DataFrame, short_window: int, long_window: int) -> pd.Series:
|
|
114
|
+
"""Generate SMA crossover signals."""
|
|
115
|
+
short_sma = hist["Close"].rolling(short_window).mean()
|
|
116
|
+
long_sma = hist["Close"].rolling(long_window).mean()
|
|
117
|
+
|
|
118
|
+
signals = pd.Series(0, index=hist.index)
|
|
119
|
+
signals[short_sma > long_sma] = 1 # Long
|
|
120
|
+
signals[short_sma < long_sma] = -1 # Exit
|
|
121
|
+
|
|
122
|
+
return signals
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _rsi_signals(hist: pd.DataFrame, rsi_period: int, oversold: int, overbought: int) -> pd.Series:
|
|
126
|
+
"""Generate RSI mean reversion signals."""
|
|
127
|
+
delta = hist["Close"].diff()
|
|
128
|
+
gain = (delta.where(delta > 0, 0)).rolling(rsi_period).mean()
|
|
129
|
+
loss = (-delta.where(delta < 0, 0)).rolling(rsi_period).mean()
|
|
130
|
+
rs = gain / loss
|
|
131
|
+
rsi = 100 - (100 / (1 + rs))
|
|
132
|
+
|
|
133
|
+
signals = pd.Series(0, index=hist.index)
|
|
134
|
+
signals[rsi < oversold] = 1 # Buy oversold
|
|
135
|
+
signals[rsi > overbought] = -1 # Sell overbought
|
|
136
|
+
|
|
137
|
+
return signals
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _macd_signals(hist: pd.DataFrame, fast: int, slow: int, signal: int) -> pd.Series:
|
|
141
|
+
"""Generate MACD momentum signals."""
|
|
142
|
+
ema_fast = hist["Close"].ewm(span=fast).mean()
|
|
143
|
+
ema_slow = hist["Close"].ewm(span=slow).mean()
|
|
144
|
+
macd = ema_fast - ema_slow
|
|
145
|
+
macd_signal = macd.ewm(span=signal).mean()
|
|
146
|
+
|
|
147
|
+
signals = pd.Series(0, index=hist.index)
|
|
148
|
+
signals[macd > macd_signal] = 1 # Bullish
|
|
149
|
+
signals[macd < macd_signal] = -1 # Bearish
|
|
150
|
+
|
|
151
|
+
return signals
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _bollinger_signals(hist: pd.DataFrame, window: int, num_std: int) -> pd.Series:
|
|
155
|
+
"""Generate Bollinger Bands signals."""
|
|
156
|
+
mid = hist["Close"].rolling(window).mean()
|
|
157
|
+
std = hist["Close"].rolling(window).std()
|
|
158
|
+
upper = mid + (std * num_std)
|
|
159
|
+
lower = mid - (std * num_std)
|
|
160
|
+
|
|
161
|
+
signals = pd.Series(0, index=hist.index)
|
|
162
|
+
signals[hist["Close"] < lower] = 1 # Buy at lower band
|
|
163
|
+
signals[hist["Close"] > upper] = -1 # Sell at upper band
|
|
164
|
+
|
|
165
|
+
return signals
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _momentum_signals(hist: pd.DataFrame, lookback: int) -> pd.Series:
|
|
169
|
+
"""Generate dual momentum signals."""
|
|
170
|
+
returns = hist["Close"].pct_change(lookback * 21) # Monthly lookback
|
|
171
|
+
|
|
172
|
+
signals = pd.Series(0, index=hist.index)
|
|
173
|
+
signals[returns > 0] = 1 # Positive momentum
|
|
174
|
+
signals[returns < 0] = -1 # Negative momentum
|
|
175
|
+
|
|
176
|
+
return signals
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _breakout_signals(hist: pd.DataFrame, lookback: int) -> pd.Series:
|
|
180
|
+
"""Generate breakout signals."""
|
|
181
|
+
high_roll = hist["High"].rolling(lookback).max()
|
|
182
|
+
low_roll = hist["Low"].rolling(lookback).min()
|
|
183
|
+
|
|
184
|
+
signals = pd.Series(0, index=hist.index)
|
|
185
|
+
signals[hist["Close"] >= high_roll] = 1 # New high breakout
|
|
186
|
+
signals[hist["Close"] <= low_roll] = -1 # New low breakdown
|
|
187
|
+
|
|
188
|
+
return signals
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _simulate_trades(hist: pd.DataFrame, signals: pd.Series, initial_capital: float) -> dict:
|
|
192
|
+
"""Simulate trades based on signals."""
|
|
193
|
+
capital = initial_capital
|
|
194
|
+
position = 0
|
|
195
|
+
shares = 0
|
|
196
|
+
trades = []
|
|
197
|
+
equity_curve = [initial_capital]
|
|
198
|
+
|
|
199
|
+
for i in range(1, len(hist)):
|
|
200
|
+
date = hist.index[i]
|
|
201
|
+
price = hist["Close"].iloc[i]
|
|
202
|
+
signal = signals.iloc[i]
|
|
203
|
+
prev_signal = signals.iloc[i-1]
|
|
204
|
+
|
|
205
|
+
# Buy signal
|
|
206
|
+
if signal == 1 and prev_signal != 1 and position == 0:
|
|
207
|
+
shares = int(capital * 0.95 / price) # Use 95% of capital
|
|
208
|
+
if shares > 0:
|
|
209
|
+
cost = shares * price
|
|
210
|
+
capital -= cost
|
|
211
|
+
position = 1
|
|
212
|
+
trades.append({
|
|
213
|
+
"date": str(date.date()),
|
|
214
|
+
"action": "BUY",
|
|
215
|
+
"price": round(price, 2),
|
|
216
|
+
"shares": shares,
|
|
217
|
+
"value": round(cost, 2),
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
# Sell signal
|
|
221
|
+
elif signal == -1 and position == 1:
|
|
222
|
+
proceeds = shares * price
|
|
223
|
+
pnl = proceeds - (trades[-1]["value"] if trades else 0)
|
|
224
|
+
capital += proceeds
|
|
225
|
+
trades.append({
|
|
226
|
+
"date": str(date.date()),
|
|
227
|
+
"action": "SELL",
|
|
228
|
+
"price": round(price, 2),
|
|
229
|
+
"shares": shares,
|
|
230
|
+
"value": round(proceeds, 2),
|
|
231
|
+
"pnl": round(pnl, 2),
|
|
232
|
+
})
|
|
233
|
+
shares = 0
|
|
234
|
+
position = 0
|
|
235
|
+
|
|
236
|
+
# Track equity
|
|
237
|
+
equity = capital + (shares * price if position == 1 else 0)
|
|
238
|
+
equity_curve.append(equity)
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"trades": trades,
|
|
242
|
+
"equity_curve": equity_curve,
|
|
243
|
+
"final_equity": equity_curve[-1],
|
|
244
|
+
"final_position": position,
|
|
245
|
+
"final_shares": shares,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _calculate_metrics(results: dict, hist: pd.DataFrame, initial_capital: float) -> dict:
|
|
250
|
+
"""Calculate backtest metrics."""
|
|
251
|
+
equity = np.array(results["equity_curve"])
|
|
252
|
+
trades = results["trades"]
|
|
253
|
+
|
|
254
|
+
# Performance metrics
|
|
255
|
+
total_return = (equity[-1] / initial_capital - 1) * 100
|
|
256
|
+
|
|
257
|
+
# Calculate daily returns
|
|
258
|
+
daily_returns = np.diff(equity) / equity[:-1]
|
|
259
|
+
|
|
260
|
+
# Annualized metrics
|
|
261
|
+
trading_days = len(equity) - 1
|
|
262
|
+
years = trading_days / 252
|
|
263
|
+
annual_return = ((equity[-1] / initial_capital) ** (1/years) - 1) * 100 if years > 0 else 0
|
|
264
|
+
|
|
265
|
+
# Risk metrics
|
|
266
|
+
volatility = np.std(daily_returns) * np.sqrt(252) * 100 if len(daily_returns) > 0 else 0
|
|
267
|
+
|
|
268
|
+
# Sharpe ratio (assuming 0% risk-free rate)
|
|
269
|
+
sharpe = (np.mean(daily_returns) * 252) / (np.std(daily_returns) * np.sqrt(252)) if np.std(daily_returns) > 0 else 0
|
|
270
|
+
|
|
271
|
+
# Max drawdown
|
|
272
|
+
peak = np.maximum.accumulate(equity)
|
|
273
|
+
drawdown = (peak - equity) / peak
|
|
274
|
+
max_drawdown = np.max(drawdown) * 100
|
|
275
|
+
|
|
276
|
+
# Sortino ratio (downside deviation)
|
|
277
|
+
negative_returns = daily_returns[daily_returns < 0]
|
|
278
|
+
downside_std = np.std(negative_returns) * np.sqrt(252) if len(negative_returns) > 0 else 0
|
|
279
|
+
sortino = (np.mean(daily_returns) * 252) / downside_std if downside_std > 0 else 0
|
|
280
|
+
|
|
281
|
+
# Calmar ratio
|
|
282
|
+
calmar = annual_return / max_drawdown if max_drawdown > 0 else 0
|
|
283
|
+
|
|
284
|
+
# Trade statistics
|
|
285
|
+
winning_trades = [t for t in trades if t.get("action") == "SELL" and t.get("pnl", 0) > 0]
|
|
286
|
+
losing_trades = [t for t in trades if t.get("action") == "SELL" and t.get("pnl", 0) <= 0]
|
|
287
|
+
|
|
288
|
+
num_trades = len([t for t in trades if t.get("action") == "SELL"])
|
|
289
|
+
win_rate = len(winning_trades) / num_trades * 100 if num_trades > 0 else 0
|
|
290
|
+
|
|
291
|
+
avg_win = np.mean([t["pnl"] for t in winning_trades]) if winning_trades else 0
|
|
292
|
+
avg_loss = np.mean([t["pnl"] for t in losing_trades]) if losing_trades else 0
|
|
293
|
+
|
|
294
|
+
profit_factor = abs(sum(t["pnl"] for t in winning_trades) / sum(t["pnl"] for t in losing_trades)) if losing_trades and sum(t["pnl"] for t in losing_trades) != 0 else 0
|
|
295
|
+
|
|
296
|
+
# Buy and hold comparison
|
|
297
|
+
buy_hold_return = (hist["Close"].iloc[-1] / hist["Close"].iloc[0] - 1) * 100
|
|
298
|
+
alpha = total_return - buy_hold_return
|
|
299
|
+
|
|
300
|
+
# Monthly returns
|
|
301
|
+
monthly_returns = []
|
|
302
|
+
if len(equity) > 21:
|
|
303
|
+
for i in range(21, len(equity), 21):
|
|
304
|
+
month_return = (equity[i] / equity[i-21] - 1) * 100
|
|
305
|
+
month_date = hist.index[min(i, len(hist)-1)]
|
|
306
|
+
monthly_returns.append({
|
|
307
|
+
"month": month_date.strftime("%Y-%m"),
|
|
308
|
+
"return": round(month_return, 2),
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
"performance": {
|
|
313
|
+
"initial_capital": f"${initial_capital:,.0f}",
|
|
314
|
+
"final_equity": f"${results['final_equity']:,.0f}",
|
|
315
|
+
"total_return": f"{total_return:.2f}%",
|
|
316
|
+
"annual_return": f"{annual_return:.2f}%",
|
|
317
|
+
"buy_hold_return": f"{buy_hold_return:.2f}%",
|
|
318
|
+
"alpha": f"{alpha:.2f}%",
|
|
319
|
+
},
|
|
320
|
+
"risk": {
|
|
321
|
+
"volatility": f"{volatility:.2f}%",
|
|
322
|
+
"max_drawdown": f"{max_drawdown:.2f}%",
|
|
323
|
+
"sharpe_ratio": f"{sharpe:.2f}",
|
|
324
|
+
"sortino_ratio": f"{sortino:.2f}",
|
|
325
|
+
"calmar_ratio": f"{calmar:.2f}",
|
|
326
|
+
},
|
|
327
|
+
"trades": {
|
|
328
|
+
"total_trades": num_trades,
|
|
329
|
+
"win_rate": f"{win_rate:.1f}%",
|
|
330
|
+
"profit_factor": f"{profit_factor:.2f}",
|
|
331
|
+
"avg_win": f"${avg_win:,.0f}",
|
|
332
|
+
"avg_loss": f"${avg_loss:,.0f}",
|
|
333
|
+
"trade_list": trades[-10:], # Last 10 trades
|
|
334
|
+
},
|
|
335
|
+
"monthly_returns": monthly_returns[-12:], # Last 12 months
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def get_available_strategies() -> dict:
|
|
340
|
+
"""Get list of available strategies."""
|
|
341
|
+
return {
|
|
342
|
+
name: {
|
|
343
|
+
"name": info["name"],
|
|
344
|
+
"description": info["description"],
|
|
345
|
+
"default_params": info["params"],
|
|
346
|
+
}
|
|
347
|
+
for name, info in STRATEGIES.items()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# Tool definition for LLM
|
|
352
|
+
BACKTEST_TOOL = {
|
|
353
|
+
"type": "function",
|
|
354
|
+
"function": {
|
|
355
|
+
"name": "run_backtest",
|
|
356
|
+
"description": "Run a backtest simulation with a trading strategy",
|
|
357
|
+
"parameters": {
|
|
358
|
+
"type": "object",
|
|
359
|
+
"properties": {
|
|
360
|
+
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
361
|
+
"strategy": {
|
|
362
|
+
"type": "string",
|
|
363
|
+
"enum": list(STRATEGIES.keys()),
|
|
364
|
+
"description": "Trading strategy to test"
|
|
365
|
+
},
|
|
366
|
+
"period": {"type": "string", "description": "Backtest period (e.g., 2y)", "default": "2y"},
|
|
367
|
+
"initial_capital": {"type": "number", "description": "Starting capital", "default": 100000},
|
|
368
|
+
},
|
|
369
|
+
"required": ["symbol", "strategy"]
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|