sigma-terminal 2.0.2__py3-none-any.whl → 3.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sigma/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}&quotesCount=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
- }