sigma-terminal 3.3.2__py3-none-any.whl → 3.4.1__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/tools.py CHANGED
@@ -583,6 +583,211 @@ def get_market_news(tickers: str = "", topics: str = "") -> dict:
583
583
  return {"error": str(e)}
584
584
 
585
585
 
586
+ # ============================================================================
587
+ # POLYGON.IO TOOLS (Real-time & Historical Market Data)
588
+ # ============================================================================
589
+
590
+ def _get_polygon_key() -> Optional[str]:
591
+ """Get Polygon.io API key from config."""
592
+ try:
593
+ from .config import get_settings
594
+ return get_settings().polygon_api_key
595
+ except:
596
+ return None
597
+
598
+
599
+ def polygon_get_quote(symbol: str) -> dict:
600
+ """Get real-time quote from Polygon.io with additional data."""
601
+ api_key = _get_polygon_key()
602
+ if not api_key:
603
+ return {"error": "Polygon API key not configured. Use /setkey polygon <key>", "fallback": True}
604
+
605
+ import requests
606
+
607
+ try:
608
+ symbol = symbol.upper()
609
+
610
+ # Get previous day's data
611
+ prev_url = f"https://api.polygon.io/v2/aggs/ticker/{symbol}/prev?adjusted=true&apiKey={api_key}"
612
+ prev_response = requests.get(prev_url, timeout=10)
613
+
614
+ if prev_response.status_code == 403:
615
+ return {"error": "Polygon API key is invalid or expired", "error_code": 1101}
616
+ elif prev_response.status_code == 429:
617
+ return {"error": "Polygon rate limit exceeded", "error_code": 1303}
618
+ elif prev_response.status_code != 200:
619
+ return {"error": f"Polygon API error: {prev_response.status_code}"}
620
+
621
+ prev_data = prev_response.json()
622
+
623
+ if prev_data.get("resultsCount", 0) == 0:
624
+ return {"error": f"No data found for {symbol}", "error_code": 1300}
625
+
626
+ result = prev_data["results"][0]
627
+
628
+ # Get ticker details
629
+ details_url = f"https://api.polygon.io/v3/reference/tickers/{symbol}?apiKey={api_key}"
630
+ details_response = requests.get(details_url, timeout=10)
631
+ details = {}
632
+ if details_response.status_code == 200:
633
+ details_data = details_response.json()
634
+ if details_data.get("results"):
635
+ details = details_data["results"]
636
+
637
+ return {
638
+ "symbol": symbol,
639
+ "name": details.get("name", symbol),
640
+ "open": result.get("o", 0),
641
+ "high": result.get("h", 0),
642
+ "low": result.get("l", 0),
643
+ "close": result.get("c", 0),
644
+ "volume": result.get("v", 0),
645
+ "vwap": result.get("vw", 0),
646
+ "timestamp": result.get("t"),
647
+ "transactions": result.get("n", 0),
648
+ "market_cap": details.get("market_cap"),
649
+ "primary_exchange": details.get("primary_exchange"),
650
+ "type": details.get("type"),
651
+ "source": "polygon.io"
652
+ }
653
+ except requests.exceptions.Timeout:
654
+ return {"error": "Request timed out", "error_code": 1002}
655
+ except requests.exceptions.ConnectionError:
656
+ return {"error": "Connection error", "error_code": 1400}
657
+ except Exception as e:
658
+ return {"error": str(e), "error_code": 1000}
659
+
660
+
661
+ def polygon_get_aggregates(symbol: str, timespan: str = "day", multiplier: int = 1,
662
+ from_date: str = "", to_date: str = "", limit: int = 120) -> dict:
663
+ """Get historical aggregated bars from Polygon.io."""
664
+ api_key = _get_polygon_key()
665
+ if not api_key:
666
+ return {"error": "Polygon API key not configured. Use /setkey polygon <key>"}
667
+
668
+ import requests
669
+
670
+ try:
671
+ symbol = symbol.upper()
672
+
673
+ # Default date range: last 6 months
674
+ if not to_date:
675
+ to_date = datetime.now().strftime("%Y-%m-%d")
676
+ if not from_date:
677
+ from_date = (datetime.now() - timedelta(days=180)).strftime("%Y-%m-%d")
678
+
679
+ url = (f"https://api.polygon.io/v2/aggs/ticker/{symbol}/range/"
680
+ f"{multiplier}/{timespan}/{from_date}/{to_date}"
681
+ f"?adjusted=true&sort=desc&limit={limit}&apiKey={api_key}")
682
+
683
+ response = requests.get(url, timeout=15)
684
+
685
+ if response.status_code != 200:
686
+ return {"error": f"Polygon API error: {response.status_code}"}
687
+
688
+ data = response.json()
689
+
690
+ if data.get("resultsCount", 0) == 0:
691
+ return {"error": f"No data found for {symbol}"}
692
+
693
+ results = data["results"]
694
+
695
+ # Calculate statistics
696
+ closes = [r["c"] for r in results]
697
+ highs = [r["h"] for r in results]
698
+ lows = [r["l"] for r in results]
699
+ volumes = [r["v"] for r in results]
700
+
701
+ latest = results[0]
702
+ oldest = results[-1]
703
+
704
+ return {
705
+ "symbol": symbol,
706
+ "timespan": timespan,
707
+ "from": from_date,
708
+ "to": to_date,
709
+ "data_points": len(results),
710
+ "latest_close": latest["c"],
711
+ "oldest_close": oldest["c"],
712
+ "period_return": round((latest["c"] / oldest["c"] - 1) * 100, 2),
713
+ "high": max(highs),
714
+ "low": min(lows),
715
+ "avg_volume": int(sum(volumes) / len(volumes)),
716
+ "total_volume": sum(volumes),
717
+ "source": "polygon.io"
718
+ }
719
+ except Exception as e:
720
+ return {"error": str(e)}
721
+
722
+
723
+ def polygon_get_ticker_news(symbol: str, limit: int = 10) -> dict:
724
+ """Get news articles for a ticker from Polygon.io."""
725
+ api_key = _get_polygon_key()
726
+ if not api_key:
727
+ return {"error": "Polygon API key not configured. Use /setkey polygon <key>"}
728
+
729
+ import requests
730
+
731
+ try:
732
+ symbol = symbol.upper()
733
+ url = f"https://api.polygon.io/v2/reference/news?ticker={symbol}&limit={limit}&apiKey={api_key}"
734
+
735
+ response = requests.get(url, timeout=10)
736
+
737
+ if response.status_code != 200:
738
+ return {"error": f"Polygon API error: {response.status_code}"}
739
+
740
+ data = response.json()
741
+ articles = []
742
+
743
+ for item in data.get("results", []):
744
+ articles.append({
745
+ "title": item.get("title", ""),
746
+ "author": item.get("author", ""),
747
+ "published": item.get("published_utc", ""),
748
+ "article_url": item.get("article_url", ""),
749
+ "tickers": item.get("tickers", []),
750
+ "description": item.get("description", "")[:300] + "..." if item.get("description") else "",
751
+ "keywords": item.get("keywords", [])[:5]
752
+ })
753
+
754
+ return {
755
+ "symbol": symbol,
756
+ "articles": articles,
757
+ "source": "polygon.io"
758
+ }
759
+ except Exception as e:
760
+ return {"error": str(e)}
761
+
762
+
763
+ def polygon_market_status() -> dict:
764
+ """Get current market status from Polygon.io."""
765
+ api_key = _get_polygon_key()
766
+ if not api_key:
767
+ return {"error": "Polygon API key not configured. Use /setkey polygon <key>"}
768
+
769
+ import requests
770
+
771
+ try:
772
+ url = f"https://api.polygon.io/v1/marketstatus/now?apiKey={api_key}"
773
+ response = requests.get(url, timeout=10)
774
+
775
+ if response.status_code != 200:
776
+ return {"error": f"Polygon API error: {response.status_code}"}
777
+
778
+ data = response.json()
779
+
780
+ return {
781
+ "market": data.get("market", "unknown"),
782
+ "server_time": data.get("serverTime"),
783
+ "exchanges": data.get("exchanges", {}),
784
+ "currencies": data.get("currencies", {}),
785
+ "source": "polygon.io"
786
+ }
787
+ except Exception as e:
788
+ return {"error": str(e)}
789
+
790
+
586
791
  # ============================================================================
