diff --git a/backend/app/data.py b/backend/app/data.py index 1a96a78..f97d70d 100644 --- a/backend/app/data.py +++ b/backend/app/data.py @@ -1,5 +1,3 @@ -import random - from app.schemas import ( ModelStatus, NewsCategory, @@ -10,66 +8,8 @@ TradingModel, ) -# Sample trading models data -SAMPLE_MODELS: list[TradingModel] = [ - TradingModel( - id="1", - name="LSTM Deep Learning Model", - performance=78.5, - accuracy=82.3, - trades=145, - profit=2340.50, - status=ModelStatus.ACTIVE, - ), - TradingModel( - id="2", - name="Random Forest Classifier", - performance=65.2, - accuracy=71.8, - trades=89, - profit=1250.75, - status=ModelStatus.ACTIVE, - ), - TradingModel( - id="3", - name="XGBoost Regressor", - performance=71.9, - accuracy=76.4, - trades=112, - profit=1890.25, - status=ModelStatus.INACTIVE, - ), - TradingModel( - id="4", - name="Support Vector Machine", - performance=55.8, - accuracy=62.1, - trades=67, - profit=-450.30, - status=ModelStatus.TRAINING, - ), - TradingModel( - id="5", - name="Neural Network Ensemble", - performance=83.2, - accuracy=87.6, - trades=203, - profit=4120.80, - status=ModelStatus.ACTIVE, - ), -] - - -def get_models_data() -> list[TradingModel]: - """Get all trading models with some random variation in performance.""" - models = [] - for model in SAMPLE_MODELS: - # Add some random variation to make it more realistic - variation = random.uniform(-2, 2) - updated_model = model.model_copy() - updated_model.performance = max(0, min(100, model.performance + variation)) - models.append(updated_model) - return models +# Import trading actions management +from app.trading_actions import get_trading_actions def get_real_models_data() -> list[TradingModel]: @@ -118,12 +58,6 @@ def get_real_models_data() -> list[TradingModel]: return llm_models -def get_trades_data() -> list[Trade]: - """Get trading history data.""" - # Return empty list since SAMPLE_TRADES is commented out - return [] - - def get_real_trades_data(ticker: str = "NVDA", days: int = 7) -> list[Trade]: """Get real trading data by fetching stock prices.""" import os @@ -199,11 +133,6 @@ def get_real_trades_data(ticker: str = "NVDA", days: int = 7) -> list[Trade]: return [] -def get_news_data() -> list[NewsItem]: - """Get news data.""" - return [] - - def get_real_news_data(query: str = "stock market", days: int = 7) -> list[NewsItem]: """Get real news data from Google News.""" import os @@ -315,7 +244,6 @@ def get_real_news_data(query: str = "stock market", days: int = 7) -> list[NewsI except Exception as e: print(f"Error fetching real news: {e}") - # Fallback to sample data return [] @@ -325,8 +253,17 @@ def get_real_social_data( """Get real social media data from Reddit across all categories.""" import os import sys + import warnings from datetime import datetime + # Suppress PRAW async warnings since we're using it correctly in FastAPI + warnings.filterwarnings("ignore", message=".*PRAW.*asynchronous.*") + + # Also suppress PRAW logging warnings + import logging + + logging.getLogger("praw").setLevel(logging.ERROR) + # Add trading_bench to path project_root = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -432,3 +369,14 @@ def get_real_social_data( except Exception as e: print(f"Error fetching real social data: {e}") return [] + + +def get_real_system_log_data( + agent_type: str = None, status: str = None, limit: int = 100, hours: int = 24 +) -> list[dict]: + """Get trading actions from system logs - ONLY trading decisions, not data fetching.""" + # Get trading actions using the new system - NO SAMPLE DATA + actions = get_trading_actions( + agent_type=agent_type, status=status, limit=limit, hours=hours + ) + return actions diff --git a/backend/app/main.py b/backend/app/main.py index bc742e1..685bad4 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,13 +1,49 @@ -from app.routers import models, news, social, trades -from fastapi import FastAPI +import logging +import time + +from app.routers import models, news, social, system_logs, trades +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + app = FastAPI( title="Live Trade Bench API", description="FastAPI backend for trading dashboard with models, trades, and news data", version="1.0.0", ) + +# Add request logging middleware +@app.middleware("http") +async def log_requests(request: Request, call_next): + start_time = time.time() + + # Log incoming request + logger.info(f"→ {request.method} {request.url.path}") + + response = await call_next(request) + + # Calculate processing time + process_time = time.time() - start_time + + # Log response with status code and time + status_emoji = ( + "✅" + if response.status_code == 200 + else "❌" + if response.status_code >= 400 + else "⚠️" + ) + logger.info( + f"{status_emoji} {response.status_code} {request.method} {request.url.path} - {process_time:.3f}s" + ) + + return response + + # Configure CORS app.add_middleware( CORSMiddleware, @@ -22,6 +58,7 @@ app.include_router(trades.router) app.include_router(news.router) app.include_router(social.router) +app.include_router(system_logs.router) @app.get("/") @@ -35,6 +72,7 @@ async def root(): "trades": "/api/trades", "news": "/api/news", "social": "/api/social", + "system-log": "/api/system-log", "docs": "/docs", "redoc": "/redoc", }, diff --git a/backend/app/routers/models.py b/backend/app/routers/models.py index 3096102..bd22b98 100644 --- a/backend/app/routers/models.py +++ b/backend/app/routers/models.py @@ -1,5 +1,5 @@ -from app.data import SAMPLE_MODELS, get_models_data, get_real_models_data -from app.schemas import APIResponse, TradingModel +from app.data import get_real_models_data +from app.schemas import TradingModel from fastapi import APIRouter, HTTPException router = APIRouter(prefix="/api/models", tags=["models"]) @@ -7,31 +7,19 @@ @router.get("/", response_model=list[TradingModel]) async def get_models(): - """Get all trading models with current performance metrics.""" - try: - models = get_models_data() - return models - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error fetching models: {str(e)}") - - -@router.get("/real", response_model=list[TradingModel]) -async def get_real_models(): """Get real LLM trading models with performance from actual predictions.""" try: models = get_real_models_data() return models except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error fetching real models: {str(e)}" - ) + raise HTTPException(status_code=500, detail=f"Error fetching models: {str(e)}") @router.get("/{model_id}", response_model=TradingModel) async def get_model(model_id: str): - """Get a specific trading model by ID.""" + """Get a specific real trading model by ID.""" try: - models = get_models_data() + models = get_real_models_data() model = next((m for m in models if m.id == model_id), None) if not model: raise HTTPException(status_code=404, detail="Model not found") @@ -42,40 +30,16 @@ async def get_model(model_id: str): raise HTTPException(status_code=500, detail=f"Error fetching model: {str(e)}") -@router.post("/{model_id}/toggle", response_model=APIResponse) -async def toggle_model(model_id: str): - """Toggle a trading model's status (active/inactive).""" - try: - model = next((m for m in SAMPLE_MODELS if m.id == model_id), None) - if not model: - raise HTTPException(status_code=404, detail="Model not found") - - # Toggle between active and inactive - if model.status.value == "active": - model.status = "inactive" - elif model.status.value == "inactive": - model.status = "active" - - return APIResponse( - success=True, - message=f"Model {model.name} is now {model.status.value}", - data={"id": model_id, "status": model.status.value}, - ) - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error toggling model: {str(e)}") - - @router.get("/{model_id}/performance") async def get_model_performance(model_id: str): - """Get detailed performance metrics for a specific model.""" + """Get detailed performance metrics for a specific real model.""" try: - model = next((m for m in SAMPLE_MODELS if m.id == model_id), None) + models = get_real_models_data() + model = next((m for m in models if m.id == model_id), None) if not model: raise HTTPException(status_code=404, detail="Model not found") - # Return detailed performance metrics + # Return real performance metrics return { "id": model.id, "name": model.name, @@ -88,15 +52,6 @@ async def get_model_performance(model_id: str): "average_profit_per_trade": model.profit / model.trades if model.trades > 0 else 0, - "risk_metrics": { - "sharpe_ratio": round( - model.performance / 15, 2 - ), # Simplified calculation - "max_drawdown": round(model.profit * 0.1, 2), # Simplified calculation - "volatility": round( - model.performance * 0.2, 2 - ), # Simplified calculation - }, } except HTTPException: raise diff --git a/backend/app/routers/news.py b/backend/app/routers/news.py index 4c66086..bc37e0a 100644 --- a/backend/app/routers/news.py +++ b/backend/app/routers/news.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta -from app.data import get_news_data, get_real_news_data +from app.data import get_real_news_data from app.schemas import NewsCategory, NewsImpact, NewsItem from fastapi import APIRouter, HTTPException, Query @@ -9,15 +9,19 @@ @router.get("/", response_model=list[NewsItem]) async def get_news( + query: str = Query(default="stock market", description="Search query for news"), + days: int = Query( + default=7, ge=1, le=30, description="Number of days to look back" + ), limit: int = Query(default=20, ge=1, le=100), offset: int = Query(default=0, ge=0), category: NewsCategory | None = Query(default=None), impact: NewsImpact | None = Query(default=None), hours: int | None = Query(default=None, ge=1, le=168), # Last X hours (max 1 week) ): - """Get news articles with optional filtering and pagination.""" + """Get real news articles with optional filtering and pagination.""" try: - news = get_news_data() + news = get_real_news_data(query=query, days=days) # Apply time filter if hours: @@ -43,225 +47,17 @@ async def get_news( raise HTTPException(status_code=500, detail=f"Error fetching news: {str(e)}") -@router.get("/real", response_model=list[NewsItem]) -async def get_real_news( - query: str = Query(default="stock market", description="Search query for news"), +@router.get("/search/{query}") +async def search_news( + query: str, days: int = Query( default=7, ge=1, le=30, description="Number of days to look back" ), + limit: int = Query(default=20, ge=1, le=100), ): - """Get real news data from Google News.""" + """Search real news articles by query.""" try: news = get_real_news_data(query=query, days=days) - return news - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error fetching real news: {str(e)}" - ) - - -@router.get("/category/{category}") -async def get_news_by_category( - category: NewsCategory, limit: int = Query(default=20, ge=1, le=100) -): - """Get news articles by category.""" - try: - news = get_news_data() - category_news = [n for n in news if n.category == category] - - # Sort by publication date (newest first) - category_news.sort(key=lambda x: x.published_at, reverse=True) - - # Apply limit - category_news = category_news[:limit] - - return { - "category": category.value, - "count": len(category_news), - "news": category_news, - } - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error fetching category news: {str(e)}" - ) - - -@router.get("/impact/{impact}") -async def get_news_by_impact( - impact: NewsImpact, limit: int = Query(default=20, ge=1, le=100) -): - """Get news articles by impact level.""" - try: - news = get_news_data() - impact_news = [n for n in news if n.impact == impact] - - # Sort by publication date (newest first) - impact_news.sort(key=lambda x: x.published_at, reverse=True) - - # Apply limit - impact_news = impact_news[:limit] - - return {"impact": impact.value, "count": len(impact_news), "news": impact_news} - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error fetching impact news: {str(e)}" - ) - - -@router.get("/search/{query}") -async def search_news(query: str, limit: int = Query(default=20, ge=1, le=100)): - """Search news articles by title or summary.""" - try: - news = get_news_data() - query_lower = query.lower() - - # Search in title and summary - matching_news = [ - n - for n in news - if query_lower in n.title.lower() or query_lower in n.summary.lower() - ] - - # Sort by publication date (newest first) - matching_news.sort(key=lambda x: x.published_at, reverse=True) - - # Apply limit - matching_news = matching_news[:limit] - - return {"query": query, "count": len(matching_news), "news": matching_news} + return {"query": query, "count": len(news), "news": news[:limit]} except Exception as e: raise HTTPException(status_code=500, detail=f"Error searching news: {str(e)}") - - -@router.get("/stats/summary") -async def get_news_stats(): - """Get news statistics summary.""" - try: - news = get_news_data() - - if not news: - return { - "total_articles": 0, - "categories": {}, - "impact_levels": {}, - "sources": {}, - "latest_article": None, - } - - # Count by category - categories = {} - for item in news: - category = item.category.value - categories[category] = categories.get(category, 0) + 1 - - # Count by impact - impact_levels = {} - for item in news: - impact = item.impact.value - impact_levels[impact] = impact_levels.get(impact, 0) + 1 - - # Count by source - sources = {} - for item in news: - source = item.source - sources[source] = sources.get(source, 0) + 1 - - # Find latest article - latest_article = max(news, key=lambda x: x.published_at) - - return { - "total_articles": len(news), - "categories": categories, - "impact_levels": impact_levels, - "sources": sources, - "latest_article": { - "id": latest_article.id, - "title": latest_article.title, - "published_at": latest_article.published_at, - "source": latest_article.source, - }, - } - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error fetching news stats: {str(e)}" - ) - - -@router.get("/{news_id}", response_model=NewsItem) -async def get_news_item(news_id: str): - """Get a specific news item by ID.""" - try: - news = get_news_data() - news_item = next((n for n in news if n.id == news_id), None) - if not news_item: - raise HTTPException(status_code=404, detail="News item not found") - return news_item - except HTTPException: - raise - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error fetching news item: {str(e)}" - ) - - -@router.get("/trending/topics") -async def get_trending_topics(): - """Get trending topics based on high-impact recent news.""" - try: - news = get_news_data() - - # Get high-impact news from last 24 hours - recent_cutoff = datetime.now() - timedelta(hours=24) - high_impact_news = [ - n - for n in news - if n.published_at >= recent_cutoff and n.impact == NewsImpact.HIGH - ] - - # Extract keywords from titles (simplified approach) - keywords = [] - for item in high_impact_news: - words = item.title.lower().split() - # Filter out common words - common_words = { - "the", - "and", - "or", - "but", - "in", - "on", - "at", - "to", - "for", - "of", - "with", - "by", - "a", - "an", - } - meaningful_words = [ - w for w in words if w not in common_words and len(w) > 3 - ] - keywords.extend(meaningful_words) - - # Count keyword frequency - keyword_counts = {} - for keyword in keywords: - keyword_counts[keyword] = keyword_counts.get(keyword, 0) + 1 - - # Sort by frequency and take top 10 - trending_topics = sorted( - keyword_counts.items(), key=lambda x: x[1], reverse=True - )[:10] - - return { - "trending_topics": [ - {"topic": topic, "mentions": count} for topic, count in trending_topics - ], - "high_impact_articles": len(high_impact_news), - "time_range": "last_24_hours", - } - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error fetching trending topics: {str(e)}" - ) diff --git a/backend/app/routers/social.py b/backend/app/routers/social.py index e453251..106336f 100644 --- a/backend/app/routers/social.py +++ b/backend/app/routers/social.py @@ -5,14 +5,7 @@ @router.get("/") -async def get_social_posts(): - """Get sample social media posts (placeholder).""" - # Return empty for now - real data comes from /real endpoint - return [] - - -@router.get("/real") -async def get_real_social_posts( +async def get_social_posts( category: str = Query( default="all", description="Reddit category to fetch from ('all' for all categories)", @@ -24,7 +17,7 @@ async def get_real_social_posts( default=7, ge=1, le=30, description="Number of days to look back" ), ): - """Get real social media data from Reddit. Fetches 5 posts from each category by default.""" + """Get real social media data from Reddit.""" try: posts = get_real_social_data(category=category, query=query, days=days) return posts diff --git a/backend/app/routers/system_logs.py b/backend/app/routers/system_logs.py new file mode 100644 index 0000000..8ddb02d --- /dev/null +++ b/backend/app/routers/system_logs.py @@ -0,0 +1,182 @@ +from datetime import datetime, timedelta + +from app.data import get_real_system_log_data +from app.schemas import ActionStatus, ActionType, Portfolio, SystemLogStats +from app.trading_actions import ( + MODEL_PORTFOLIOS, + add_trading_action, + get_model_portfolio, + update_action_status, +) +from fastapi import APIRouter, HTTPException, Query + +router = APIRouter(prefix="/api/system-log", tags=["system-logs"]) + + +@router.get("/") +async def get_system_log( + agent_type: str = Query( + default=None, + description="Filter by agent type (data_collector, trading_agent, etc.)", + ), + status: str = Query( + default=None, description="Filter by status (success, warning, error, info)" + ), + limit: int = Query(default=100, ge=1, le=500), + hours: int = Query( + default=24, ge=1, le=168, description="Number of hours to look back" + ), +): + """Get real system log data from trading operations.""" + try: + actions = get_real_system_log_data( + agent_type=agent_type, status=status, limit=limit, hours=hours + ) + return actions + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error fetching system log: {str(e)}" + ) + + +@router.get("/stats", response_model=SystemLogStats) +async def get_system_log_stats(): + """Get system log statistics for trading actions.""" + try: + actions = get_real_system_log_data(limit=1000, hours=24) + + if not actions: + return SystemLogStats( + total_actions=0, + pending_actions=0, + executed_actions=0, + evaluated_actions=0, + models_active=0, + recent_activity=0, + ) + + # Calculate statistics + total_actions = len(actions) + pending_actions = len([a for a in actions if a["status"] == "pending"]) + executed_actions = len([a for a in actions if a["status"] == "executed"]) + evaluated_actions = len([a for a in actions if a["status"] == "evaluated"]) + + # Count active models (models with actions in last 24 hours) + active_models = len(set(a["agent_id"] for a in actions)) + + # Recent activity (last hour) + one_hour_ago = datetime.now() - timedelta(hours=1) + recent_activity = len( + [ + a + for a in actions + if datetime.fromisoformat(a["timestamp"].replace("Z", "+00:00")) + >= one_hour_ago + ] + ) + + return SystemLogStats( + total_actions=total_actions, + pending_actions=pending_actions, + executed_actions=executed_actions, + evaluated_actions=evaluated_actions, + models_active=active_models, + recent_activity=recent_activity, + ) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error calculating stats: {str(e)}" + ) + + +@router.get("/portfolios") +async def get_all_portfolios(): + """Get portfolios for all trading models.""" + try: + portfolios = {} + for model_id, portfolio in MODEL_PORTFOLIOS.items(): + portfolios[model_id] = { + "model_id": model_id, + "model_name": _get_model_name(model_id), + "cash": portfolio.cash, + "holdings": dict(portfolio.holdings), + "total_value": portfolio.cash + + sum( + quantity * 150.0 # Simplified price for demo + for quantity in portfolio.holdings.values() + ), + } + return portfolios + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error fetching portfolios: {str(e)}" + ) + + +@router.get("/portfolios/{model_id}", response_model=Portfolio) +async def get_portfolio(model_id: str): + """Get portfolio for a specific trading model.""" + try: + portfolio = get_model_portfolio(model_id) + return portfolio + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error fetching portfolio: {str(e)}" + ) + + +@router.post("/actions") +async def create_trading_action( + model_id: str = Query(..., description="Model ID (e.g., claude-3.5-sonnet)"), + action_type: ActionType = Query(..., description="Action type: BUY, SELL, or HOLD"), + ticker: str = Query(..., description="Stock ticker symbol"), + quantity: float = Query(..., description="Number of shares"), + price: float = Query(..., description="Price per share"), + reasoning: str = Query(default="", description="Reasoning for the action"), +): + """Create a new trading action for a model.""" + try: + action_id = add_trading_action( + model_id=model_id, + action_type=action_type, + ticker=ticker, + quantity=quantity, + price=price, + reasoning=reasoning, + ) + return { + "success": True, + "action_id": action_id, + "message": "Trading action created successfully", + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating action: {str(e)}") + + +@router.put("/actions/{action_id}/status") +async def update_trading_action_status( + action_id: str, + status: ActionStatus = Query(..., description="New status for the action"), +): + """Update the status of a trading action.""" + try: + success = update_action_status(action_id, status) + if not success: + raise HTTPException(status_code=404, detail="Action not found") + + return {"success": True, "message": f"Action status updated to {status.value}"} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error updating action: {str(e)}") + + +def _get_model_name(model_id: str) -> str: + """Get human readable model name.""" + name_mapping = { + "claude-3.5-sonnet": "Claude 3.5 Sonnet", + "gpt-4": "GPT-4", + "gemini-1.5-pro": "Gemini 1.5 Pro", + "claude-4-haiku": "Claude 4 Haiku", + } + return name_mapping.get(model_id, model_id) diff --git a/backend/app/routers/trades.py b/backend/app/routers/trades.py index 650e5d0..1a03ec5 100644 --- a/backend/app/routers/trades.py +++ b/backend/app/routers/trades.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta -from app.data import get_real_trades_data, get_trades_data +from app.data import get_real_trades_data from app.schemas import Trade, TradingSummary from fastapi import APIRouter, HTTPException, Query @@ -9,14 +9,18 @@ @router.get("/", response_model=list[Trade]) async def get_trades( + ticker: str = Query(default="NVDA", description="Stock ticker symbol"), + days: int = Query( + default=7, ge=1, le=30, description="Number of days of trading data" + ), limit: int = Query(default=50, ge=1, le=1000), offset: int = Query(default=0, ge=0), symbol: str | None = Query(default=None), model: str | None = Query(default=None), ): - """Get trading history with optional filtering and pagination.""" + """Get real trading history with optional filtering and pagination.""" try: - trades = get_trades_data() + trades = get_real_trades_data(ticker=ticker, days=days) # Apply filters if symbol: @@ -29,7 +33,7 @@ async def get_trades( trades.sort(key=lambda x: x.timestamp, reverse=True) # Apply pagination - total_trades = len(trades) + # total_trades = len(trades) trades = trades[offset : offset + limit] return trades @@ -38,10 +42,15 @@ async def get_trades( @router.get("/summary", response_model=TradingSummary) -async def get_trading_summary(): - """Get trading performance summary.""" +async def get_trading_summary( + ticker: str = Query(default="NVDA", description="Stock ticker symbol"), + days: int = Query( + default=7, ge=1, le=30, description="Number of days of trading data" + ), +): + """Get real trading performance summary.""" try: - trades = get_trades_data() + trades = get_real_trades_data(ticker=ticker, days=days) if not trades: return TradingSummary( @@ -80,10 +89,15 @@ async def get_trading_summary(): @router.get("/stats") -async def get_trading_stats(): - """Get detailed trading statistics.""" +async def get_trading_stats( + ticker: str = Query(default="NVDA", description="Stock ticker symbol"), + days: int = Query( + default=7, ge=1, le=30, description="Number of days of trading data" + ), +): + """Get detailed real trading statistics.""" try: - trades = get_trades_data() + trades = get_real_trades_data(ticker=ticker, days=days) if not trades: return { @@ -135,10 +149,15 @@ async def get_trading_stats(): @router.get("/by-symbol/{symbol}") -async def get_trades_by_symbol(symbol: str): - """Get all trades for a specific symbol.""" +async def get_trades_by_symbol( + symbol: str, + days: int = Query( + default=7, ge=1, le=30, description="Number of days of trading data" + ), +): + """Get all real trades for a specific symbol.""" try: - trades = get_trades_data() + trades = get_real_trades_data(ticker=symbol, days=days) symbol_trades = [t for t in trades if t.symbol.upper() == symbol.upper()] if not symbol_trades: @@ -160,9 +179,9 @@ async def get_trades_by_symbol(symbol: str): "total_profit": round(total_profit, 2), "profitable_trades": profitable_trades, "win_rate": round(win_rate, 2), - "average_profit": round(total_profit / total_trades, 2) - if total_trades > 0 - else 0, + "average_profit": ( + round(total_profit / total_trades, 2) if total_trades > 0 else 0 + ), }, } except HTTPException: @@ -174,10 +193,16 @@ async def get_trades_by_symbol(symbol: str): @router.get("/by-model/{model_name}") -async def get_trades_by_model(model_name: str): - """Get all trades for a specific model.""" +async def get_trades_by_model( + model_name: str, + ticker: str = Query(default="NVDA", description="Stock ticker symbol"), + days: int = Query( + default=7, ge=1, le=30, description="Number of days of trading data" + ), +): + """Get all real trades for a specific model.""" try: - trades = get_trades_data() + trades = get_real_trades_data(ticker=ticker, days=days) model_trades = [t for t in trades if model_name.lower() in t.model.lower()] if not model_trades: @@ -199,9 +224,9 @@ async def get_trades_by_model(model_name: str): "total_profit": round(total_profit, 2), "profitable_trades": profitable_trades, "win_rate": round(win_rate, 2), - "average_profit": round(total_profit / total_trades, 2) - if total_trades > 0 - else 0, + "average_profit": ( + round(total_profit / total_trades, 2) if total_trades > 0 else 0 + ), }, } except HTTPException: diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 925c227..57d4af6 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -15,6 +15,18 @@ class TradeType(str, Enum): SELL = "sell" +class ActionType(str, Enum): + BUY = "BUY" + SELL = "SELL" + HOLD = "HOLD" + + +class ActionStatus(str, Enum): + PENDING = "pending" + EXECUTED = "executed" + EVALUATED = "evaluated" + + class NewsImpact(str, Enum): HIGH = "high" MEDIUM = "medium" @@ -97,6 +109,33 @@ class NewsItemCreate(BaseModel): url: str +class Portfolio(BaseModel): + cash: float + holdings: dict[str, float] # ticker -> quantity + + +class TradingAction(BaseModel): + id: str + agent_id: str # model identifier (e.g., "claude-3.5-sonnet") + agent_name: str # human readable name + agent_type: str = "trading_agent" + action: ActionType + description: str + status: ActionStatus + timestamp: datetime + targets: list[str] # tickers affected + metadata: dict + + +class SystemLogStats(BaseModel): + total_actions: int + pending_actions: int + executed_actions: int + evaluated_actions: int + models_active: int + recent_activity: int # last hour + + class APIResponse(BaseModel): success: bool message: str diff --git a/backend/app/trading_actions.py b/backend/app/trading_actions.py new file mode 100644 index 0000000..93c323d --- /dev/null +++ b/backend/app/trading_actions.py @@ -0,0 +1,178 @@ +""" +Trading actions management for system logs. +Handles portfolio tracking and action logging for trading models. +""" + +import uuid +from datetime import datetime, timedelta +from typing import Dict, List + +from app.schemas import ActionStatus, ActionType, Portfolio + +# In-memory storage for model portfolios and trading actions +# In production, this would be a database +MODEL_PORTFOLIOS: Dict[str, Portfolio] = { + "claude-3.5-sonnet": Portfolio(cash=100.0, holdings={}), + "gpt-4": Portfolio(cash=100.0, holdings={}), + "gemini-1.5-pro": Portfolio(cash=100.0, holdings={}), + "claude-4-haiku": Portfolio(cash=100.0, holdings={}), +} + +# Trading actions storage - list of TradingAction dicts +TRADING_ACTIONS: List[Dict] = [] + + +def add_trading_action( + model_id: str, + action_type: ActionType, + ticker: str, + quantity: float, + price: float, + reasoning: str = "", +) -> str: + """Add a trading action to system logs when a model makes a decision.""" + + # Get current portfolio + portfolio_before = MODEL_PORTFOLIOS.get( + model_id, Portfolio(cash=100.0, holdings={}) + ) + + # Create action + action_id = str(uuid.uuid4())[:8] + + # Calculate portfolio changes + portfolio_after = Portfolio( + cash=portfolio_before.cash, holdings=portfolio_before.holdings.copy() + ) + + total_cost = quantity * price + + if action_type == ActionType.BUY: + if portfolio_after.cash >= total_cost: + portfolio_after.cash -= total_cost + portfolio_after.holdings[ticker] = ( + portfolio_after.holdings.get(ticker, 0) + quantity + ) + description = f"BUY {quantity} shares of {ticker} at ${price:.2f} (Total: ${total_cost:.2f})" + status = ActionStatus.PENDING + else: + description = f"ATTEMPTED BUY {quantity} shares of {ticker} - Insufficient funds (${portfolio_after.cash:.2f} < ${total_cost:.2f})" + status = ActionStatus.PENDING + + elif action_type == ActionType.SELL: + current_holdings = portfolio_after.holdings.get(ticker, 0) + if current_holdings >= quantity: + portfolio_after.cash += total_cost + portfolio_after.holdings[ticker] = current_holdings - quantity + if portfolio_after.holdings[ticker] == 0: + del portfolio_after.holdings[ticker] + description = f"SELL {quantity} shares of {ticker} at ${price:.2f} (Total: ${total_cost:.2f})" + status = ActionStatus.PENDING + else: + description = f"ATTEMPTED SELL {quantity} shares of {ticker} - Insufficient holdings ({current_holdings} shares)" + status = ActionStatus.PENDING + + elif action_type == ActionType.HOLD: + description = f"HOLD positions for {ticker}" + status = ActionStatus.PENDING + + # Create trading action + action = { + "id": action_id, + "agent_id": model_id, + "agent_name": _get_model_name(model_id), + "agent_type": "trading_agent", + "action": action_type.value, + "description": description, + "status": status.value, + "timestamp": datetime.now().isoformat(), + "targets": [ticker], + "metadata": { + "ticker": ticker, + "action_type": action_type.value, + "quantity": quantity, + "price": price, + "reasoning": reasoning, + "portfolio_before": { + "cash": portfolio_before.cash, + "holdings": dict(portfolio_before.holdings), + }, + "portfolio_after": { + "cash": portfolio_after.cash, + "holdings": dict(portfolio_after.holdings), + }, + "total_cost": total_cost, + }, + } + + # Add to storage + TRADING_ACTIONS.append(action) + + # Update portfolio only if action is valid + if action_type != ActionType.HOLD and total_cost > 0: + if (action_type == ActionType.BUY and portfolio_before.cash >= total_cost) or ( + action_type == ActionType.SELL + and portfolio_before.holdings.get(ticker, 0) >= quantity + ): + MODEL_PORTFOLIOS[model_id] = portfolio_after + + return action_id + + +def _get_model_name(model_id: str) -> str: + """Get human readable model name.""" + name_mapping = { + "claude-3.5-sonnet": "Claude 3.5 Sonnet", + "gpt-4": "GPT-4", + "gemini-1.5-pro": "Gemini 1.5 Pro", + "claude-4-haiku": "Claude 4 Haiku", + } + return name_mapping.get(model_id, model_id) + + +def get_model_portfolio(model_id: str) -> Portfolio: + """Get current portfolio for a model.""" + return MODEL_PORTFOLIOS.get(model_id, Portfolio(cash=100.0, holdings={})) + + +def update_action_status(action_id: str, new_status: ActionStatus) -> bool: + """Update the status of a trading action.""" + for action in TRADING_ACTIONS: + if action["id"] == action_id: + action["status"] = new_status.value + return True + return False + + +def get_trading_actions( + agent_type: str = None, status: str = None, limit: int = 100, hours: int = 24 +) -> List[Dict]: + """Get trading actions from system logs - ONLY trading decisions.""" + + # Filter actions by time window + cutoff_time = datetime.now() - timedelta(hours=hours) + recent_actions = [] + + for action in TRADING_ACTIONS: + action_time = datetime.fromisoformat(action["timestamp"]) + if action_time >= cutoff_time: + recent_actions.append(action) + + # Apply filters + filtered_actions = recent_actions + + if agent_type: + filtered_actions = [ + a for a in filtered_actions if a["agent_type"] == agent_type + ] + + if status: + filtered_actions = [a for a in filtered_actions if a["status"] == status] + + # Sort by timestamp (newest first) and limit + filtered_actions.sort(key=lambda x: x["timestamp"], reverse=True) + + return filtered_actions[:limit] + + +# Sample actions removed - system logs will be empty until models generate real actions diff --git a/frontend/src/components/ModelsDisplay.tsx b/frontend/src/components/ModelsDisplay.tsx index dd0f093..0179f1c 100644 --- a/frontend/src/components/ModelsDisplay.tsx +++ b/frontend/src/components/ModelsDisplay.tsx @@ -35,7 +35,7 @@ const ModelsDisplay: React.FC = ({ setLoading(true); try { // Fetch real LLM models data - const response = await fetch('http://localhost:8000/api/models/real'); + const response = await fetch('http://localhost:8000/api/models/'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -79,6 +79,7 @@ const ModelsDisplay: React.FC = ({ const interval = setInterval(fetchModels, 24 * 60 * 60 * 1000); return () => clearInterval(interval); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const getStatusColor = (status: string) => { diff --git a/frontend/src/components/News.tsx b/frontend/src/components/News.tsx index 7376f2c..685f607 100644 --- a/frontend/src/components/News.tsx +++ b/frontend/src/components/News.tsx @@ -25,7 +25,7 @@ const News: React.FC = ({ newsData, setNewsData, lastRefresh, setLast setLoading(true); try { // Fetch real news data instead of sample data - const response = await fetch('http://localhost:8000/api/news/real?query=stock%20market&days=7'); + const response = await fetch('http://localhost:8000/api/news/?query=stock%20market&days=7'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -66,6 +66,7 @@ const News: React.FC = ({ newsData, setNewsData, lastRefresh, setLast const interval = setInterval(fetchNews, 60 * 60 * 1000); return () => clearInterval(interval); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const getImpactColor = (impact: string) => { diff --git a/frontend/src/components/SocialMedia.tsx b/frontend/src/components/SocialMedia.tsx index e0da2c3..c9bac10 100644 --- a/frontend/src/components/SocialMedia.tsx +++ b/frontend/src/components/SocialMedia.tsx @@ -44,7 +44,7 @@ const SocialMedia: React.FC = ({ setLoading(true); try { // Fetch real social media data from Reddit - get 5 posts from each category - const response = await fetch('http://localhost:8000/api/social/real?category=all'); + const response = await fetch('http://localhost:8000/api/social/?category=all'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -92,6 +92,7 @@ const SocialMedia: React.FC = ({ const interval = setInterval(fetchSocialPosts, 24 * 60 * 60 * 1000); return () => clearInterval(interval); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const getPlatformColor = (platform: string) => { diff --git a/frontend/src/components/SystemLog.tsx b/frontend/src/components/SystemLog.tsx index 17d7512..d40a92e 100644 --- a/frontend/src/components/SystemLog.tsx +++ b/frontend/src/components/SystemLog.tsx @@ -4,15 +4,28 @@ interface SystemAction { id: string; agentId: string; agentName: string; - agentType: 'data_collector' | 'trading_agent' | 'monitoring_agent' | 'risk_manager' | 'sentiment_analyzer' | 'market_analyzer'; - action: string; + agentType: 'trading_agent'; + action: 'BUY' | 'SELL' | 'HOLD'; description: string; - status: 'success' | 'warning' | 'error' | 'info'; + status: 'pending' | 'executed' | 'evaluated'; timestamp: Date; - duration?: number; // in milliseconds - dataProcessed?: number; // number of records processed - targets?: string[]; // affected symbols, markets, etc. - metadata?: Record; // additional context + targets?: string[]; // affected tickers + metadata?: { + ticker: string; + action_type: string; + quantity: number; + price: number; + reasoning: string; + portfolio_before: { + cash: number; + holdings: Record; + }; + portfolio_after: { + cash: number; + holdings: Record; + }; + total_cost: number; + }; } interface SystemLogProps { @@ -22,8 +35,8 @@ interface SystemLogProps { const SystemLog: React.FC = ({ lastRefresh }) => { const [actions, setActions] = useState([]); const [loading, setLoading] = useState(false); - const [selectedAgentType, setSelectedAgentType] = useState<'all' | 'data_collector' | 'trading_agent' | 'monitoring_agent' | 'risk_manager' | 'sentiment_analyzer' | 'market_analyzer'>('all'); - const [selectedStatus, setSelectedStatus] = useState<'all' | 'success' | 'warning' | 'error' | 'info'>('all'); + const [selectedAgentType, setSelectedAgentType] = useState<'all' | 'trading_agent'>('all'); + const [selectedStatus, setSelectedStatus] = useState<'all' | 'pending' | 'executed' | 'evaluated'>('all'); const fetchSystemLog = async () => { setLoading(true); @@ -70,44 +83,50 @@ const SystemLog: React.FC = ({ lastRefresh }) => { const getAgentTypeColor = (agentType: string) => { switch (agentType) { - case 'data_collector': return '#17a2b8'; case 'trading_agent': return '#28a745'; - case 'monitoring_agent': return '#ffc107'; - case 'risk_manager': return '#dc3545'; - case 'sentiment_analyzer': return '#6f42c1'; - case 'market_analyzer': return '#fd7e14'; default: return '#6c757d'; } }; const getAgentTypeIcon = (agentType: string) => { switch (agentType) { - case 'data_collector': return '📊'; case 'trading_agent': return '🤖'; - case 'monitoring_agent': return '👁️'; - case 'risk_manager': return '🛡️'; - case 'sentiment_analyzer': return '💭'; - case 'market_analyzer': return '📈'; default: return '⚙️'; } }; + const getActionIcon = (action: string) => { + switch (action) { + case 'BUY': return '📈'; + case 'SELL': return '📉'; + case 'HOLD': return '⏸️'; + default: return '📝'; + } + }; + + const getActionColor = (action: string) => { + switch (action) { + case 'BUY': return '#28a745'; + case 'SELL': return '#dc3545'; + case 'HOLD': return '#ffc107'; + default: return '#6c757d'; + } + }; + const getStatusColor = (status: string) => { switch (status) { - case 'success': return '#28a745'; - case 'warning': return '#ffc107'; - case 'error': return '#dc3545'; - case 'info': return '#17a2b8'; + case 'pending': return '#ffc107'; + case 'executed': return '#17a2b8'; + case 'evaluated': return '#28a745'; default: return '#6c757d'; } }; const getStatusIcon = (status: string) => { switch (status) { - case 'success': return '✅'; - case 'warning': return '⚠️'; - case 'error': return '❌'; - case 'info': return 'ℹ️'; + case 'pending': return '⏳'; + case 'executed': return '✅'; + case 'evaluated': return '📊'; default: return '📝'; } }; @@ -142,20 +161,21 @@ const SystemLog: React.FC = ({ lastRefresh }) => { .slice(0, 20); // Show only the 20 most recent actions const agentTypeStats = { - data_collector: actions.filter(a => a.agentType === 'data_collector').length, trading_agent: actions.filter(a => a.agentType === 'trading_agent').length, - monitoring_agent: actions.filter(a => a.agentType === 'monitoring_agent').length, - risk_manager: actions.filter(a => a.agentType === 'risk_manager').length, - sentiment_analyzer: actions.filter(a => a.agentType === 'sentiment_analyzer').length, - market_analyzer: actions.filter(a => a.agentType === 'market_analyzer').length, total: actions.length }; const statusStats = { - success: actions.filter(a => a.status === 'success').length, - warning: actions.filter(a => a.status === 'warning').length, - error: actions.filter(a => a.status === 'error').length, - info: actions.filter(a => a.status === 'info').length, + pending: actions.filter(a => a.status === 'pending').length, + executed: actions.filter(a => a.status === 'executed').length, + evaluated: actions.filter(a => a.status === 'evaluated').length, + total: actions.length + }; + + const actionStats = { + buy: actions.filter(a => a.action === 'BUY').length, + sell: actions.filter(a => a.action === 'SELL').length, + hold: actions.filter(a => a.action === 'HOLD').length, total: actions.length }; @@ -169,114 +189,9 @@ const SystemLog: React.FC = ({ lastRefresh }) => { - {/* Agent Type Filter */} -
-

