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/__init__.py +5 -5
- sigma/app.py +570 -171
- sigma/charts.py +33 -8
- sigma/cli.py +11 -11
- sigma/config.py +175 -34
- sigma/llm.py +160 -13
- sigma/setup.py +16 -16
- sigma/tools.py +868 -3
- sigma_terminal-3.4.1.dist-info/METADATA +272 -0
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.1.dist-info}/RECORD +13 -13
- sigma_terminal-3.3.2.dist-info/METADATA +0 -444
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.1.dist-info}/WHEEL +0 -0
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.1.dist-info}/entry_points.txt +0 -0
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.1.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
|
1043
|
-
|
|
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}
|