akankshar639 commited on
Commit
a6afbdd
·
verified ·
1 Parent(s): eea6142

Upload 6 files

Browse files
Files changed (6) hide show
  1. agents.py +420 -0
  2. graph.py +24 -0
  3. main.py +325 -0
  4. requirements.txt +17 -0
  5. state.py +18 -0
  6. style.css +140 -0
agents.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yfinance as yf
2
+ import requests
3
+ from bs4 import BeautifulSoup
4
+ import pandas as pd
5
+ import numpy as np
6
+ from langchain_openai import ChatOpenAI
7
+ import os
8
+ import pytz
9
+ from typing import Dict, Any
10
+ from state import TraderState
11
+ from datetime import datetime, timedelta
12
+
13
+ os.environ["OPENAI_API_KEY"] = os.getenv("OPENROUTER_API_KEY", "")
14
+ llm = ChatOpenAI(model="openai/gpt-3.5-turbo", openai_api_base="https://openrouter.ai/api/v1", temperature=0.2)
15
+
16
+ ALPHA_VANTAGE_KEY = os.getenv("ALPHA_VANTAGE_API_KEY", "")
17
+ NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "")
18
+
19
+ def fetch_data(symbol: str, horizon: str) -> pd.DataFrame:
20
+ """Fetch data precisely for each trading type"""
21
+ if horizon == "intraday":
22
+ period = "1d"
23
+ interval = "1m"
24
+ elif horizon == "scalping":
25
+ period = "1d"
26
+ interval = "1m"
27
+ elif horizon == "swing":
28
+ period = "3mo"
29
+ interval = "1d"
30
+ elif horizon == "momentum":
31
+ period = "6mo"
32
+ interval = "1d"
33
+ else: # long_term
34
+ period = "2y"
35
+ interval = "1d"
36
+
37
+ try:
38
+ stock = yf.Ticker(symbol)
39
+ hist = stock.history(period=period, interval=interval)
40
+ if hist.empty or len(hist) < 50:
41
+ hist = stock.history(period="1y", interval="1d") # Fallback
42
+ if hist.empty:
43
+ raise ValueError(f"No data from yfinance for {symbol}")
44
+
45
+ eastern = pytz.timezone('US/Eastern')
46
+ ist = pytz.timezone('Asia/Kolkata')
47
+ hist.index = hist.index.tz_convert(eastern).tz_convert(ist)
48
+
49
+ print(f"Data fetched for {symbol} ({horizon}): {len(hist)} rows ({interval} intervals)")
50
+ return hist
51
+ except Exception as e:
52
+ print(f"Yfinance failed for {symbol}: {e}")
53
+ return pd.DataFrame()
54
+
55
+ def compute_indicators(hist: pd.DataFrame, horizon: str) -> Dict[str, Any]:
56
+ if hist.empty:
57
+ return {"error": "No data available", "current_price": 0, "rsi": 50, "macd": 0, "sma_20": 0, "ema_50": 0, "bb_upper": 0, "bb_lower": 0, "volume_avg": 0}
58
+
59
+ try:
60
+ hist = hist.sort_index()
61
+ close = hist['Close']
62
+ if len(close) < 50:
63
+ return {"error": "Insufficient data"}
64
+ delta = close.diff()
65
+ gain = (delta.where(delta > 0, 0)).rolling(14).mean()
66
+ loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
67
+ rs = gain / loss
68
+ rsi = 100 - (100 / (1 + rs)).iloc[-1]
69
+ ema12 = close.ewm(12).mean()
70
+ ema26 = close.ewm(26).mean()
71
+ macd = ema12 - ema26
72
+ macd_value = macd.iloc[-1]
73
+ sma_20 = close.rolling(20).mean().iloc[-1]
74
+ ema_50 = close.ewm(50).mean().iloc[-1]
75
+ if len(close) >= 20:
76
+ std_bb = close.rolling(20).std()
77
+ bb_upper = (sma_20 + 2 * std_bb).iloc[-1]
78
+ bb_lower = (sma_20 - 2 * std_bb).iloc[-1]
79
+ else:
80
+ bb_upper = bb_lower = None
81
+ volume_avg = hist['Volume'].rolling(20).mean().iloc[-1] if 'Volume' in hist else 0
82
+ return {
83
+ "current_price": close.iloc[-1], "rsi": rsi, "macd": macd_value,
84
+ "sma_20": sma_20, "ema_50": ema_50, "bb_upper": bb_upper, "bb_lower": bb_lower,
85
+ "volume_avg": volume_avg
86
+ }
87
+ except Exception as e:
88
+ return {"error": str(e)}
89
+
90
+ def fetch_news_sentiment(symbol: str) -> dict:
91
+ """
92
+ Fetch recent news from NewsAPI (free forever) and analyze sentiment.
93
+ """
94
+ try:
95
+ base_symbol = symbol.replace(".NS", "").replace(".BO", "")
96
+ query = f'"{base_symbol}" OR "{base_symbol} stock"'
97
+ url = f"https://newsapi.org/v2/everything?q={query}&sortBy=publishedAt&apiKey={NEWSAPI_KEY}&pageSize=10&language=en"
98
+ response = requests.get(url, timeout=10)
99
+ data = response.json()
100
+
101
+ if 'articles' in data and data['articles']:
102
+ articles = data['articles'][:5]
103
+ headlines = [art['title'] for art in articles]
104
+ descriptions = [art.get('description', '') for art in articles]
105
+
106
+ positive_words = ['gain', 'rise', 'profit', 'growth', 'earnings', 'bullish', 'up', 'positive', 'strong']
107
+ negative_words = ['loss', 'fall', 'decline', 'drop', 'bearish', 'down', 'negative', 'weak', 'crash']
108
+
109
+ pos_count = sum(1 for h in headlines + descriptions for word in positive_words if word in h.lower())
110
+ neg_count = sum(1 for h in headlines + descriptions for word in negative_words if word in h.lower())
111
+
112
+ sentiment = 'positive' if pos_count > neg_count else 'negative' if neg_count > pos_count else 'neutral'
113
+ summary = f"Recent news sentiment: {sentiment}. Key headlines: {'; '.join(headlines[:3])}."
114
+ return {'sentiment': sentiment, 'headlines': headlines, 'summary': summary}
115
+ else:
116
+ return {'sentiment': 'neutral', 'headlines': ["No recent news found."], 'summary': "No recent news available; sentiment neutral."}
117
+ except Exception as e:
118
+ print(f"NewsAPI error: {e}")
119
+ return {'sentiment': 'neutral', 'headlines': ["News fetch failed."], 'summary': "News unavailable; assuming neutral sentiment."}
120
+
121
+ def summarize_features(indicators: Dict[str, Any], hist: pd.DataFrame, news: list, action: str, symbol: str, news_sentiment: str = 'neutral') -> str:
122
+ if indicators.get("error"):
123
+ try:
124
+ stock = yf.Ticker(symbol)
125
+ info = stock.info
126
+ price = info.get('currentPrice', 0)
127
+ change = info.get('regularMarketChangePercent', 0)
128
+ signals = [f"Recent change: {change:.1f}%"]
129
+ if change > 0:
130
+ signals.append("Positive momentum")
131
+ else:
132
+ signals.append("Negative momentum")
133
+ return f"Fallback Signals: {', '.join(signals)}; News: {news[0] if news else 'Neutral'}"
134
+ except:
135
+ return "No reliable data; default to caution."
136
+
137
+ rsi = indicators.get('rsi', 50)
138
+ macd = indicators.get('macd', 0)
139
+ price = indicators.get('current_price', 0)
140
+ sma_20 = indicators.get('sma_20', price)
141
+ bb_upper = indicators.get('bb_upper', price * 1.1)
142
+ bb_lower = indicators.get('bb_lower', price * 0.9)
143
+
144
+ recent_prices = hist['Close'].tail(10) if not hist.empty else pd.Series([price])
145
+ trend = "upward" if recent_prices.iloc[-1] > recent_prices.iloc[0] else "downward"
146
+ change_pct = ((recent_prices.iloc[-1] - recent_prices.iloc[0]) / recent_prices.iloc[0]) * 100 if len(recent_prices) > 1 else 0
147
+
148
+ signals = []
149
+ if rsi < 30:
150
+ signals.append("Oversold RSI (potential buy reversal)")
151
+ elif rsi > 70:
152
+ signals.append("Overbought RSI (sell risk)")
153
+ if macd > 0:
154
+ signals.append("Bullish MACD")
155
+ else:
156
+ signals.append("Bearish MACD")
157
+ if price < bb_lower:
158
+ signals.append("Price near support (buy opportunity)")
159
+ elif price > bb_upper:
160
+ signals.append("Price near resistance (sell signal)")
161
+ if price > sma_20:
162
+ signals.append("Above SMA20 (momentum)")
163
+ else:
164
+ signals.append("Below SMA20 (weakness)")
165
+
166
+ news_sentiment_str = f"News Sentiment: {news_sentiment.capitalize()} (e.g., {news[0] if news else 'No updates'})"
167
+
168
+ action_bias = "favorable for buying" if action == "buy" and trend == "upward" else "favorable for selling" if action == "sell" and trend == "downward" else "mixed"
169
+
170
+ features = [
171
+ f"Trend: {trend} ({change_pct:.1f}% change)",
172
+ f"Key Signals: {', '.join(signals)}",
173
+ news_sentiment_str,
174
+ f"Overall Bias for {action}: {action_bias}"
175
+ ]
176
+ return "; ".join(features)
177
+
178
+ def calculate_holding_metrics(hist: pd.DataFrame, holding_period: str, current_price: float) -> Dict[str, Any]:
179
+ if not holding_period or hist.empty:
180
+ return {}
181
+
182
+ import re
183
+ match = re.search(r'(\d+)\s*(month|week|day)', holding_period.lower())
184
+ if match:
185
+ num, unit = int(match.group(1)), match.group(2)
186
+ days = num * 30 if unit == 'month' else num * 7 if unit == 'week' else num
187
+ purchase_date = datetime.now(pytz.timezone('Asia/Kolkata')) - timedelta(days=days)
188
+ hist_before = hist[hist.index < purchase_date]
189
+ if not hist_before.empty:
190
+ purchase_price = hist_before['Close'].iloc[-1]
191
+ else:
192
+ # Use oldest price in data if exact date not found
193
+ purchase_price = hist['Close'].iloc[0] if not hist.empty else 0
194
+
195
+ try:
196
+ live_price = yf.Ticker(hist.index.name or 'AAPL').info.get('currentPrice', current_price)
197
+ if live_price <= 0:
198
+ live_price = current_price
199
+ except:
200
+ live_price = current_price
201
+
202
+ if purchase_price > 0:
203
+ holding_return = ((live_price - purchase_price) / purchase_price) * 100
204
+ else:
205
+ holding_return = 0
206
+
207
+ return {
208
+ "purchase_price": purchase_price,
209
+ "holding_return": holding_return,
210
+ "days_held": days,
211
+ "pnl": "profit" if holding_return > 0 else "loss"
212
+ }
213
+ return {}
214
+
215
+ def predict(horizon: str, indicators: Dict[str, Any], hist: pd.DataFrame, news: list, news_sentiment: str = 'neutral') -> str:
216
+ if hist.empty or indicators.get("error"):
217
+ return "Unable to predict due to lack of data."
218
+
219
+ rsi = indicators.get('rsi', 50)
220
+ macd = indicators.get('macd', 0)
221
+ close = hist['Close']
222
+
223
+ if horizon == "intraday":
224
+ # minute changes for same-day prediction
225
+ recent_change = ((close.iloc[-1] - close.iloc[-10]) / close.iloc[-10]) * 100 if len(close) >= 10 else 0 # Last 10 minutes
226
+ prob = 70 if (rsi < 40 and macd > 0) else 50 if rsi > 60 else 30
227
+ direction = "rise" if recent_change > 0 else "fall"
228
+ pct = abs(recent_change) * 0.8
229
+ sentiment_boost = 1.15 if news_sentiment == 'positive' else 0.85 if news_sentiment == 'negative' else 1.0
230
+ prob = min(95, int(prob * sentiment_boost))
231
+ return f"{prob}% chance of {pct:.1f}% {direction} in the next 1-2 hours (same-day intraday) based on RSI {rsi:.1f}, MACD {macd:.2f}, recent {recent_change:.1f}% change, and {news_sentiment} news."
232
+
233
+ elif horizon == "scalping":
234
+ # Ultra-short minute prediction
235
+ recent_change = ((close.iloc[-1] - close.iloc[-5]) / close.iloc[-5]) * 100 if len(close) >= 5 else 0 # Last 5 minutes
236
+ prob = 80 if rsi < 35 else 40
237
+ direction = "quick rise" if rsi < 35 else "quick fall"
238
+ pct = 1.5
239
+ return f"{prob}% chance of {pct:.1f}% {direction} in the next 5-10 minutes (ultra-short scalping) based on RSI ({rsi:.1f})."
240
+
241
+ elif horizon == "swing":
242
+ # Formula: Weekly changes for 1-4 week prediction
243
+ avg_weekly = close.tail(20).pct_change().mean() * 100 if len(close) >= 20 else 0 # Approx weekly
244
+ prob = 65 if avg_weekly > 0 and macd > 0 else 45 if avg_weekly < 0 else 50
245
+ direction = "up" if avg_weekly > 0 else "down"
246
+ pct = abs(avg_weekly) * 3
247
+ return f"{prob}% chance of {pct:.1f}% {direction} over the next 1-4 weeks (swing trading)."
248
+
249
+ elif horizon == "momentum":
250
+ # Formula: Momentum over weeks
251
+ prob = 70 if macd > 0 and rsi < 60 else 45
252
+ direction = "continued rise" if macd > 0 else "fall"
253
+ pct = 4.0
254
+ return f"{prob}% chance of {pct:.1f}% {direction} over the next 2-4 weeks (momentum trading) based on MACD ({macd:.2f})."
255
+
256
+ else: # long_term
257
+ # Formula: Monthly changes for months/years
258
+ avg_monthly = close.tail(60).pct_change().mean() * 100 if len(close) >= 60 else 0
259
+ prob = 70 if avg_monthly > 1 and macd > 0 else 40 if avg_monthly < 0 else 50
260
+ direction = "growth" if avg_monthly > 0 else "decline"
261
+ pct = abs(avg_monthly) * 2
262
+ sentiment_boost = 1.15 if news_sentiment == 'positive' else 0.85 if news_sentiment == 'negative' else 1.0
263
+ prob = min(95, int(prob * sentiment_boost))
264
+ return f"{prob}% chance of {pct:.1f}% {direction} over months (position trading) based on {avg_monthly:.1f}% avg monthly change and {news_sentiment} news."
265
+
266
+ def hunter_agent(state: TraderState) -> TraderState:
267
+ try:
268
+ symbol = state['input_symbol']
269
+ action = state['action']
270
+ horizon = state['horizon']
271
+
272
+ if state.get('raw_data') and state['raw_data'].get('hist') is not None:
273
+ hist = state['raw_data']['hist']
274
+ else:
275
+ hist = fetch_data(symbol, horizon)
276
+ if state.get('raw_data') is None:
277
+ state['raw_data'] = {}
278
+ state['raw_data']['hist'] = hist
279
+
280
+ indicators = compute_indicators(hist, horizon)
281
+ news_data = fetch_news_sentiment(symbol)
282
+ headlines = news_data['headlines']
283
+ news_sentiment = news_data['sentiment']
284
+
285
+ features_summary = summarize_features(indicators, hist, headlines, action, symbol, news_sentiment)
286
+ prediction = predict(horizon, indicators, hist, headlines, news_sentiment)
287
+ holding_metrics = calculate_holding_metrics(hist, state.get('holding_period'), indicators.get('current_price', 0))
288
+
289
+ state['raw_data']['indicators'] = indicators
290
+ state['raw_data']['news'] = headlines
291
+ state['raw_data']['features'] = features_summary
292
+ state['raw_data']['prediction'] = prediction
293
+ state['raw_data']['holding'] = holding_metrics
294
+ state['raw_data']['news_sentiment'] = news_sentiment
295
+
296
+ if holding_metrics:
297
+ features_summary += f"; Holding: {holding_metrics['days_held']} days, {holding_metrics['holding_return']:.1f}% {holding_metrics['pnl']}"
298
+
299
+ rsi = indicators.get('rsi', 50)
300
+ macd = indicators.get('macd', 0)
301
+
302
+ if action == "buy":
303
+ if rsi < 30 and macd > 0:
304
+ state['claim'] = f"Strong buy signal: Oversold RSI ({rsi:.1f}) with bullish MACD ({macd:.2f}) indicates reversal."
305
+ elif rsi < 40 or macd > 5:
306
+ state['claim'] = f"Moderate buy signal: RSI at {rsi:.1f}, MACD at {macd:.2f} shows positive momentum."
307
+ else:
308
+ state['claim'] = f"Weak buy signal: RSI ({rsi:.1f}) and MACD ({macd:.2f}) don't strongly support buying now."
309
+ else:
310
+ if rsi > 70 or macd < -5:
311
+ state['claim'] = f"Strong sell signal: Overbought RSI ({rsi:.1f}) or weak MACD ({macd:.2f}) suggests taking profits."
312
+ elif rsi > 60 or macd < 0:
313
+ state['claim'] = f"Moderate sell signal: RSI at {rsi:.1f}, MACD at {macd:.2f} shows weakening momentum."
314
+ else:
315
+ state['claim'] = f"Weak sell signal: RSI ({rsi:.1f}) and MACD ({macd:.2f}) don't strongly support selling now."
316
+
317
+ return state
318
+ except Exception as e:
319
+ print(f"Error in hunter_agent: {e}")
320
+ return state # Always return state
321
+
322
+ def skeptic_agent(state: TraderState) -> TraderState:
323
+ try:
324
+ # Use cached raw_data
325
+ raw_data = state.get('raw_data', {}) or {}
326
+ features_summary = raw_data.get("features", "No features available.")
327
+ action = state['action']
328
+ prompt = f"Counter claim '{state.get('claim', '')}' for {action}: {features_summary}. Provide 1 specific risk."
329
+ try:
330
+ state['skepticism'] = llm.invoke(prompt).content.strip()
331
+ except:
332
+ rsi = raw_data.get("indicators", {}).get('rsi', 50)
333
+ state['skepticism'] = f"Risk: RSI at {rsi} indicates potential reversal."
334
+ return state
335
+ except Exception as e:
336
+ print(f"Error in skeptic_agent: {e}")
337
+ return state # Always return state
338
+
339
+ def calibrator_agent(state: TraderState) -> TraderState:
340
+ try:
341
+ raw_data = state.get('raw_data', {}) or {}
342
+ indicators = raw_data.get("indicators", {})
343
+ action = state['action']
344
+ news_sentiment = raw_data.get('news_sentiment', 'neutral')
345
+ holding = raw_data.get('holding', {})
346
+
347
+ state['iterations'] = state.get('iterations', 0) + 1
348
+
349
+ rsi = indicators.get('rsi', 50)
350
+ macd = indicators.get('macd', 0)
351
+
352
+ base = 50
353
+
354
+ if action == "buy":
355
+ if rsi < 30:
356
+ base += 30
357
+ elif rsi < 40:
358
+ base += 15
359
+ elif rsi > 70:
360
+ base -= 25
361
+ elif rsi > 60:
362
+ base -= 10
363
+
364
+ if macd > 5:
365
+ base += 20
366
+ elif macd > 0:
367
+ base += 10
368
+ elif macd < -5:
369
+ base -= 20
370
+ elif macd < 0:
371
+ base -= 10
372
+
373
+ if news_sentiment == 'positive':
374
+ base += 15
375
+ elif news_sentiment == 'negative':
376
+ base -= 15
377
+
378
+ else:
379
+ if rsi > 70:
380
+ base += 30
381
+ elif rsi > 60:
382
+ base += 15
383
+ elif rsi < 30:
384
+ base -= 25
385
+ elif rsi < 40:
386
+ base -= 10
387
+
388
+ if macd < -5:
389
+ base += 20
390
+ elif macd < 0:
391
+ base += 10
392
+ elif macd > 5:
393
+ base -= 20
394
+ elif macd > 0:
395
+ base -= 10
396
+
397
+ if news_sentiment == 'negative':
398
+ base += 15
399
+ elif news_sentiment == 'positive':
400
+ base -= 15
401
+
402
+ if holding:
403
+ pnl_pct = holding.get('holding_return', 0)
404
+ if action == "sell":
405
+ if pnl_pct > 10:
406
+ base += 15
407
+ elif pnl_pct > 5:
408
+ base += 10
409
+ elif pnl_pct < -10:
410
+ base += 20
411
+ elif pnl_pct < -5:
412
+ base += 10
413
+
414
+ state['confidence'] = min(95, max(5, base))
415
+ state['stop'] = state['iterations'] >= 5 or state['confidence'] >= 85 or state['confidence'] <= 15
416
+
417
+ return state
418
+ except Exception as e:
419
+ print(f"Error in calibrator_agent: {e}")
420
+ return state
graph.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, END
2
+ from agents import hunter_agent, skeptic_agent, calibrator_agent
3
+ from state import TraderState
4
+
5
+
6
+ graph = StateGraph(TraderState)
7
+
8
+ graph.add_node("hunter", hunter_agent)
9
+ graph.add_node("skeptic", skeptic_agent)
10
+ graph.add_node("calibrator", calibrator_agent)
11
+
12
+ # hunter -> skeptic -> calibrator
13
+ graph.add_edge("hunter", "skeptic")
14
+ graph.add_edge("skeptic", "calibrator")
15
+
16
+ # Conditional edge from calibrator: If stop is True that means it end and go to END; else it will loop back to hunter
17
+ def should_continue(state: TraderState) -> str:
18
+ return END if state.get('stop', False) else "hunter"
19
+
20
+ graph.add_conditional_edges("calibrator", should_continue)
21
+
22
+ graph.set_entry_point("hunter")
23
+
24
+ compiled_graph = graph.compile()
main.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from state import TraderState
2
+ from agents import hunter_agent, skeptic_agent, calibrator_agent, fetch_data
3
+ from dotenv import load_dotenv
4
+ from langchain_openai import ChatOpenAI
5
+ import os
6
+ import re
7
+ import json
8
+ import pytz
9
+ from datetime import datetime, timedelta
10
+ import yfinance as yf
11
+
12
+ load_dotenv()
13
+ os.environ["OPENAI_API_KEY"] = os.getenv("OPENROUTER_API_KEY", "")
14
+ llm_parser = ChatOpenAI(model="openai/gpt-3.5-turbo", openai_api_base="https://openrouter.ai/api/v1", temperature=0.0)
15
+
16
+ def resolve_symbol(user_input: str) -> str:
17
+ """
18
+ Handles Indian stocks (.NS/.BO).
19
+ """
20
+ prompt = f"""
21
+ Analyze this user query: "{user_input}".
22
+ - Extract the company/brand name.
23
+ - Infer the likely stock market.
24
+ - Suggest the stock ticker symbol with the appropriate exchange suffix
25
+ (e.g., "GOOGL" for US, "TCS.NS" for NSE India).
26
+ - If it is an Indian stock, it MUST end with ".NS" or ".BO".
27
+ - Respond only with the symbol. If unsure, say "UNKNOWN".
28
+ """
29
+ try:
30
+ suggested_symbol = llm_parser.invoke(prompt).content.strip().upper()
31
+ if suggested_symbol == "UNKNOWN":
32
+ return None
33
+
34
+ if not (suggested_symbol.endswith(".NS") or suggested_symbol.endswith(".BO")):
35
+ is_india_prompt = f"Is the stock/company '{suggested_symbol}' from the query '{user_input}' traded on Indian exchanges? Answer only YES or NO."
36
+ is_india = llm_parser.invoke(is_india_prompt).content.strip().upper()
37
+
38
+ if "YES" in is_india:
39
+ suggested_symbol += ".NS"
40
+
41
+ # Try NSE, fallback to BSE
42
+ ticker = yf.Ticker(suggested_symbol)
43
+
44
+ if not ticker.history(period="1d").empty:
45
+ return suggested_symbol
46
+
47
+ # If NSE (.NS) failed, it might be a BSE-only stock
48
+ if suggested_symbol.endswith(".NS"):
49
+ bse_symbol = suggested_symbol.replace(".NS", ".BO")
50
+ if not yf.Ticker(bse_symbol).history(period="1d").empty:
51
+ return bse_symbol
52
+ # Fallback
53
+ refine_prompt = f"The symbol '{suggested_symbol}' didn't validate. Suggest the EXACT ticker for: '{user_input}'. Respond ONLY with the ticker."
54
+ refined_symbol = llm_parser.invoke(refine_prompt).content.strip().upper()
55
+
56
+ # The same suffix logic
57
+ if not (refined_symbol.endswith(".NS") or refined_symbol.endswith(".BO")):
58
+ if "YES" in is_india:
59
+ refined_symbol += ".NS"
60
+
61
+ if not yf.Ticker(refined_symbol).history(period="1d").empty:
62
+ return refined_symbol
63
+
64
+ except Exception as e:
65
+ print(f"Symbol resolution error: {e}")
66
+ return None
67
+
68
+
69
+ def parse_query(user_input: str) -> tuple:
70
+ """FIXED: Proper horizon detection based on correct trading logic"""
71
+ prompt = f"""
72
+ Analyze this user query: "{user_input}".
73
+
74
+ TRADING HORIZONS (CRITICAL):
75
+ - "intraday": same day buy and same day sell (hours)
76
+ - "scalping": ultra-short (minutes)
77
+ - "swing": buy this week, sell next week (1-4 weeks)
78
+ - "momentum": trend-following over weeks (2-4 weeks)
79
+ - "long_term": months to years (position trading, e.g., "bought 1 month ago")
80
+
81
+ Extract:
82
+ - stock symbol (e.g., "HINDUNILVR.NS")
83
+ - action: "buy" or "sell" (default "buy")
84
+ - horizon: match to above definitions
85
+ - date: "today", "tomorrow", or future (e.g., "after 2 weeks")
86
+ - holding_period: If mentioned (e.g., "1 week ago", "a week ago"), provide as "X unit ago". Else, None.
87
+
88
+ CRITICAL: If user says "bought X time ago" → horizon = "long_term"
89
+ If user says "tomorrow" without history → horizon = "intraday" (same-day strategy for next day)
90
+
91
+ Respond ONLY in JSON: {{"symbol": "HINDUNILVR.NS", "action": "sell", "horizon": "long_term", "date": "2025-12-27", "holding_period": "1 week ago"}}. Respond sholud be clean.
92
+ """
93
+ try:
94
+ response = llm_parser.invoke(prompt).content.strip()
95
+ parsed = json.loads(response)
96
+ symbol = parsed.get("symbol")
97
+ action = parsed.get("action", "buy")
98
+ horizon = parsed.get("horizon", "intraday")
99
+ date_str = parsed.get("date")
100
+ holding_period = parsed.get("holding_period")
101
+
102
+ # Override horizon detection with explicit logic
103
+ if "ago" in user_input.lower() or "bought" in user_input.lower() or "purchased" in user_input.lower():
104
+ horizon = "long_term"
105
+ elif "tomorrow" in user_input.lower() and "ago" not in user_input.lower():
106
+ horizon = "intraday"
107
+ elif "week" in user_input.lower() and "ago" not in user_input.lower():
108
+ horizon = "swing"
109
+ elif "minute" in user_input.lower() or "scalp" in user_input.lower():
110
+ horizon = "scalping"
111
+ elif "month" in user_input.lower() and "ago" in user_input.lower():
112
+ horizon = "long_term"
113
+
114
+ ist = pytz.timezone('Asia/Kolkata')
115
+ now_ist = datetime.now(ist)
116
+ date = None
117
+ if date_str == "today":
118
+ date = now_ist.strftime("%Y-%m-%d")
119
+ elif date_str == "tomorrow":
120
+ date = (now_ist + timedelta(days=1)).strftime("%Y-%m-%d")
121
+ elif date_str and "after" in user_input.lower():
122
+ match = re.search(r'after\s+(\d+)\s*(week|month)', user_input.lower())
123
+ if match:
124
+ num, unit = int(match.group(1)), match.group(2)
125
+ days = num * 7 if unit == "week" else num * 30
126
+ date = (now_ist + timedelta(days=days)).strftime("%Y-%m-%d")
127
+
128
+ return symbol, date, action, horizon, holding_period
129
+ except Exception as e:
130
+ print(f"Parsing error: {e}. Fallback.")
131
+ symbol = resolve_symbol(user_input)
132
+ action = "sell" if "sell" in user_input.lower() else "buy"
133
+
134
+ # CRITICAL FIX: Correct horizon fallback
135
+ if "ago" in user_input.lower() or "month" in user_input.lower() or "bought" in user_input.lower():
136
+ horizon = "long_term"
137
+ elif "week" in user_input.lower() and "ago" not in user_input.lower():
138
+ horizon = "swing"
139
+ elif "minute" in user_input.lower():
140
+ horizon = "scalping"
141
+ elif "tomorrow" in user_input.lower():
142
+ horizon = "intraday"
143
+ else:
144
+ horizon = "intraday"
145
+
146
+ ist = pytz.timezone('Asia/Kolkata')
147
+ now_ist = datetime.now(ist)
148
+ date = now_ist.strftime("%Y-%m-%d") if "today" in user_input.lower() else (now_ist + timedelta(days=1)).strftime("%Y-%m-%d") if "tomorrow" in user_input.lower() else None
149
+
150
+ match = re.search(r'(\d+)\s*(week|month|day)\s*ago', user_input.lower())
151
+ holding_period = f"{match.group(1)} {match.group(2)} ago" if match else None
152
+
153
+ return symbol, date, action, horizon, holding_period
154
+
155
+ def generate_alert_message(state: TraderState, symbol: str, action: str, horizon: str) -> str:
156
+ confidence = state['confidence']
157
+ raw_data = state.get('raw_data', {}) or {}
158
+ indicators = raw_data.get("indicators", {})
159
+ news = raw_data.get("news", [])
160
+ prediction = raw_data.get("prediction", "No prediction available.")
161
+ holding = raw_data.get("holding", {})
162
+ claim = state.get('claim', 'No claim.')
163
+ skepticism = state.get('skepticism', 'No skepticism.')
164
+ features = raw_data.get("features", "")
165
+
166
+ rsi = indicators.get('rsi', 50)
167
+ macd = indicators.get('macd', 0)
168
+ current_price = indicators.get('current_price', 1)
169
+
170
+ volatility = 0
171
+ if current_price > 0:
172
+ bb_upper = indicators.get('bb_upper', current_price * 1.1)
173
+ bb_lower = indicators.get('bb_lower', current_price * 0.9)
174
+ volatility = (bb_upper - bb_lower) / current_price * 100
175
+
176
+ risk_adjustment = min(20, volatility / 5) if volatility > 0 else 0
177
+
178
+ if holding:
179
+ pnl_adjustment = 10 if holding.get('holding_return', 0) > 5 else -10 if holding.get('holding_return', 0) < -5 else 0
180
+ allocation = max(0, min(100, confidence - risk_adjustment + pnl_adjustment))
181
+ else:
182
+ allocation = max(0, min(100, confidence - risk_adjustment))
183
+
184
+ if indicators.get("error") == "No data available":
185
+ return f" No Data Available\nSorry, we couldn't fetch reliable data for {symbol}. Check official sources like NSE or Yahoo Finance. Try searching for '{symbol}.NS' if it's an Indian stock."
186
+
187
+ # HORIZON-AWARE best date suggestion
188
+ horizon_desc = {
189
+ "intraday": "same-day trading (hours)",
190
+ "scalping": "ultra-short (minutes)",
191
+ "swing": "1-4 weeks",
192
+ "momentum": "2-4 weeks",
193
+ "long_term": "months to years"
194
+ }
195
+
196
+ best_date_prompt = f"""
197
+ Based on prediction: '{prediction}', indicators: RSI {rsi:.1f}, MACD {macd:.2f}, trend: {features.split(';')[0] if features else 'N/A'}.
198
+ User query involves '{action}' for '{horizon}' ({horizon_desc.get(horizon, horizon)}).
199
+ Suggest the SPECIFIC best date/time to {action} {symbol} for maximum benefit.
200
+ - For intraday/scalping: suggest time of day (e.g., "Sell tomorrow at 10:30 AM IST for 1.5% gain")
201
+ - For swing/momentum: suggest specific date within the horizon (e.g., "Buy on Jan 2, 2026")
202
+ - For long_term: suggest holding duration (e.g., "Hold until Feb 2026 for 8% gain")
203
+ Include % potential gain/loss. Be accurate and align with user intent.
204
+ """
205
+ try:
206
+ best_date = llm_parser.invoke(best_date_prompt).content.strip()
207
+ except:
208
+ best_date = f"Based on trends, {action} within {horizon_desc.get(horizon, horizon)} for potential 1-3% {'gain' if action == 'buy' else 'exit'}."
209
+
210
+ why_prompt = f"""
211
+ Based on claim: '{claim}', skepticism: '{skepticism}', features: '{features}', prediction: '{prediction}', holding: '{holding}'.
212
+ RSI is {rsi:.1f} (low = oversold/buy signal, high = overbought/sell signal). MACD is {macd:.2f} (positive = bullish, negative = bearish).
213
+ Explain why the signal is Green/Yellow/Red for {action} {symbol} ({horizon} = {horizon_desc.get(horizon, horizon)}).
214
+ Use layman terms, reference indicators accurately, tie to data. Be genuine—align with RSI/MACD signals.
215
+ """
216
+ try:
217
+ why = llm_parser.invoke(why_prompt).content.strip()
218
+ except:
219
+ why = f"Based on RSI ({rsi:.1f}) and MACD ({macd:.2f}), conditions are {'favorable' if confidence > 60 else 'mixed' if confidence > 40 else 'unfavorable'} for {action}."
220
+
221
+ if confidence >= 70:
222
+ color = " Green: 'Yes, Go Ahead!'"
223
+ should_action = f"Yes, {action} now. Allocate {allocation:.0f}% as signals are strong."
224
+ elif confidence >= 40:
225
+ color = " Yellow: 'Wait and Watch'"
226
+ should_action = f"Maybe, monitor closely. Allocate {allocation:.0f}% cautiously due to mixed signals and potential risk."
227
+ else:
228
+ color = " Red: 'No, Stop!'"
229
+ should_action = f"No, avoid {action}. Signals are weak; wait for better conditions."
230
+
231
+ holding_summary = ""
232
+ if holding:
233
+ h_return = holding['holding_return']
234
+ holding_summary = f"\n**Holding Summary**: Purchased {holding['days_held']} days ago at ₹{holding['purchase_price']:.2f}, current: ₹{holding.get('current_price', current_price):.2f}, return: {h_return:.1f}% ({holding['pnl']})."
235
+
236
+
237
+ if h_return > 0:
238
+ advice = "lock in gains"
239
+ elif h_return < 0:
240
+ advice = "wait for recovery or cut losses"
241
+ else:
242
+ advice = "exit at breakeven"
243
+
244
+ should_action += f" Based on {h_return:.1f}% {holding['pnl']}, {advice}."
245
+
246
+ report = f"""
247
+ --- AI Trader Analyzer Report ---
248
+ Symbol: {symbol}
249
+ **Current Price**: {current_price:,.4f}
250
+ Action: {action.capitalize()} ({horizon.replace('_', '-').capitalize()} = {horizon_desc.get(horizon, horizon)}) "/n"
251
+ Date: {state.get('query_date', 'N/A')} (IST)
252
+ Signal: {color}
253
+ Confidence: {confidence}%
254
+ Key Metrics: RSI {rsi:.1f}, MACD {macd:.2f}, Trend {features.split(';')[0] if features else 'N/A'}
255
+ {holding_summary}
256
+ Should I {action.capitalize()}?** {should_action}
257
+ Best Date/Time to {action.capitalize()}**: {best_date}
258
+ Why? {why}
259
+ Prediction: {prediction}
260
+ Recommendation: Allocate {allocation:.0f}% based on analysis. Not financial advice.
261
+ """
262
+ return report.strip()
263
+
264
+ def main():
265
+ symbol, date, action, horizon, holding_period = None, None, None, None, None
266
+ while not symbol:
267
+ user_input = input("Describe the stock (e.g., 'I want to buy Apple stock for today' or 'I purchased TCS.NS stock 1 month ago, now sell today'): ").strip()
268
+ symbol, date, action, horizon, holding_period = parse_query(user_input)
269
+ if not symbol:
270
+ print("No symbol detected. Try again.")
271
+
272
+ print(f"Analyzing: {symbol} on {date or 'recent'} (IST) for {action} ({horizon})" + (f", holding: {holding_period}" if holding_period else ""))
273
+
274
+ # Fetch data ONCE before the loop
275
+ hist = fetch_data(symbol, horizon)
276
+
277
+ state = TraderState(
278
+ input_symbol=symbol,
279
+ query_date=date,
280
+ action=action,
281
+ horizon=horizon,
282
+ holding_period=holding_period,
283
+ # Pre-load data
284
+ raw_data={'hist': hist},
285
+ claim=None,
286
+ skepticism=None,
287
+ confidence=50,
288
+ iterations=0,
289
+ stop=False,
290
+ alert_message=None
291
+ )
292
+
293
+ max_iterations = 5
294
+ for _ in range(max_iterations):
295
+ state = hunter_agent(state)
296
+ state = skeptic_agent(state)
297
+ state = calibrator_agent(state)
298
+ if state['stop']:
299
+ break
300
+
301
+ state['alert_message'] = generate_alert_message(state, symbol, action, horizon)
302
+
303
+ result = {
304
+ "symbol": symbol,
305
+ "date": date,
306
+ "action": action,
307
+ "horizon": horizon,
308
+ "holding_period": holding_period,
309
+ "alert_message": state['alert_message'],
310
+ "iterations": state['iterations'],
311
+ "confidence": state['confidence']
312
+ }
313
+
314
+ print("\n--- AI Trader Analyzer Result ---")
315
+ print(f"Symbol: {result['symbol']}")
316
+ print(f"Date: {result['date']} (IST)")
317
+ print(f"Action: {result['action']}")
318
+ print(f"Horizon: {result['horizon']}")
319
+ print(f"Holding: {result['holding_period'] or 'None'}")
320
+ print(f"Message:\n{result['alert_message']}")
321
+ print(f"Iterations: {result['iterations']}")
322
+ print(f"Confidence: {result['confidence']}%")
323
+
324
+ if __name__ == "__main__":
325
+ main()
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ openai
2
+ langchain
3
+ langgraph
4
+ yfinance
5
+ requests
6
+ uvicorn
7
+ matplotlib
8
+ pydantic
9
+ beautifulsoup4
10
+ python-dotenv
11
+ numpy
12
+ pytz
13
+ weasyprint
14
+ taipy
15
+ fpdf2
16
+ streamlit
17
+ fastapi
state.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TypedDict, Optional, List, Dict, Any
2
+
3
+ class TraderState(TypedDict):
4
+ input_symbol: str
5
+ query_date: Optional[str]
6
+ # 'buy' or 'sell'
7
+ action: str
8
+ # 'intraday', 'scalping', 'swing', 'momentum', 'long_term'
9
+ horizon: str
10
+ # "1 month ago", "2 month ago",etc.
11
+ holding_period: Optional[str]
12
+ raw_data: Optional[Dict[str, Any]]
13
+ claim: Optional[str]
14
+ skepticism: Optional[str]
15
+ confidence: int
16
+ iterations: int
17
+ stop: bool
18
+ alert_message: Optional[str]
style.css ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Make Comparison/Analysis subheaders white and bold */
2
+ h3 {
3
+ color: white !important;
4
+ font-size: 1.8rem !important;
5
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 1);
6
+ margin-top: 30px !important;
7
+ }
8
+
9
+ /* Ensure stock names and "Analysis:" text stay white */
10
+ .stMarkdown h3, .stMarkdown b, .stMarkdown strong {
11
+ color: white !important;
12
+ }
13
+
14
+ /* Your existing white-box result style stays for the report itself */
15
+ [data-testid="stText"] {
16
+ background-color: white !important;
17
+ color: black !important;
18
+ padding: 15px !important;
19
+ border-radius: 10px !important;
20
+ }
21
+
22
+ /* Target the result alert blocks specifically */
23
+ [data-testid="stText"] {
24
+ background-color: white !important;
25
+ color: black !important;
26
+ padding: 15px !important;
27
+ border-radius: 10px !important;
28
+ border: 1px solid #ccc !important;
29
+ }
30
+
31
+ /* Ensure the preformatted text inside the block is also black */
32
+ [data-testid="stText"] pre {
33
+ color: black !important;
34
+ }
35
+
36
+ .block-container {
37
+ padding-top: 1rem !important;
38
+ }
39
+
40
+ h1 {
41
+ margin-top: -20px !important;
42
+ margin-bottom: 90px !important;
43
+ color: white !important;
44
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
45
+ text-align: center;
46
+ }
47
+
48
+ .stButton button:hover {
49
+ background-color: transparent !important;
50
+ color: white !important;
51
+ border: 1px solid white;
52
+ }
53
+
54
+ .block-container {
55
+ padding-top: 1rem !important;
56
+ padding-bottom: 0rem;
57
+ }
58
+
59
+ /* Specifically target the heading to remove any extra margin */
60
+ h1 {
61
+ margin-top: -20px !important;
62
+ padding-top: 0px !important;
63
+ margin-bottom: 90px !important;
64
+ color: white !important;
65
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
66
+ text-align: center;
67
+ }
68
+
69
+ /* Ensures the header bar doesn't block your title */
70
+ header.stAppHeader {
71
+ background-color: transparent;
72
+ }
73
+
74
+ /* Target all Streamlit buttons */
75
+ .stButton button {
76
+ background-color: #0e3558;
77
+ color: #FFFFFF;
78
+ border: 1px solid #FFFFFF;
79
+ border-radius: 25px;
80
+ transition: background-color 0.3s ease, color 0.3s ease;
81
+ }
82
+
83
+
84
+ .stButton button:hover {
85
+ background-color: transparent !important;
86
+ color: white !important;
87
+ border: 1px solid white;
88
+ }
89
+
90
+
91
+ .stButton button:focus {
92
+ color: white !important;
93
+ }
94
+ /* Fix the Title (st.title) visibility */
95
+ h1 {
96
+ color: white !important;
97
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
98
+ text-align: center;
99
+ }
100
+
101
+ /* Fix the Label for the text input box */
102
+ .stTextInput label {
103
+ color: white !important;
104
+ font-size: 1.2rem;
105
+ font-weight: bold;
106
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
107
+ }
108
+
109
+ .stTextInput input {
110
+ color: #000000; /* Keeping input text black for clarity against white box */
111
+ border-radius: 8px; /* Your 'br' fixing */
112
+ }
113
+
114
+ /* Target the main container for a full-screen background */
115
+
116
+ [data-testid="stAppViewContainer"] {
117
+ background-image: url("data:image/jpg;base64,IMAGE_PLACEHOLDER");
118
+ background-size: cover;
119
+ background-position: center;
120
+ background-attachment: fixed;
121
+ }
122
+
123
+ /* Fix your alignment and borders (br) here */
124
+ .main {
125
+ background-color: transparent;
126
+ padding: 2rem;
127
+ }
128
+
129
+ /* Centered container for your title */
130
+ .centered-title-container {
131
+ text-align: center;
132
+ border-radius: 12px;
133
+ background-color: rgba(0, 0, 0, 0.5);
134
+ padding: 20px;
135
+ margin-bottom: 25px;
136
+ }
137
+
138
+ [data-testid="stSidebar"] {
139
+ background-color: rgba(38, 39, 48, 0.9);
140
+ }