Agent Types

-
- - - - - - - -
-
- {/* Status Filter */}
-

Status

+

Trading Action Status

-
+ {/* Action Summary */} +
+

Action Summary

+
+ + 📈 {actionStats.buy} BUY + + + 📉 {actionStats.sell} SELL + + + ⏸️ {actionStats.hold} HOLD + +
+
+
{recentActions.map(action => (
= ({ lastRefresh }) => {
{getAgentTypeIcon(action.agentType)} + {getActionIcon(action.action)} {getStatusIcon(action.status)}
{action.agentName}
- {action.action} + {action.action} {action.targets ? action.targets[0] : ''}
= ({ lastRefresh }) => { textTransform: 'uppercase' }} > - {action.agentType.replace('_', ' ')} + {action.action} + + + {action.status} {formatTimeAgo(action.timestamp)} @@ -401,22 +353,39 @@ const SystemLog: React.FC = ({ lastRefresh }) => {
- {action.duration && ( - ⏱️ {formatDuration(action.duration)} - )} - {action.dataProcessed && ( - 📊 {action.dataProcessed.toLocaleString()} records + {action.metadata && ( + <> + 💰 ${action.metadata.price.toFixed(2)} + 📊 {action.metadata.quantity} shares + {action.metadata.total_cost > 0 && ( + 💵 ${action.metadata.total_cost.toFixed(2)} total + )} + )} {action.targets && action.targets.length > 0 && ( 🎯 {action.targets.join(', ')} )}
- ID: {action.agentId} + Model: {action.agentId}
- {action.metadata && Object.keys(action.metadata).length > 0 && ( + {action.metadata?.reasoning && ( +
+ Reasoning: {action.metadata.reasoning} +
+ )} + + {action.metadata && (action.metadata.portfolio_before || action.metadata.portfolio_after) && (
= ({ lastRefresh }) => { marginTop: '6px' }}>
- Details -
-                    {JSON.stringify(action.metadata, null, 2)}
-                  
+ Portfolio Changes +
+ {action.metadata.portfolio_before && ( +
+
Before:
+
Cash: ${action.metadata.portfolio_before.cash.toFixed(2)}
+
Holdings: {Object.keys(action.metadata.portfolio_before.holdings).length > 0 + ? Object.entries(action.metadata.portfolio_before.holdings) + .map(([ticker, qty]) => `${ticker}: ${qty}`) + .join(', ') + : 'None'}
+
+ )} + {action.metadata.portfolio_after && ( +
+
After:
+
Cash: ${action.metadata.portfolio_after.cash.toFixed(2)}
+
Holdings: {Object.keys(action.metadata.portfolio_after.holdings).length > 0 + ? Object.entries(action.metadata.portfolio_after.holdings) + .map(([ticker, qty]) => `${ticker}: ${qty}`) + .join(', ') + : 'None'}
+
+ )} +
)} @@ -445,7 +435,11 @@ const SystemLog: React.FC = ({ lastRefresh }) => { background: '#f8f9fa', borderRadius: '8px' }}> -

No system actions found for the selected filters.

+

No Trading Actions Yet

+

Trading models haven't generated any BUY/SELL/HOLD decisions yet.

+

+ Actions will appear here when models analyze market data and make trading decisions. +

)}
diff --git a/trading_bench/evaluators/__init__.py b/trading_bench/evaluators/__init__.py index a59ce6f..750abf1 100644 --- a/trading_bench/evaluators/__init__.py +++ b/trading_bench/evaluators/__init__.py @@ -15,12 +15,7 @@ from .base_evaluator import BaseEvaluator, PositionTracker # Polymarket evaluation -from .polymarket_evaluator import ( - PolymarketEvaluator, - analyze_market_efficiency, - calculate_kelly_criterion, - eval_polymarket, -) +from .polymarket_evaluator import PolymarketEvaluator, eval_polymarket # Stock evaluation from .stock_evaluator import StockEvaluator, eval_stock @@ -39,9 +34,6 @@ # Polymarket evaluation "PolymarketEvaluator", "eval_polymarket", - # Utility functions - "calculate_kelly_criterion", - "analyze_market_efficiency", ] # Version info