587
792
  # EXA SEARCH TOOLS (Financial News, SEC Filings)
588
793
  # ============================================================================
@@ -757,6 +962,466 @@ def search_earnings_transcripts(company: str, num_results: int = 3) -> dict:
757
962
  return {"error": str(e)}
758
963
 
759
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
+
760
1425
  # ============================================================================
761
1426
  # TOOL DEFINITIONS FOR LLM
762
1427
  # ============================================================================
@@ -1008,6 +1673,188 @@ TOOLS = [
1008
1673
  }
1009
1674
  }
1010
1675
  },
1676
+ # Polygon.io tools (enhanced market data)
1677
+ {
1678
+ "type": "function",
1679
+ "function": {
1680
+ "name": "polygon_get_quote",
1681
+ "description": "Get real-time stock quote with extended data from Polygon.io (requires API key)",
1682
+ "parameters": {
1683
+ "type": "object",
1684
+ "properties": {
1685
+ "symbol": {"type": "string", "description": "Stock ticker symbol (e.g., AAPL, MSFT)"}
1686
+ },
1687
+ "required": ["symbol"]
1688
+ }
1689
+ }
1690
+ },
1691
+ {
1692
+ "type": "function",
1693
+ "function": {
1694
+ "name": "polygon_get_aggregates",
1695
+ "description": "Get historical price aggregates/bars from Polygon.io with custom timespan",
1696
+ "parameters": {
1697
+ "type": "object",
1698
+ "properties": {
1699
+ "symbol": {"type": "string", "description": "Stock ticker symbol"},
1700
+ "timespan": {"type": "string", "enum": ["minute", "hour", "day", "week", "month"], "description": "Size of time window", "default": "day"},
1701
+ "multiplier": {"type": "integer", "description": "Size multiplier for timespan", "default": 1},
1702
+ "from_date": {"type": "string", "description": "Start date (YYYY-MM-DD)", "default": ""},
1703
+ "to_date": {"type": "string", "description": "End date (YYYY-MM-DD)", "default": ""},
1704
+ "limit": {"type": "integer", "description": "Number of results", "default": 120}
1705
+ },
1706
+ "required": ["symbol"]
1707
+ }
1708
+ }
1709
+ },
1710
+ {
1711
+ "type": "function",
1712
+ "function": {
1713
+ "name": "polygon_get_ticker_news",
1714
+ "description": "Get recent news articles for a stock from Polygon.io",
1715
+ "parameters": {
1716
+ "type": "object",
1717
+ "properties": {
1718
+ "symbol": {"type": "string", "description": "Stock ticker symbol"},
1719
+ "limit": {"type": "integer", "description": "Number of articles", "default": 10}
1720
+ },
1721
+ "required": ["symbol"]
1722
+ }
1723
+ }
1724
+ },
1725
+ {
1726
+ "type": "function",
1727
+ "function": {
1728
+ "name": "polygon_market_status",
1729
+ "description": "Get current market status (open/closed) from Polygon.io",
1730
+ "parameters": {
1731
+ "type": "object",
1732
+ "properties": {},
1733
+ "required": []
1734
+ }
1735
+ }
1736
+ },
1737
+ # Chart generation tools
1738
+ {
1739
+ "type": "function",
1740
+ "function": {
1741
+ "name": "generate_stock_chart",
1742
+ "description": "Generate a stock price chart (candlestick, line, or technical) with optional indicators. Returns file path where chart is saved.",
1743
+ "parameters": {
1744
+ "type": "object",
1745
+ "properties": {
1746
+ "symbol": {"type": "string", "description": "Stock ticker symbol (e.g., AAPL, NVDA)"},
1747
+ "period": {"type": "string", "description": "Time period: 1mo, 3mo, 6mo, 1y, 2y, 5y", "default": "6mo"},
1748
+ "chart_type": {"type": "string", "enum": ["candlestick", "line", "technical"], "description": "Type of chart", "default": "candlestick"},
1749
+ "show_volume": {"type": "boolean", "description": "Show volume bars", "default": True},
1750
+ "show_indicators": {"type": "boolean", "description": "Show moving averages/indicators", "default": True}
1751
+ },
1752
+ "required": ["symbol"]
1753
+ }
1754
+ }
1755
+ },
1756
+ {
1757
+ "type": "function",
1758
+ "function": {
1759
+ "name": "generate_comparison_chart",
1760
+ "description": "Generate a comparison chart showing multiple stocks' performance over time",
1761
+ "parameters": {
1762
+ "type": "object",
1763
+ "properties": {
1764
+ "symbols": {"type": "array", "items": {"type": "string"}, "description": "List of stock symbols to compare"},
1765
+ "period": {"type": "string", "description": "Time period for comparison", "default": "1y"},
1766
+ "normalize": {"type": "boolean", "description": "Normalize to percentage returns", "default": True}
1767
+ },
1768
+ "required": ["symbols"]
1769
+ }
1770
+ }
1771
+ },
1772
+ # Advanced analysis tools
1773
+ {
1774
+ "type": "function",
1775
+ "function": {
1776
+ "name": "get_valuation_metrics",
1777
+ "description": "Get comprehensive valuation metrics (P/E, P/B, PEG, EV/EBITDA) with assessment",
1778
+ "parameters": {
1779
+ "type": "object",
1780
+ "properties": {
1781
+ "symbol": {"type": "string", "description": "Stock ticker symbol"}
1782
+ },
1783
+ "required": ["symbol"]
1784
+ }
1785
+ }
1786
+ },
1787
+ {
1788
+ "type": "function",
1789
+ "function": {
1790
+ "name": "get_risk_metrics",
1791
+ "description": "Calculate risk metrics: volatility, VaR, max drawdown, Sharpe, Sortino, Beta, Alpha",
1792
+ "parameters": {
1793
+ "type": "object",
1794
+ "properties": {
1795
+ "symbol": {"type": "string", "description": "Stock ticker symbol"},
1796
+ "period": {"type": "string", "description": "Analysis period (1y, 2y, 5y)", "default": "1y"}
1797
+ },
1798
+ "required": ["symbol"]
1799
+ }
1800
+ }
1801
+ },
1802
+ {
1803
+ "type": "function",
1804
+ "function": {
1805
+ "name": "get_earnings_analysis",
1806
+ "description": "Get earnings analysis: EPS, upcoming dates, quarterly history, growth",
1807
+ "parameters": {
1808
+ "type": "object",
1809
+ "properties": {
1810
+ "symbol": {"type": "string", "description": "Stock ticker symbol"}
1811
+ },
1812
+ "required": ["symbol"]
1813
+ }
1814
+ }
1815
+ },
1816
+ {
1817
+ "type": "function",
1818
+ "function": {
1819
+ "name": "get_dividend_analysis",
1820
+ "description": "Get dividend analysis: yield, payout ratio, ex-date, dividend history and growth",
1821
+ "parameters": {
1822
+ "type": "object",
1823
+ "properties": {
1824
+ "symbol": {"type": "string", "description": "Stock ticker symbol"}
1825
+ },
1826
+ "required": ["symbol"]
1827
+ }
1828
+ }
1829
+ },
1830
+ {
1831
+ "type": "function",
1832
+ "function": {
1833
+ "name": "get_options_summary",
1834
+ "description": "Get options chain summary: put/call ratio, implied volatility, ATM options",
1835
+ "parameters": {
1836
+ "type": "object",
1837
+ "properties": {
1838
+ "symbol": {"type": "string", "description": "Stock ticker symbol"}
1839
+ },
1840
+ "required": ["symbol"]
1841
+ }
1842
+ }
1843
+ },
1844
+ {
1845
+ "type": "function",
1846
+ "function": {
1847
+ "name": "get_peer_comparison",
1848
+ "description": "Compare a stock with its industry peers on key metrics",
1849
+ "parameters": {
1850
+ "type": "object",
1851
+ "properties": {
1852
+ "symbol": {"type": "string", "description": "Stock ticker symbol"}
1853
+ },
1854
+ "required": ["symbol"]
1855
+ }
1856
+ }
1857
+ },
1011
1858
  ]
