Spaces:
Sleeping
Sleeping
| from state import TraderState | |
| from agents import hunter_agent, skeptic_agent, calibrator_agent, fetch_data | |
| from dotenv import load_dotenv | |
| from langchain_openai import ChatOpenAI | |
| from langchain_groq import ChatGroq | |
| import os | |
| import re | |
| import json | |
| import pytz | |
| from datetime import datetime, timedelta | |
| import yfinance as yf | |
| load_dotenv() | |
| # os.environ["OPENAI_API_KEY"] = os.getenv("OPENROUTER_API_KEY", "") | |
| # llm_parser = ChatOpenAI(model="openai/gpt-3.5-turbo", openai_api_base="https://openrouter.ai/api/v1", temperature=0.0) | |
| llm_parser = ChatGroq(model = "moonshotai/kimi-k2-instruct-0905",api_key=os.getenv("GROQ_API_KEY"), temperature=0.0) | |
| def resolve_symbol(user_input: str) -> str: | |
| """ | |
| Handles Indian stocks (.NS/.BO). | |
| """ | |
| prompt = f""" | |
| Analyze this user query: "{user_input}". | |
| - Extract the company/brand name. | |
| - Infer the likely stock market. | |
| - Suggest the stock ticker symbol with the appropriate exchange suffix | |
| (e.g., "GOOGL" for US, "TCS.NS" for NSE India). | |
| - If it is an Indian stock, it MUST end with ".NS" or ".BO". | |
| - Respond only with the symbol. If unsure, say "UNKNOWN". | |
| """ | |
| try: | |
| suggested_symbol = llm_parser.invoke(prompt).content.strip().upper() | |
| if suggested_symbol == "UNKNOWN": | |
| return None | |
| if not (suggested_symbol.endswith(".NS") or suggested_symbol.endswith(".BO")): | |
| is_india_prompt = f"Is the stock/company '{suggested_symbol}' from the query '{user_input}' traded on Indian exchanges? Answer only YES or NO." | |
| is_india = llm_parser.invoke(is_india_prompt).content.strip().upper() | |
| if "YES" in is_india: | |
| suggested_symbol += ".NS" | |
| # Try NSE, fallback to BSE | |
| ticker = yf.Ticker(suggested_symbol) | |
| if not ticker.history(period="1d").empty: | |
| return suggested_symbol | |
| # If NSE (.NS) failed, it might be a BSE-only stock | |
| if suggested_symbol.endswith(".NS"): | |
| bse_symbol = suggested_symbol.replace(".NS", ".BO") | |
| if not yf.Ticker(bse_symbol).history(period="1d").empty: | |
| return bse_symbol | |
| # Fallback | |
| refine_prompt = f"The symbol '{suggested_symbol}' didn't validate. Suggest the EXACT ticker for: '{user_input}'. Respond ONLY with the ticker." | |
| refined_symbol = llm_parser.invoke(refine_prompt).content.strip().upper() | |
| # The same suffix logic | |
| if not (refined_symbol.endswith(".NS") or refined_symbol.endswith(".BO")): | |
| if "YES" in is_india: | |
| refined_symbol += ".NS" | |
| if not yf.Ticker(refined_symbol).history(period="1d").empty: | |
| return refined_symbol | |
| except Exception as e: | |
| print(f"Symbol resolution error: {e}") | |
| return None | |
| def parse_query(user_input: str) -> tuple: | |
| """FIXED: Proper horizon detection based on correct trading logic""" | |
| prompt = f""" | |
| Analyze this user query: "{user_input}". | |
| TRADING HORIZONS (CRITICAL): | |
| - "intraday": same day buy and same day sell (hours) | |
| - "scalping": ultra-short (minutes) | |
| - "swing": buy this week, sell next week (1-4 weeks) | |
| - "momentum": trend-following over weeks (2-4 weeks) | |
| - "long_term": months to years (position trading, e.g., "bought 1 month ago") | |
| Extract: | |
| - stock symbol (e.g., "HINDUNILVR.NS") | |
| - action: "buy" or "sell" (default "buy") | |
| - horizon: match to above definitions | |
| - date: "today", "tomorrow", or future (e.g., "after 2 weeks") | |
| - holding_period: If mentioned (e.g., "1 week ago", "a week ago"), provide as "X unit ago". Else, None. | |
| CRITICAL: If user says "bought X time ago" → horizon = "long_term" | |
| If user says "tomorrow" without history → horizon = "intraday" (same-day strategy for next day) | |
| 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. | |
| """ | |
| try: | |
| response = llm_parser.invoke(prompt).content.strip() | |
| parsed = json.loads(response) | |
| symbol = parsed.get("symbol") | |
| action = parsed.get("action", "buy") | |
| horizon = parsed.get("horizon", "intraday") | |
| date_str = parsed.get("date") | |
| holding_period = parsed.get("holding_period") | |
| # Override horizon detection with explicit logic | |
| if "ago" in user_input.lower() or "bought" in user_input.lower() or "purchased" in user_input.lower(): | |
| horizon = "long_term" | |
| elif "tomorrow" in user_input.lower() and "ago" not in user_input.lower(): | |
| horizon = "intraday" | |
| elif "week" in user_input.lower() and "ago" not in user_input.lower(): | |
| horizon = "swing" | |
| elif "minute" in user_input.lower() or "scalp" in user_input.lower(): | |
| horizon = "scalping" | |
| elif "month" in user_input.lower() and "ago" in user_input.lower(): | |
| horizon = "long_term" | |
| ist = pytz.timezone('Asia/Kolkata') | |
| now_ist = datetime.now(ist) | |
| date = None | |
| if date_str == "today": | |
| date = now_ist.strftime("%Y-%m-%d") | |
| elif date_str == "tomorrow": | |
| date = (now_ist + timedelta(days=1)).strftime("%Y-%m-%d") | |
| elif date_str and "after" in user_input.lower(): | |
| match = re.search(r'after\s+(\d+)\s*(week|month)', user_input.lower()) | |
| if match: | |
| num, unit = int(match.group(1)), match.group(2) | |
| days = num * 7 if unit == "week" else num * 30 | |
| date = (now_ist + timedelta(days=days)).strftime("%Y-%m-%d") | |
| return symbol, date, action, horizon, holding_period | |
| except Exception as e: | |
| print(f"Parsing error: {e}. Fallback.") | |
| symbol = resolve_symbol(user_input) | |
| action = "sell" if "sell" in user_input.lower() else "buy" | |
| # CRITICAL FIX: Correct horizon fallback | |
| if "ago" in user_input.lower() or "month" in user_input.lower() or "bought" in user_input.lower(): | |
| horizon = "long_term" | |
| elif "week" in user_input.lower() and "ago" not in user_input.lower(): | |
| horizon = "swing" | |
| elif "minute" in user_input.lower(): | |
| horizon = "scalping" | |
| elif "tomorrow" in user_input.lower(): | |
| horizon = "intraday" | |
| else: | |
| horizon = "intraday" | |
| ist = pytz.timezone('Asia/Kolkata') | |
| now_ist = datetime.now(ist) | |
| 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 | |
| match = re.search(r'(\d+)\s*(week|month|day)\s*ago', user_input.lower()) | |
| holding_period = f"{match.group(1)} {match.group(2)} ago" if match else None | |
| return symbol, date, action, horizon, holding_period | |
| def generate_alert_message(state: TraderState, symbol: str, action: str, horizon: str) -> str: | |
| confidence = state['confidence'] | |
| raw_data = state.get('raw_data', {}) or {} | |
| indicators = raw_data.get("indicators", {}) | |
| news = raw_data.get("news", []) | |
| prediction = raw_data.get("prediction", "No prediction available.") | |
| holding = raw_data.get("holding", {}) | |
| claim = state.get('claim', 'No claim.') | |
| skepticism = state.get('skepticism', 'No skepticism.') | |
| features = raw_data.get("features", "") | |
| rsi = indicators.get('rsi', 50) | |
| macd = indicators.get('macd', 0) | |
| current_price = indicators.get('current_price', 1) | |
| volatility = 0 | |
| if current_price > 0: | |
| bb_upper = indicators.get('bb_upper', current_price * 1.1) | |
| bb_lower = indicators.get('bb_lower', current_price * 0.9) | |
| volatility = (bb_upper - bb_lower) / current_price * 100 | |
| risk_adjustment = min(20, volatility / 5) if volatility > 0 else 0 | |
| if holding: | |
| pnl_adjustment = 10 if holding.get('holding_return', 0) > 5 else -10 if holding.get('holding_return', 0) < -5 else 0 | |
| allocation = max(0, min(100, confidence - risk_adjustment + pnl_adjustment)) | |
| else: | |
| allocation = max(0, min(100, confidence - risk_adjustment)) | |
| if indicators.get("error") == "No data available": | |
| 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." | |
| # HORIZON-AWARE best date suggestion | |
| horizon_desc = { | |
| "intraday": "same-day trading (hours)", | |
| "scalping": "ultra-short (minutes)", | |
| "swing": "1-4 weeks", | |
| "momentum": "2-4 weeks", | |
| "long_term": "months to years" | |
| } | |
| best_date_prompt = f""" | |
| Based on prediction: '{prediction}', indicators: RSI {rsi:.1f}, MACD {macd:.2f}, trend: {features.split(';')[0] if features else 'N/A'}. | |
| User query involves '{action}' for '{horizon}' ({horizon_desc.get(horizon, horizon)}). | |
| Suggest the SPECIFIC best date/time to {action} {symbol} for maximum benefit. | |
| - For intraday/scalping: suggest time of day (e.g., "Sell tomorrow at 10:30 AM IST for 1.5% gain") | |
| - For swing/momentum: suggest specific date within the horizon (e.g., "Buy on Jan 2, 2026") | |
| - For long_term: suggest holding duration (e.g., "Hold until Feb 2026 for 8% gain") | |
| Include % potential gain/loss. Be accurate and align with user intent. | |
| """ | |
| try: | |
| best_date = llm_parser.invoke(best_date_prompt).content.strip() | |
| except: | |
| best_date = f"Based on trends, {action} within {horizon_desc.get(horizon, horizon)} for potential 1-3% {'gain' if action == 'buy' else 'exit'}." | |
| why_prompt = f""" | |
| Based on claim: '{claim}', skepticism: '{skepticism}', features: '{features}', prediction: '{prediction}', holding: '{holding}'. | |
| RSI is {rsi:.1f} (low = oversold/buy signal, high = overbought/sell signal). MACD is {macd:.2f} (positive = bullish, negative = bearish). | |
| Explain why the signal is Green/Yellow/Red for {action} {symbol} ({horizon} = {horizon_desc.get(horizon, horizon)}). | |
| Use layman terms, reference indicators accurately, tie to data. Be genuine—align with RSI/MACD signals. | |
| """ | |
| try: | |
| why = llm_parser.invoke(why_prompt).content.strip() | |
| except: | |
| 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}." | |
| if confidence >= 70: | |
| color = " Green: 'Yes, Go Ahead!'" | |
| should_action = f"Yes, {action} now. Allocate {allocation:.0f}% as signals are strong." | |
| elif confidence >= 40: | |
| color = " Yellow: 'Wait and Watch'" | |
| should_action = f"Maybe, monitor closely. Allocate {allocation:.0f}% cautiously due to mixed signals and potential risk." | |
| else: | |
| color = " Red: 'No, Stop!'" | |
| should_action = f"No, avoid {action}. Signals are weak; wait for better conditions." | |
| holding_summary = "" | |
| if holding: | |
| h_return = holding['holding_return'] | |
| 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']})." | |
| if h_return > 0: | |
| advice = "lock in gains" | |
| elif h_return < 0: | |
| advice = "wait for recovery or cut losses" | |
| else: | |
| advice = "exit at breakeven" | |
| should_action += f" Based on {h_return:.1f}% {holding['pnl']}, {advice}." | |
| report = f""" | |
| --- AI Trader Analyzer Report --- | |
| Symbol: {symbol} | |
| **Current Price**: {current_price:,.4f} | |
| Action: {action.capitalize()} ({horizon.replace('_', '-').capitalize()} = {horizon_desc.get(horizon, horizon)}) "/n" | |
| Date: {state.get('query_date', 'N/A')} (IST) | |
| Signal: {color} | |
| Confidence: {confidence}% | |
| Key Metrics: RSI {rsi:.1f}, MACD {macd:.2f}, Trend {features.split(';')[0] if features else 'N/A'} | |
| {holding_summary} | |
| Should I {action.capitalize()}?** {should_action} | |
| Best Date/Time to {action.capitalize()}**: {best_date} | |
| Why? {why} | |
| Prediction: {prediction} | |
| Recommendation: Allocate {allocation:.0f}% based on analysis. Not financial advice. | |
| """ | |
| return report.strip() | |
| def main(): | |
| symbol, date, action, horizon, holding_period = None, None, None, None, None | |
| while not symbol: | |
| 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() | |
| symbol, date, action, horizon, holding_period = parse_query(user_input) | |
| if not symbol: | |
| print("No symbol detected. Try again.") | |
| print(f"Analyzing: {symbol} on {date or 'recent'} (IST) for {action} ({horizon})" + (f", holding: {holding_period}" if holding_period else "")) | |
| # Fetch data ONCE before the loop | |
| hist = fetch_data(symbol, horizon) | |
| state = TraderState( | |
| input_symbol=symbol, | |
| query_date=date, | |
| action=action, | |
| horizon=horizon, | |
| holding_period=holding_period, | |
| # Pre-load data | |
| raw_data={'hist': hist}, | |
| claim=None, | |
| skepticism=None, | |
| confidence=50, | |
| iterations=0, | |
| stop=False, | |
| alert_message=None | |
| ) | |
| max_iterations = 5 | |
| for _ in range(max_iterations): | |
| state = hunter_agent(state) | |
| state = skeptic_agent(state) | |
| state = calibrator_agent(state) | |
| if state['stop']: | |
| break | |
| state['alert_message'] = generate_alert_message(state, symbol, action, horizon) | |
| result = { | |
| "symbol": symbol, | |
| "date": date, | |
| "action": action, | |
| "horizon": horizon, | |
| "holding_period": holding_period, | |
| "alert_message": state['alert_message'], | |
| "iterations": state['iterations'], | |
| "confidence": state['confidence'] | |
| } | |
| print("\n--- AI Trader Analyzer Result ---") | |
| print(f"Symbol: {result['symbol']}") | |
| print(f"Date: {result['date']} (IST)") | |
| print(f"Action: {result['action']}") | |
| print(f"Horizon: {result['horizon']}") | |
| print(f"Holding: {result['holding_period'] or 'None'}") | |
| print(f"Message:\n{result['alert_message']}") | |
| print(f"Iterations: {result['iterations']}") | |
| print(f"Confidence: {result['confidence']}%") | |
| if __name__ == "__main__": | |
| main() |