sigma-terminal 2.0.1__py3-none-any.whl → 3.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigma/__init__.py +182 -6
- sigma/__main__.py +2 -2
- sigma/analytics/__init__.py +636 -0
- sigma/app.py +563 -898
- sigma/backtest.py +372 -0
- sigma/charts.py +407 -0
- sigma/cli.py +434 -0
- sigma/comparison.py +611 -0
- sigma/config.py +195 -0
- sigma/core/__init__.py +4 -17
- sigma/core/engine.py +493 -0
- sigma/core/intent.py +595 -0
- sigma/core/models.py +516 -125
- sigma/data/__init__.py +681 -0
- sigma/data/models.py +130 -0
- sigma/llm.py +401 -0
- sigma/monitoring.py +666 -0
- sigma/portfolio.py +697 -0
- sigma/reporting.py +658 -0
- sigma/robustness.py +675 -0
- sigma/setup.py +305 -402
- sigma/strategy.py +753 -0
- sigma/tools/backtest.py +23 -5
- sigma/tools.py +617 -0
- sigma/visualization.py +766 -0
- sigma_terminal-3.2.0.dist-info/METADATA +298 -0
- sigma_terminal-3.2.0.dist-info/RECORD +30 -0
- sigma_terminal-3.2.0.dist-info/entry_points.txt +6 -0
- sigma_terminal-3.2.0.dist-info/licenses/LICENSE +25 -0
- sigma/core/agent.py +0 -205
- sigma/core/config.py +0 -119
- sigma/core/llm.py +0 -794
- sigma/tools/__init__.py +0 -5
- sigma/tools/charts.py +0 -400
- sigma/tools/financial.py +0 -1457
- sigma/ui/__init__.py +0 -1
- sigma_terminal-2.0.1.dist-info/METADATA +0 -222
- sigma_terminal-2.0.1.dist-info/RECORD +0 -19
- sigma_terminal-2.0.1.dist-info/entry_points.txt +0 -2
- sigma_terminal-2.0.1.dist-info/licenses/LICENSE +0 -42
- {sigma_terminal-2.0.1.dist-info → sigma_terminal-3.2.0.dist-info}/WHEEL +0 -0
sigma/tools/financial.py
DELETED
|
@@ -1,1457 +0,0 @@
|
|
|
1
|
-
"""Financial tools for real market data."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
from datetime import datetime, timedelta
|
|
5
|
-
from typing import Any, Optional, Union
|
|
6
|
-
import time
|
|
7
|
-
|
|
8
|
-
import httpx
|
|
9
|
-
import pandas as pd
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# Tool registry
|
|
13
|
-
_tools: dict[str, dict[str, Any]] = {}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def tool(name: str, description: str, parameters: dict[str, Any]):
|
|
17
|
-
"""Register a tool."""
|
|
18
|
-
def decorator(func):
|
|
19
|
-
_tools[name] = {
|
|
20
|
-
"name": name,
|
|
21
|
-
"description": description,
|
|
22
|
-
"parameters": parameters,
|
|
23
|
-
"function": func,
|
|
24
|
-
}
|
|
25
|
-
return func
|
|
26
|
-
return decorator
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def get_all_tools() -> list[dict[str, Any]]:
|
|
30
|
-
"""Get all tool definitions."""
|
|
31
|
-
return [
|
|
32
|
-
{"name": t["name"], "description": t["description"], "parameters": t["parameters"]}
|
|
33
|
-
for t in _tools.values()
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
async def execute_tool(name: str, arguments: dict[str, Any]) -> Any:
|
|
38
|
-
"""Execute a tool."""
|
|
39
|
-
if name not in _tools:
|
|
40
|
-
return {"error": f"Tool not found: {name}"}
|
|
41
|
-
|
|
42
|
-
try:
|
|
43
|
-
func = _tools[name]["function"]
|
|
44
|
-
if asyncio.iscoroutinefunction(func):
|
|
45
|
-
return await func(**arguments)
|
|
46
|
-
return func(**arguments)
|
|
47
|
-
except Exception as e:
|
|
48
|
-
return {"error": str(e)}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _format_date(val: Any) -> str:
|
|
52
|
-
"""Safely format a date value to string."""
|
|
53
|
-
if hasattr(val, 'strftime'):
|
|
54
|
-
return val.strftime("%Y-%m-%d")
|
|
55
|
-
elif isinstance(val, pd.Timestamp):
|
|
56
|
-
return val.strftime("%Y-%m-%d")
|
|
57
|
-
return str(val)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _is_dataframe(obj: Any) -> bool:
|
|
61
|
-
"""Check if object is a DataFrame."""
|
|
62
|
-
return isinstance(obj, pd.DataFrame)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# ============================================================================
|
|
66
|
-
# MARKET DATA TOOLS (yfinance)
|
|
67
|
-
# ============================================================================
|
|
68
|
-
|
|
69
|
-
@tool(
|
|
70
|
-
name="get_stock_quote",
|
|
71
|
-
description="Get real-time stock quote with price, volume, market cap, P/E ratio, and other key metrics",
|
|
72
|
-
parameters={
|
|
73
|
-
"type": "object",
|
|
74
|
-
"properties": {
|
|
75
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol (e.g., AAPL, GOOGL)"}
|
|
76
|
-
},
|
|
77
|
-
"required": ["symbol"]
|
|
78
|
-
}
|
|
79
|
-
)
|
|
80
|
-
def get_stock_quote(symbol: str) -> dict[str, Any]:
|
|
81
|
-
"""Get stock quote using yfinance."""
|
|
82
|
-
import yfinance as yf
|
|
83
|
-
|
|
84
|
-
ticker = yf.Ticker(symbol.upper())
|
|
85
|
-
info = ticker.info
|
|
86
|
-
|
|
87
|
-
if not info or "regularMarketPrice" not in info:
|
|
88
|
-
# Try fast_info
|
|
89
|
-
try:
|
|
90
|
-
fast = ticker.fast_info
|
|
91
|
-
return {
|
|
92
|
-
"symbol": symbol.upper(),
|
|
93
|
-
"price": fast.get("lastPrice", fast.get("regularMarketPrice")),
|
|
94
|
-
"previous_close": fast.get("previousClose"),
|
|
95
|
-
"market_cap": fast.get("marketCap"),
|
|
96
|
-
"error": None
|
|
97
|
-
}
|
|
98
|
-
except:
|
|
99
|
-
return {"error": f"Could not fetch quote for {symbol}"}
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
"symbol": symbol.upper(),
|
|
103
|
-
"name": info.get("shortName", info.get("longName", "")),
|
|
104
|
-
"price": info.get("regularMarketPrice", info.get("currentPrice")),
|
|
105
|
-
"previous_close": info.get("previousClose"),
|
|
106
|
-
"open": info.get("regularMarketOpen"),
|
|
107
|
-
"day_high": info.get("regularMarketDayHigh"),
|
|
108
|
-
"day_low": info.get("regularMarketDayLow"),
|
|
109
|
-
"volume": info.get("regularMarketVolume"),
|
|
110
|
-
"avg_volume": info.get("averageVolume"),
|
|
111
|
-
"market_cap": info.get("marketCap"),
|
|
112
|
-
"pe_ratio": info.get("trailingPE"),
|
|
113
|
-
"forward_pe": info.get("forwardPE"),
|
|
114
|
-
"eps": info.get("trailingEps"),
|
|
115
|
-
"dividend_yield": info.get("dividendYield"),
|
|
116
|
-
"52_week_high": info.get("fiftyTwoWeekHigh"),
|
|
117
|
-
"52_week_low": info.get("fiftyTwoWeekLow"),
|
|
118
|
-
"50_day_avg": info.get("fiftyDayAverage"),
|
|
119
|
-
"200_day_avg": info.get("twoHundredDayAverage"),
|
|
120
|
-
"beta": info.get("beta"),
|
|
121
|
-
"currency": info.get("currency", "USD"),
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
@tool(
|
|
126
|
-
name="get_stock_history",
|
|
127
|
-
description="Get historical price data for a stock",
|
|
128
|
-
parameters={
|
|
129
|
-
"type": "object",
|
|
130
|
-
"properties": {
|
|
131
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
132
|
-
"period": {"type": "string", "description": "Time period: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max", "default": "1mo"},
|
|
133
|
-
"interval": {"type": "string", "description": "Data interval: 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo", "default": "1d"}
|
|
134
|
-
},
|
|
135
|
-
"required": ["symbol"]
|
|
136
|
-
}
|
|
137
|
-
)
|
|
138
|
-
def get_stock_history(symbol: str, period: str = "1mo", interval: str = "1d") -> dict[str, Any]:
|
|
139
|
-
"""Get historical stock data."""
|
|
140
|
-
import yfinance as yf
|
|
141
|
-
|
|
142
|
-
ticker = yf.Ticker(symbol.upper())
|
|
143
|
-
hist = ticker.history(period=period, interval=interval)
|
|
144
|
-
|
|
145
|
-
if hist.empty:
|
|
146
|
-
return {"error": f"No history for {symbol}"}
|
|
147
|
-
|
|
148
|
-
records = []
|
|
149
|
-
for date, row in hist.iterrows():
|
|
150
|
-
records.append({
|
|
151
|
-
"date": _format_date(date),
|
|
152
|
-
"open": round(row["Open"], 2) if row["Open"] else None,
|
|
153
|
-
"high": round(row["High"], 2) if row["High"] else None,
|
|
154
|
-
"low": round(row["Low"], 2) if row["Low"] else None,
|
|
155
|
-
"close": round(row["Close"], 2) if row["Close"] else None,
|
|
156
|
-
"volume": int(row["Volume"]) if row["Volume"] else None,
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
# Calculate returns
|
|
160
|
-
if len(records) > 1:
|
|
161
|
-
start_price = records[0]["close"]
|
|
162
|
-
end_price = records[-1]["close"]
|
|
163
|
-
if start_price and end_price:
|
|
164
|
-
total_return = ((end_price - start_price) / start_price) * 100
|
|
165
|
-
else:
|
|
166
|
-
total_return = None
|
|
167
|
-
else:
|
|
168
|
-
total_return = None
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
"symbol": symbol.upper(),
|
|
172
|
-
"period": period,
|
|
173
|
-
"interval": interval,
|
|
174
|
-
"data_points": len(records),
|
|
175
|
-
"start_date": records[0]["date"] if records else None,
|
|
176
|
-
"end_date": records[-1]["date"] if records else None,
|
|
177
|
-
"total_return_pct": round(total_return, 2) if total_return else None,
|
|
178
|
-
"history": records[-20:], # Last 20 for brevity
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
@tool(
|
|
183
|
-
name="get_company_info",
|
|
184
|
-
description="Get detailed company information including business description, sector, industry, and key statistics",
|
|
185
|
-
parameters={
|
|
186
|
-
"type": "object",
|
|
187
|
-
"properties": {
|
|
188
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
189
|
-
},
|
|
190
|
-
"required": ["symbol"]
|
|
191
|
-
}
|
|
192
|
-
)
|
|
193
|
-
def get_company_info(symbol: str) -> dict[str, Any]:
|
|
194
|
-
"""Get company information."""
|
|
195
|
-
import yfinance as yf
|
|
196
|
-
|
|
197
|
-
ticker = yf.Ticker(symbol.upper())
|
|
198
|
-
info = ticker.info
|
|
199
|
-
|
|
200
|
-
if not info:
|
|
201
|
-
return {"error": f"Could not fetch info for {symbol}"}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
"symbol": symbol.upper(),
|
|
205
|
-
"name": info.get("shortName", info.get("longName", "")),
|
|
206
|
-
"description": info.get("longBusinessSummary", ""),
|
|
207
|
-
"sector": info.get("sector"),
|
|
208
|
-
"industry": info.get("industry"),
|
|
209
|
-
"website": info.get("website"),
|
|
210
|
-
"country": info.get("country"),
|
|
211
|
-
"employees": info.get("fullTimeEmployees"),
|
|
212
|
-
"market_cap": info.get("marketCap"),
|
|
213
|
-
"enterprise_value": info.get("enterpriseValue"),
|
|
214
|
-
"revenue": info.get("totalRevenue"),
|
|
215
|
-
"gross_profit": info.get("grossProfits"),
|
|
216
|
-
"ebitda": info.get("ebitda"),
|
|
217
|
-
"net_income": info.get("netIncomeToCommon"),
|
|
218
|
-
"profit_margin": info.get("profitMargins"),
|
|
219
|
-
"operating_margin": info.get("operatingMargins"),
|
|
220
|
-
"roe": info.get("returnOnEquity"),
|
|
221
|
-
"roa": info.get("returnOnAssets"),
|
|
222
|
-
"debt_to_equity": info.get("debtToEquity"),
|
|
223
|
-
"current_ratio": info.get("currentRatio"),
|
|
224
|
-
"quick_ratio": info.get("quickRatio"),
|
|
225
|
-
"free_cash_flow": info.get("freeCashflow"),
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
@tool(
|
|
230
|
-
name="get_financial_statements",
|
|
231
|
-
description="Get income statement, balance sheet, and cash flow statement data",
|
|
232
|
-
parameters={
|
|
233
|
-
"type": "object",
|
|
234
|
-
"properties": {
|
|
235
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
236
|
-
"statement": {"type": "string", "description": "Statement type: income, balance, cashflow", "default": "income"},
|
|
237
|
-
"period": {"type": "string", "description": "Period: annual or quarterly", "default": "annual"}
|
|
238
|
-
},
|
|
239
|
-
"required": ["symbol"]
|
|
240
|
-
}
|
|
241
|
-
)
|
|
242
|
-
def get_financial_statements(symbol: str, statement: str = "income", period: str = "annual") -> dict[str, Any]:
|
|
243
|
-
"""Get financial statements."""
|
|
244
|
-
import yfinance as yf
|
|
245
|
-
|
|
246
|
-
ticker = yf.Ticker(symbol.upper())
|
|
247
|
-
|
|
248
|
-
if statement == "income":
|
|
249
|
-
df = ticker.income_stmt if period == "annual" else ticker.quarterly_income_stmt
|
|
250
|
-
elif statement == "balance":
|
|
251
|
-
df = ticker.balance_sheet if period == "annual" else ticker.quarterly_balance_sheet
|
|
252
|
-
elif statement == "cashflow":
|
|
253
|
-
df = ticker.cashflow if period == "annual" else ticker.quarterly_cashflow
|
|
254
|
-
else:
|
|
255
|
-
return {"error": f"Invalid statement type: {statement}"}
|
|
256
|
-
|
|
257
|
-
if df is None or df.empty:
|
|
258
|
-
return {"error": f"No {statement} statement for {symbol}"}
|
|
259
|
-
|
|
260
|
-
# Convert to dict
|
|
261
|
-
data = {}
|
|
262
|
-
for col in df.columns[:4]: # Last 4 periods
|
|
263
|
-
period_key = _format_date(col)
|
|
264
|
-
data[period_key] = {}
|
|
265
|
-
for idx, val in df[col].items():
|
|
266
|
-
if val is not None and not (isinstance(val, float) and val != val): # Check for NaN
|
|
267
|
-
data[period_key][str(idx)] = float(val) if isinstance(val, (int, float)) else val
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
"symbol": symbol.upper(),
|
|
271
|
-
"statement_type": statement,
|
|
272
|
-
"period": period,
|
|
273
|
-
"data": data,
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
@tool(
|
|
278
|
-
name="get_analyst_recommendations",
|
|
279
|
-
description="Get analyst recommendations and price targets",
|
|
280
|
-
parameters={
|
|
281
|
-
"type": "object",
|
|
282
|
-
"properties": {
|
|
283
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
284
|
-
},
|
|
285
|
-
"required": ["symbol"]
|
|
286
|
-
}
|
|
287
|
-
)
|
|
288
|
-
def get_analyst_recommendations(symbol: str) -> dict[str, Any]:
|
|
289
|
-
"""Get analyst recommendations."""
|
|
290
|
-
import yfinance as yf
|
|
291
|
-
|
|
292
|
-
ticker = yf.Ticker(symbol.upper())
|
|
293
|
-
|
|
294
|
-
# Get recommendations
|
|
295
|
-
recs = ticker.recommendations
|
|
296
|
-
info = ticker.info
|
|
297
|
-
|
|
298
|
-
result: dict[str, Any] = {
|
|
299
|
-
"symbol": symbol.upper(),
|
|
300
|
-
"recommendation": info.get("recommendationKey"),
|
|
301
|
-
"mean_rating": info.get("recommendationMean"),
|
|
302
|
-
"num_analysts": info.get("numberOfAnalystOpinions"),
|
|
303
|
-
"target_high": info.get("targetHighPrice"),
|
|
304
|
-
"target_low": info.get("targetLowPrice"),
|
|
305
|
-
"target_mean": info.get("targetMeanPrice"),
|
|
306
|
-
"target_median": info.get("targetMedianPrice"),
|
|
307
|
-
"current_price": info.get("regularMarketPrice"),
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
# Add upside/downside
|
|
311
|
-
if result.get("target_mean") and result.get("current_price"):
|
|
312
|
-
upside = ((result["target_mean"] - result["current_price"]) / result["current_price"]) * 100
|
|
313
|
-
result["upside_pct"] = round(upside, 2)
|
|
314
|
-
|
|
315
|
-
# Recent recommendations
|
|
316
|
-
if recs is not None and _is_dataframe(recs):
|
|
317
|
-
recs_df: pd.DataFrame = recs # type: ignore
|
|
318
|
-
if not recs_df.empty:
|
|
319
|
-
recent = recs_df.tail(10).to_dict("records")
|
|
320
|
-
result["recent_recommendations"] = recent
|
|
321
|
-
|
|
322
|
-
return result
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
@tool(
|
|
326
|
-
name="get_insider_trades",
|
|
327
|
-
description="Get recent insider trading activity",
|
|
328
|
-
parameters={
|
|
329
|
-
"type": "object",
|
|
330
|
-
"properties": {
|
|
331
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
332
|
-
},
|
|
333
|
-
"required": ["symbol"]
|
|
334
|
-
}
|
|
335
|
-
)
|
|
336
|
-
def get_insider_trades(symbol: str) -> dict[str, Any]:
|
|
337
|
-
"""Get insider trades."""
|
|
338
|
-
import yfinance as yf
|
|
339
|
-
|
|
340
|
-
ticker = yf.Ticker(symbol.upper())
|
|
341
|
-
insiders = ticker.insider_transactions
|
|
342
|
-
|
|
343
|
-
if insiders is None or insiders.empty:
|
|
344
|
-
return {"symbol": symbol.upper(), "trades": [], "message": "No insider trades found"}
|
|
345
|
-
|
|
346
|
-
trades = []
|
|
347
|
-
for _, row in insiders.head(20).iterrows():
|
|
348
|
-
trade = {}
|
|
349
|
-
for col in insiders.columns:
|
|
350
|
-
val = row[col]
|
|
351
|
-
if hasattr(val, 'strftime'):
|
|
352
|
-
trade[col] = val.strftime("%Y-%m-%d")
|
|
353
|
-
elif val is None or (isinstance(val, float) and val != val):
|
|
354
|
-
trade[col] = None
|
|
355
|
-
else:
|
|
356
|
-
trade[col] = val
|
|
357
|
-
trades.append(trade)
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
"symbol": symbol.upper(),
|
|
361
|
-
"total_trades": len(insiders),
|
|
362
|
-
"trades": trades,
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
@tool(
|
|
367
|
-
name="get_institutional_holders",
|
|
368
|
-
description="Get institutional ownership data",
|
|
369
|
-
parameters={
|
|
370
|
-
"type": "object",
|
|
371
|
-
"properties": {
|
|
372
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
373
|
-
},
|
|
374
|
-
"required": ["symbol"]
|
|
375
|
-
}
|
|
376
|
-
)
|
|
377
|
-
def get_institutional_holders(symbol: str) -> dict[str, Any]:
|
|
378
|
-
"""Get institutional holders."""
|
|
379
|
-
import yfinance as yf
|
|
380
|
-
|
|
381
|
-
ticker = yf.Ticker(symbol.upper())
|
|
382
|
-
holders = ticker.institutional_holders
|
|
383
|
-
|
|
384
|
-
if holders is None or holders.empty:
|
|
385
|
-
return {"symbol": symbol.upper(), "holders": [], "message": "No institutional holders found"}
|
|
386
|
-
|
|
387
|
-
holder_list = []
|
|
388
|
-
for _, row in holders.iterrows():
|
|
389
|
-
holder = {}
|
|
390
|
-
for col in holders.columns:
|
|
391
|
-
val = row[col]
|
|
392
|
-
if hasattr(val, 'strftime'):
|
|
393
|
-
holder[col] = val.strftime("%Y-%m-%d")
|
|
394
|
-
elif val is None or (isinstance(val, float) and val != val):
|
|
395
|
-
holder[col] = None
|
|
396
|
-
else:
|
|
397
|
-
holder[col] = val
|
|
398
|
-
holder_list.append(holder)
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
"symbol": symbol.upper(),
|
|
402
|
-
"holders": holder_list,
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
@tool(
|
|
407
|
-
name="get_earnings_calendar",
|
|
408
|
-
description="Get earnings history and upcoming earnings dates",
|
|
409
|
-
parameters={
|
|
410
|
-
"type": "object",
|
|
411
|
-
"properties": {
|
|
412
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
413
|
-
},
|
|
414
|
-
"required": ["symbol"]
|
|
415
|
-
}
|
|
416
|
-
)
|
|
417
|
-
def get_earnings_calendar(symbol: str) -> dict[str, Any]:
|
|
418
|
-
"""Get earnings data."""
|
|
419
|
-
import yfinance as yf
|
|
420
|
-
|
|
421
|
-
ticker = yf.Ticker(symbol.upper())
|
|
422
|
-
earnings = ticker.earnings_history
|
|
423
|
-
calendar = ticker.calendar
|
|
424
|
-
|
|
425
|
-
result: dict[str, Any] = {"symbol": symbol.upper()}
|
|
426
|
-
|
|
427
|
-
if earnings is not None and _is_dataframe(earnings) and not earnings.empty:
|
|
428
|
-
history = []
|
|
429
|
-
for _, row in earnings.iterrows():
|
|
430
|
-
entry = {}
|
|
431
|
-
for col in earnings.columns:
|
|
432
|
-
val = row[col]
|
|
433
|
-
if hasattr(val, 'strftime'):
|
|
434
|
-
entry[col] = _format_date(val)
|
|
435
|
-
elif val is None or (isinstance(val, float) and val != val):
|
|
436
|
-
entry[col] = None
|
|
437
|
-
else:
|
|
438
|
-
entry[col] = val
|
|
439
|
-
history.append(entry)
|
|
440
|
-
result["earnings_history"] = history
|
|
441
|
-
|
|
442
|
-
if calendar is not None:
|
|
443
|
-
try:
|
|
444
|
-
if isinstance(calendar, dict):
|
|
445
|
-
result["calendar"] = calendar
|
|
446
|
-
elif hasattr(calendar, 'to_dict'):
|
|
447
|
-
# DataFrame calendar
|
|
448
|
-
raw_dict = dict(calendar.to_dict()) # type: ignore[union-attr]
|
|
449
|
-
result["calendar"] = {str(k): str(v) if v is not None else None for k, v in raw_dict.items()}
|
|
450
|
-
else:
|
|
451
|
-
result["calendar"] = str(calendar)
|
|
452
|
-
except Exception:
|
|
453
|
-
result["calendar"] = str(calendar)
|
|
454
|
-
|
|
455
|
-
return result
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
@tool(
|
|
459
|
-
name="get_options_chain",
|
|
460
|
-
description="Get options chain data including calls and puts",
|
|
461
|
-
parameters={
|
|
462
|
-
"type": "object",
|
|
463
|
-
"properties": {
|
|
464
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
465
|
-
"expiration": {"type": "string", "description": "Expiration date (YYYY-MM-DD) or 'next' for nearest expiration"}
|
|
466
|
-
},
|
|
467
|
-
"required": ["symbol"]
|
|
468
|
-
}
|
|
469
|
-
)
|
|
470
|
-
def get_options_chain(symbol: str, expiration: Optional[str] = None) -> dict[str, Any]:
|
|
471
|
-
"""Get options chain."""
|
|
472
|
-
import yfinance as yf
|
|
473
|
-
|
|
474
|
-
ticker = yf.Ticker(symbol.upper())
|
|
475
|
-
expirations = ticker.options
|
|
476
|
-
|
|
477
|
-
if not expirations:
|
|
478
|
-
return {"error": f"No options available for {symbol}"}
|
|
479
|
-
|
|
480
|
-
# Use first expiration if not specified
|
|
481
|
-
exp_date = expiration if expiration and expiration != "next" else expirations[0]
|
|
482
|
-
|
|
483
|
-
if exp_date not in expirations:
|
|
484
|
-
return {"error": f"Invalid expiration. Available: {expirations[:5]}"}
|
|
485
|
-
|
|
486
|
-
chain = ticker.option_chain(exp_date)
|
|
487
|
-
|
|
488
|
-
def process_options(df, limit=10):
|
|
489
|
-
options = []
|
|
490
|
-
for _, row in df.head(limit).iterrows():
|
|
491
|
-
opt = {}
|
|
492
|
-
for col in df.columns:
|
|
493
|
-
val = row[col]
|
|
494
|
-
if val is None or (isinstance(val, float) and val != val):
|
|
495
|
-
opt[col] = None
|
|
496
|
-
elif isinstance(val, (int, float)):
|
|
497
|
-
opt[col] = round(float(val), 4)
|
|
498
|
-
else:
|
|
499
|
-
opt[col] = val
|
|
500
|
-
options.append(opt)
|
|
501
|
-
return options
|
|
502
|
-
|
|
503
|
-
return {
|
|
504
|
-
"symbol": symbol.upper(),
|
|
505
|
-
"expiration": exp_date,
|
|
506
|
-
"available_expirations": list(expirations[:10]),
|
|
507
|
-
"calls": process_options(chain.calls),
|
|
508
|
-
"puts": process_options(chain.puts),
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
@tool(
|
|
513
|
-
name="get_dividends",
|
|
514
|
-
description="Get dividend history and yield information",
|
|
515
|
-
parameters={
|
|
516
|
-
"type": "object",
|
|
517
|
-
"properties": {
|
|
518
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
519
|
-
},
|
|
520
|
-
"required": ["symbol"]
|
|
521
|
-
}
|
|
522
|
-
)
|
|
523
|
-
def get_dividends(symbol: str) -> dict[str, Any]:
|
|
524
|
-
"""Get dividend data."""
|
|
525
|
-
import yfinance as yf
|
|
526
|
-
|
|
527
|
-
ticker = yf.Ticker(symbol.upper())
|
|
528
|
-
dividends = ticker.dividends
|
|
529
|
-
info = ticker.info
|
|
530
|
-
|
|
531
|
-
result: dict[str, Any] = {
|
|
532
|
-
"symbol": symbol.upper(),
|
|
533
|
-
"dividend_yield": info.get("dividendYield"),
|
|
534
|
-
"dividend_rate": info.get("dividendRate"),
|
|
535
|
-
"payout_ratio": info.get("payoutRatio"),
|
|
536
|
-
"ex_dividend_date": info.get("exDividendDate"),
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if dividends is not None and not dividends.empty:
|
|
540
|
-
history = []
|
|
541
|
-
for date, amount in dividends.tail(20).items():
|
|
542
|
-
history.append({
|
|
543
|
-
"date": _format_date(date),
|
|
544
|
-
"amount": round(amount, 4)
|
|
545
|
-
})
|
|
546
|
-
result["history"] = history
|
|
547
|
-
result["total_dividends_1y"] = round(dividends.tail(4).sum(), 4)
|
|
548
|
-
|
|
549
|
-
return result
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
@tool(
|
|
553
|
-
name="compare_stocks",
|
|
554
|
-
description="Compare multiple stocks on key metrics",
|
|
555
|
-
parameters={
|
|
556
|
-
"type": "object",
|
|
557
|
-
"properties": {
|
|
558
|
-
"symbols": {"type": "array", "items": {"type": "string"}, "description": "List of stock symbols to compare"}
|
|
559
|
-
},
|
|
560
|
-
"required": ["symbols"]
|
|
561
|
-
}
|
|
562
|
-
)
|
|
563
|
-
def compare_stocks(symbols: list[str]) -> dict[str, Any]:
|
|
564
|
-
"""Compare multiple stocks."""
|
|
565
|
-
import yfinance as yf
|
|
566
|
-
|
|
567
|
-
comparisons = []
|
|
568
|
-
for symbol in symbols[:10]: # Limit to 10
|
|
569
|
-
ticker = yf.Ticker(symbol.upper())
|
|
570
|
-
info = ticker.info
|
|
571
|
-
|
|
572
|
-
if info:
|
|
573
|
-
comparisons.append({
|
|
574
|
-
"symbol": symbol.upper(),
|
|
575
|
-
"name": info.get("shortName"),
|
|
576
|
-
"price": info.get("regularMarketPrice"),
|
|
577
|
-
"market_cap": info.get("marketCap"),
|
|
578
|
-
"pe_ratio": info.get("trailingPE"),
|
|
579
|
-
"forward_pe": info.get("forwardPE"),
|
|
580
|
-
"peg_ratio": info.get("pegRatio"),
|
|
581
|
-
"ps_ratio": info.get("priceToSalesTrailing12Months"),
|
|
582
|
-
"pb_ratio": info.get("priceToBook"),
|
|
583
|
-
"dividend_yield": info.get("dividendYield"),
|
|
584
|
-
"profit_margin": info.get("profitMargins"),
|
|
585
|
-
"roe": info.get("returnOnEquity"),
|
|
586
|
-
"debt_to_equity": info.get("debtToEquity"),
|
|
587
|
-
"revenue_growth": info.get("revenueGrowth"),
|
|
588
|
-
"earnings_growth": info.get("earningsGrowth"),
|
|
589
|
-
"52w_change": info.get("52WeekChange"),
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
return {
|
|
593
|
-
"comparison": comparisons,
|
|
594
|
-
"symbols_compared": len(comparisons),
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
@tool(
|
|
599
|
-
name="get_market_movers",
|
|
600
|
-
description="Get top market gainers and losers",
|
|
601
|
-
parameters={
|
|
602
|
-
"type": "object",
|
|
603
|
-
"properties": {
|
|
604
|
-
"category": {"type": "string", "description": "Category: gainers, losers, or active", "default": "gainers"}
|
|
605
|
-
},
|
|
606
|
-
"required": []
|
|
607
|
-
}
|
|
608
|
-
)
|
|
609
|
-
async def get_market_movers(category: str = "gainers") -> dict[str, Any]:
|
|
610
|
-
"""Get market movers."""
|
|
611
|
-
import yfinance as yf
|
|
612
|
-
|
|
613
|
-
# Major indices and popular stocks to check
|
|
614
|
-
symbols = [
|
|
615
|
-
"AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "BRK-B",
|
|
616
|
-
"JPM", "JNJ", "V", "UNH", "HD", "PG", "MA", "DIS", "PYPL", "NFLX",
|
|
617
|
-
"ADBE", "CRM", "INTC", "AMD", "CSCO", "PFE", "MRK", "ABT", "TMO",
|
|
618
|
-
"NKE", "COST", "WMT", "XOM", "CVX", "BA", "CAT", "GS", "MS"
|
|
619
|
-
]
|
|
620
|
-
|
|
621
|
-
movers = []
|
|
622
|
-
for symbol in symbols:
|
|
623
|
-
try:
|
|
624
|
-
ticker = yf.Ticker(symbol)
|
|
625
|
-
info = ticker.info
|
|
626
|
-
if info and info.get("regularMarketPrice") and info.get("previousClose"):
|
|
627
|
-
price = info["regularMarketPrice"]
|
|
628
|
-
prev = info["previousClose"]
|
|
629
|
-
change = price - prev
|
|
630
|
-
change_pct = (change / prev) * 100
|
|
631
|
-
movers.append({
|
|
632
|
-
"symbol": symbol,
|
|
633
|
-
"name": info.get("shortName"),
|
|
634
|
-
"price": round(price, 2),
|
|
635
|
-
"change": round(change, 2),
|
|
636
|
-
"change_pct": round(change_pct, 2),
|
|
637
|
-
"volume": info.get("regularMarketVolume"),
|
|
638
|
-
})
|
|
639
|
-
except:
|
|
640
|
-
continue
|
|
641
|
-
|
|
642
|
-
if category == "gainers":
|
|
643
|
-
movers.sort(key=lambda x: x["change_pct"], reverse=True)
|
|
644
|
-
elif category == "losers":
|
|
645
|
-
movers.sort(key=lambda x: x["change_pct"])
|
|
646
|
-
else: # active
|
|
647
|
-
movers.sort(key=lambda x: x.get("volume", 0) or 0, reverse=True)
|
|
648
|
-
|
|
649
|
-
return {
|
|
650
|
-
"category": category,
|
|
651
|
-
"movers": movers[:15],
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
@tool(
|
|
656
|
-
name="get_sector_performance",
|
|
657
|
-
description="Get sector ETF performance",
|
|
658
|
-
parameters={
|
|
659
|
-
"type": "object",
|
|
660
|
-
"properties": {},
|
|
661
|
-
"required": []
|
|
662
|
-
}
|
|
663
|
-
)
|
|
664
|
-
def get_sector_performance() -> dict[str, Any]:
|
|
665
|
-
"""Get sector performance via ETFs."""
|
|
666
|
-
import yfinance as yf
|
|
667
|
-
|
|
668
|
-
sectors = {
|
|
669
|
-
"XLK": "Technology",
|
|
670
|
-
"XLF": "Financials",
|
|
671
|
-
"XLV": "Healthcare",
|
|
672
|
-
"XLE": "Energy",
|
|
673
|
-
"XLI": "Industrials",
|
|
674
|
-
"XLY": "Consumer Discretionary",
|
|
675
|
-
"XLP": "Consumer Staples",
|
|
676
|
-
"XLU": "Utilities",
|
|
677
|
-
"XLB": "Materials",
|
|
678
|
-
"XLRE": "Real Estate",
|
|
679
|
-
"XLC": "Communication Services",
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
performance = []
|
|
683
|
-
for symbol, name in sectors.items():
|
|
684
|
-
try:
|
|
685
|
-
ticker = yf.Ticker(symbol)
|
|
686
|
-
info = ticker.info
|
|
687
|
-
if info and info.get("regularMarketPrice") and info.get("previousClose"):
|
|
688
|
-
price = info["regularMarketPrice"]
|
|
689
|
-
prev = info["previousClose"]
|
|
690
|
-
change_pct = ((price - prev) / prev) * 100
|
|
691
|
-
performance.append({
|
|
692
|
-
"sector": name,
|
|
693
|
-
"etf": symbol,
|
|
694
|
-
"price": round(price, 2),
|
|
695
|
-
"change_pct": round(change_pct, 2),
|
|
696
|
-
"52w_change": info.get("52WeekChange"),
|
|
697
|
-
})
|
|
698
|
-
except:
|
|
699
|
-
continue
|
|
700
|
-
|
|
701
|
-
performance.sort(key=lambda x: x["change_pct"], reverse=True)
|
|
702
|
-
|
|
703
|
-
return {
|
|
704
|
-
"sectors": performance,
|
|
705
|
-
"best_sector": performance[0]["sector"] if performance else None,
|
|
706
|
-
"worst_sector": performance[-1]["sector"] if performance else None,
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
@tool(
|
|
711
|
-
name="get_market_indices",
|
|
712
|
-
description="Get major market indices",
|
|
713
|
-
parameters={
|
|
714
|
-
"type": "object",
|
|
715
|
-
"properties": {},
|
|
716
|
-
"required": []
|
|
717
|
-
}
|
|
718
|
-
)
|
|
719
|
-
def get_market_indices() -> dict[str, Any]:
|
|
720
|
-
"""Get market indices."""
|
|
721
|
-
import yfinance as yf
|
|
722
|
-
|
|
723
|
-
indices = {
|
|
724
|
-
"^GSPC": "S&P 500",
|
|
725
|
-
"^DJI": "Dow Jones",
|
|
726
|
-
"^IXIC": "NASDAQ",
|
|
727
|
-
"^RUT": "Russell 2000",
|
|
728
|
-
"^VIX": "VIX",
|
|
729
|
-
"^TNX": "10Y Treasury",
|
|
730
|
-
"GC=F": "Gold",
|
|
731
|
-
"CL=F": "Crude Oil",
|
|
732
|
-
"BTC-USD": "Bitcoin",
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
data = []
|
|
736
|
-
for symbol, name in indices.items():
|
|
737
|
-
try:
|
|
738
|
-
ticker = yf.Ticker(symbol)
|
|
739
|
-
info = ticker.info
|
|
740
|
-
if info:
|
|
741
|
-
price = info.get("regularMarketPrice", info.get("previousClose"))
|
|
742
|
-
prev = info.get("previousClose")
|
|
743
|
-
if price and prev:
|
|
744
|
-
change_pct = ((price - prev) / prev) * 100
|
|
745
|
-
else:
|
|
746
|
-
change_pct = None
|
|
747
|
-
data.append({
|
|
748
|
-
"name": name,
|
|
749
|
-
"symbol": symbol,
|
|
750
|
-
"price": round(price, 2) if price else None,
|
|
751
|
-
"change_pct": round(change_pct, 2) if change_pct else None,
|
|
752
|
-
})
|
|
753
|
-
except:
|
|
754
|
-
continue
|
|
755
|
-
|
|
756
|
-
return {"indices": data}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
@tool(
|
|
760
|
-
name="calculate_portfolio_metrics",
|
|
761
|
-
description="Calculate portfolio metrics given holdings",
|
|
762
|
-
parameters={
|
|
763
|
-
"type": "object",
|
|
764
|
-
"properties": {
|
|
765
|
-
"holdings": {
|
|
766
|
-
"type": "array",
|
|
767
|
-
"items": {
|
|
768
|
-
"type": "object",
|
|
769
|
-
"properties": {
|
|
770
|
-
"symbol": {"type": "string"},
|
|
771
|
-
"shares": {"type": "number"},
|
|
772
|
-
"cost_basis": {"type": "number"}
|
|
773
|
-
}
|
|
774
|
-
},
|
|
775
|
-
"description": "List of holdings with symbol, shares, and cost basis"
|
|
776
|
-
}
|
|
777
|
-
},
|
|
778
|
-
"required": ["holdings"]
|
|
779
|
-
}
|
|
780
|
-
)
|
|
781
|
-
def calculate_portfolio_metrics(holdings: list[dict]) -> dict[str, Any]:
|
|
782
|
-
"""Calculate portfolio metrics."""
|
|
783
|
-
import yfinance as yf
|
|
784
|
-
|
|
785
|
-
results = []
|
|
786
|
-
total_value = 0
|
|
787
|
-
total_cost = 0
|
|
788
|
-
|
|
789
|
-
for holding in holdings:
|
|
790
|
-
symbol = holding["symbol"]
|
|
791
|
-
shares = holding["shares"]
|
|
792
|
-
cost_basis = holding.get("cost_basis", 0)
|
|
793
|
-
|
|
794
|
-
ticker = yf.Ticker(symbol.upper())
|
|
795
|
-
info = ticker.info
|
|
796
|
-
|
|
797
|
-
if info and info.get("regularMarketPrice"):
|
|
798
|
-
price = info["regularMarketPrice"]
|
|
799
|
-
value = price * shares
|
|
800
|
-
cost = cost_basis * shares if cost_basis else 0
|
|
801
|
-
gain = value - cost if cost else None
|
|
802
|
-
gain_pct = ((value - cost) / cost * 100) if cost else None
|
|
803
|
-
|
|
804
|
-
results.append({
|
|
805
|
-
"symbol": symbol.upper(),
|
|
806
|
-
"shares": shares,
|
|
807
|
-
"price": round(price, 2),
|
|
808
|
-
"value": round(value, 2),
|
|
809
|
-
"cost_basis": cost_basis,
|
|
810
|
-
"total_cost": round(cost, 2),
|
|
811
|
-
"gain": round(gain, 2) if gain else None,
|
|
812
|
-
"gain_pct": round(gain_pct, 2) if gain_pct else None,
|
|
813
|
-
})
|
|
814
|
-
|
|
815
|
-
total_value += value
|
|
816
|
-
total_cost += cost
|
|
817
|
-
|
|
818
|
-
total_gain = total_value - total_cost if total_cost else None
|
|
819
|
-
total_gain_pct = ((total_value - total_cost) / total_cost * 100) if total_cost else None
|
|
820
|
-
|
|
821
|
-
# Calculate weights
|
|
822
|
-
for r in results:
|
|
823
|
-
r["weight_pct"] = round((r["value"] / total_value * 100), 2) if total_value else 0
|
|
824
|
-
|
|
825
|
-
return {
|
|
826
|
-
"holdings": results,
|
|
827
|
-
"total_value": round(total_value, 2),
|
|
828
|
-
"total_cost": round(total_cost, 2),
|
|
829
|
-
"total_gain": round(total_gain, 2) if total_gain else None,
|
|
830
|
-
"total_gain_pct": round(total_gain_pct, 2) if total_gain_pct else None,
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
@tool(
|
|
835
|
-
name="search_stocks",
|
|
836
|
-
description="Search for stocks by company name or symbol",
|
|
837
|
-
parameters={
|
|
838
|
-
"type": "object",
|
|
839
|
-
"properties": {
|
|
840
|
-
"query": {"type": "string", "description": "Search query (company name or symbol)"}
|
|
841
|
-
},
|
|
842
|
-
"required": ["query"]
|
|
843
|
-
}
|
|
844
|
-
)
|
|
845
|
-
async def search_stocks(query: str) -> dict[str, Any]:
|
|
846
|
-
"""Search for stocks."""
|
|
847
|
-
url = f"https://query1.finance.yahoo.com/v1/finance/search?q={query}"esCount=10&newsCount=0"
|
|
848
|
-
|
|
849
|
-
async with httpx.AsyncClient() as client:
|
|
850
|
-
resp = await client.get(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
851
|
-
data = resp.json()
|
|
852
|
-
|
|
853
|
-
results = []
|
|
854
|
-
for quote in data.get("quotes", []):
|
|
855
|
-
if quote.get("quoteType") == "EQUITY":
|
|
856
|
-
results.append({
|
|
857
|
-
"symbol": quote.get("symbol"),
|
|
858
|
-
"name": quote.get("shortname") or quote.get("longname"),
|
|
859
|
-
"exchange": quote.get("exchange"),
|
|
860
|
-
"type": quote.get("typeDisp"),
|
|
861
|
-
})
|
|
862
|
-
|
|
863
|
-
return {"results": results, "query": query}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
@tool(
|
|
867
|
-
name="get_stock_news",
|
|
868
|
-
description="Get recent news for a stock",
|
|
869
|
-
parameters={
|
|
870
|
-
"type": "object",
|
|
871
|
-
"properties": {
|
|
872
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
873
|
-
},
|
|
874
|
-
"required": ["symbol"]
|
|
875
|
-
}
|
|
876
|
-
)
|
|
877
|
-
def get_stock_news(symbol: str) -> dict[str, Any]:
|
|
878
|
-
"""Get stock news."""
|
|
879
|
-
import yfinance as yf
|
|
880
|
-
|
|
881
|
-
ticker = yf.Ticker(symbol.upper())
|
|
882
|
-
news = ticker.news
|
|
883
|
-
|
|
884
|
-
if not news:
|
|
885
|
-
return {"symbol": symbol.upper(), "news": [], "message": "No news found"}
|
|
886
|
-
|
|
887
|
-
articles = []
|
|
888
|
-
for item in news[:10]:
|
|
889
|
-
articles.append({
|
|
890
|
-
"title": item.get("title"),
|
|
891
|
-
"publisher": item.get("publisher"),
|
|
892
|
-
"link": item.get("link"),
|
|
893
|
-
"published": datetime.fromtimestamp(item.get("providerPublishTime", 0)).isoformat() if item.get("providerPublishTime") else None,
|
|
894
|
-
"type": item.get("type"),
|
|
895
|
-
})
|
|
896
|
-
|
|
897
|
-
return {
|
|
898
|
-
"symbol": symbol.upper(),
|
|
899
|
-
"news": articles,
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
@tool(
|
|
904
|
-
name="technical_analysis",
|
|
905
|
-
description="Perform basic technical analysis on a stock",
|
|
906
|
-
parameters={
|
|
907
|
-
"type": "object",
|
|
908
|
-
"properties": {
|
|
909
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
910
|
-
},
|
|
911
|
-
"required": ["symbol"]
|
|
912
|
-
}
|
|
913
|
-
)
|
|
914
|
-
def technical_analysis(symbol: str) -> dict[str, Any]:
|
|
915
|
-
"""Perform technical analysis."""
|
|
916
|
-
import yfinance as yf
|
|
917
|
-
import numpy as np
|
|
918
|
-
|
|
919
|
-
ticker = yf.Ticker(symbol.upper())
|
|
920
|
-
hist = ticker.history(period="6mo")
|
|
921
|
-
|
|
922
|
-
if hist.empty:
|
|
923
|
-
return {"error": f"No data for {symbol}"}
|
|
924
|
-
|
|
925
|
-
close = hist["Close"]
|
|
926
|
-
|
|
927
|
-
# Moving averages
|
|
928
|
-
sma_20 = close.rolling(window=20).mean().iloc[-1]
|
|
929
|
-
sma_50 = close.rolling(window=50).mean().iloc[-1]
|
|
930
|
-
sma_200 = close.rolling(window=200).mean().iloc[-1] if len(close) >= 200 else None
|
|
931
|
-
|
|
932
|
-
# RSI
|
|
933
|
-
delta = close.diff()
|
|
934
|
-
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
|
|
935
|
-
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
|
|
936
|
-
rs = gain / loss
|
|
937
|
-
rsi = 100 - (100 / (1 + rs)).iloc[-1]
|
|
938
|
-
|
|
939
|
-
# MACD
|
|
940
|
-
ema_12 = close.ewm(span=12, adjust=False).mean()
|
|
941
|
-
ema_26 = close.ewm(span=26, adjust=False).mean()
|
|
942
|
-
macd = ema_12 - ema_26
|
|
943
|
-
signal = macd.ewm(span=9, adjust=False).mean()
|
|
944
|
-
|
|
945
|
-
current_price = close.iloc[-1]
|
|
946
|
-
|
|
947
|
-
# Signals
|
|
948
|
-
signals = []
|
|
949
|
-
if current_price > sma_20:
|
|
950
|
-
signals.append("Above SMA20 (bullish)")
|
|
951
|
-
else:
|
|
952
|
-
signals.append("Below SMA20 (bearish)")
|
|
953
|
-
|
|
954
|
-
if current_price > sma_50:
|
|
955
|
-
signals.append("Above SMA50 (bullish)")
|
|
956
|
-
else:
|
|
957
|
-
signals.append("Below SMA50 (bearish)")
|
|
958
|
-
|
|
959
|
-
if rsi > 70:
|
|
960
|
-
signals.append("RSI > 70 (overbought)")
|
|
961
|
-
elif rsi < 30:
|
|
962
|
-
signals.append("RSI < 30 (oversold)")
|
|
963
|
-
else:
|
|
964
|
-
signals.append(f"RSI neutral ({round(rsi, 1)})")
|
|
965
|
-
|
|
966
|
-
if macd.iloc[-1] > signal.iloc[-1]:
|
|
967
|
-
signals.append("MACD above signal (bullish)")
|
|
968
|
-
else:
|
|
969
|
-
signals.append("MACD below signal (bearish)")
|
|
970
|
-
|
|
971
|
-
return {
|
|
972
|
-
"symbol": symbol.upper(),
|
|
973
|
-
"price": round(current_price, 2),
|
|
974
|
-
"sma_20": round(sma_20, 2),
|
|
975
|
-
"sma_50": round(sma_50, 2),
|
|
976
|
-
"sma_200": round(sma_200, 2) if sma_200 else None,
|
|
977
|
-
"rsi": round(rsi, 2),
|
|
978
|
-
"macd": round(macd.iloc[-1], 4),
|
|
979
|
-
"macd_signal": round(signal.iloc[-1], 4),
|
|
980
|
-
"signals": signals,
|
|
981
|
-
"support": round(hist["Low"].tail(20).min(), 2),
|
|
982
|
-
"resistance": round(hist["High"].tail(20).max(), 2),
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
# ============================================================================
|
|
987
|
-
# CHART TOOLS
|
|
988
|
-
# ============================================================================
|
|
989
|
-
|
|
990
|
-
@tool(
|
|
991
|
-
name="generate_price_chart",
|
|
992
|
-
description="Generate a beautiful ASCII price chart in the terminal for a stock",
|
|
993
|
-
parameters={
|
|
994
|
-
"type": "object",
|
|
995
|
-
"properties": {
|
|
996
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
997
|
-
"period": {"type": "string", "description": "Time period: 1mo, 3mo, 6mo, 1y, 2y", "default": "3mo"},
|
|
998
|
-
"chart_type": {"type": "string", "description": "Chart type: line, candle, bar, area", "default": "line"},
|
|
999
|
-
"show_volume": {"type": "boolean", "description": "Show volume subplot", "default": True}
|
|
1000
|
-
},
|
|
1001
|
-
"required": ["symbol"]
|
|
1002
|
-
}
|
|
1003
|
-
)
|
|
1004
|
-
def generate_price_chart(symbol: str, period: str = "3mo", chart_type: str = "line", show_volume: bool = True) -> dict[str, Any]:
|
|
1005
|
-
"""Generate a price chart."""
|
|
1006
|
-
from sigma.tools.charts import create_price_chart
|
|
1007
|
-
|
|
1008
|
-
chart = create_price_chart(symbol, period, chart_type, show_volume)
|
|
1009
|
-
return {
|
|
1010
|
-
"symbol": symbol.upper(),
|
|
1011
|
-
"period": period,
|
|
1012
|
-
"chart_type": chart_type,
|
|
1013
|
-
"chart": chart,
|
|
1014
|
-
"display_as_chart": True, # Flag for UI to render properly
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
@tool(
|
|
1019
|
-
name="generate_comparison_chart",
|
|
1020
|
-
description="Generate a comparison chart for multiple stocks showing percentage change",
|
|
1021
|
-
parameters={
|
|
1022
|
-
"type": "object",
|
|
1023
|
-
"properties": {
|
|
1024
|
-
"symbols": {"type": "array", "items": {"type": "string"}, "description": "List of stock symbols to compare"},
|
|
1025
|
-
"period": {"type": "string", "description": "Time period", "default": "3mo"}
|
|
1026
|
-
},
|
|
1027
|
-
"required": ["symbols"]
|
|
1028
|
-
}
|
|
1029
|
-
)
|
|
1030
|
-
def generate_comparison_chart(symbols: list[str], period: str = "3mo") -> dict[str, Any]:
|
|
1031
|
-
"""Generate a comparison chart."""
|
|
1032
|
-
from sigma.tools.charts import create_comparison_chart
|
|
1033
|
-
|
|
1034
|
-
chart = create_comparison_chart(symbols, period)
|
|
1035
|
-
return {
|
|
1036
|
-
"symbols": [s.upper() for s in symbols],
|
|
1037
|
-
"period": period,
|
|
1038
|
-
"chart": chart,
|
|
1039
|
-
"display_as_chart": True,
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
@tool(
|
|
1044
|
-
name="generate_rsi_chart",
|
|
1045
|
-
description="Generate a price chart with RSI indicator showing overbought/oversold levels",
|
|
1046
|
-
parameters={
|
|
1047
|
-
"type": "object",
|
|
1048
|
-
"properties": {
|
|
1049
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
1050
|
-
"period": {"type": "string", "description": "Time period", "default": "3mo"}
|
|
1051
|
-
},
|
|
1052
|
-
"required": ["symbol"]
|
|
1053
|
-
}
|
|
1054
|
-
)
|
|
1055
|
-
def generate_rsi_chart(symbol: str, period: str = "3mo") -> dict[str, Any]:
|
|
1056
|
-
"""Generate RSI chart."""
|
|
1057
|
-
from sigma.tools.charts import create_rsi_chart
|
|
1058
|
-
|
|
1059
|
-
chart = create_rsi_chart(symbol, period)
|
|
1060
|
-
return {
|
|
1061
|
-
"symbol": symbol.upper(),
|
|
1062
|
-
"period": period,
|
|
1063
|
-
"chart": chart,
|
|
1064
|
-
"display_as_chart": True,
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
@tool(
|
|
1069
|
-
name="generate_sector_chart",
|
|
1070
|
-
description="Generate a sector performance chart showing daily changes for all major sectors",
|
|
1071
|
-
parameters={
|
|
1072
|
-
"type": "object",
|
|
1073
|
-
"properties": {},
|
|
1074
|
-
"required": []
|
|
1075
|
-
}
|
|
1076
|
-
)
|
|
1077
|
-
def generate_sector_chart() -> dict[str, Any]:
|
|
1078
|
-
"""Generate sector chart."""
|
|
1079
|
-
from sigma.tools.charts import create_sector_chart
|
|
1080
|
-
|
|
1081
|
-
chart = create_sector_chart()
|
|
1082
|
-
return {
|
|
1083
|
-
"chart": chart,
|
|
1084
|
-
"display_as_chart": True,
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
# ============================================================================
|
|
1089
|
-
# BACKTEST TOOLS
|
|
1090
|
-
# ============================================================================
|
|
1091
|
-
|
|
1092
|
-
@tool(
|
|
1093
|
-
name="list_backtest_strategies",
|
|
1094
|
-
description="List all available backtesting strategies with descriptions",
|
|
1095
|
-
parameters={
|
|
1096
|
-
"type": "object",
|
|
1097
|
-
"properties": {},
|
|
1098
|
-
"required": []
|
|
1099
|
-
}
|
|
1100
|
-
)
|
|
1101
|
-
def list_backtest_strategies() -> dict[str, Any]:
|
|
1102
|
-
"""List available strategies."""
|
|
1103
|
-
from sigma.tools.backtest import get_available_strategies
|
|
1104
|
-
|
|
1105
|
-
strategies = get_available_strategies()
|
|
1106
|
-
return {
|
|
1107
|
-
"strategies": strategies,
|
|
1108
|
-
"count": len(strategies),
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
@tool(
|
|
1113
|
-
name="generate_backtest",
|
|
1114
|
-
description="Generate a LEAN engine compatible backtest algorithm for a trading strategy",
|
|
1115
|
-
parameters={
|
|
1116
|
-
"type": "object",
|
|
1117
|
-
"properties": {
|
|
1118
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
1119
|
-
"strategy": {"type": "string", "description": "Strategy type: sma_crossover, rsi_mean_reversion, macd_momentum, bollinger_bands, dual_momentum, breakout"},
|
|
1120
|
-
"start_date": {"type": "string", "description": "Start date (YYYY-MM-DD)", "default": None},
|
|
1121
|
-
"end_date": {"type": "string", "description": "End date (YYYY-MM-DD)", "default": None},
|
|
1122
|
-
"initial_capital": {"type": "number", "description": "Starting capital", "default": 100000},
|
|
1123
|
-
"params": {"type": "object", "description": "Strategy-specific parameters (optional)"}
|
|
1124
|
-
},
|
|
1125
|
-
"required": ["symbol", "strategy"]
|
|
1126
|
-
}
|
|
1127
|
-
)
|
|
1128
|
-
def generate_backtest(
|
|
1129
|
-
symbol: str,
|
|
1130
|
-
strategy: str,
|
|
1131
|
-
start_date: Optional[str] = None,
|
|
1132
|
-
end_date: Optional[str] = None,
|
|
1133
|
-
initial_capital: float = 100000,
|
|
1134
|
-
params: Optional[dict] = None,
|
|
1135
|
-
) -> dict[str, Any]:
|
|
1136
|
-
"""Generate a backtest algorithm."""
|
|
1137
|
-
from sigma.tools.backtest import generate_lean_algorithm
|
|
1138
|
-
|
|
1139
|
-
result = generate_lean_algorithm(
|
|
1140
|
-
symbol=symbol,
|
|
1141
|
-
strategy=strategy,
|
|
1142
|
-
start_date=start_date,
|
|
1143
|
-
end_date=end_date,
|
|
1144
|
-
initial_capital=initial_capital,
|
|
1145
|
-
params=params,
|
|
1146
|
-
)
|
|
1147
|
-
return result
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
@tool(
|
|
1151
|
-
name="generate_custom_backtest",
|
|
1152
|
-
description="Generate a custom backtest algorithm with user-specified entry/exit conditions",
|
|
1153
|
-
parameters={
|
|
1154
|
-
"type": "object",
|
|
1155
|
-
"properties": {
|
|
1156
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
1157
|
-
"entry_conditions": {"type": "array", "items": {"type": "string"}, "description": "List of entry conditions (e.g., 'RSI below 30', 'Price above SMA 50')"},
|
|
1158
|
-
"exit_conditions": {"type": "array", "items": {"type": "string"}, "description": "List of exit conditions"},
|
|
1159
|
-
"indicators": {"type": "array", "items": {"type": "string"}, "description": "Indicators to use: sma, ema, rsi, macd, bb, atr, adx"},
|
|
1160
|
-
"initial_capital": {"type": "number", "description": "Starting capital", "default": 100000}
|
|
1161
|
-
},
|
|
1162
|
-
"required": ["symbol", "entry_conditions", "exit_conditions", "indicators"]
|
|
1163
|
-
}
|
|
1164
|
-
)
|
|
1165
|
-
def generate_custom_backtest(
|
|
1166
|
-
symbol: str,
|
|
1167
|
-
entry_conditions: list[str],
|
|
1168
|
-
exit_conditions: list[str],
|
|
1169
|
-
indicators: list[str],
|
|
1170
|
-
initial_capital: float = 100000,
|
|
1171
|
-
) -> dict[str, Any]:
|
|
1172
|
-
"""Generate custom backtest."""
|
|
1173
|
-
from sigma.tools.backtest import generate_custom_algorithm
|
|
1174
|
-
|
|
1175
|
-
return generate_custom_algorithm(
|
|
1176
|
-
symbol=symbol,
|
|
1177
|
-
entry_conditions=entry_conditions,
|
|
1178
|
-
exit_conditions=exit_conditions,
|
|
1179
|
-
indicators=indicators,
|
|
1180
|
-
initial_capital=initial_capital,
|
|
1181
|
-
)
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
@tool(
|
|
1185
|
-
name="run_backtest",
|
|
1186
|
-
description="Run a full backtest using LEAN CLI. Installs LEAN if needed, generates algorithm, and runs backtest.",
|
|
1187
|
-
parameters={
|
|
1188
|
-
"type": "object",
|
|
1189
|
-
"properties": {
|
|
1190
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
1191
|
-
"strategy": {
|
|
1192
|
-
"type": "string",
|
|
1193
|
-
"description": "Strategy: sma_crossover, rsi_mean_reversion, macd_momentum, bollinger_bands, dual_momentum, breakout",
|
|
1194
|
-
"default": "sma_crossover"
|
|
1195
|
-
},
|
|
1196
|
-
"initial_capital": {"type": "number", "description": "Starting capital", "default": 100000}
|
|
1197
|
-
},
|
|
1198
|
-
"required": ["symbol"]
|
|
1199
|
-
}
|
|
1200
|
-
)
|
|
1201
|
-
def run_backtest_tool(
|
|
1202
|
-
symbol: str,
|
|
1203
|
-
strategy: str = "sma_crossover",
|
|
1204
|
-
initial_capital: float = 100000,
|
|
1205
|
-
) -> dict[str, Any]:
|
|
1206
|
-
"""Run a full backtest with LEAN."""
|
|
1207
|
-
from sigma.tools.backtest import run_backtest
|
|
1208
|
-
|
|
1209
|
-
return run_backtest(
|
|
1210
|
-
symbol=symbol,
|
|
1211
|
-
strategy=strategy,
|
|
1212
|
-
initial_capital=initial_capital,
|
|
1213
|
-
)
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
@tool(
|
|
1217
|
-
name="check_lean_status",
|
|
1218
|
-
description="Check if LEAN CLI is installed and provide setup instructions",
|
|
1219
|
-
parameters={
|
|
1220
|
-
"type": "object",
|
|
1221
|
-
"properties": {},
|
|
1222
|
-
"required": []
|
|
1223
|
-
}
|
|
1224
|
-
)
|
|
1225
|
-
def check_lean_status_tool() -> dict[str, Any]:
|
|
1226
|
-
"""Check LEAN installation status."""
|
|
1227
|
-
from sigma.tools.backtest import check_lean_status
|
|
1228
|
-
|
|
1229
|
-
return check_lean_status()
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
# ============================================================================
|
|
1233
|
-
# PREDICTION TOOLS
|
|
1234
|
-
# ============================================================================
|
|
1235
|
-
|
|
1236
|
-
@tool(
|
|
1237
|
-
name="price_forecast",
|
|
1238
|
-
description="Generate price predictions using multiple technical models",
|
|
1239
|
-
parameters={
|
|
1240
|
-
"type": "object",
|
|
1241
|
-
"properties": {
|
|
1242
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"},
|
|
1243
|
-
"horizon": {"type": "string", "description": "Forecast horizon: 1w, 1m, 3m, 6m", "default": "1m"}
|
|
1244
|
-
},
|
|
1245
|
-
"required": ["symbol"]
|
|
1246
|
-
}
|
|
1247
|
-
)
|
|
1248
|
-
def price_forecast(symbol: str, horizon: str = "1m") -> dict[str, Any]:
|
|
1249
|
-
"""Generate price predictions."""
|
|
1250
|
-
import yfinance as yf
|
|
1251
|
-
import numpy as np
|
|
1252
|
-
|
|
1253
|
-
ticker = yf.Ticker(symbol.upper())
|
|
1254
|
-
hist = ticker.history(period="1y")
|
|
1255
|
-
|
|
1256
|
-
if hist.empty:
|
|
1257
|
-
return {"error": f"No data for {symbol}"}
|
|
1258
|
-
|
|
1259
|
-
# Convert to numpy array explicitly
|
|
1260
|
-
close = np.array(hist["Close"].values, dtype=float)
|
|
1261
|
-
current_price = float(close[-1])
|
|
1262
|
-
|
|
1263
|
-
# Map horizon to days
|
|
1264
|
-
horizon_days = {"1w": 5, "1m": 21, "3m": 63, "6m": 126}.get(horizon, 21)
|
|
1265
|
-
|
|
1266
|
-
# Multiple prediction methods
|
|
1267
|
-
predictions = {}
|
|
1268
|
-
|
|
1269
|
-
# 1. Simple Moving Average Projection
|
|
1270
|
-
sma_20 = float(np.mean(close[-20:]))
|
|
1271
|
-
sma_50 = float(np.mean(close[-50:])) if len(close) >= 50 else sma_20
|
|
1272
|
-
sma_trend = (sma_20 - sma_50) / sma_50 if sma_50 else 0
|
|
1273
|
-
sma_prediction = current_price * (1 + sma_trend * (horizon_days / 20))
|
|
1274
|
-
predictions["sma_trend"] = round(sma_prediction, 2)
|
|
1275
|
-
|
|
1276
|
-
# 2. Linear Regression
|
|
1277
|
-
x = np.arange(len(close))
|
|
1278
|
-
coeffs = np.polyfit(x, close, 1)
|
|
1279
|
-
lr_prediction = float(coeffs[0]) * (len(close) + horizon_days) + float(coeffs[1])
|
|
1280
|
-
predictions["linear_regression"] = round(lr_prediction, 2)
|
|
1281
|
-
|
|
1282
|
-
# 3. Volatility-adjusted (Monte Carlo simple)
|
|
1283
|
-
daily_returns = np.diff(close) / close[:-1]
|
|
1284
|
-
avg_return = float(np.mean(daily_returns))
|
|
1285
|
-
std_return = float(np.std(daily_returns))
|
|
1286
|
-
|
|
1287
|
-
# Expected value with drift
|
|
1288
|
-
mc_prediction = current_price * (1 + avg_return * horizon_days)
|
|
1289
|
-
predictions["monte_carlo_expected"] = round(mc_prediction, 2)
|
|
1290
|
-
|
|
1291
|
-
# Confidence interval (1 std)
|
|
1292
|
-
upper = current_price * np.exp((avg_return - 0.5 * std_return**2) * horizon_days + std_return * np.sqrt(horizon_days) * 1.96)
|
|
1293
|
-
lower = current_price * np.exp((avg_return - 0.5 * std_return**2) * horizon_days - std_return * np.sqrt(horizon_days) * 1.96)
|
|
1294
|
-
|
|
1295
|
-
# 4. Mean reversion to SMA 200
|
|
1296
|
-
sma_200 = float(np.mean(close[-200:])) if len(close) >= 200 else sma_50
|
|
1297
|
-
mean_rev_prediction = current_price + (sma_200 - current_price) * 0.3 # 30% reversion
|
|
1298
|
-
predictions["mean_reversion"] = round(mean_rev_prediction, 2)
|
|
1299
|
-
|
|
1300
|
-
# Consensus
|
|
1301
|
-
consensus = float(np.mean(list(predictions.values())))
|
|
1302
|
-
|
|
1303
|
-
# Analyst targets for comparison
|
|
1304
|
-
info = ticker.info
|
|
1305
|
-
analyst_target = info.get("targetMeanPrice")
|
|
1306
|
-
|
|
1307
|
-
return {
|
|
1308
|
-
"symbol": symbol.upper(),
|
|
1309
|
-
"current_price": round(current_price, 2),
|
|
1310
|
-
"horizon": horizon,
|
|
1311
|
-
"horizon_days": horizon_days,
|
|
1312
|
-
"predictions": predictions,
|
|
1313
|
-
"consensus_prediction": round(consensus, 2),
|
|
1314
|
-
"confidence_interval": {
|
|
1315
|
-
"lower_95": round(lower, 2),
|
|
1316
|
-
"upper_95": round(upper, 2),
|
|
1317
|
-
},
|
|
1318
|
-
"analyst_target": analyst_target,
|
|
1319
|
-
"volatility": {
|
|
1320
|
-
"daily": round(std_return * 100, 2),
|
|
1321
|
-
"annualized": round(std_return * np.sqrt(252) * 100, 2),
|
|
1322
|
-
},
|
|
1323
|
-
"disclaimer": "Predictions are based on historical patterns and should not be considered financial advice."
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
@tool(
|
|
1328
|
-
name="sentiment_analysis",
|
|
1329
|
-
description="Analyze market sentiment for a stock based on various signals",
|
|
1330
|
-
parameters={
|
|
1331
|
-
"type": "object",
|
|
1332
|
-
"properties": {
|
|
1333
|
-
"symbol": {"type": "string", "description": "Stock ticker symbol"}
|
|
1334
|
-
},
|
|
1335
|
-
"required": ["symbol"]
|
|
1336
|
-
}
|
|
1337
|
-
)
|
|
1338
|
-
def sentiment_analysis(symbol: str) -> dict[str, Any]:
|
|
1339
|
-
"""Analyze sentiment from multiple sources."""
|
|
1340
|
-
import yfinance as yf
|
|
1341
|
-
|
|
1342
|
-
ticker = yf.Ticker(symbol.upper())
|
|
1343
|
-
info = ticker.info
|
|
1344
|
-
hist = ticker.history(period="3mo")
|
|
1345
|
-
|
|
1346
|
-
if hist.empty:
|
|
1347
|
-
return {"error": f"No data for {symbol}"}
|
|
1348
|
-
|
|
1349
|
-
signals = []
|
|
1350
|
-
score = 0
|
|
1351
|
-
|
|
1352
|
-
# 1. Analyst sentiment
|
|
1353
|
-
rec = info.get("recommendationKey", "").lower()
|
|
1354
|
-
if "strong buy" in rec or rec == "buy":
|
|
1355
|
-
signals.append(("Analyst Rating", rec.upper(), "bullish", 2))
|
|
1356
|
-
score += 2
|
|
1357
|
-
elif "hold" in rec:
|
|
1358
|
-
signals.append(("Analyst Rating", rec.upper(), "neutral", 0))
|
|
1359
|
-
elif "sell" in rec:
|
|
1360
|
-
signals.append(("Analyst Rating", rec.upper(), "bearish", -2))
|
|
1361
|
-
score -= 2
|
|
1362
|
-
|
|
1363
|
-
# 2. Price vs 52-week range
|
|
1364
|
-
high_52 = info.get("fiftyTwoWeekHigh", 0)
|
|
1365
|
-
low_52 = info.get("fiftyTwoWeekLow", 0)
|
|
1366
|
-
price = info.get("regularMarketPrice", 0)
|
|
1367
|
-
|
|
1368
|
-
if high_52 and low_52 and price:
|
|
1369
|
-
position = (price - low_52) / (high_52 - low_52) if high_52 != low_52 else 0.5
|
|
1370
|
-
if position > 0.8:
|
|
1371
|
-
signals.append(("52-Week Position", f"{position*100:.0f}%", "overbought", -1))
|
|
1372
|
-
score -= 1
|
|
1373
|
-
elif position < 0.2:
|
|
1374
|
-
signals.append(("52-Week Position", f"{position*100:.0f}%", "oversold", 1))
|
|
1375
|
-
score += 1
|
|
1376
|
-
else:
|
|
1377
|
-
signals.append(("52-Week Position", f"{position*100:.0f}%", "neutral", 0))
|
|
1378
|
-
|
|
1379
|
-
# 3. RSI
|
|
1380
|
-
close = hist["Close"].values
|
|
1381
|
-
if len(close) > 14:
|
|
1382
|
-
deltas = [close[i] - close[i-1] for i in range(1, len(close))]
|
|
1383
|
-
gains = [d if d > 0 else 0 for d in deltas]
|
|
1384
|
-
losses = [-d if d < 0 else 0 for d in deltas]
|
|
1385
|
-
avg_gain = sum(gains[-14:]) / 14
|
|
1386
|
-
avg_loss = sum(losses[-14:]) / 14
|
|
1387
|
-
if avg_loss > 0:
|
|
1388
|
-
rs = avg_gain / avg_loss
|
|
1389
|
-
rsi = 100 - (100 / (1 + rs))
|
|
1390
|
-
else:
|
|
1391
|
-
rsi = 100
|
|
1392
|
-
|
|
1393
|
-
if rsi > 70:
|
|
1394
|
-
signals.append(("RSI", f"{rsi:.1f}", "overbought", -1))
|
|
1395
|
-
score -= 1
|
|
1396
|
-
elif rsi < 30:
|
|
1397
|
-
signals.append(("RSI", f"{rsi:.1f}", "oversold", 1))
|
|
1398
|
-
score += 1
|
|
1399
|
-
else:
|
|
1400
|
-
signals.append(("RSI", f"{rsi:.1f}", "neutral", 0))
|
|
1401
|
-
|
|
1402
|
-
# 4. Moving average trend
|
|
1403
|
-
sma_50 = sum(close[-50:]) / 50 if len(close) >= 50 else None
|
|
1404
|
-
sma_200 = sum(close[-200:]) / 200 if len(close) >= 200 else None
|
|
1405
|
-
|
|
1406
|
-
if sma_50 and sma_200:
|
|
1407
|
-
if sma_50 > sma_200:
|
|
1408
|
-
signals.append(("Golden Cross", "SMA50 > SMA200", "bullish", 1))
|
|
1409
|
-
score += 1
|
|
1410
|
-
else:
|
|
1411
|
-
signals.append(("Death Cross", "SMA50 < SMA200", "bearish", -1))
|
|
1412
|
-
score -= 1
|
|
1413
|
-
|
|
1414
|
-
# 5. Volume trend
|
|
1415
|
-
avg_vol = info.get("averageVolume", 0)
|
|
1416
|
-
curr_vol = info.get("regularMarketVolume", 0)
|
|
1417
|
-
|
|
1418
|
-
if avg_vol and curr_vol:
|
|
1419
|
-
vol_ratio = curr_vol / avg_vol
|
|
1420
|
-
if vol_ratio > 1.5:
|
|
1421
|
-
signals.append(("Volume Surge", f"{vol_ratio:.1f}x avg", "high_interest", 0))
|
|
1422
|
-
|
|
1423
|
-
# 6. Insider activity
|
|
1424
|
-
try:
|
|
1425
|
-
insiders = ticker.insider_transactions
|
|
1426
|
-
if insiders is not None and not insiders.empty:
|
|
1427
|
-
buys = insiders[insiders["Shares"].fillna(0) > 0].shape[0]
|
|
1428
|
-
sells = insiders[insiders["Shares"].fillna(0) < 0].shape[0]
|
|
1429
|
-
if buys > sells * 2:
|
|
1430
|
-
signals.append(("Insider Activity", f"{buys} buys vs {sells} sells", "bullish", 1))
|
|
1431
|
-
score += 1
|
|
1432
|
-
elif sells > buys * 2:
|
|
1433
|
-
signals.append(("Insider Activity", f"{sells} sells vs {buys} buys", "bearish", -1))
|
|
1434
|
-
score -= 1
|
|
1435
|
-
except:
|
|
1436
|
-
pass
|
|
1437
|
-
|
|
1438
|
-
# Overall sentiment
|
|
1439
|
-
if score >= 3:
|
|
1440
|
-
overall = "STRONGLY BULLISH"
|
|
1441
|
-
elif score >= 1:
|
|
1442
|
-
overall = "BULLISH"
|
|
1443
|
-
elif score <= -3:
|
|
1444
|
-
overall = "STRONGLY BEARISH"
|
|
1445
|
-
elif score <= -1:
|
|
1446
|
-
overall = "BEARISH"
|
|
1447
|
-
else:
|
|
1448
|
-
overall = "NEUTRAL"
|
|
1449
|
-
|
|
1450
|
-
return {
|
|
1451
|
-
"symbol": symbol.upper(),
|
|
1452
|
-
"overall_sentiment": overall,
|
|
1453
|
-
"sentiment_score": score,
|
|
1454
|
-
"max_score": 5,
|
|
1455
|
-
"min_score": -5,
|
|
1456
|
-
"signals": [{"indicator": s[0], "value": s[1], "signal": s[2], "score": s[3]} for s in signals],
|
|
1457
|
-
}
|