1012
1859
 
1013
1860
 
@@ -1032,12 +1879,30 @@ TOOL_FUNCTIONS = {
1032
1879
  "search_financial_news": search_financial_news,
1033
1880
  "search_sec_filings": search_sec_filings,
1034
1881
  "search_earnings_transcripts": search_earnings_transcripts,
1882
+ # Polygon.io
1883
+ "polygon_get_quote": polygon_get_quote,
1884
+ "polygon_get_aggregates": polygon_get_aggregates,
1885
+ "polygon_get_ticker_news": polygon_get_ticker_news,
1886
+ "polygon_market_status": polygon_market_status,
1887
+ # Chart generation
1888
+ "generate_stock_chart": generate_stock_chart,
1889
+ "generate_comparison_chart": generate_comparison_chart,
1890
+ # Advanced analysis
1891
+ "get_valuation_metrics": get_valuation_metrics,
1892
+ "get_risk_metrics": get_risk_metrics,
1893
+ "get_earnings_analysis": get_earnings_analysis,
1894
+ "get_dividend_analysis": get_dividend_analysis,
1895
+ "get_options_summary": get_options_summary,
1896
+ "get_peer_comparison": get_peer_comparison,
1035
1897
  }
1036
1898
 
1037
1899
 
1038
1900
  def execute_tool(name: str, args: dict) -> Any:
1039
- """Execute a tool by name."""
1901
+ """Execute a tool by name with error handling."""
1040
1902
  func = TOOL_FUNCTIONS.get(name)
1041
1903
  if func:
1042
- return func(**args)
1043
- return {"error": f"Unknown tool: {name}"}
1904
+ try:
1905
+ return func(**args)
1906
+ except Exception as e:
1907
+ return {"error": f"Tool execution failed: {str(e)}", "error_code": 1000}
1908
+ return {"error": f"Unknown tool: {name}", "error_code": 1001}