| from fastapi import APIRouter, Query, HTTPException, Depends
|
| from app.services.yahoo_finance import yahoo_finance_service
|
| from app.models.market import MarketIndicesResponse, SearchResponse
|
| from app.models.database import User, UserAgenticScan
|
| from app.db.session import get_db
|
| from app.core.auth import get_user_or_demo
|
| from sqlalchemy.ext.asyncio import AsyncSession
|
| from pydantic import BaseModel
|
| import json
|
| from typing import Optional, List, Dict, Any
|
|
|
| router = APIRouter()
|
|
|
| @router.get("/market-indices", response_model=MarketIndicesResponse)
|
| async def get_market_indices():
|
| results = await yahoo_finance_service.get_market_indices()
|
| return {"results": results}
|
|
|
| @router.get("/search", response_model=SearchResponse)
|
| async def search_ticker(q: str = Query(..., min_length=1)):
|
| results = await yahoo_finance_service.search_tickers(q)
|
| return {"results": results}
|
|
|
|
|
| class OptionsAnalysisRequest(BaseModel):
|
| symbol: str
|
| provider: str = "openai"
|
|
|
|
|
| class AgentLog(BaseModel):
|
| source: str
|
| content: str
|
| avatar: str
|
|
|
|
|
| class OptionsAnalysisResponse(BaseModel):
|
| logs: List[AgentLog]
|
| result: Optional[Dict[str, Any]] = None
|
|
|
|
|
| @router.post("/options-analysis", response_model=OptionsAnalysisResponse)
|
| async def analyze_options_strategy(
|
| request: OptionsAnalysisRequest,
|
| db: AsyncSession = Depends(get_db),
|
| user: User = Depends(get_user_or_demo)
|
| ):
|
| """
|
| Runs multi-agent analysis for option strategy recommendations.
|
| Uses the local agentic_analyst module.
|
| """
|
| print(f"[DEBUG] POST /api/options-analysis for symbol: {request.symbol} (provider: {request.provider})")
|
| try:
|
|
|
| from app.core.model_factory import AutoGenModelFactory
|
| from app.agentic_analyst.team import get_trading_team, extract_json
|
|
|
|
|
| AGENT_ICONS = {
|
| "MarketAnalyst": "π",
|
| "SentimentAnalyst": "π°",
|
| "StrategyAdvisor": "π§ ",
|
| "RiskManager": "π‘οΈ",
|
| "System": "π€",
|
| "User": "π΅οΈββοΈ"
|
| }
|
|
|
|
|
| if request.provider == "gemini" or request.provider == "google" or request.provider == "openai":
|
|
|
| model_name = "gemini-2.0-flash"
|
| family = "gemini"
|
| request.provider = "google"
|
| elif request.provider == "groq":
|
| model_name = "llama-3.3-70b-versatile"
|
| family = "groq"
|
| elif request.provider == "ollama":
|
| model_name = "llama3.2:3b"
|
| family = "llama"
|
| else:
|
|
|
| model_name = "gemini-2.0-flash"
|
| family = "gemini"
|
| request.provider = "google"
|
|
|
| print(f"[DEBUG] Using provider: {request.provider}, model: {model_name}")
|
|
|
| try:
|
| model_client = AutoGenModelFactory.get_model(
|
| provider=request.provider,
|
| model_name=model_name,
|
| temperature=0.2,
|
| model_info={
|
| "family": family,
|
| "function_calling": True,
|
| }
|
| )
|
| except Exception as e:
|
| print(f"[DEBUG] Failed to initialize model client: {str(e)}")
|
| raise HTTPException(status_code=500, detail=f"Error initializing model: {str(e)}")
|
|
|
|
|
| print("[DEBUG] Getting trading team...")
|
| team = get_trading_team(model_client)
|
|
|
|
|
| task = f"""
|
| Perform a real-time trade analysis for {request.symbol}.
|
| 1. MarketAnalyst: detailed technicals.
|
| 2. SentimentAnalyst: news sentiment (Top 5 stories).
|
| 3. StrategyAdvisor: recommend a spread with >70% confidence.
|
| 4. RiskManager: validate. Output JSON with "final_decision" (TRADE/WAIT), "confidence", and "actionable_recommendation".
|
| """
|
|
|
| logs = []
|
| final_output = {}
|
|
|
|
|
| logs.append({
|
| "source": "User",
|
| "content": f"Analyze {request.symbol} ({request.provider})",
|
| "avatar": AGENT_ICONS["User"]
|
| })
|
|
|
| print(f"[DEBUG] Starting team stream for {request.symbol}...")
|
| print(f"[DEBUG] Task being sent to team: {task[:100]}...")
|
|
|
| try:
|
|
|
| async for message in team.run_stream(task=task):
|
| raw_source = getattr(message, 'source', 'System')
|
| source = raw_source
|
|
|
|
|
| if source.lower() == 'user':
|
| source = 'User'
|
|
|
|
|
| if source == 'User' and "Perform a real-time trade analysis" in getattr(message, 'content', ''):
|
| continue
|
|
|
|
|
| message_type = type(message).__name__
|
|
|
|
|
|
|
| if "ToolCall" in message_type:
|
| print(f"[DEBUG] {source} is executing tools...")
|
|
|
| content = getattr(message, 'content', '')
|
|
|
|
|
| if not isinstance(content, str):
|
| if isinstance(content, list):
|
| content = "\n".join([str(c) for c in content])
|
| else:
|
| content = str(content)
|
|
|
| if not content:
|
| continue
|
|
|
| print(f"[DEBUG] Agent Message -> Source: {source}, Length: {len(content)}")
|
|
|
|
|
| avatar_icon = AGENT_ICONS.get(source, "π€")
|
| logs.append({
|
| "source": source,
|
| "content": content,
|
| "avatar": avatar_icon
|
| })
|
|
|
|
|
| if source == "RiskManager":
|
| print("[DEBUG] RiskManager produced content, attempting extraction...")
|
| parsed = extract_json(content)
|
| if parsed:
|
| print(f"[DEBUG] Successfully extracted final decision: {parsed.get('final_decision')}")
|
| final_output = parsed
|
| except Exception as stream_err:
|
| print(f"[ERROR] Stream error during analysis: {stream_err}")
|
|
|
| logs.append({
|
| "source": "System",
|
| "content": f"ERROR: Analysis interrupted. {str(stream_err)}",
|
| "avatar": "β οΈ"
|
| })
|
|
|
|
|
| if not final_output and logs:
|
| print("[DEBUG] No structured final output found. Attempting fallback save.")
|
|
|
| final_output = {
|
| "final_decision": "INCOMPLETE",
|
| "confidence": 0.0,
|
| "actionable_recommendation": "Analysis terminated early. Check logs for partial details.",
|
| "entry_price": "N/A",
|
| "risk_warning": "Abrupt termination detected."
|
| }
|
|
|
| if final_output:
|
| try:
|
|
|
| db_scan = UserAgenticScan(
|
| user_id=user.id,
|
| symbol=request.symbol,
|
| decision=final_output.get("final_decision", "WAIT"),
|
| confidence=float(final_output.get("confidence", 0.0)),
|
| recommendation=final_output.get("actionable_recommendation", "Analysis complete"),
|
| entry_price=str(final_output.get("entry_price", "")),
|
| raw_logs=json.dumps([{"source": l["source"], "content": l["content"]} for l in logs]),
|
| risk_warning=final_output.get("risk_warning", "")
|
| )
|
| db.add(db_scan)
|
| await db.commit()
|
| print(f"[DEBUG] Saved Agentic Scan to DB: ID {db_scan.id}")
|
| except Exception as db_err:
|
| print(f"[ERROR] Failed to save scan to DB: {db_err}")
|
|
|
| print(f"[DEBUG] Analysis results ready for {request.symbol}. Logs: {len(logs)}")
|
| return {
|
| "logs": logs,
|
| "result": final_output if final_output else None
|
| }
|
|
|
| except ImportError as e:
|
| raise HTTPException(
|
| status_code=500,
|
| detail=f"Market analyst modules not available: {str(e)}"
|
| )
|
| except Exception as e:
|
| raise HTTPException(
|
| status_code=500,
|
| detail=f"Analysis failed: {str(e)}"
|
| )
|
|
|
| from app.models.database import HistoricalData
|
| from sqlalchemy import select, desc
|
|
|
| class FeatureDataPoint(BaseModel):
|
| date: str
|
| close: float
|
| volume: float
|
| indicators: Optional[Dict[str, Any]] = None
|
|
|
| class AgenticScanSummary(BaseModel):
|
| id: int
|
| decision: str
|
| confidence: float
|
| recommendation: str
|
| created_at: str
|
| risk_warning: Optional[str] = None
|
| entry_price: Optional[str] = None
|
|
|
| class TickerDetailResponse(BaseModel):
|
| symbol: str
|
| price: float
|
| change: float
|
| features: List[FeatureDataPoint]
|
| latest_strategy: Optional[AgenticScanSummary] = None
|
|
|
| @router.get("/ticker/{symbol}", response_model=TickerDetailResponse)
|
| async def get_ticker_details(
|
| symbol: str,
|
| db: AsyncSession = Depends(get_db),
|
| user: User = Depends(get_user_or_demo)
|
| ):
|
|
|
| raw_symbol = symbol.upper()
|
| try:
|
| quotes = await yahoo_finance_service.get_quotes([raw_symbol])
|
| quote = quotes[0] if quotes else None
|
|
|
| price_val = 0.0
|
| change_val = 0.0
|
| if quote:
|
|
|
| p = quote.price.replace(',', '')
|
| if p != "N/A": price_val = float(p)
|
|
|
| c = quote.change.replace('+', '').replace(',', '')
|
| if c != "N/A": change_val = float(c)
|
| except Exception:
|
| price_val = 0.0
|
| change_val = 0.0
|
|
|
|
|
|
|
| hist_result = await db.execute(
|
| select(HistoricalData)
|
| .where(HistoricalData.symbol == raw_symbol)
|
| .order_by(desc(HistoricalData.date))
|
|
|
| )
|
| hist_rows = hist_result.scalars().all()
|
|
|
| features = []
|
| for row in hist_rows:
|
| indicators = {}
|
| if row.indicators:
|
| try:
|
| indicators = json.loads(row.indicators)
|
| except:
|
| pass
|
|
|
| features.append({
|
| "date": row.date.strftime("%Y-%m-%d"),
|
| "close": row.close,
|
| "volume": row.volume,
|
| "indicators": indicators
|
| })
|
| features.reverse()
|
|
|
|
|
| scan_result = await db.execute(
|
| select(UserAgenticScan)
|
| .where(UserAgenticScan.user_id == user.id)
|
| .where(UserAgenticScan.symbol == raw_symbol)
|
| .order_by(desc(UserAgenticScan.created_at))
|
| .limit(1)
|
| )
|
| latest_scan = scan_result.scalar_one_or_none()
|
|
|
| scan_summary = None
|
| if latest_scan:
|
| scan_summary = {
|
| "id": latest_scan.id,
|
| "decision": latest_scan.decision,
|
| "confidence": latest_scan.confidence,
|
| "recommendation": latest_scan.recommendation,
|
| "created_at": latest_scan.created_at.isoformat(),
|
| "risk_warning": latest_scan.risk_warning,
|
| "entry_price": latest_scan.entry_price
|
| }
|
|
|
| return {
|
| "symbol": raw_symbol,
|
| "price": price_val,
|
| "change": change_val,
|
| "features": features,
|
| "latest_strategy": scan_summary
|
| }
|
|
|