sigma-terminal 3.4.0__py3-none-any.whl → 3.5.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 +4 -5
- sigma/analytics/__init__.py +11 -9
- sigma/app.py +384 -1125
- sigma/backtest/__init__.py +2 -0
- sigma/backtest/service.py +116 -0
- sigma/charts.py +2 -2
- sigma/cli.py +15 -13
- sigma/comparison.py +2 -2
- sigma/config.py +25 -12
- sigma/core/command_router.py +93 -0
- sigma/llm/__init__.py +3 -0
- sigma/llm/providers/anthropic_provider.py +196 -0
- sigma/llm/providers/base.py +29 -0
- sigma/llm/providers/google_provider.py +197 -0
- sigma/llm/providers/ollama_provider.py +156 -0
- sigma/llm/providers/openai_provider.py +168 -0
- sigma/llm/providers/sigma_cloud_provider.py +57 -0
- sigma/llm/rate_limit.py +40 -0
- sigma/llm/registry.py +66 -0
- sigma/llm/router.py +122 -0
- sigma/setup_agent.py +188 -0
- sigma/tools/__init__.py +23 -0
- sigma/tools/adapter.py +38 -0
- sigma/{tools.py → tools/library.py} +593 -1
- sigma/tools/registry.py +108 -0
- sigma/utils/extraction.py +83 -0
- sigma_terminal-3.5.0.dist-info/METADATA +184 -0
- sigma_terminal-3.5.0.dist-info/RECORD +46 -0
- sigma/llm.py +0 -786
- sigma/setup.py +0 -440
- sigma_terminal-3.4.0.dist-info/METADATA +0 -264
- sigma_terminal-3.4.0.dist-info/RECORD +0 -30
- /sigma/{backtest.py → backtest/simple_engine.py} +0 -0
- {sigma_terminal-3.4.0.dist-info → sigma_terminal-3.5.0.dist-info}/WHEEL +0 -0
- {sigma_terminal-3.4.0.dist-info → sigma_terminal-3.5.0.dist-info}/entry_points.txt +0 -0
- {sigma_terminal-3.4.0.dist-info → sigma_terminal-3.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -962,6 +962,466 @@ def search_earnings_transcripts(company: str, num_results: int = 3) -> dict:
|
|
|
962
962
|
return {"error": str(e)}
|
|
963
963
|
|
|
964
964
|
|
|
965
|
+
# ============================================================================
|
|
966
|
+
# CHART GENERATION TOOLS
|
|
967
|
+
# ============================================================================
|
|
968
|
+
|
|
969
|
+
def generate_stock_chart(symbol: str, period: str = "6mo", chart_type: str = "candlestick",
|
|
970
|
+
show_volume: bool = True, show_indicators: bool = True) -> dict:
|
|
971
|
+
"""Generate a stock chart and save it to file."""
|
|
972
|
+
try:
|
|
973
|
+
from .charts import create_candlestick_chart, create_line_chart, create_technical_chart
|
|
974
|
+
|
|
975
|
+
ticker = yf.Ticker(symbol.upper())
|
|
976
|
+
data = ticker.history(period=period)
|
|
977
|
+
|
|
978
|
+
if data.empty:
|
|
979
|
+
return {"error": f"No data found for {symbol}", "symbol": symbol}
|
|
980
|
+
|
|
981
|
+
# Generate chart based on type
|
|
982
|
+
if chart_type == "candlestick":
|
|
983
|
+
chart_path = create_candlestick_chart(
|
|
984
|
+
symbol=symbol,
|
|
985
|
+
data=data,
|
|
986
|
+
show_volume=show_volume,
|
|
987
|
+
show_sma=show_indicators
|
|
988
|
+
)
|
|
989
|
+
elif chart_type == "line":
|
|
990
|
+
chart_path = create_line_chart(
|
|
991
|
+
symbol=symbol,
|
|
992
|
+
data=data,
|
|
993
|
+
show_volume=show_volume
|
|
994
|
+
)
|
|
995
|
+
elif chart_type == "technical":
|
|
996
|
+
chart_path = create_technical_chart(
|
|
997
|
+
symbol=symbol,
|
|
998
|
+
data=data,
|
|
999
|
+
indicators=["rsi", "macd"] if show_indicators else []
|
|
1000
|
+
)
|
|
1001
|
+
else:
|
|
1002
|
+
chart_path = create_candlestick_chart(symbol=symbol, data=data)
|
|
1003
|
+
|
|
1004
|
+
return {
|
|
1005
|
+
"symbol": symbol.upper(),
|
|
1006
|
+
"chart_type": chart_type,
|
|
1007
|
+
"period": period,
|
|
1008
|
+
"chart_path": chart_path,
|
|
1009
|
+
"message": f"Chart generated and saved to: {chart_path}",
|
|
1010
|
+
"data_points": len(data),
|
|
1011
|
+
"start_date": str(data.index[0].date()),
|
|
1012
|
+
"end_date": str(data.index[-1].date()),
|
|
1013
|
+
}
|
|
1014
|
+
except Exception as e:
|
|
1015
|
+
return {"error": str(e), "symbol": symbol}
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def generate_comparison_chart(symbols: list, period: str = "1y", normalize: bool = True) -> dict:
|
|
1019
|
+
"""Generate a comparison chart for multiple stocks."""
|
|
1020
|
+
try:
|
|
1021
|
+
from .charts import create_comparison_chart
|
|
1022
|
+
|
|
1023
|
+
data_dict = {}
|
|
1024
|
+
for symbol in symbols:
|
|
1025
|
+
ticker = yf.Ticker(symbol.upper())
|
|
1026
|
+
data = ticker.history(period=period)
|
|
1027
|
+
if not data.empty:
|
|
1028
|
+
data_dict[symbol.upper()] = data
|
|
1029
|
+
|
|
1030
|
+
if not data_dict:
|
|
1031
|
+
return {"error": "No data found for any symbols", "symbols": symbols}
|
|
1032
|
+
|
|
1033
|
+
chart_path = create_comparison_chart(
|
|
1034
|
+
symbols=[s.upper() for s in symbols],
|
|
1035
|
+
data_dict=data_dict,
|
|
1036
|
+
normalize=normalize
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
return {
|
|
1040
|
+
"symbols": list(data_dict.keys()),
|
|
1041
|
+
"period": period,
|
|
1042
|
+
"normalized": normalize,
|
|
1043
|
+
"chart_path": chart_path,
|
|
1044
|
+
"message": f"Comparison chart saved to: {chart_path}"
|
|
1045
|
+
}
|
|
1046
|
+
except Exception as e:
|
|
1047
|
+
return {"error": str(e)}
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
# ============================================================================
|
|
1051
|
+
# ADVANCED ANALYSIS TOOLS
|
|
1052
|
+
# ============================================================================
|
|
1053
|
+
|
|
1054
|
+
def get_valuation_metrics(symbol: str) -> dict:
|
|
1055
|
+
"""Get comprehensive valuation metrics for a stock."""
|
|
1056
|
+
try:
|
|
1057
|
+
ticker = yf.Ticker(symbol.upper())
|
|
1058
|
+
info = ticker.info
|
|
1059
|
+
|
|
1060
|
+
# Calculate valuation ratios
|
|
1061
|
+
pe_ratio = info.get("trailingPE", None)
|
|
1062
|
+
forward_pe = info.get("forwardPE", None)
|
|
1063
|
+
peg_ratio = info.get("pegRatio", None)
|
|
1064
|
+
pb_ratio = info.get("priceToBook", None)
|
|
1065
|
+
ps_ratio = info.get("priceToSalesTrailing12Months", None)
|
|
1066
|
+
ev_ebitda = info.get("enterpriseToEbitda", None)
|
|
1067
|
+
ev_revenue = info.get("enterpriseToRevenue", None)
|
|
1068
|
+
|
|
1069
|
+
# Get growth metrics
|
|
1070
|
+
earnings_growth = info.get("earningsGrowth", None)
|
|
1071
|
+
revenue_growth = info.get("revenueGrowth", None)
|
|
1072
|
+
|
|
1073
|
+
# Get profitability
|
|
1074
|
+
profit_margin = info.get("profitMargins", None)
|
|
1075
|
+
operating_margin = info.get("operatingMargins", None)
|
|
1076
|
+
roe = info.get("returnOnEquity", None)
|
|
1077
|
+
roa = info.get("returnOnAssets", None)
|
|
1078
|
+
|
|
1079
|
+
# Determine valuation assessment
|
|
1080
|
+
assessment = "FAIR"
|
|
1081
|
+
if pe_ratio and forward_pe:
|
|
1082
|
+
if pe_ratio > 30 and forward_pe > 25:
|
|
1083
|
+
assessment = "EXPENSIVE"
|
|
1084
|
+
elif pe_ratio < 15 and forward_pe < 12:
|
|
1085
|
+
assessment = "CHEAP"
|
|
1086
|
+
|
|
1087
|
+
return {
|
|
1088
|
+
"symbol": symbol.upper(),
|
|
1089
|
+
"name": info.get("shortName", symbol),
|
|
1090
|
+
"valuation": {
|
|
1091
|
+
"pe_ratio": round(pe_ratio, 2) if pe_ratio else "N/A",
|
|
1092
|
+
"forward_pe": round(forward_pe, 2) if forward_pe else "N/A",
|
|
1093
|
+
"peg_ratio": round(peg_ratio, 2) if peg_ratio else "N/A",
|
|
1094
|
+
"price_to_book": round(pb_ratio, 2) if pb_ratio else "N/A",
|
|
1095
|
+
"price_to_sales": round(ps_ratio, 2) if ps_ratio else "N/A",
|
|
1096
|
+
"ev_to_ebitda": round(ev_ebitda, 2) if ev_ebitda else "N/A",
|
|
1097
|
+
"ev_to_revenue": round(ev_revenue, 2) if ev_revenue else "N/A",
|
|
1098
|
+
},
|
|
1099
|
+
"growth": {
|
|
1100
|
+
"earnings_growth": f"{earnings_growth*100:.1f}%" if earnings_growth else "N/A",
|
|
1101
|
+
"revenue_growth": f"{revenue_growth*100:.1f}%" if revenue_growth else "N/A",
|
|
1102
|
+
},
|
|
1103
|
+
"profitability": {
|
|
1104
|
+
"profit_margin": f"{profit_margin*100:.1f}%" if profit_margin else "N/A",
|
|
1105
|
+
"operating_margin": f"{operating_margin*100:.1f}%" if operating_margin else "N/A",
|
|
1106
|
+
"return_on_equity": f"{roe*100:.1f}%" if roe else "N/A",
|
|
1107
|
+
"return_on_assets": f"{roa*100:.1f}%" if roa else "N/A",
|
|
1108
|
+
},
|
|
1109
|
+
"assessment": assessment,
|
|
1110
|
+
}
|
|
1111
|
+
except Exception as e:
|
|
1112
|
+
return {"error": str(e), "symbol": symbol}
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def get_risk_metrics(symbol: str, period: str = "1y") -> dict:
|
|
1116
|
+
"""Calculate comprehensive risk metrics for a stock."""
|
|
1117
|
+
try:
|
|
1118
|
+
ticker = yf.Ticker(symbol.upper())
|
|
1119
|
+
hist = ticker.history(period=period)
|
|
1120
|
+
|
|
1121
|
+
if hist.empty or len(hist) < 30:
|
|
1122
|
+
return {"error": "Insufficient data for risk analysis", "symbol": symbol}
|
|
1123
|
+
|
|
1124
|
+
# Calculate daily returns
|
|
1125
|
+
returns = hist["Close"].pct_change().dropna()
|
|
1126
|
+
|
|
1127
|
+
# Basic risk metrics
|
|
1128
|
+
volatility = returns.std() * np.sqrt(252) * 100
|
|
1129
|
+
|
|
1130
|
+
# Value at Risk (VaR) - 95% confidence
|
|
1131
|
+
var_95 = np.percentile(returns, 5) * 100
|
|
1132
|
+
|
|
1133
|
+
# Conditional VaR (Expected Shortfall)
|
|
1134
|
+
cvar_95 = returns[returns <= np.percentile(returns, 5)].mean() * 100
|
|
1135
|
+
|
|
1136
|
+
# Maximum Drawdown
|
|
1137
|
+
cumulative = (1 + returns).cumprod()
|
|
1138
|
+
peak = cumulative.cummax()
|
|
1139
|
+
drawdown = (cumulative - peak) / peak
|
|
1140
|
+
max_drawdown = drawdown.min() * 100
|
|
1141
|
+
|
|
1142
|
+
# Sharpe Ratio (assuming 0% risk-free rate)
|
|
1143
|
+
sharpe = (returns.mean() * 252) / (returns.std() * np.sqrt(252))
|
|
1144
|
+
|
|
1145
|
+
# Sortino Ratio
|
|
1146
|
+
negative_returns = returns[returns < 0]
|
|
1147
|
+
downside_std = negative_returns.std() * np.sqrt(252)
|
|
1148
|
+
sortino = (returns.mean() * 252) / downside_std if downside_std > 0 else 0
|
|
1149
|
+
|
|
1150
|
+
# Beta calculation vs SPY
|
|
1151
|
+
try:
|
|
1152
|
+
spy = yf.Ticker("SPY")
|
|
1153
|
+
spy_hist = spy.history(period=period)
|
|
1154
|
+
spy_returns = spy_hist["Close"].pct_change().dropna()
|
|
1155
|
+
|
|
1156
|
+
# Align dates
|
|
1157
|
+
common_dates = returns.index.intersection(spy_returns.index)
|
|
1158
|
+
if len(common_dates) > 30:
|
|
1159
|
+
stock_r = returns.loc[common_dates]
|
|
1160
|
+
spy_r = spy_returns.loc[common_dates]
|
|
1161
|
+
|
|
1162
|
+
covariance = np.cov(stock_r, spy_r)[0, 1]
|
|
1163
|
+
spy_variance = np.var(spy_r)
|
|
1164
|
+
beta = covariance / spy_variance if spy_variance > 0 else 1.0
|
|
1165
|
+
|
|
1166
|
+
# Alpha (annualized)
|
|
1167
|
+
alpha = (returns.mean() * 252) - (beta * spy_returns.mean() * 252)
|
|
1168
|
+
else:
|
|
1169
|
+
beta = 1.0
|
|
1170
|
+
alpha = 0
|
|
1171
|
+
except:
|
|
1172
|
+
beta = 1.0
|
|
1173
|
+
alpha = 0
|
|
1174
|
+
|
|
1175
|
+
# Risk assessment
|
|
1176
|
+
risk_level = "MODERATE"
|
|
1177
|
+
if volatility > 40 or abs(max_drawdown) > 30:
|
|
1178
|
+
risk_level = "HIGH"
|
|
1179
|
+
elif volatility < 20 and abs(max_drawdown) < 15:
|
|
1180
|
+
risk_level = "LOW"
|
|
1181
|
+
|
|
1182
|
+
return {
|
|
1183
|
+
"symbol": symbol.upper(),
|
|
1184
|
+
"period": period,
|
|
1185
|
+
"volatility": {
|
|
1186
|
+
"annualized": f"{volatility:.2f}%",
|
|
1187
|
+
"daily": f"{returns.std()*100:.3f}%",
|
|
1188
|
+
},
|
|
1189
|
+
"drawdown": {
|
|
1190
|
+
"max_drawdown": f"{max_drawdown:.2f}%",
|
|
1191
|
+
"current_drawdown": f"{drawdown.iloc[-1]*100:.2f}%",
|
|
1192
|
+
},
|
|
1193
|
+
"value_at_risk": {
|
|
1194
|
+
"var_95": f"{var_95:.2f}%",
|
|
1195
|
+
"cvar_95": f"{cvar_95:.2f}%",
|
|
1196
|
+
},
|
|
1197
|
+
"ratios": {
|
|
1198
|
+
"sharpe": f"{sharpe:.2f}",
|
|
1199
|
+
"sortino": f"{sortino:.2f}",
|
|
1200
|
+
"beta": f"{beta:.2f}",
|
|
1201
|
+
"alpha": f"{alpha*100:.2f}%",
|
|
1202
|
+
},
|
|
1203
|
+
"risk_level": risk_level,
|
|
1204
|
+
}
|
|
1205
|
+
except Exception as e:
|
|
1206
|
+
return {"error": str(e), "symbol": symbol}
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def get_earnings_analysis(symbol: str) -> dict:
|
|
1210
|
+
"""Get detailed earnings analysis including surprises and estimates."""
|
|
1211
|
+
try:
|
|
1212
|
+
ticker = yf.Ticker(symbol.upper())
|
|
1213
|
+
info = ticker.info
|
|
1214
|
+
|
|
1215
|
+
# Get earnings dates and history
|
|
1216
|
+
earnings_dates = ticker.earnings_dates
|
|
1217
|
+
earnings_history = ticker.earnings_history if hasattr(ticker, 'earnings_history') else None
|
|
1218
|
+
|
|
1219
|
+
# Build earnings data
|
|
1220
|
+
upcoming = None
|
|
1221
|
+
if earnings_dates is not None and not earnings_dates.empty:
|
|
1222
|
+
future_dates = earnings_dates[earnings_dates.index > pd.Timestamp.now()]
|
|
1223
|
+
if not future_dates.empty:
|
|
1224
|
+
next_date = future_dates.index[0]
|
|
1225
|
+
upcoming = {
|
|
1226
|
+
"date": str(next_date.date()) if hasattr(next_date, 'date') else str(next_date)[:10],
|
|
1227
|
+
"eps_estimate": future_dates.iloc[0].get("EPS Estimate", "N/A"),
|
|
1228
|
+
"revenue_estimate": future_dates.iloc[0].get("Revenue Estimate", "N/A"),
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
# Get quarterly earnings
|
|
1232
|
+
quarterly_earnings = []
|
|
1233
|
+
if hasattr(ticker, 'quarterly_earnings') and ticker.quarterly_earnings is not None:
|
|
1234
|
+
qe = ticker.quarterly_earnings
|
|
1235
|
+
if not qe.empty:
|
|
1236
|
+
for date, row in qe.tail(4).iterrows():
|
|
1237
|
+
quarterly_earnings.append({
|
|
1238
|
+
"quarter": str(date),
|
|
1239
|
+
"revenue": row.get("Revenue", "N/A"),
|
|
1240
|
+
"earnings": row.get("Earnings", "N/A"),
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
return {
|
|
1244
|
+
"symbol": symbol.upper(),
|
|
1245
|
+
"name": info.get("shortName", symbol),
|
|
1246
|
+
"eps_trailing": info.get("trailingEps", "N/A"),
|
|
1247
|
+
"eps_forward": info.get("forwardEps", "N/A"),
|
|
1248
|
+
"pe_ratio": info.get("trailingPE", "N/A"),
|
|
1249
|
+
"forward_pe": info.get("forwardPE", "N/A"),
|
|
1250
|
+
"upcoming_earnings": upcoming,
|
|
1251
|
+
"quarterly_history": quarterly_earnings,
|
|
1252
|
+
"earnings_growth": f"{info.get('earningsGrowth', 0)*100:.1f}%" if info.get('earningsGrowth') else "N/A",
|
|
1253
|
+
}
|
|
1254
|
+
except Exception as e:
|
|
1255
|
+
return {"error": str(e), "symbol": symbol}
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def get_dividend_analysis(symbol: str) -> dict:
|
|
1259
|
+
"""Get comprehensive dividend analysis."""
|
|
1260
|
+
try:
|
|
1261
|
+
ticker = yf.Ticker(symbol.upper())
|
|
1262
|
+
info = ticker.info
|
|
1263
|
+
|
|
1264
|
+
# Get dividend data
|
|
1265
|
+
div_rate = info.get("dividendRate", 0)
|
|
1266
|
+
div_yield = info.get("dividendYield", 0)
|
|
1267
|
+
payout_ratio = info.get("payoutRatio", 0)
|
|
1268
|
+
ex_div_date = info.get("exDividendDate")
|
|
1269
|
+
|
|
1270
|
+
# Get dividend history
|
|
1271
|
+
dividends = ticker.dividends
|
|
1272
|
+
div_history = []
|
|
1273
|
+
if dividends is not None and not dividends.empty:
|
|
1274
|
+
for dt, amount in dividends.tail(8).items():
|
|
1275
|
+
date_str = str(dt.date()) if hasattr(dt, 'date') else str(dt)[:10] # type: ignore[union-attr]
|
|
1276
|
+
div_history.append({
|
|
1277
|
+
"date": date_str,
|
|
1278
|
+
"amount": f"${amount:.4f}",
|
|
1279
|
+
})
|
|
1280
|
+
|
|
1281
|
+
# Calculate dividend growth
|
|
1282
|
+
if len(dividends) >= 8:
|
|
1283
|
+
recent_divs = dividends.tail(4).sum()
|
|
1284
|
+
older_divs = dividends.tail(8).head(4).sum()
|
|
1285
|
+
div_growth = ((recent_divs / older_divs) - 1) * 100 if older_divs > 0 else 0
|
|
1286
|
+
else:
|
|
1287
|
+
div_growth = None
|
|
1288
|
+
|
|
1289
|
+
return {
|
|
1290
|
+
"symbol": symbol.upper(),
|
|
1291
|
+
"name": info.get("shortName", symbol),
|
|
1292
|
+
"dividend_rate": f"${div_rate:.2f}" if div_rate else "N/A",
|
|
1293
|
+
"dividend_yield": f"{div_yield*100:.2f}%" if div_yield else "N/A",
|
|
1294
|
+
"payout_ratio": f"{payout_ratio*100:.1f}%" if payout_ratio else "N/A",
|
|
1295
|
+
"ex_dividend_date": str(datetime.fromtimestamp(ex_div_date).date()) if ex_div_date else "N/A",
|
|
1296
|
+
"annual_dividend": f"${div_rate:.2f}" if div_rate else "N/A",
|
|
1297
|
+
"dividend_growth_yoy": f"{div_growth:.1f}%" if div_growth else "N/A",
|
|
1298
|
+
"history": div_history,
|
|
1299
|
+
}
|
|
1300
|
+
except Exception as e:
|
|
1301
|
+
return {"error": str(e), "symbol": symbol}
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def get_options_summary(symbol: str) -> dict:
|
|
1305
|
+
"""Get options chain summary with key metrics."""
|
|
1306
|
+
try:
|
|
1307
|
+
ticker = yf.Ticker(symbol.upper())
|
|
1308
|
+
|
|
1309
|
+
# Get expiration dates
|
|
1310
|
+
expirations = ticker.options
|
|
1311
|
+
if not expirations:
|
|
1312
|
+
return {"error": "No options available", "symbol": symbol}
|
|
1313
|
+
|
|
1314
|
+
# Get nearest expiration
|
|
1315
|
+
nearest_exp = expirations[0]
|
|
1316
|
+
opt_chain = ticker.option_chain(nearest_exp)
|
|
1317
|
+
|
|
1318
|
+
calls = opt_chain.calls
|
|
1319
|
+
puts = opt_chain.puts
|
|
1320
|
+
|
|
1321
|
+
# Calculate put/call ratio
|
|
1322
|
+
total_call_volume = calls["volume"].sum() if "volume" in calls else 0
|
|
1323
|
+
total_put_volume = puts["volume"].sum() if "volume" in puts else 0
|
|
1324
|
+
pc_ratio = total_put_volume / total_call_volume if total_call_volume > 0 else 0
|
|
1325
|
+
|
|
1326
|
+
# Get ATM options
|
|
1327
|
+
current_price = ticker.info.get("regularMarketPrice", 0)
|
|
1328
|
+
|
|
1329
|
+
atm_call = calls.iloc[(calls["strike"] - current_price).abs().argsort()[:1]]
|
|
1330
|
+
atm_put = puts.iloc[(puts["strike"] - current_price).abs().argsort()[:1]]
|
|
1331
|
+
|
|
1332
|
+
# Implied volatility
|
|
1333
|
+
atm_call_iv = atm_call["impliedVolatility"].values[0] if not atm_call.empty else 0
|
|
1334
|
+
atm_put_iv = atm_put["impliedVolatility"].values[0] if not atm_put.empty else 0
|
|
1335
|
+
avg_iv = (atm_call_iv + atm_put_iv) / 2
|
|
1336
|
+
|
|
1337
|
+
return {
|
|
1338
|
+
"symbol": symbol.upper(),
|
|
1339
|
+
"current_price": f"${current_price:.2f}",
|
|
1340
|
+
"expirations_available": len(expirations),
|
|
1341
|
+
"nearest_expiration": nearest_exp,
|
|
1342
|
+
"put_call_ratio": f"{pc_ratio:.2f}",
|
|
1343
|
+
"implied_volatility": f"{avg_iv*100:.1f}%",
|
|
1344
|
+
"call_volume": int(total_call_volume) if total_call_volume else 0,
|
|
1345
|
+
"put_volume": int(total_put_volume) if total_put_volume else 0,
|
|
1346
|
+
"atm_call": {
|
|
1347
|
+
"strike": float(atm_call["strike"].values[0]) if not atm_call.empty else 0,
|
|
1348
|
+
"bid": float(atm_call["bid"].values[0]) if not atm_call.empty else 0,
|
|
1349
|
+
"ask": float(atm_call["ask"].values[0]) if not atm_call.empty else 0,
|
|
1350
|
+
"iv": f"{atm_call_iv*100:.1f}%",
|
|
1351
|
+
},
|
|
1352
|
+
"atm_put": {
|
|
1353
|
+
"strike": float(atm_put["strike"].values[0]) if not atm_put.empty else 0,
|
|
1354
|
+
"bid": float(atm_put["bid"].values[0]) if not atm_put.empty else 0,
|
|
1355
|
+
"ask": float(atm_put["ask"].values[0]) if not atm_put.empty else 0,
|
|
1356
|
+
"iv": f"{atm_put_iv*100:.1f}%",
|
|
1357
|
+
},
|
|
1358
|
+
"sentiment": "BEARISH" if pc_ratio > 1.2 else ("BULLISH" if pc_ratio < 0.7 else "NEUTRAL"),
|
|
1359
|
+
}
|
|
1360
|
+
except Exception as e:
|
|
1361
|
+
return {"error": str(e), "symbol": symbol}
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
def get_peer_comparison(symbol: str) -> dict:
|
|
1365
|
+
"""Compare a stock with its industry peers."""
|
|
1366
|
+
try:
|
|
1367
|
+
ticker = yf.Ticker(symbol.upper())
|
|
1368
|
+
info = ticker.info
|
|
1369
|
+
|
|
1370
|
+
# Get sector and find peers
|
|
1371
|
+
sector = info.get("sector", "")
|
|
1372
|
+
industry = info.get("industry", "")
|
|
1373
|
+
|
|
1374
|
+
# Define peer groups by industry
|
|
1375
|
+
tech_peers = ["AAPL", "MSFT", "GOOGL", "META", "AMZN"]
|
|
1376
|
+
semi_peers = ["NVDA", "AMD", "INTC", "AVGO", "QCOM"]
|
|
1377
|
+
finance_peers = ["JPM", "BAC", "GS", "MS", "C"]
|
|
1378
|
+
healthcare_peers = ["JNJ", "PFE", "UNH", "MRK", "ABBV"]
|
|
1379
|
+
|
|
1380
|
+
# Select peer group
|
|
1381
|
+
symbol_upper = symbol.upper()
|
|
1382
|
+
if symbol_upper in tech_peers or "Technology" in sector:
|
|
1383
|
+
peers = [p for p in tech_peers if p != symbol_upper][:4]
|
|
1384
|
+
elif symbol_upper in semi_peers or "Semiconductor" in industry:
|
|
1385
|
+
peers = [p for p in semi_peers if p != symbol_upper][:4]
|
|
1386
|
+
elif symbol_upper in finance_peers or "Financial" in sector:
|
|
1387
|
+
peers = [p for p in finance_peers if p != symbol_upper][:4]
|
|
1388
|
+
elif symbol_upper in healthcare_peers or "Healthcare" in sector:
|
|
1389
|
+
peers = [p for p in healthcare_peers if p != symbol_upper][:4]
|
|
1390
|
+
else:
|
|
1391
|
+
peers = []
|
|
1392
|
+
|
|
1393
|
+
# Get metrics for target and peers
|
|
1394
|
+
all_symbols = [symbol_upper] + peers
|
|
1395
|
+
comparison = []
|
|
1396
|
+
|
|
1397
|
+
for sym in all_symbols:
|
|
1398
|
+
try:
|
|
1399
|
+
t = yf.Ticker(sym)
|
|
1400
|
+
i = t.info
|
|
1401
|
+
comparison.append({
|
|
1402
|
+
"symbol": sym,
|
|
1403
|
+
"name": i.get("shortName", sym),
|
|
1404
|
+
"price": i.get("regularMarketPrice", 0),
|
|
1405
|
+
"market_cap": i.get("marketCap", 0),
|
|
1406
|
+
"pe_ratio": round(i.get("trailingPE", 0), 2) if i.get("trailingPE") else "N/A",
|
|
1407
|
+
"pb_ratio": round(i.get("priceToBook", 0), 2) if i.get("priceToBook") else "N/A",
|
|
1408
|
+
"dividend_yield": f"{i.get('dividendYield', 0)*100:.2f}%" if i.get("dividendYield") else "N/A",
|
|
1409
|
+
"profit_margin": f"{i.get('profitMargins', 0)*100:.1f}%" if i.get("profitMargins") else "N/A",
|
|
1410
|
+
})
|
|
1411
|
+
except:
|
|
1412
|
+
continue
|
|
1413
|
+
|
|
1414
|
+
return {
|
|
1415
|
+
"target": symbol.upper(),
|
|
1416
|
+
"sector": sector,
|
|
1417
|
+
"industry": industry,
|
|
1418
|
+
"peer_count": len(peers),
|
|
1419
|
+
"comparison": comparison,
|
|
1420
|
+
}
|
|
1421
|
+
except Exception as e:
|
|
1422
|
+
return {"error": str(e), "symbol": symbol}
|
|
1423
|
+
|
|
1424
|
+
|
|
965
1425
|
# ============================================================================
|
|
966
1426
|
# TOOL DEFINITIONS FOR LLM
|
|
967
1427
|
# ============================================================================
|
|
@@ -990,7 +1450,8 @@ TOOLS = [
|
|
|
990
1450
|
"type": "object",
|
|
991
1451
|
"properties": {
|
|
992
1452
|
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
993
|
-
"period": {"type": "string", "description": "Time period: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max", "default": "3mo"}
|
|
1453
|
+
"period": {"type": "string", "description": "Time period: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max", "default": "3mo"},
|
|
1454
|
+
"interval": {"type": "string", "description": "Data interval: 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo", "default": "1d"}
|
|
994
1455
|
},
|
|
995
1456
|
"required": ["symbol"]
|
|
996
1457
|
}
|
|
@@ -1274,6 +1735,127 @@ TOOLS = [
|
|
|
1274
1735
|
}
|
|
1275
1736
|
}
|
|
1276
1737
|
},
|
|
1738
|
+
# Chart generation tools
|
|
1739
|
+
{
|
|
1740
|
+
"type": "function",
|
|
1741
|
+
"function": {
|
|
1742
|
+
"name": "generate_stock_chart",
|
|
1743
|
+
"description": "Generate a stock price chart (candlestick, line, or technical) with optional indicators. Returns file path where chart is saved.",
|
|
1744
|
+
"parameters": {
|
|
1745
|
+
"type": "object",
|
|
1746
|
+
"properties": {
|
|
1747
|
+
"symbol": {"type": "string", "description": "Stock ticker symbol (e.g., AAPL, NVDA)"},
|
|
1748
|
+
"period": {"type": "string", "description": "Time period: 1mo, 3mo, 6mo, 1y, 2y, 5y", "default": "6mo"},
|
|
1749
|
+
"chart_type": {"type": "string", "enum": ["candlestick", "line", "technical"], "description": "Type of chart", "default": "candlestick"},
|
|
1750
|
+
"show_volume": {"type": "boolean", "description": "Show volume bars", "default": True},
|
|
1751
|
+
"show_indicators": {"type": "boolean", "description": "Show moving averages/indicators", "default": True}
|
|
1752
|
+
},
|
|
1753
|
+
"required": ["symbol"]
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
{
|
|
1758
|
+
"type": "function",
|
|
1759
|
+
"function": {
|
|
1760
|
+
"name": "generate_comparison_chart",
|
|
1761
|
+
"description": "Generate a comparison chart showing multiple stocks' performance over time",
|
|
1762
|
+
"parameters": {
|
|
1763
|
+
"type": "object",
|
|
1764
|
+
"properties": {
|
|
1765
|
+
"symbols": {"type": "array", "items": {"type": "string"}, "description": "List of stock symbols to compare"},
|
|
1766
|
+
"period": {"type": "string", "description": "Time period for comparison", "default": "1y"},
|
|
1767
|
+
"normalize": {"type": "boolean", "description": "Normalize to percentage returns", "default": True}
|
|
1768
|
+
},
|
|
1769
|
+
"required": ["symbols"]
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
},
|
|
1773
|
+
# Advanced analysis tools
|
|
1774
|
+
{
|
|
1775
|
+
"type": "function",
|
|
1776
|
+
"function": {
|
|
1777
|
+
"name": "get_valuation_metrics",
|
|
1778
|
+
"description": "Get comprehensive valuation metrics (P/E, P/B, PEG, EV/EBITDA) with assessment",
|
|
1779
|
+
"parameters": {
|
|
1780
|
+
"type": "object",
|
|
1781
|
+
"properties": {
|
|
1782
|
+
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
1783
|
+
},
|
|
1784
|
+
"required": ["symbol"]
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
},
|
|
1788
|
+
{
|
|
1789
|
+
"type": "function",
|
|
1790
|
+
"function": {
|
|
1791
|
+
"name": "get_risk_metrics",
|
|
1792
|
+
"description": "Calculate risk metrics: volatility, VaR, max drawdown, Sharpe, Sortino, Beta, Alpha",
|
|
1793
|
+
"parameters": {
|
|
1794
|
+
"type": "object",
|
|
1795
|
+
"properties": {
|
|
1796
|
+
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
1797
|
+
"period": {"type": "string", "description": "Analysis period (1y, 2y, 5y)", "default": "1y"}
|
|
1798
|
+
},
|
|
1799
|
+
"required": ["symbol"]
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
},
|
|
1803
|
+
{
|
|
1804
|
+
"type": "function",
|
|
1805
|
+
"function": {
|
|
1806
|
+
"name": "get_earnings_analysis",
|
|
1807
|
+
"description": "Get earnings analysis: EPS, upcoming dates, quarterly history, growth",
|
|
1808
|
+
"parameters": {
|
|
1809
|
+
"type": "object",
|
|
1810
|
+
"properties": {
|
|
1811
|
+
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
1812
|
+
},
|
|
1813
|
+
"required": ["symbol"]
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
"type": "function",
|
|
1819
|
+
"function": {
|
|
1820
|
+
"name": "get_dividend_analysis",
|
|
1821
|
+
"description": "Get dividend analysis: yield, payout ratio, ex-date, dividend history and growth",
|
|
1822
|
+
"parameters": {
|
|
1823
|
+
"type": "object",
|
|
1824
|
+
"properties": {
|
|
1825
|
+
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
1826
|
+
},
|
|
1827
|
+
"required": ["symbol"]
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
"type": "function",
|
|
1833
|
+
"function": {
|
|
1834
|
+
"name": "get_options_summary",
|
|
1835
|
+
"description": "Get options chain summary: put/call ratio, implied volatility, ATM options",
|
|
1836
|
+
"parameters": {
|
|
1837
|
+
"type": "object",
|
|
1838
|
+
"properties": {
|
|
1839
|
+
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
1840
|
+
},
|
|
1841
|
+
"required": ["symbol"]
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
"type": "function",
|
|
1847
|
+
"function": {
|
|
1848
|
+
"name": "get_peer_comparison",
|
|
1849
|
+
"description": "Compare a stock with its industry peers on key metrics",
|
|
1850
|
+
"parameters": {
|
|
1851
|
+
"type": "object",
|
|
1852
|
+
"properties": {
|
|
1853
|
+
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
1854
|
+
},
|
|
1855
|
+
"required": ["symbol"]
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
},
|
|
1277
1859
|
]
|
|
1278
1860
|
|
|
1279
1861
|
|
|
@@ -1303,6 +1885,16 @@ TOOL_FUNCTIONS = {
|
|
|
1303
1885
|
"polygon_get_aggregates": polygon_get_aggregates,
|
|
1304
1886
|
"polygon_get_ticker_news": polygon_get_ticker_news,
|
|
1305
1887
|
"polygon_market_status": polygon_market_status,
|
|
1888
|
+
# Chart generation
|
|
1889
|
+
"generate_stock_chart": generate_stock_chart,
|
|
1890
|
+
"generate_comparison_chart": generate_comparison_chart,
|
|
1891
|
+
# Advanced analysis
|
|
1892
|
+
"get_valuation_metrics": get_valuation_metrics,
|
|
1893
|
+
"get_risk_metrics": get_risk_metrics,
|
|
1894
|
+
"get_earnings_analysis": get_earnings_analysis,
|
|
1895
|
+
"get_dividend_analysis": get_dividend_analysis,
|
|
1896
|
+
"get_options_summary": get_options_summary,
|
|
1897
|
+
"get_peer_comparison": get_peer_comparison,
|
|
1306
1898
|
}
|
|
1307
1899
|
|
|
1308
1900
|
|