diff --git a/backend/app/models/schemas.py b/backend/app/models/schemas.py index 50783cb6..6c16dd4b 100644 --- a/backend/app/models/schemas.py +++ b/backend/app/models/schemas.py @@ -30,6 +30,26 @@ class AnalysisRequest(BaseModel): ) +class PriceData(BaseModel): + """Stock price data model""" + Date: str + Open: float + High: float + Low: float + Close: float + Volume: int + + +class PriceStats(BaseModel): + """Price statistics model""" + growth_rate: float = Field(..., description="Price growth rate in percentage") + duration_days: int = Field(..., description="Data duration in days") + start_date: str + end_date: str + start_price: float + end_price: float + + class AnalysisResponse(BaseModel): """Response model for trading analysis""" status: str = Field(..., description="Analysis status (success, error, processing)") @@ -38,6 +58,8 @@ class AnalysisResponse(BaseModel): decision: Optional[Dict[str, Any]] = Field(None, description="Trading decision details") reports: Optional[Dict[str, Any]] = Field(None, description="Analysis reports from different teams") error: Optional[str] = Field(None, description="Error message if analysis failed") + price_data: Optional[List[PriceData]] = Field(None, description="Historical price data") + price_stats: Optional[PriceStats] = Field(None, description="Price statistics") class ConfigResponse(BaseModel): diff --git a/backend/app/services/price_service.py b/backend/app/services/price_service.py new file mode 100644 index 00000000..b71fc39a --- /dev/null +++ b/backend/app/services/price_service.py @@ -0,0 +1,102 @@ +""" +Price data service for loading and processing stock price data +""" +import pandas as pd +from pathlib import Path +from typing import List, Dict, Any, Optional +import logging + +logger = logging.getLogger(__name__) + + +class PriceService: + """Service for loading and processing price data from data_cache""" + + @staticmethod + def load_price_data(ticker: str, data_cache_dir: str) -> Optional[pd.DataFrame]: + """ + Load price data from data_cache CSV files + + Args: + ticker: Stock ticker symbol + data_cache_dir: Path to data cache directory + + Returns: + DataFrame with price data or None if not found + """ + try: + cache_path = Path(data_cache_dir) + + # Search for {ticker}-YFin-data-*.csv files + csv_files = list(cache_path.glob(f"{ticker}-YFin-data-*.csv")) + + if not csv_files: + logger.warning(f"No price data found for {ticker} in {data_cache_dir}") + return None + + # Use the most recent file + latest_file = max(csv_files, key=lambda p: p.stat().st_mtime) + logger.info(f"Loading price data from {latest_file}") + + df = pd.read_csv(latest_file) + df['Date'] = pd.to_datetime(df['Date']) + + return df.sort_values('Date') + + except Exception as e: + logger.error(f"Error loading price data for {ticker}: {e}") + return None + + @staticmethod + def calculate_stats(df: pd.DataFrame) -> Dict[str, Any]: + """ + Calculate price statistics + + Args: + df: DataFrame with price data + + Returns: + Dictionary with statistics + """ + start_price = float(df.iloc[0]['Close']) + end_price = float(df.iloc[-1]['Close']) + growth_rate = ((end_price - start_price) / start_price) * 100 + duration_days = (df.iloc[-1]['Date'] - df.iloc[0]['Date']).days + + return { + "growth_rate": round(growth_rate, 2), + "duration_days": int(duration_days), + "start_date": df.iloc[0]['Date'].strftime('%Y-%m-%d'), + "end_date": df.iloc[-1]['Date'].strftime('%Y-%m-%d'), + "start_price": round(start_price, 2), + "end_price": round(end_price, 2), + } + + @staticmethod + def prepare_chart_data(df: pd.DataFrame, limit: int = 365) -> List[Dict[str, Any]]: + """ + Prepare price data for charting (limit to recent data) + + Args: + df: DataFrame with price data + limit: Maximum number of data points to return + + Returns: + List of dictionaries with price data + """ + # Get recent data + recent_df = df.tail(limit) + + # Convert to list of dicts + data = [] + for _, row in recent_df.iterrows(): + data.append({ + "Date": row['Date'].strftime('%Y-%m-%d'), + "Open": round(float(row['Open']), 2), + "High": round(float(row['High']), 2), + "Low": round(float(row['Low']), 2), + "Close": round(float(row['Close']), 2), + "Volume": int(row['Volume']), + }) + + return data diff --git a/backend/app/services/trading_service.py b/backend/app/services/trading_service.py index d3a2cd9e..20850d96 100644 --- a/backend/app/services/trading_service.py +++ b/backend/app/services/trading_service.py @@ -111,12 +111,28 @@ class TradingService: "risk_debate_state": final_state.get("risk_debate_state"), } + # Load price data + from backend.app.services.price_service import PriceService + price_data = None + price_stats = None + + try: + price_df = PriceService.load_price_data(ticker, config.get("data_cache_dir")) + if price_df is not None: + price_data = PriceService.prepare_chart_data(price_df) + price_stats = PriceService.calculate_stats(price_df) + logger.info(f"Loaded {len(price_data)} price data points for {ticker}") + except Exception as e: + logger.warning(f"Could not load price data for {ticker}: {e}") + return { "status": "success", "ticker": ticker, "analysis_date": analysis_date, "decision": decision, "reports": reports, + "price_data": price_data, + "price_stats": price_stats, } finally: diff --git a/frontend/app/analysis/page.tsx b/frontend/app/analysis/page.tsx index c30378b1..32f6e80b 100644 --- a/frontend/app/analysis/page.tsx +++ b/frontend/app/analysis/page.tsx @@ -7,6 +7,7 @@ import { useState } from "react"; import { AnalysisForm } from "@/components/analysis/AnalysisForm"; import { TradingDecision } from "@/components/analysis/TradingDecision"; import { AnalystReport } from "@/components/analysis/AnalystReport"; +import { PriceChart } from "@/components/analysis/PriceChart"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { useAnalysis } from "@/hooks/useAnalysis"; import type { AnalysisRequest } from "@/lib/types"; @@ -48,7 +49,19 @@ export default function AnalysisPage() { {result && !loading && (