diff --git a/.gitignore b/.gitignore index d929ad86..d3aca358 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ node_modules/ # Frontend dev artifacts .frontend-dev/ + +# Runtime config +schedule_config.json diff --git a/01-dashboard.png b/01-dashboard.png new file mode 100644 index 00000000..85a7e8ba Binary files /dev/null and b/01-dashboard.png differ diff --git a/02-settings-modal.png b/02-settings-modal.png new file mode 100644 index 00000000..539c50f2 Binary files /dev/null and b/02-settings-modal.png differ diff --git a/03-stock-detail-overview.png b/03-stock-detail-overview.png new file mode 100644 index 00000000..5cf6321a Binary files /dev/null and b/03-stock-detail-overview.png differ diff --git a/04-analysis-pipeline.png b/04-analysis-pipeline.png new file mode 100644 index 00000000..5002edba Binary files /dev/null and b/04-analysis-pipeline.png differ diff --git a/05-debates-tab.png b/05-debates-tab.png new file mode 100644 index 00000000..23eab428 Binary files /dev/null and b/05-debates-tab.png differ diff --git a/06-investment-debate-expanded.png b/06-investment-debate-expanded.png new file mode 100644 index 00000000..06bb4a67 Binary files /dev/null and b/06-investment-debate-expanded.png differ diff --git a/07-data-sources-tab.png b/07-data-sources-tab.png new file mode 100644 index 00000000..2906f052 Binary files /dev/null and b/07-data-sources-tab.png differ diff --git a/08-dashboard-dark-mode.png b/08-dashboard-dark-mode.png new file mode 100644 index 00000000..f6019fcf Binary files /dev/null and b/08-dashboard-dark-mode.png differ diff --git a/09-how-it-works.png b/09-how-it-works.png new file mode 100644 index 00000000..eea55595 Binary files /dev/null and b/09-how-it-works.png differ diff --git a/10-history-page.png b/10-history-page.png new file mode 100644 index 00000000..cce0b712 Binary files /dev/null and b/10-history-page.png differ diff --git a/11-history-stocks-expanded.png b/11-history-stocks-expanded.png new file mode 100644 index 00000000..5833d96b Binary files /dev/null and b/11-history-stocks-expanded.png differ diff --git a/README.md b/README.md index f9774110..84cd4002 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ npm run dev # http://localhost:5173 | **Stock Ranking (1-50)** | Composite scoring algorithm ranks stocks from best to worst investment opportunity | | **Analysis Pipeline** | 12-step visualization showing data collection, agent analysis, debate, and decision | | **Investment Debates** | Full bull vs bear debate transcripts with research manager synthesis | -| **Backtesting** | Prediction accuracy tracking, risk metrics (Sharpe, drawdown), win/loss ratios | +| **Backtesting** | Prediction accuracy tracking, risk metrics (Sharpe, drawdown), win/loss ratios, date backtest runner with cancel support | | **Portfolio Simulator** | Paper trading simulation with Zerodha-accurate brokerage charges and Nifty50 benchmarking | | **Settings Panel** | Configure LLM provider (Claude/OpenAI), model tiers, debate rounds, parallel workers | | **Dark Mode** | Automatic system theme detection with manual toggle | diff --git a/cli/main.py b/cli/main.py index 3f4ddc0c..279a220e 100644 --- a/cli/main.py +++ b/cli/main.py @@ -196,7 +196,7 @@ def update_display(layout, spinner_text=None): layout["header"].update( Panel( "[bold green]Welcome to TradingAgents CLI[/bold green]\n" - "[dim]© [Tauric Research](https://github.com/TauricResearch)[/dim]", + "[dim]© [hjlabs.in](https://hjlabs.in)[/dim]", title="Welcome to TradingAgents", border_style="green", padding=(1, 2), @@ -408,7 +408,7 @@ def get_user_selections(): welcome_content += "[bold]Workflow Steps:[/bold]\n" welcome_content += "I. Analyst Team → II. Research Team → III. Trader → IV. Risk Management → V. Portfolio Management\n\n" welcome_content += ( - "[dim]Built by [Tauric Research](https://github.com/TauricResearch)[/dim]" + "[dim]Built by [hjlabs.in](https://hjlabs.in)[/dim]" ) # Create and center the welcome box diff --git a/debug-dark-after-fix.png b/debug-dark-after-fix.png new file mode 100644 index 00000000..5392f20e Binary files /dev/null and b/debug-dark-after-fix.png differ diff --git a/debug-dark-on-light-system.png b/debug-dark-on-light-system.png new file mode 100644 index 00000000..131e69fe Binary files /dev/null and b/debug-dark-on-light-system.png differ diff --git a/debug-light-after-fix.png b/debug-light-after-fix.png new file mode 100644 index 00000000..f98e6578 Binary files /dev/null and b/debug-light-after-fix.png differ diff --git a/frontend/README.md b/frontend/README.md index 17b788a1..3199506e 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -80,12 +80,19 @@ Track AI performance over time with comprehensive analytics: - **Prediction Accuracy**: Overall and per-recommendation-type accuracy - **Accuracy Trend**: Visualize accuracy over time - **Risk Metrics**: Sharpe ratio, max drawdown, win rate -- **Portfolio Simulator**: Test different investment amounts +- **Portfolio Simulator**: Test different investment amounts with Zerodha-accurate brokerage charges - **AI vs Nifty50**: Compare AI strategy performance against the index -- **Return Distribution**: Histogram of next-day returns +- **Return Distribution**: Histogram of hold-period returns +- **Date Backtest Runner**: Run AI analysis for any date directly from the History page +- **Cancel Support**: Cancel in-progress bulk analysis ![History Page](docs/screenshots/10-history-page.png) +#### Date Selection & Stock List +Select any date to view all 50 ranked stocks with decisions, hold periods, and returns: + +![History Stocks Expanded](docs/screenshots/11-history-stocks-expanded.png) + ## Tech Stack - **Frontend**: React 18 + TypeScript + Vite diff --git a/frontend/backend/database.py b/frontend/backend/database.py index 98ab787d..c031466c 100644 --- a/frontend/backend/database.py +++ b/frontend/backend/database.py @@ -1219,6 +1219,42 @@ def get_backtest_results_by_date(date: str) -> list: conn.close() +def get_all_backtest_results_grouped() -> dict: + """Get all backtest results grouped by date for the History page bundle. + + Returns: { date: { symbol: { return_1d, return_1w, return_1m, return_at_hold, hold_days, prediction_correct, decision } } } + """ + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT date, symbol, decision, return_1d, return_1w, return_1m, + return_at_hold, hold_days, prediction_correct, + price_at_prediction + FROM backtest_results + ORDER BY date + """) + + grouped: dict = {} + for row in cursor.fetchall(): + date = row['date'] + if date not in grouped: + grouped[date] = {} + grouped[date][row['symbol']] = { + 'return_1d': row['return_1d'], + 'return_1w': row['return_1w'], + 'return_1m': row['return_1m'], + 'return_at_hold': row['return_at_hold'], + 'hold_days': row['hold_days'] if 'hold_days' in row.keys() else None, + 'prediction_correct': bool(row['prediction_correct']) if row['prediction_correct'] is not None else None, + 'decision': row['decision'], + } + return grouped + finally: + conn.close() + + def get_all_backtest_results() -> list: """Get all backtest results for accuracy calculation.""" conn = get_connection() diff --git a/frontend/backend/recommendations.db b/frontend/backend/recommendations.db index d2152884..393c3ac8 100644 Binary files a/frontend/backend/recommendations.db and b/frontend/backend/recommendations.db differ diff --git a/frontend/backend/server.py b/frontend/backend/server.py index 31efca37..cbdd48b9 100644 --- a/frontend/backend/server.py +++ b/frontend/backend/server.py @@ -8,7 +8,7 @@ import database as db import sys import os from pathlib import Path -from datetime import datetime +from datetime import datetime, timedelta import threading from concurrent.futures import ThreadPoolExecutor, as_completed import asyncio @@ -549,6 +549,29 @@ bulk_analysis_state = { "cancelled": False # Flag to signal cancellation } +# Auto-analyze schedule config +SCHEDULE_FILE = Path(__file__).parent / "schedule_config.json" + +def _load_schedule_config(): + """Load schedule config from JSON file.""" + if SCHEDULE_FILE.exists(): + try: + with open(SCHEDULE_FILE, "r") as f: + return json.load(f) + except Exception: + pass + return {"enabled": False, "time": "09:00", "config": {}, "last_run_date": None} + +def _save_schedule_config(config): + """Persist schedule config to JSON file.""" + try: + with open(SCHEDULE_FILE, "w") as f: + json.dump(config, f, indent=2) + except Exception as e: + print(f"[AutoSchedule] Failed to save config: {e}") + +schedule_config = _load_schedule_config() + # List of Nifty 50 stocks NIFTY_50_SYMBOLS = [ "RELIANCE", "TCS", "HDFCBANK", "INFY", "ICICIBANK", "HINDUNILVR", "ITC", "SBIN", @@ -919,6 +942,69 @@ async def cancel_analysis(symbol: str): } +# ============== History Bundle Endpoint ============== + +# In-memory cache for Nifty50 index prices (fetched once, refreshed lazily) +_nifty50_cache = {"prices": {}, "fetched_at": None} + +def _fetch_nifty50_prices_sync(): + """Fetch Nifty50 index prices (called once and cached).""" + try: + import yfinance as yf + from datetime import timedelta + + dates = db.get_all_dates() + if not dates: + return {} + + start_date = (datetime.strptime(min(dates), "%Y-%m-%d") - timedelta(days=7)).strftime("%Y-%m-%d") + end_date = (datetime.strptime(max(dates), "%Y-%m-%d") + timedelta(days=7)).strftime("%Y-%m-%d") + + nifty = yf.Ticker("^NSEI") + hist = nifty.history(start=start_date, end=end_date, interval="1d") + + prices = {} + for idx, row in hist.iterrows(): + date_str = idx.strftime("%Y-%m-%d") + prices[date_str] = round(float(row['Close']), 2) + return prices + except Exception: + return {} + + +@app.get("/history/bundle") +async def get_history_bundle(): + """Return ALL data the History page needs in a single response. + + Combines: recommendations + all backtest results + accuracy metrics. + Everything comes from SQLite (instant), no yfinance calls. + Nifty50 prices are served from cache. + """ + recommendations = db.get_all_recommendations() + backtest_by_date = db.get_all_backtest_results_grouped() + accuracy = db.calculate_accuracy_metrics() + + # Serve Nifty50 from cache, refresh in background if stale + nifty_prices = _nifty50_cache.get("prices", {}) + if not _nifty50_cache.get("fetched_at"): + # First request — return empty, trigger background fetch + def bg_fetch(): + prices = _fetch_nifty50_prices_sync() + _nifty50_cache["prices"] = prices + _nifty50_cache["fetched_at"] = datetime.now().isoformat() + thread = threading.Thread(target=bg_fetch, daemon=True) + thread.start() + else: + nifty_prices = _nifty50_cache["prices"] + + return { + "recommendations": recommendations, + "backtest_by_date": backtest_by_date, + "accuracy": accuracy, + "nifty50_prices": nifty_prices, + } + + # ============== Backtest Endpoints ============== # NOTE: Static routes must come BEFORE parameterized routes to avoid # "accuracy" being matched as a {date} parameter. @@ -930,6 +1016,149 @@ async def get_accuracy_metrics(): return metrics +@app.get("/backtest/{date}/detailed") +async def get_detailed_backtest(date: str): + """Get enriched backtest data with live prices, formulas, agent reports, and debate summaries.""" + import yfinance as yf + + rec = db.get_recommendation_by_date(date) + if not rec or 'analysis' not in rec: + return {"date": date, "total_stocks": 0, "stocks": []} + + analysis = rec['analysis'] + backtest_results = db.get_backtest_results_by_date(date) + bt_by_symbol = {r['symbol']: r for r in backtest_results} + + pred_date = datetime.strptime(date, '%Y-%m-%d') + today = datetime.now() + + # Collect symbols that need live prices (active hold periods) + symbols_needing_live = [] + for symbol, stock_data in analysis.items(): + hold_days = stock_data.get('hold_days') or 0 + hold_end = pred_date + timedelta(days=hold_days) if hold_days > 0 else pred_date + timedelta(days=1) + if today < hold_end: + symbols_needing_live.append(symbol) + + # Batch-fetch live prices for active holds + live_prices = {} + if symbols_needing_live: + def fetch_live_batch(): + for sym in symbols_needing_live: + try: + yf_sym = sym if '.' in sym else f"{sym}.NS" + t = yf.Ticker(yf_sym) + hist = t.history(period='1d') + if not hist.empty: + live_prices[sym] = round(float(hist['Close'].iloc[-1]), 2) + except Exception: + pass + fetch_live_batch() + + stocks = [] + for symbol, stock_data in analysis.items(): + decision = stock_data.get('decision', 'HOLD') + confidence = stock_data.get('confidence', 'MEDIUM') + risk = stock_data.get('risk', 'MEDIUM') + hold_days = stock_data.get('hold_days') or 0 + raw_analysis = stock_data.get('raw_analysis', '') + + bt = bt_by_symbol.get(symbol, {}) + price_pred = bt.get('price_at_prediction') + + # Calculate hold period status + hold_end_date = pred_date + timedelta(days=hold_days) if hold_days > 0 else pred_date + timedelta(days=1) + days_elapsed = (today - pred_date).days + hold_period_active = today < hold_end_date and hold_days > 0 + + # Determine display price and return + price_current = live_prices.get(symbol) + price_at_hold_end = None + return_current = None + return_at_hold = bt.get('return_at_hold') + + if price_pred: + if hold_period_active and price_current: + return_current = round(((price_current - price_pred) / price_pred) * 100, 2) + elif not hold_period_active: + # Hold completed — use stored data + if return_at_hold is not None: + price_at_hold_end = round(price_pred * (1 + return_at_hold / 100), 2) + elif bt.get('return_1d') is not None: + return_current = bt['return_1d'] + + # Build formula string + formula = "" + if price_pred: + if hold_period_active and price_current: + ret = return_current or 0 + sign = "+" if ret >= 0 else "" + formula = f"Return = (₹{price_current} - ₹{price_pred}) / ₹{price_pred} × 100 = {sign}{ret}%" + elif return_at_hold is not None: + p_end = price_at_hold_end or round(price_pred * (1 + return_at_hold / 100), 2) + sign = "+" if return_at_hold >= 0 else "" + formula = f"Return = (₹{p_end} - ₹{price_pred}) / ₹{price_pred} × 100 = {sign}{return_at_hold}%" + elif bt.get('return_1d') is not None: + p_1d = bt.get('price_1d_later', 0) + r_1d = bt['return_1d'] + sign = "+" if r_1d >= 0 else "" + formula = f"Return = (₹{p_1d} - ₹{price_pred}) / ₹{price_pred} × 100 = {sign}{r_1d}%" + + # Prediction correctness + prediction_correct = bt.get('prediction_correct') + if hold_period_active: + prediction_correct = None # Can't judge while hold is active + + # Agent reports (condensed) + agent_summary = {} + try: + reports = db.get_agent_reports(date, symbol) + for agent_type, report_data in reports.items(): + content = report_data.get('report_content', '') + # Take first 300 chars as summary + agent_summary[agent_type] = content[:300] + ('...' if len(content) > 300 else '') + except Exception: + pass + + # Debate summary + debate_summary = {} + try: + debates = db.get_debate_history(date, symbol) + for debate_type, debate_data in debates.items(): + judge = debate_data.get('judge_decision', '') + judge_short = judge[:200] + ('...' if len(judge) > 200 else '') if judge else '' + debate_summary[debate_type] = judge_short + except Exception: + pass + + stocks.append({ + "symbol": symbol, + "company_name": stock_data.get('company_name', symbol), + "rank": stock_data.get('rank'), + "decision": decision, + "confidence": confidence, + "risk": risk, + "hold_days": hold_days, + "hold_days_elapsed": min(days_elapsed, hold_days) if hold_days > 0 else days_elapsed, + "hold_period_active": hold_period_active, + "price_at_prediction": price_pred, + "price_current": price_current, + "price_at_hold_end": price_at_hold_end, + "return_current": return_current, + "return_at_hold": return_at_hold, + "prediction_correct": prediction_correct, + "formula": formula, + "raw_analysis": raw_analysis[:500] if raw_analysis else '', + "agent_summary": agent_summary, + "debate_summary": debate_summary, + }) + + # Sort by rank + stocks.sort(key=lambda s: s.get('rank') or 999) + + return {"date": date, "total_stocks": len(stocks), "stocks": stocks} + + @app.get("/backtest/{date}/{symbol}") async def get_backtest_result(date: str, symbol: str): """Get backtest result for a specific stock and date. @@ -1045,11 +1274,198 @@ async def get_nifty50_history(): return {"dates": [], "prices": {}, "error": str(e)} +# ============== Schedule Endpoints ============== + +class ScheduleRequest(BaseModel): + enabled: bool = False + time: str = "09:00" + timezone: str = "Asia/Kolkata" + config: dict = {} + +@app.post("/settings/schedule") +async def set_schedule(request: ScheduleRequest): + """Set the auto-analyze schedule.""" + global schedule_config + schedule_config["enabled"] = request.enabled + schedule_config["time"] = request.time + schedule_config["timezone"] = request.timezone + schedule_config["config"] = request.config + _save_schedule_config(schedule_config) + status = "enabled" if request.enabled else "disabled" + print(f"[AutoSchedule] Schedule updated: {request.time} {request.timezone} ({status})") + return {"status": "ok", "message": f"Schedule {status} at {request.time} {request.timezone}"} + +@app.get("/settings/schedule") +async def get_schedule(): + """Get the current auto-analyze schedule.""" + return { + "enabled": schedule_config.get("enabled", False), + "time": schedule_config.get("time", "09:00"), + "timezone": schedule_config.get("timezone", "Asia/Kolkata"), + "config": schedule_config.get("config", {}), + "last_run_date": schedule_config.get("last_run_date"), + } + + +# ============== Scheduler Thread ============== + +def _auto_analyze_scheduler(): + """Background thread that triggers Analyze All at the scheduled time daily.""" + from zoneinfo import ZoneInfo + global schedule_config, bulk_analysis_state + print("[AutoSchedule] Scheduler thread started") + + while True: + try: + time.sleep(30) + + if not schedule_config.get("enabled"): + continue + + # Get current time in the configured timezone + tz_name = schedule_config.get("timezone", "Asia/Kolkata") + try: + tz = ZoneInfo(tz_name) + except Exception: + tz = ZoneInfo("Asia/Kolkata") + + now = datetime.now(tz) + scheduled_time = schedule_config.get("time", "09:00") + today_str = now.strftime("%Y-%m-%d") + + # Already ran today (in the configured timezone)? + if schedule_config.get("last_run_date") == today_str: + continue + + # Parse scheduled hour:minute + try: + sched_hour, sched_minute = map(int, scheduled_time.split(":")) + except (ValueError, AttributeError): + continue + + # Check if we're within a 2-minute window of the scheduled time + current_minutes = now.hour * 60 + now.minute + scheduled_minutes = sched_hour * 60 + sched_minute + if abs(current_minutes - scheduled_minutes) > 1: + continue + + # Don't trigger if already running + if bulk_analysis_state.get("status") == "running": + print(f"[AutoSchedule] Skipping — bulk analysis already running") + continue + + print(f"[AutoSchedule] Triggering daily analysis at {scheduled_time} {tz_name}") + schedule_config["last_run_date"] = today_str + _save_schedule_config(schedule_config) + + # Build analysis config + config = schedule_config.get("config", {}) + analysis_config = { + "deep_think_model": config.get("deep_think_model", "opus"), + "quick_think_model": config.get("quick_think_model", "sonnet"), + "provider": config.get("provider", "claude_subscription"), + "api_key": config.get("api_key"), + "max_debate_rounds": config.get("max_debate_rounds", 1), + } + parallel_workers = max(1, min(5, config.get("parallel_workers", 3))) + + # Same logic as POST /analyze/all + analysis_date = today_str + already_analyzed = set(db.get_analyzed_symbols_for_date(analysis_date)) + symbols_to_analyze = [s for s in NIFTY_50_SYMBOLS if s not in already_analyzed] + + if not symbols_to_analyze: + print(f"[AutoSchedule] All stocks already analyzed for {analysis_date}") + continue + + def run_auto_bulk(): + global bulk_analysis_state + bulk_analysis_state = { + "status": "running", + "total": len(symbols_to_analyze), + "total_all": len(NIFTY_50_SYMBOLS), + "skipped": len(already_analyzed), + "completed": 0, + "failed": 0, + "current_symbols": [], + "started_at": datetime.now().isoformat(), + "completed_at": None, + "results": {}, + "parallel_workers": parallel_workers, + "cancelled": False, + } + + with ThreadPoolExecutor(max_workers=parallel_workers) as executor: + def analyze_one(symbol): + try: + if bulk_analysis_state.get("cancelled"): + return (symbol, "cancelled", None) + run_analysis_task(symbol, analysis_date, analysis_config) + max_wait = 600 + waited = 0 + while waited < max_wait: + if bulk_analysis_state.get("cancelled"): + return (symbol, "cancelled", None) + if symbol not in running_analyses: + return (symbol, "unknown", None) + status = running_analyses[symbol].get("status") + if status not in ("running", "initializing"): + return (symbol, status, None) + time.sleep(2) + waited += 2 + return (symbol, "timeout", None) + except Exception as e: + return (symbol, "error", str(e)) + + future_to_sym = { + executor.submit(analyze_one, sym): sym + for sym in symbols_to_analyze + } + bulk_analysis_state["current_symbols"] = list(symbols_to_analyze[:parallel_workers]) + + for future in as_completed(future_to_sym): + sym = future_to_sym[future] + try: + sym, status, error = future.result() + bulk_analysis_state["results"][sym] = status if not error else f"error: {error}" + if status == "completed": + bulk_analysis_state["completed"] += 1 + else: + bulk_analysis_state["failed"] += 1 + remaining = [s for s in symbols_to_analyze if s not in bulk_analysis_state["results"]] + bulk_analysis_state["current_symbols"] = remaining[:parallel_workers] + except Exception as e: + bulk_analysis_state["results"][sym] = f"error: {str(e)}" + bulk_analysis_state["failed"] += 1 + + bulk_analysis_state["status"] = "completed" + bulk_analysis_state["current_symbols"] = [] + bulk_analysis_state["completed_at"] = datetime.now().isoformat() + print(f"[AutoSchedule] Daily analysis completed: {bulk_analysis_state['completed']} succeeded, {bulk_analysis_state['failed']} failed") + + threading.Thread(target=run_auto_bulk, daemon=True).start() + + except Exception as e: + print(f"[AutoSchedule] Scheduler error: {e}") + time.sleep(60) + + @app.on_event("startup") async def startup_event(): """Rebuild daily_recommendations and trigger backtest calculations at startup.""" db.rebuild_all_daily_recommendations() + # Start auto-analyze scheduler + threading.Thread(target=_auto_analyze_scheduler, daemon=True).start() + + # Warm Nifty50 cache in background + def warm_nifty_cache(): + prices = _fetch_nifty50_prices_sync() + _nifty50_cache["prices"] = prices + _nifty50_cache["fetched_at"] = datetime.now().isoformat() + print(f"[Nifty50] Cached {len(prices)} index prices") + threading.Thread(target=warm_nifty_cache, daemon=True).start() + # Trigger backtest calculation for all dates in background def startup_backtest(): import backtest_service as bt diff --git a/frontend/docs/screenshots/01-dashboard.png b/frontend/docs/screenshots/01-dashboard.png index 4bb213b5..85a7e8ba 100644 Binary files a/frontend/docs/screenshots/01-dashboard.png and b/frontend/docs/screenshots/01-dashboard.png differ diff --git a/frontend/docs/screenshots/02-settings-modal.png b/frontend/docs/screenshots/02-settings-modal.png index 539c50f2..92275525 100644 Binary files a/frontend/docs/screenshots/02-settings-modal.png and b/frontend/docs/screenshots/02-settings-modal.png differ diff --git a/frontend/docs/screenshots/03-stock-detail-overview.png b/frontend/docs/screenshots/03-stock-detail-overview.png index 185bff8f..30943d29 100644 Binary files a/frontend/docs/screenshots/03-stock-detail-overview.png and b/frontend/docs/screenshots/03-stock-detail-overview.png differ diff --git a/frontend/docs/screenshots/04-analysis-pipeline.png b/frontend/docs/screenshots/04-analysis-pipeline.png index ff13ced7..5002edba 100644 Binary files a/frontend/docs/screenshots/04-analysis-pipeline.png and b/frontend/docs/screenshots/04-analysis-pipeline.png differ diff --git a/frontend/docs/screenshots/05-debates-tab.png b/frontend/docs/screenshots/05-debates-tab.png index 45c40c5d..23eab428 100644 Binary files a/frontend/docs/screenshots/05-debates-tab.png and b/frontend/docs/screenshots/05-debates-tab.png differ diff --git a/frontend/docs/screenshots/06-investment-debate-expanded.png b/frontend/docs/screenshots/06-investment-debate-expanded.png index 02bc602f..06bb4a67 100644 Binary files a/frontend/docs/screenshots/06-investment-debate-expanded.png and b/frontend/docs/screenshots/06-investment-debate-expanded.png differ diff --git a/frontend/docs/screenshots/07-data-sources-tab.png b/frontend/docs/screenshots/07-data-sources-tab.png index 2df93b64..2906f052 100644 Binary files a/frontend/docs/screenshots/07-data-sources-tab.png and b/frontend/docs/screenshots/07-data-sources-tab.png differ diff --git a/frontend/docs/screenshots/08-dashboard-dark-mode.png b/frontend/docs/screenshots/08-dashboard-dark-mode.png index 9ba2e382..131e69fe 100644 Binary files a/frontend/docs/screenshots/08-dashboard-dark-mode.png and b/frontend/docs/screenshots/08-dashboard-dark-mode.png differ diff --git a/frontend/docs/screenshots/09-how-it-works.png b/frontend/docs/screenshots/09-how-it-works.png index b9fe7e96..eea55595 100644 Binary files a/frontend/docs/screenshots/09-how-it-works.png and b/frontend/docs/screenshots/09-how-it-works.png differ diff --git a/frontend/docs/screenshots/10-history-page.png b/frontend/docs/screenshots/10-history-page.png index b60e1b48..cce0b712 100644 Binary files a/frontend/docs/screenshots/10-history-page.png and b/frontend/docs/screenshots/10-history-page.png differ diff --git a/frontend/docs/screenshots/11-history-stocks-expanded.png b/frontend/docs/screenshots/11-history-stocks-expanded.png index 03fb1b75..5833d96b 100644 Binary files a/frontend/docs/screenshots/11-history-stocks-expanded.png and b/frontend/docs/screenshots/11-history-stocks-expanded.png differ diff --git a/frontend/src/components/AccuracyExplainModal.tsx b/frontend/src/components/AccuracyExplainModal.tsx index 361a4278..9f22f0f2 100644 --- a/frontend/src/components/AccuracyExplainModal.tsx +++ b/frontend/src/components/AccuracyExplainModal.tsx @@ -1,4 +1,5 @@ import { X, HelpCircle, TrendingUp, TrendingDown, Minus, CheckCircle } from 'lucide-react'; +import { createPortal } from 'react-dom'; import type { AccuracyMetrics } from '../types'; interface AccuracyExplainModalProps { @@ -17,7 +18,7 @@ export default function AccuracyExplainModal({ isOpen, onClose, metrics }: Accur const holdCorrect = Math.round(metrics.hold_accuracy * metrics.total_predictions * 0.66); // ~33 hold signals const holdTotal = Math.round(metrics.total_predictions * 0.66); - return ( + return createPortal(
{/* Backdrop */}
- + , + document.body ); } diff --git a/frontend/src/components/AccuracyTrendChart.tsx b/frontend/src/components/AccuracyTrendChart.tsx index 44148fae..8c35bd19 100644 --- a/frontend/src/components/AccuracyTrendChart.tsx +++ b/frontend/src/components/AccuracyTrendChart.tsx @@ -1,5 +1,4 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; -import { getAccuracyTrend } from '../data/recommendations'; export interface AccuracyTrendPoint { date: string; @@ -12,12 +11,11 @@ export interface AccuracyTrendPoint { interface AccuracyTrendChartProps { height?: number; className?: string; - data?: AccuracyTrendPoint[]; // Optional prop for real data + data?: AccuracyTrendPoint[]; } export default function AccuracyTrendChart({ height = 200, className = '', data: propData }: AccuracyTrendChartProps) { - // Use provided data or fall back to mock data - const data = propData || getAccuracyTrend(); + const data = propData || []; if (data.length === 0) { return ( diff --git a/frontend/src/components/CumulativeReturnChart.tsx b/frontend/src/components/CumulativeReturnChart.tsx index 6bf7f04f..02292207 100644 --- a/frontend/src/components/CumulativeReturnChart.tsx +++ b/frontend/src/components/CumulativeReturnChart.tsx @@ -1,16 +1,14 @@ import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts'; -import { getCumulativeReturns } from '../data/recommendations'; import type { CumulativeReturnPoint } from '../types'; interface CumulativeReturnChartProps { height?: number; className?: string; - data?: CumulativeReturnPoint[]; // Optional prop for real data + data?: CumulativeReturnPoint[]; } export default function CumulativeReturnChart({ height = 160, className = '', data: propData }: CumulativeReturnChartProps) { - // Use provided data or fall back to mock data - const data = propData || getCumulativeReturns(); + const data = propData || []; if (data.length === 0) { return ( diff --git a/frontend/src/components/FilterPanel.tsx b/frontend/src/components/FilterPanel.tsx index 5b51ba58..ce0411b2 100644 --- a/frontend/src/components/FilterPanel.tsx +++ b/frontend/src/components/FilterPanel.tsx @@ -1,5 +1,5 @@ import { SlidersHorizontal, ArrowUpDown } from 'lucide-react'; -import { getAllSectors } from '../data/recommendations'; +import { NIFTY_50_STOCKS } from '../types'; import type { FilterState } from '../types'; interface FilterPanelProps { @@ -9,7 +9,7 @@ interface FilterPanelProps { } export default function FilterPanel({ filters, onFilterChange, className = '' }: FilterPanelProps) { - const sectors = getAllSectors(); + const sectors = ['All', ...Array.from(new Set(NIFTY_50_STOCKS.map(s => s.sector).filter(Boolean))).sort()]; const decisions: Array = ['ALL', 'BUY', 'SELL', 'HOLD']; const sortOptions: Array<{ value: FilterState['sortBy']; label: string }> = [ diff --git a/frontend/src/components/IndexComparisonChart.tsx b/frontend/src/components/IndexComparisonChart.tsx index 1545c2b7..6018c5f1 100644 --- a/frontend/src/components/IndexComparisonChart.tsx +++ b/frontend/src/components/IndexComparisonChart.tsx @@ -1,17 +1,15 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, ReferenceLine } from 'recharts'; import { TrendingUp, TrendingDown } from 'lucide-react'; -import { getCumulativeReturns } from '../data/recommendations'; import type { CumulativeReturnPoint } from '../types'; export interface IndexComparisonChartProps { height?: number; className?: string; - data?: CumulativeReturnPoint[]; // Optional prop for real data + data?: CumulativeReturnPoint[]; } export default function IndexComparisonChart({ height = 220, className = '', data: propData }: IndexComparisonChartProps) { - // Use provided data or fall back to mock data - const data = propData || getCumulativeReturns(); + const data = propData || []; if (data.length === 0) { return ( diff --git a/frontend/src/components/InfoModal.tsx b/frontend/src/components/InfoModal.tsx index 9fe2b746..b63717f7 100644 --- a/frontend/src/components/InfoModal.tsx +++ b/frontend/src/components/InfoModal.tsx @@ -1,4 +1,5 @@ import { X, Info } from 'lucide-react'; +import { createPortal } from 'react-dom'; import type { ReactNode } from 'react'; interface InfoModalProps { @@ -12,7 +13,7 @@ interface InfoModalProps { export default function InfoModal({ isOpen, onClose, title, children, icon }: InfoModalProps) { if (!isOpen) return null; - return ( + return createPortal(
{/* Backdrop */}
{/* Content */} -
+
{children}
@@ -53,7 +54,8 @@ export default function InfoModal({ isOpen, onClose, title, children, icon }: In
- + , + document.body ); } diff --git a/frontend/src/components/OverallReturnModal.tsx b/frontend/src/components/OverallReturnModal.tsx index 95b99724..79debee4 100644 --- a/frontend/src/components/OverallReturnModal.tsx +++ b/frontend/src/components/OverallReturnModal.tsx @@ -1,5 +1,5 @@ import { X, Activity } from 'lucide-react'; -import { getOverallReturnBreakdown } from '../data/recommendations'; +import { createPortal } from 'react-dom'; import CumulativeReturnChart from './CumulativeReturnChart'; import type { CumulativeReturnPoint } from '../types'; @@ -20,10 +20,9 @@ interface OverallReturnModalProps { export default function OverallReturnModal({ isOpen, onClose, breakdown: propBreakdown, cumulativeData }: OverallReturnModalProps) { if (!isOpen) return null; - // Use provided breakdown or fall back to mock data - const breakdown = propBreakdown || getOverallReturnBreakdown(); + const breakdown = propBreakdown || { dailyReturns: [], finalMultiplier: 1, finalReturn: 0, formula: '' }; - return ( + return createPortal(
{/* Backdrop */}
- + , + document.body ); } diff --git a/frontend/src/components/PortfolioSimulator.tsx b/frontend/src/components/PortfolioSimulator.tsx index c8988f94..745ea68a 100644 --- a/frontend/src/components/PortfolioSimulator.tsx +++ b/frontend/src/components/PortfolioSimulator.tsx @@ -1,7 +1,6 @@ import { useState, useMemo } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Legend, BarChart, Bar, Cell, LabelList } from 'recharts'; -import { Calculator, ChevronDown, ChevronUp, IndianRupee, Settings2, BarChart3, Info, TrendingUp, TrendingDown, ArrowRightLeft, Wallet, PiggyBank, Receipt, HelpCircle, AlertCircle } from 'lucide-react'; -import { sampleRecommendations, getNifty50IndexHistory, getBacktestResult } from '../data/recommendations'; +import { Calculator, ChevronDown, ChevronUp, IndianRupee, Settings2, BarChart3, Info, TrendingUp, TrendingDown, ArrowRightLeft, Wallet, PiggyBank, Receipt, HelpCircle } from 'lucide-react'; import { calculateBrokerage, formatINR, type BrokerageBreakdown } from '../utils/brokerageCalculator'; import InfoModal, { InfoButton } from './InfoModal'; import type { Decision, DailyRecommendation } from '../types'; @@ -9,7 +8,6 @@ import type { Decision, DailyRecommendation } from '../types'; interface PortfolioSimulatorProps { className?: string; recommendations?: DailyRecommendation[]; - isUsingMockData?: boolean; nifty50Prices?: Record; allBacktestData?: Record>; } @@ -37,7 +35,7 @@ interface TradeStats { // Smart trade counting logic using Zerodha brokerage for Equity Delivery function calculateSmartTrades( - recommendations: typeof sampleRecommendations, + recommendations: DailyRecommendation[], mode: InvestmentMode, startingAmount: number, nifty50Prices?: Record, @@ -48,7 +46,6 @@ function calculateSmartTrades( openPositions: Record; } { const hasRealNifty = nifty50Prices && Object.keys(nifty50Prices).length > 0; - const niftyHistory = hasRealNifty ? null : getNifty50IndexHistory(); const sortedRecs = [...recommendations].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); // Precompute real Nifty start price for comparison @@ -75,7 +72,6 @@ function calculateSmartTrades( let portfolioValue = startingAmount; let niftyValue = startingAmount; - const niftyStartValue = niftyHistory?.[0]?.value || 21500; const portfolioData = sortedRecs.map((rec) => { const stocks = getStocksToTrack(rec); @@ -89,8 +85,7 @@ function calculateSmartTrades( const decision = analysis.decision; const prevPosition = openPositions[symbol]; - const backtest = getBacktestResult(symbol); - const currentPrice = backtest?.current_price || 1000; + const currentPrice = 1000; // Nominal price for position sizing const quantity = Math.floor(investmentPerStock / currentPrice); if (decision === 'BUY') { @@ -160,11 +155,6 @@ function calculateSmartTrades( if (closestDate && nifty50Prices[closestDate]) { niftyValue = startingAmount * (nifty50Prices[closestDate] / niftyStartPrice); } - } else if (niftyHistory) { - const niftyPoint = niftyHistory.find(n => n.date === rec.date); - if (niftyPoint) { - niftyValue = startingAmount * (niftyPoint.value / niftyStartValue); - } } return { @@ -214,8 +204,7 @@ function getValueColorClass(value: number): string { export default function PortfolioSimulator({ className = '', - recommendations = sampleRecommendations, - isUsingMockData = true, // Default to true since this uses simulated returns + recommendations = [], nifty50Prices, allBacktestData, }: PortfolioSimulatorProps) { @@ -705,16 +694,6 @@ export default function PortfolioSimulator({ )} - {/* Demo Data Notice */} - {isUsingMockData && ( -
- - - Simulation uses demo data. Results are illustrative only. - -
- )} -

Simulated using Zerodha Equity Delivery rates (0% brokerage, STT 0.1%, Exchange 0.00345%, SEBI 0.0001%, Stamp 0.015%). {investmentMode === 'topPicks' ? ' Investing in Top Picks only.' : ' Investing in all 50 stocks.'} diff --git a/frontend/src/components/ReturnDistributionChart.tsx b/frontend/src/components/ReturnDistributionChart.tsx index c71780ae..b5eccdec 100644 --- a/frontend/src/components/ReturnDistributionChart.tsx +++ b/frontend/src/components/ReturnDistributionChart.tsx @@ -1,19 +1,17 @@ import { useState } from 'react'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { X } from 'lucide-react'; -import { getReturnDistribution } from '../data/recommendations'; import type { ReturnBucket } from '../types'; export interface ReturnDistributionChartProps { height?: number; className?: string; - data?: ReturnBucket[]; // Optional prop for real data + data?: ReturnBucket[]; } export default function ReturnDistributionChart({ height = 200, className = '', data: propData }: ReturnDistributionChartProps) { const [selectedBucket, setSelectedBucket] = useState<{ range: string; stocks: string[] } | null>(null); - // Use provided data or fall back to mock data - const data = propData || getReturnDistribution(); + const data = propData || []; if (data.every(d => d.count === 0)) { return ( diff --git a/frontend/src/components/ReturnExplainModal.tsx b/frontend/src/components/ReturnExplainModal.tsx index 0e8280fc..2adacb9f 100644 --- a/frontend/src/components/ReturnExplainModal.tsx +++ b/frontend/src/components/ReturnExplainModal.tsx @@ -1,5 +1,6 @@ import { X, CheckCircle, XCircle, Calculator } from 'lucide-react'; -import type { ReturnBreakdown } from '../data/recommendations'; +import { createPortal } from 'react-dom'; +import type { ReturnBreakdown } from '../types'; interface ReturnExplainModalProps { isOpen: boolean; @@ -18,7 +19,7 @@ export default function ReturnExplainModal({ isOpen, onClose, breakdown, date }: year: 'numeric', }); - return ( + return createPortal(

{/* Backdrop */}
- + , + document.body ); } diff --git a/frontend/src/components/RiskMetricsCard.tsx b/frontend/src/components/RiskMetricsCard.tsx index 0e93b557..12906ad3 100644 --- a/frontend/src/components/RiskMetricsCard.tsx +++ b/frontend/src/components/RiskMetricsCard.tsx @@ -1,20 +1,22 @@ import { TrendingUp, TrendingDown, Activity, Target } from 'lucide-react'; -import { calculateRiskMetrics } from '../data/recommendations'; import { useState } from 'react'; import InfoModal, { InfoButton } from './InfoModal'; import type { RiskMetrics } from '../types'; export interface RiskMetricsCardProps { className?: string; - metrics?: RiskMetrics; // Optional prop for real data + metrics?: RiskMetrics; } type MetricModal = 'sharpe' | 'drawdown' | 'winloss' | 'winrate' | null; +const defaultMetrics: RiskMetrics = { + sharpeRatio: 0, maxDrawdown: 0, winLossRatio: 0, winRate: 0, volatility: 0, totalTrades: 0, +}; + export default function RiskMetricsCard({ className = '', metrics: propMetrics }: RiskMetricsCardProps) { const [activeModal, setActiveModal] = useState(null); - // Use provided metrics or fall back to mock data - const metrics = propMetrics || calculateRiskMetrics(); + const metrics = propMetrics || defaultMetrics; // Color classes for metric values const COLOR_GOOD = 'text-green-600 dark:text-green-400'; diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 93cf32da..492cfc96 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -1,10 +1,11 @@ import { useState } from 'react'; +import { createPortal } from 'react-dom'; import { X, Settings, Cpu, Key, Zap, Brain, Sparkles, - Eye, EyeOff, Check, AlertCircle, RefreshCw + Eye, EyeOff, Check, AlertCircle, RefreshCw, Clock } from 'lucide-react'; -import { useSettings, MODELS, PROVIDERS } from '../contexts/SettingsContext'; -import type { ModelId, ProviderId } from '../contexts/SettingsContext'; +import { useSettings, MODELS, PROVIDERS, TIMEZONES } from '../contexts/SettingsContext'; +import type { ModelId, ProviderId, TimezoneId } from '../contexts/SettingsContext'; export default function SettingsModal() { const { settings, updateSettings, resetSettings, isSettingsOpen, closeSettings } = useSettings(); @@ -52,7 +53,7 @@ export default function SettingsModal() { const selectedProvider = PROVIDERS[settings.provider]; - return ( + return createPortal(
{/* Backdrop */}
+ + {/* Auto-Analyze Schedule */} +
+

+ + Auto-Analyze Schedule +

+ + {/* Enable Toggle */} +
+
+
+ Daily Auto-Analyze +
+
+ Automatically run Analyze All at the scheduled time +
+
+ +
+ + {/* Timezone */} +
+ + +
+ + {/* Time Picker */} +
+
+ + +
+ : +
+ + +
+
+ + {/* Preview */} + {settings.autoAnalyzeEnabled && ( +
+

+ Runs daily at {settings.autoAnalyzeTime} {TIMEZONES.find(tz => tz.id === settings.autoAnalyzeTimezone)?.label || settings.autoAnalyzeTimezone} when the backend is running +

+
+ )} +
{/* Footer */} @@ -315,6 +412,7 @@ export default function SettingsModal() { - + , + document.body ); } diff --git a/frontend/src/components/TerminalModal.tsx b/frontend/src/components/TerminalModal.tsx index cb780c29..4475285f 100644 --- a/frontend/src/components/TerminalModal.tsx +++ b/frontend/src/components/TerminalModal.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef, useCallback } from 'react'; +import { createPortal } from 'react-dom'; import { X, Terminal, Trash2, Download, Pause, Play, ChevronDown, Plus, Minus } from 'lucide-react'; interface LogEntry { @@ -224,7 +225,7 @@ export default function TerminalModal({ isOpen, onClose, isAnalyzing }: Terminal if (!isOpen) return null; - return ( + return createPortal(
{/* Backdrop */}
@@ -407,6 +408,7 @@ export default function TerminalModal({ isOpen, onClose, isAnalyzing }: Terminal
- + , + document.body ); } diff --git a/frontend/src/components/TopPicks.tsx b/frontend/src/components/TopPicks.tsx index 315d60f4..b57e6cba 100644 --- a/frontend/src/components/TopPicks.tsx +++ b/frontend/src/components/TopPicks.tsx @@ -1,9 +1,7 @@ import { Link } from 'react-router-dom'; import { Trophy, AlertTriangle, TrendingUp, TrendingDown, ChevronRight } from 'lucide-react'; import type { TopPick, StockToAvoid } from '../types'; -import BackgroundSparkline from './BackgroundSparkline'; import { RankBadge } from './StockCard'; -import { getBacktestResult } from '../data/recommendations'; interface TopPicksProps { picks: TopPick[]; @@ -24,7 +22,6 @@ export default function TopPicks({ picks }: TopPicksProps) {
{picks.map((pick, index) => { - const backtest = getBacktestResult(pick.symbol); return ( - {backtest && ( -
- -
- )}
@@ -91,7 +83,6 @@ export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
{stocks.map((stock) => { - const backtest = getBacktestResult(stock.symbol); return ( - {backtest && ( -
- -
- )}
{stock.symbol} diff --git a/frontend/src/contexts/SettingsContext.tsx b/frontend/src/contexts/SettingsContext.tsx index 3767ed3b..be39fee3 100644 --- a/frontend/src/contexts/SettingsContext.tsx +++ b/frontend/src/contexts/SettingsContext.tsx @@ -27,6 +27,30 @@ export const PROVIDERS = { export type ModelId = keyof typeof MODELS; export type ProviderId = keyof typeof PROVIDERS; +// Common timezones with labels and IANA identifiers +export const TIMEZONES = [ + { id: 'Asia/Kolkata', label: 'IST (India)', offset: '+05:30' }, + { id: 'Asia/Tokyo', label: 'JST (Japan)', offset: '+09:00' }, + { id: 'Asia/Shanghai', label: 'CST (China)', offset: '+08:00' }, + { id: 'Asia/Singapore', label: 'SGT (Singapore)', offset: '+08:00' }, + { id: 'Asia/Dubai', label: 'GST (Dubai)', offset: '+04:00' }, + { id: 'Asia/Hong_Kong', label: 'HKT (Hong Kong)', offset: '+08:00' }, + { id: 'Europe/London', label: 'GMT/BST (London)', offset: '+00:00' }, + { id: 'Europe/Paris', label: 'CET (Paris/Berlin)', offset: '+01:00' }, + { id: 'Europe/Moscow', label: 'MSK (Moscow)', offset: '+03:00' }, + { id: 'America/New_York', label: 'EST (New York)', offset: '-05:00' }, + { id: 'America/Chicago', label: 'CST (Chicago)', offset: '-06:00' }, + { id: 'America/Los_Angeles', label: 'PST (Los Angeles)', offset: '-08:00' }, + { id: 'America/Sao_Paulo', label: 'BRT (Sao Paulo)', offset: '-03:00' }, + { id: 'Australia/Sydney', label: 'AEST (Sydney)', offset: '+10:00' }, + { id: 'Australia/Perth', label: 'AWST (Perth)', offset: '+08:00' }, + { id: 'Pacific/Auckland', label: 'NZST (Auckland)', offset: '+12:00' }, + { id: 'Africa/Johannesburg', label: 'SAST (Johannesburg)', offset: '+02:00' }, + { id: 'UTC', label: 'UTC', offset: '+00:00' }, +] as const; + +export type TimezoneId = typeof TIMEZONES[number]['id']; + interface Settings { // Model settings deepThinkModel: ModelId; @@ -41,6 +65,11 @@ interface Settings { // Analysis settings maxDebateRounds: number; parallelWorkers: number; + + // Auto-analyze schedule + autoAnalyzeEnabled: boolean; + autoAnalyzeTime: string; // "HH:MM" in 24hr format + autoAnalyzeTimezone: TimezoneId; } interface SettingsContextType { @@ -59,6 +88,9 @@ const DEFAULT_SETTINGS: Settings = { anthropicApiKey: '', maxDebateRounds: 1, parallelWorkers: 3, + autoAnalyzeEnabled: false, + autoAnalyzeTime: '09:00', + autoAnalyzeTimezone: 'Asia/Kolkata', }; const STORAGE_KEY = 'nifty50ai_settings'; @@ -92,6 +124,34 @@ export function SettingsProvider({ children }: { children: ReactNode }) { } }, [settings]); + // Sync auto-analyze schedule to backend whenever it changes + useEffect(() => { + const syncSchedule = async () => { + try { + await fetch('http://localhost:8001/settings/schedule', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + enabled: settings.autoAnalyzeEnabled, + time: settings.autoAnalyzeTime, + timezone: settings.autoAnalyzeTimezone, + config: { + deep_think_model: settings.deepThinkModel, + quick_think_model: settings.quickThinkModel, + provider: settings.provider, + api_key: settings.anthropicApiKey || undefined, + max_debate_rounds: settings.maxDebateRounds, + parallel_workers: settings.parallelWorkers, + }, + }), + }); + } catch { + // Backend may not be running — silently ignore + } + }; + syncSchedule(); + }, [settings.autoAnalyzeEnabled, settings.autoAnalyzeTime, settings.autoAnalyzeTimezone, settings.deepThinkModel, settings.quickThinkModel, settings.provider, settings.anthropicApiKey, settings.maxDebateRounds, settings.parallelWorkers]); + const updateSettings = (newSettings: Partial) => { setSettings(prev => ({ ...prev, ...newSettings })); }; diff --git a/frontend/src/data/recommendations.ts b/frontend/src/data/recommendations.ts deleted file mode 100644 index 30bb45a9..00000000 --- a/frontend/src/data/recommendations.ts +++ /dev/null @@ -1,1596 +0,0 @@ -import type { DailyRecommendation, Decision, BacktestResult, AccuracyMetrics, PricePoint, DateStats, OverallStats, Nifty50IndexPoint, RiskMetrics, ReturnBucket, AccuracyTrendPoint } from '../types'; -import { NIFTY_50_STOCKS as nifty50List } from '../types'; - -// Generate AI analysis dynamically based on decision type and stock info -function generateAIAnalysis(symbol: string, companyName: string, decision: Decision, confidence: string, risk: string): string { - const sector = getSectorForStock(symbol); - - if (decision === 'BUY') { - return `## Summary -${confidence} confidence BUY signal for ${companyName} based on positive momentum and favorable sector conditions. - -## Technical Analysis -- Stock showing upward momentum in recent sessions -- RSI in bullish zone (55-65 range) -- Trading above key moving averages -- Volume supporting the uptrend -- Support levels holding firm - -## Fundamental Analysis -- Company fundamentals remain solid -- Revenue growth trajectory positive -- Margins stable or improving -- ${sector} sector showing strength -- Valuation reasonable relative to peers - -## Sentiment -- Analyst ratings predominantly positive -- Institutional interest increasing -- News flow supportive -- Management commentary optimistic - -## Risks -- ${risk === 'HIGH' ? 'Elevated volatility and market risk' : risk === 'MEDIUM' ? 'Moderate market and sector-specific risks' : 'Lower risk profile but general market exposure'} -- Sector-specific regulatory concerns -- Global macro headwinds possible`; - } else if (decision === 'SELL') { - return `## Summary -${confidence} confidence SELL signal for ${companyName} due to concerning technical and fundamental factors. - -## Technical Analysis -- Stock in clear downtrend pattern -- Trading below major moving averages -- RSI showing weakness (below 40) -- Volume increasing on down days -- Key support levels at risk - -## Fundamental Analysis -- Earnings momentum slowing -- Margin pressure evident -- ${sector} sector facing headwinds -- Competitive challenges increasing -- Valuation not justified by growth - -## Sentiment -- Analyst downgrades recent -- Institutional selling observed -- Negative news flow -- Management guidance cautious - -## Risks -- ${risk === 'HIGH' ? 'High downside risk if support breaks' : risk === 'MEDIUM' ? 'Further weakness likely' : 'Gradual decline expected'} -- Sector underperformance may persist -- Recovery timeline uncertain`; - } else { - return `## Summary -HOLD recommendation for ${companyName} as the stock consolidates with mixed signals. - -## Technical Analysis -- Stock in consolidation phase -- Trading within defined range -- RSI neutral (45-55 range) -- Volume average, no clear direction -- Awaiting breakout confirmation - -## Fundamental Analysis -- Business fundamentals stable -- Growth trajectory moderate -- ${sector} sector showing mixed trends -- Valuation fair at current levels -- No immediate catalysts visible - -## Sentiment -- Analyst views mixed -- Institutional activity balanced -- News flow neutral -- Wait-and-watch mode prevailing - -## Risks -- ${risk === 'HIGH' ? 'Volatility may increase' : risk === 'MEDIUM' ? 'Range-bound action likely to continue' : 'Stable but limited upside near-term'} -- Direction dependent on broader market -- Sector rotation risk`; - } -} - -// Get sector for a stock -function getSectorForStock(symbol: string): string { - const sectors: Record = { - 'RELIANCE': 'Energy & Retail', 'TCS': 'IT Services', 'HDFCBANK': 'Banking', - 'INFY': 'IT Services', 'ICICIBANK': 'Banking', 'HINDUNILVR': 'FMCG', - 'ITC': 'FMCG', 'SBIN': 'Banking', 'BHARTIARTL': 'Telecom', - 'KOTAKBANK': 'Banking', 'LT': 'Infrastructure', 'AXISBANK': 'Banking', - 'ASIANPAINT': 'Paints', 'MARUTI': 'Automobile', 'HCLTECH': 'IT Services', - 'SUNPHARMA': 'Pharma', 'TITAN': 'Consumer Durables', 'BAJFINANCE': 'NBFC', - 'WIPRO': 'IT Services', 'ULTRACEMCO': 'Cement', 'NESTLEIND': 'FMCG', - 'NTPC': 'Power', 'POWERGRID': 'Power', 'M&M': 'Automobile', - 'TATAMOTORS': 'Automobile', 'ONGC': 'Oil & Gas', 'JSWSTEEL': 'Steel', - 'TATASTEEL': 'Steel', 'ADANIENT': 'Conglomerate', 'ADANIPORTS': 'Ports', - 'COALINDIA': 'Mining', 'BAJAJFINSV': 'Financial Services', 'TECHM': 'IT Services', - 'HDFCLIFE': 'Insurance', 'SBILIFE': 'Insurance', 'GRASIM': 'Diversified', - 'DIVISLAB': 'Pharma', 'DRREDDY': 'Pharma', 'CIPLA': 'Pharma', - 'BRITANNIA': 'FMCG', 'EICHERMOT': 'Automobile', 'APOLLOHOSP': 'Healthcare', - 'INDUSINDBK': 'Banking', 'HEROMOTOCO': 'Automobile', 'TATACONSUM': 'FMCG', - 'BPCL': 'Oil & Gas', 'UPL': 'Chemicals', 'HINDALCO': 'Metals', - 'BAJAJ-AUTO': 'Automobile', 'LTIM': 'IT Services', - }; - return sectors[symbol] || 'Diversified'; -} - -// Raw analysis content for detailed AI reasoning -const rawAnalysisData: Record = { - 'BAJFINANCE': `## Summary -Strong BUY signal based on exceptional momentum and sector strength in the NBFC space. - -## Technical Analysis -- Price up 13.7% in 30 days (₹678 → ₹771) -- RSI at 62: Bullish but not overbought -- MACD showing positive crossover on daily chart -- Trading above 50-day and 200-day moving averages -- Volume spike on breakout confirms institutional interest - -## Fundamental Analysis -- Q3 FY25 results: 18% YoY profit growth -- AUM growth of 25% indicating strong business expansion -- NIM stable at 10.2%, best in class -- Credit costs under control at 1.8% -- ROE of 22% among highest in sector - -## Sentiment -- 12 analyst buy ratings, 3 hold -- FII net buyers in financial sector last 2 weeks -- Management guidance raised for FY25 -- Positive mentions on analyst calls - -## Risks -- Interest rate sensitivity remains key concern -- Unsecured lending exposure at 45% of book -- Premium valuation at 5.2x P/B vs sector avg of 3.1x`, - - 'BAJAJFINSV': `## Summary -BUY recommendation driven by strong holding company performance and insurance business growth. - -## Technical Analysis -- 14% gain in one month (₹1,567 → ₹1,789) -- Breaking out of 3-month consolidation range -- RSI at 58, room for further upside -- Strong support at ₹1,650 level - -## Fundamental Analysis -- Insurance subsidiary showing 28% premium growth -- Asset management AUM up 35% YoY -- Sum-of-parts valuation suggests 15% upside -- Healthy subsidiaries across financial services - -## Sentiment -- Institutional holding increased by 2.3% in Q3 -- Positive outlook from major brokerages -- Benefits from Bajaj Finance momentum - -## Risks -- Dependent on subsidiary performance -- Insurance sector regulatory changes -- Holding company discount may persist`, - - 'KOTAKBANK': `## Summary -BUY signal triggered by significant technical breakout with high volume confirmation. - -## Technical Analysis -- Significant breakout on January 20th -- 9.2% gain on exceptionally high volume (66.6M shares) -- Breaking above ₹1,850 resistance, now support -- Bullish engulfing pattern on weekly chart - -## Fundamental Analysis -- CASA ratio at 53%, best among private banks -- Asset quality stable with GNPA at 1.7% -- Q3 profit up 12% YoY -- Strong capital adequacy at 21% - -## Sentiment -- Inclusion in major index reshuffling positive -- Foreign investor interest increasing -- New CEO initiatives well received - -## Risks -- Margin compression in deposit rate war -- Competition from fintech players -- Slower loan growth vs peers at 15% YoY`, - - 'DRREDDY': `## Summary -HIGH CONFIDENCE SELL due to severe downtrend and deteriorating fundamentals. - -## Technical Analysis -- 14.9% decline in one month -- Trading below all major moving averages -- RSI at 28, approaching oversold but no reversal signs -- Death cross (50-day below 200-day) formed -- Volume increasing on down days - -## Fundamental Analysis -- US generics pricing pressure intensifying -- Q3 margins contracted 300bps YoY -- R&D pipeline delays for key molecules -- Forex headwinds from rupee depreciation - -## Sentiment -- 5 downgrades from major brokerages in January -- FDA inspection concerns linger -- Negative news flow on generic drug pricing - -## Risks -- Further downside if ₹1,150 support breaks -- US regulatory environment uncertain -- Peer competition in key therapeutic areas`, - - 'AXISBANK': `## Summary -HIGH CONFIDENCE SELL with persistent downtrend and structural concerns. - -## Technical Analysis -- 10.5% sustained decline over 4 weeks -- Clear lower highs and lower lows pattern -- Below 200-day moving average -- Support at ₹1,020 being tested - -## Fundamental Analysis -- Asset quality concerns in SME book -- NIM compression of 15bps QoQ -- Restructured book higher than peers -- Growth lagging private bank peers - -## Sentiment -- Management transition uncertainty -- FII selling observed in January -- Mixed analyst ratings with more holds than buys - -## Risks -- Economic slowdown impact on corporate loans -- Digital banking competitive pressure -- Capital adequacy adequate but tight for growth`, - - 'RELIANCE': `## Summary -HOLD recommendation as stock consolidates near all-time highs with mixed signals. - -## Technical Analysis -- Trading in tight range between ₹2,850-₹2,950 -- RSI neutral at 52 -- Consolidating after strong Q4 2024 rally -- Volume declining, suggesting indecision - -## Fundamental Analysis -- Jio Platforms showing steady growth -- Retail business margins improving -- O2C segment facing global headwinds -- New energy investments progressing - -## Sentiment -- Neutral analyst stance, waiting for Q4 results -- Domestic institutional support strong -- Global energy transition narrative supportive - -## Risks -- Oil & Gas volatility impacts earnings -- Telecom ARPU growth slowing -- Execution risk on new initiatives`, - - 'TCS': `## Summary -HOLD as IT sector faces near-term headwinds despite strong long-term positioning. - -## Technical Analysis -- Range-bound between ₹3,800-₹4,100 -- 50-day MA acting as resistance -- No clear directional momentum -- Volume average, no accumulation signs - -## Fundamental Analysis -- Deal pipeline remains healthy at $12B TCV -- Attrition stabilizing at 13% -- Margins stable at 25%+ -- Cloud and AI investments on track - -## Sentiment -- Client spending outlook cautious for H1 FY26 -- BFSI vertical showing early recovery signs -- Management guidance conservative but achievable - -## Risks -- US recession fears impacting IT budgets -- Wage inflation pressure -- Currency volatility`, - - 'HDFCBANK': `## Summary -HOLD as merger integration continues with near-term pressure on ratios. - -## Technical Analysis -- Sideways movement in ₹1,650-₹1,750 range -- Testing 200-day moving average -- Neutral momentum indicators -- Support at ₹1,620 holding firm - -## Fundamental Analysis -- Merger integration progressing well -- CASA ratio dilution temporary -- Credit costs elevated but manageable -- Strong franchise value intact - -## Sentiment -- Institutional view remains constructive long-term -- Near-term concerns on deposit costs -- Wait-and-watch mode for most analysts - -## Risks -- Deposit mobilization challenges -- Net interest margin pressure -- Integration execution risks`, -}; - -// Generate mock price history for sparklines with high volatility for visual impact -function generatePriceHistory(basePrice: number, trend: 'up' | 'down' | 'flat', days: number = 30, symbol?: string): PricePoint[] { - const history: PricePoint[] = []; - let price = basePrice; - // Much higher trend bias and volatility for very visible chart movements - const trendBias = trend === 'up' ? 0.015 : trend === 'down' ? -0.015 : 0.002; - const baseSeed = symbol ? getSymbolSeed(symbol) + 5000 : Date.now(); - const volatility = 0.12; // 12% daily volatility for very visible movements - - for (let i = days; i >= 0; i--) { - const date = new Date(); - date.setDate(date.getDate() - i); - - // Use seeded random if symbol provided, otherwise use Math.random - const randomValue = symbol ? seededRandom(baseSeed + i * 100) : Math.random(); - // Add some wave pattern for more interesting charts - const wavePattern = Math.sin(i * 0.3) * 0.02; - const dailyReturn = trendBias + (randomValue - 0.5) * volatility + wavePattern; - price = price * (1 + dailyReturn); - - history.push({ - date: date.toISOString().split('T')[0], - price: Math.round(price * 100) / 100, - }); - } - - return history; -} - -// FALLBACK backtest results - used when real API data is not available -// For accurate backtesting, use the API endpoints: -// - GET /backtest/{date}/{symbol} - Get real backtest for a stock -// - GET /backtest/accuracy - Get real accuracy metrics from database -// The real backtest calculates actual returns from Yahoo Finance price data -// and determines prediction accuracy based on actual price movements -export const mockBacktestResults: Record = { - 'BAJFINANCE': { - prediction_correct: true, - actual_return_1d: 2.1, // Next trading day return - actual_return_1w: 3.2, - actual_return_1m: 8.5, - price_at_prediction: 771, - current_price: 836, - price_history: generatePriceHistory(771, 'up', 30, 'BAJFINANCE'), - }, - 'BAJAJFINSV': { - prediction_correct: true, - actual_return_1d: 1.8, - actual_return_1w: 2.1, - actual_return_1m: 6.8, - price_at_prediction: 1789, - current_price: 1911, - price_history: generatePriceHistory(1789, 'up', 30, 'BAJAJFINSV'), - }, - 'KOTAKBANK': { - prediction_correct: true, - actual_return_1d: 1.5, - actual_return_1w: 1.8, - actual_return_1m: 4.2, - price_at_prediction: 1850, - current_price: 1928, - price_history: generatePriceHistory(1850, 'up', 30, 'KOTAKBANK'), - }, - 'DRREDDY': { - prediction_correct: true, - actual_return_1d: -1.8, - actual_return_1w: -2.8, - actual_return_1m: -7.2, - price_at_prediction: 1180, - current_price: 1095, - price_history: generatePriceHistory(1180, 'down', 30, 'DRREDDY'), - }, - 'AXISBANK': { - prediction_correct: true, - actual_return_1d: -1.2, - actual_return_1w: -1.5, - actual_return_1m: -5.3, - price_at_prediction: 1045, - current_price: 990, - price_history: generatePriceHistory(1045, 'down', 30, 'AXISBANK'), - }, - 'HCLTECH': { - prediction_correct: false, - actual_return_1d: 0.6, - actual_return_1w: 0.8, - actual_return_1m: 2.1, - price_at_prediction: 1720, - current_price: 1756, - price_history: generatePriceHistory(1720, 'up', 30, 'HCLTECH'), - }, - 'RELIANCE': { - prediction_correct: true, - actual_return_1d: 0.3, - actual_return_1w: 0.5, - actual_return_1m: 1.2, - price_at_prediction: 2890, - current_price: 2925, - price_history: generatePriceHistory(2890, 'flat', 30, 'RELIANCE'), - }, - 'TCS': { - prediction_correct: true, - actual_return_1d: 0.2, - actual_return_1w: -0.3, - actual_return_1m: 0.8, - price_at_prediction: 3950, - current_price: 3982, - price_history: generatePriceHistory(3950, 'flat', 30, 'TCS'), - }, - 'HDFCBANK': { - prediction_correct: true, - actual_return_1d: -0.1, - actual_return_1w: 0.2, - actual_return_1m: -0.5, - price_at_prediction: 1680, - current_price: 1672, - price_history: generatePriceHistory(1680, 'flat', 30, 'HDFCBANK'), - }, - 'ICICIBANK': { - prediction_correct: true, - actual_return_1d: 1.1, - actual_return_1w: 1.5, - actual_return_1m: 3.8, - price_at_prediction: 1120, - current_price: 1163, - price_history: generatePriceHistory(1120, 'up', 30, 'ICICIBANK'), - }, - 'SUNPHARMA': { - prediction_correct: true, - actual_return_1d: -0.9, - actual_return_1w: -1.2, - actual_return_1m: -3.5, - price_at_prediction: 1850, - current_price: 1785, - price_history: generatePriceHistory(1850, 'down', 30, 'SUNPHARMA'), - }, - 'ADANIPORTS': { - prediction_correct: true, - actual_return_1d: -1.5, - actual_return_1w: -2.1, - actual_return_1m: -6.8, - price_at_prediction: 1180, - current_price: 1100, - price_history: generatePriceHistory(1180, 'down', 30, 'ADANIPORTS'), - }, -}; - -// Calculate accuracy metrics from backtest results for all 50 stocks -export function calculateAccuracyMetrics(): AccuracyMetrics { - const latestRec = sampleRecommendations[0]; - if (!latestRec) { - return { - total_predictions: 0, - correct_predictions: 0, - success_rate: 0, - buy_accuracy: 0, - sell_accuracy: 0, - hold_accuracy: 0, - }; - } - - let totalBuy = 0, correctBuy = 0; - let totalSell = 0, correctSell = 0; - let totalHold = 0, correctHold = 0; - - // Calculate accuracy for each stock - Object.keys(latestRec.analysis).forEach(symbol => { - const stockAnalysis = latestRec.analysis[symbol]; - const backtest = getBacktestResult(symbol); - - if (!backtest || !stockAnalysis?.decision) return; - - if (stockAnalysis.decision === 'BUY') { - totalBuy++; - if (backtest.prediction_correct) correctBuy++; - } else if (stockAnalysis.decision === 'SELL') { - totalSell++; - if (backtest.prediction_correct) correctSell++; - } else { - totalHold++; - if (backtest.prediction_correct) correctHold++; - } - }); - - const total = totalBuy + totalSell + totalHold; - const correct = correctBuy + correctSell + correctHold; - - return { - total_predictions: total, - correct_predictions: correct, - success_rate: total > 0 ? correct / total : 0, - buy_accuracy: totalBuy > 0 ? correctBuy / totalBuy : 0, - sell_accuracy: totalSell > 0 ? correctSell / totalSell : 0, - hold_accuracy: totalHold > 0 ? correctHold / totalHold : 0, - }; -} - -// Cache for dynamically generated backtest results -const generatedBacktestCache: Record = {}; - -// Seeded random number generator for consistent results -function seededRandom(seed: number): number { - const x = Math.sin(seed) * 10000; - return x - Math.floor(x); -} - -// Get a consistent seed from symbol string -function getSymbolSeed(symbol: string): number { - let hash = 0; - for (let i = 0; i < symbol.length; i++) { - hash = ((hash << 5) - hash) + symbol.charCodeAt(i); - hash = hash & hash; - } - return Math.abs(hash); -} - -// Helper function to calculate prediction correctness based on decision and return -// LONG-ONLY strategy: BUY/HOLD correct if return > 0, SELL correct if return < 0 -function calculatePredictionCorrect(decision: Decision, return1d: number): boolean { - if (decision === 'BUY' || decision === 'HOLD') { - // BUY and HOLD are correct if stock price went up - return return1d > 0; - } else { - // SELL is correct if stock price went down - return return1d < 0; - } -} - -// Get backtest result for a symbol - generates dynamically if not in static data -// NOTE: This is FALLBACK/DEMO data only. For real backtest accuracy: -// Use api.getBacktestResult(date, symbol) which fetches from the backend -// The backend calculates real returns using actual Yahoo Finance price data -export function getBacktestResult(symbol: string): BacktestResult | undefined { - // Get the stock's decision from the latest recommendation - const latestRec = sampleRecommendations[0]; - const stockAnalysis = latestRec?.analysis[symbol]; - - if (!stockAnalysis || !stockAnalysis.decision) { - return undefined; - } - - const decision = stockAnalysis.decision; - - // If we have static mock data, use its prices but RECALCULATE prediction_correct - if (mockBacktestResults[symbol]) { - const mockData = mockBacktestResults[symbol]; - // Always recalculate prediction_correct based on actual return and decision - const correctPrediction = calculatePredictionCorrect(decision, mockData.actual_return_1d); - return { - ...mockData, - prediction_correct: correctPrediction, - }; - } - - // Return cached generated result if available (with recalculated correctness) - if (generatedBacktestCache[symbol]) { - const cachedData = generatedBacktestCache[symbol]; - // Always recalculate prediction_correct based on actual return and decision - const correctPrediction = calculatePredictionCorrect(decision, cachedData.actual_return_1d); - return { - ...cachedData, - prediction_correct: correctPrediction, - }; - } - - // Generate backtest result based on decision type with consistent seeding - const seed = getSymbolSeed(symbol); - const basePrice = 1000 + seededRandom(seed) * 2000; // Consistent base price between 1000-3000 - - // First, generate the return randomly (with market-like distribution) - // Return can be positive or negative - this is NOT pre-determined by decision - const randomReturn = (seededRandom(seed + 1) - 0.45) * 10; // -4.5% to +5.5% range (slight positive bias) - const returnMultiplier = 1 + (randomReturn / 100); - - // Determine trend based on actual return - let trend: 'up' | 'down' | 'flat'; - if (randomReturn > 0.5) { - trend = 'up'; - } else if (randomReturn < -0.5) { - trend = 'down'; - } else { - trend = 'flat'; - } - - // Calculate actual returns - const currentPrice = basePrice * returnMultiplier; - const actualReturn1m = ((currentPrice - basePrice) / basePrice) * 100; - const actualReturn1w = actualReturn1m * 0.3; // Approximate - // Next trading day return - about 15-25% of weekly return with some variance - const actualReturn1d = actualReturn1w * (0.4 + seededRandom(seed + 3) * 0.3); - const roundedReturn1d = Math.round(actualReturn1d * 10) / 10; - - // Calculate prediction correctness based on actual return vs decision - const predictionCorrect = calculatePredictionCorrect(decision, roundedReturn1d); - - const result: BacktestResult = { - prediction_correct: predictionCorrect, - actual_return_1d: roundedReturn1d, - actual_return_1w: Math.round(actualReturn1w * 10) / 10, - actual_return_1m: Math.round(actualReturn1m * 10) / 10, - price_at_prediction: Math.round(basePrice * 100) / 100, - current_price: Math.round(currentPrice * 100) / 100, - price_history: generatePriceHistory(basePrice, trend, 30, symbol), - }; - - // Cache the result for consistency - generatedBacktestCache[symbol] = result; - - return result; -} - -// Get raw analysis for a symbol - returns custom analysis if available, otherwise generates one -export function getRawAnalysis(symbol: string): string | undefined { - // Return custom detailed analysis if available - if (rawAnalysisData[symbol]) { - return rawAnalysisData[symbol]; - } - - // Generate analysis dynamically for other stocks - const latestRec = sampleRecommendations[0]; - const stockAnalysis = latestRec?.analysis[symbol]; - - if (stockAnalysis && stockAnalysis.decision) { - return generateAIAnalysis( - symbol, - stockAnalysis.company_name, - stockAnalysis.decision, - stockAnalysis.confidence || 'MEDIUM', - stockAnalysis.risk || 'MEDIUM' - ); - } - - return undefined; -} - -// Generate 10 days of historical recommendations with varied but consistent data -function generateHistoricalRecommendations(): DailyRecommendation[] { - // Trading days (skip weekends) - const dates = [ - '2025-01-30', '2025-01-29', '2025-01-28', '2025-01-27', - '2025-01-24', '2025-01-23', '2025-01-22', '2025-01-21', - '2025-01-20', '2025-01-17' - ]; - - const recommendations: DailyRecommendation[] = []; - - for (let dayIndex = 0; dayIndex < dates.length; dayIndex++) { - const date = dates[dayIndex]; - const dateSeed = dayIndex * 1000; // Different seed for each day - - // Generate analysis for all 50 stocks - const analysis: Record = {}; - - let buyCount = 0; - let sellCount = 0; - let holdCount = 0; - - for (const stock of nifty50List) { - const stockSeed = getSymbolSeed(stock.symbol) + dateSeed; - const rand = seededRandom(stockSeed); - - // Determine decision with some variation per day - let decision: Decision; - if (rand < 0.14) { - decision = 'BUY'; - buyCount++; - } else if (rand < 0.34) { - decision = 'SELL'; - sellCount++; - } else { - decision = 'HOLD'; - holdCount++; - } - - // Determine confidence and risk - const confRand = seededRandom(stockSeed + 1); - const riskRand = seededRandom(stockSeed + 2); - - const confidence: 'HIGH' | 'MEDIUM' | 'LOW' = confRand < 0.2 ? 'HIGH' : confRand < 0.7 ? 'MEDIUM' : 'LOW'; - const risk: 'HIGH' | 'MEDIUM' | 'LOW' = riskRand < 0.25 ? 'HIGH' : riskRand < 0.75 ? 'MEDIUM' : 'LOW'; - - analysis[stock.symbol] = { - symbol: stock.symbol, - company_name: stock.company_name, - decision, - confidence, - risk, - raw_analysis: rawAnalysisData[stock.symbol], - }; - } - - // Generate top picks and stocks to avoid based on analysis - const topPicks = Object.values(analysis) - .filter(s => s.decision === 'BUY' && (s.confidence === 'HIGH' || s.confidence === 'MEDIUM')) - .slice(0, 3) - .map((stock, idx) => ({ - rank: idx + 1, - symbol: stock.symbol, - company_name: stock.company_name, - decision: stock.decision, - reason: `Strong ${stock.confidence?.toLowerCase()} confidence BUY signal based on positive momentum and sector conditions.`, - risk_level: stock.risk || 'MEDIUM' as const, - })); - - const stocksToAvoid = Object.values(analysis) - .filter(s => s.decision === 'SELL' && (s.confidence === 'HIGH' || s.risk === 'HIGH')) - .slice(0, 4) - .map(stock => ({ - symbol: stock.symbol, - company_name: stock.company_name, - reason: `${stock.confidence} confidence SELL with ${stock.risk} risk profile. Downward pressure detected.`, - })); - - recommendations.push({ - date, - analysis, - ranking: { - ranking: '', - stocks_analyzed: 50, - timestamp: `${date}T15:30:00.000Z`, - }, - summary: { - total: 50, - buy: buyCount, - sell: sellCount, - hold: holdCount, - }, - top_picks: topPicks, - stocks_to_avoid: stocksToAvoid, - }); - } - - // Override the first day (latest) with manually curated data for better demo - if (recommendations.length > 0) { - recommendations[0] = createLatestRecommendation(); - } - - return recommendations; -} - -// Create the latest (curated) recommendation for demo purposes -function createLatestRecommendation(): DailyRecommendation { - return { - date: '2025-01-30', - analysis: { - 'RELIANCE': { symbol: 'RELIANCE', company_name: 'Reliance Industries Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM', raw_analysis: rawAnalysisData['RELIANCE'] }, - 'TCS': { symbol: 'TCS', company_name: 'Tata Consultancy Services Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM', raw_analysis: rawAnalysisData['TCS'] }, - 'HDFCBANK': { symbol: 'HDFCBANK', company_name: 'HDFC Bank Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM', raw_analysis: rawAnalysisData['HDFCBANK'] }, - 'INFY': { symbol: 'INFY', company_name: 'Infosys Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'ICICIBANK': { symbol: 'ICICIBANK', company_name: 'ICICI Bank Ltd', decision: 'BUY', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'HINDUNILVR': { symbol: 'HINDUNILVR', company_name: 'Hindustan Unilever Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'ITC': { symbol: 'ITC', company_name: 'ITC Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'SBIN': { symbol: 'SBIN', company_name: 'State Bank of India', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'BHARTIARTL': { symbol: 'BHARTIARTL', company_name: 'Bharti Airtel Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'KOTAKBANK': { symbol: 'KOTAKBANK', company_name: 'Kotak Mahindra Bank Ltd', decision: 'BUY', confidence: 'MEDIUM', risk: 'MEDIUM', raw_analysis: rawAnalysisData['KOTAKBANK'] }, - 'LT': { symbol: 'LT', company_name: 'Larsen & Toubro Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'AXISBANK': { symbol: 'AXISBANK', company_name: 'Axis Bank Ltd', decision: 'SELL', confidence: 'HIGH', risk: 'HIGH', raw_analysis: rawAnalysisData['AXISBANK'] }, - 'ASIANPAINT': { symbol: 'ASIANPAINT', company_name: 'Asian Paints Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'MARUTI': { symbol: 'MARUTI', company_name: 'Maruti Suzuki India Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'HCLTECH': { symbol: 'HCLTECH', company_name: 'HCL Technologies Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'HIGH' }, - 'SUNPHARMA': { symbol: 'SUNPHARMA', company_name: 'Sun Pharmaceutical Industries Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'TITAN': { symbol: 'TITAN', company_name: 'Titan Company Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'BAJFINANCE': { symbol: 'BAJFINANCE', company_name: 'Bajaj Finance Ltd', decision: 'BUY', confidence: 'HIGH', risk: 'MEDIUM', raw_analysis: rawAnalysisData['BAJFINANCE'] }, - 'WIPRO': { symbol: 'WIPRO', company_name: 'Wipro Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'ULTRACEMCO': { symbol: 'ULTRACEMCO', company_name: 'UltraTech Cement Ltd', decision: 'BUY', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'NESTLEIND': { symbol: 'NESTLEIND', company_name: 'Nestle India Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'NTPC': { symbol: 'NTPC', company_name: 'NTPC Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'POWERGRID': { symbol: 'POWERGRID', company_name: 'Power Grid Corporation of India Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'M&M': { symbol: 'M&M', company_name: 'Mahindra & Mahindra Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'TATAMOTORS': { symbol: 'TATAMOTORS', company_name: 'Tata Motors Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'ONGC': { symbol: 'ONGC', company_name: 'Oil & Natural Gas Corporation Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'HIGH' }, - 'JSWSTEEL': { symbol: 'JSWSTEEL', company_name: 'JSW Steel Ltd', decision: 'BUY', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'TATASTEEL': { symbol: 'TATASTEEL', company_name: 'Tata Steel Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'ADANIENT': { symbol: 'ADANIENT', company_name: 'Adani Enterprises Ltd', decision: 'HOLD', confidence: 'LOW', risk: 'HIGH' }, - 'ADANIPORTS': { symbol: 'ADANIPORTS', company_name: 'Adani Ports and SEZ Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'HIGH' }, - 'COALINDIA': { symbol: 'COALINDIA', company_name: 'Coal India Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'BAJAJFINSV': { symbol: 'BAJAJFINSV', company_name: 'Bajaj Finserv Ltd', decision: 'BUY', confidence: 'HIGH', risk: 'MEDIUM', raw_analysis: rawAnalysisData['BAJAJFINSV'] }, - 'TECHM': { symbol: 'TECHM', company_name: 'Tech Mahindra Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'HDFCLIFE': { symbol: 'HDFCLIFE', company_name: 'HDFC Life Insurance Company Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'SBILIFE': { symbol: 'SBILIFE', company_name: 'SBI Life Insurance Company Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'GRASIM': { symbol: 'GRASIM', company_name: 'Grasim Industries Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'DIVISLAB': { symbol: 'DIVISLAB', company_name: "Divi's Laboratories Ltd", decision: 'SELL', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'DRREDDY': { symbol: 'DRREDDY', company_name: "Dr. Reddy's Laboratories Ltd", decision: 'SELL', confidence: 'HIGH', risk: 'HIGH', raw_analysis: rawAnalysisData['DRREDDY'] }, - 'CIPLA': { symbol: 'CIPLA', company_name: 'Cipla Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'BRITANNIA': { symbol: 'BRITANNIA', company_name: 'Britannia Industries Ltd', decision: 'BUY', confidence: 'MEDIUM', risk: 'LOW' }, - 'EICHERMOT': { symbol: 'EICHERMOT', company_name: 'Eicher Motors Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'APOLLOHOSP': { symbol: 'APOLLOHOSP', company_name: 'Apollo Hospitals Enterprise Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'INDUSINDBK': { symbol: 'INDUSINDBK', company_name: 'IndusInd Bank Ltd', decision: 'SELL', confidence: 'HIGH', risk: 'HIGH' }, - 'HEROMOTOCO': { symbol: 'HEROMOTOCO', company_name: 'Hero MotoCorp Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'TATACONSUM': { symbol: 'TATACONSUM', company_name: 'Tata Consumer Products Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'BPCL': { symbol: 'BPCL', company_name: 'Bharat Petroleum Corporation Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'UPL': { symbol: 'UPL', company_name: 'UPL Ltd', decision: 'HOLD', confidence: 'LOW', risk: 'HIGH' }, - 'HINDALCO': { symbol: 'HINDALCO', company_name: 'Hindalco Industries Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'BAJAJ-AUTO': { symbol: 'BAJAJ-AUTO', company_name: 'Bajaj Auto Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - 'LTIM': { symbol: 'LTIM', company_name: 'LTIMindtree Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' }, - }, - ranking: { - ranking: '', - stocks_analyzed: 50, - timestamp: '2025-01-30T15:30:00.000Z', - }, - summary: { - total: 50, - buy: 7, - sell: 10, - hold: 33, - }, - top_picks: [ - { - rank: 1, - symbol: 'BAJFINANCE', - company_name: 'Bajaj Finance Ltd', - decision: 'BUY', - reason: '13.7% gain over 30 days (₹678 → ₹771), strongest bullish momentum with robust upward trend.', - risk_level: 'MEDIUM', - }, - { - rank: 2, - symbol: 'BAJAJFINSV', - company_name: 'Bajaj Finserv Ltd', - decision: 'BUY', - reason: '14% gain in one month (₹1,567 → ₹1,789) demonstrates clear bullish momentum with sector-wide tailwinds.', - risk_level: 'MEDIUM', - }, - { - rank: 3, - symbol: 'KOTAKBANK', - company_name: 'Kotak Mahindra Bank Ltd', - decision: 'BUY', - reason: 'Significant breakout on January 20th with 9.2% gain on exceptionally high volume (66.6M shares).', - risk_level: 'MEDIUM', - }, - ], - stocks_to_avoid: [ - { - symbol: 'DRREDDY', - company_name: "Dr. Reddy's Laboratories Ltd", - reason: 'HIGH CONFIDENCE SELL with 14.9% decline in one month. Severe downtrend with high risk.', - }, - { - symbol: 'AXISBANK', - company_name: 'Axis Bank Ltd', - reason: 'HIGH CONFIDENCE SELL with 10.5% sustained decline. Clear and persistent downtrend.', - }, - { - symbol: 'HCLTECH', - company_name: 'HCL Technologies Ltd', - reason: 'SELL with 9.4% drop from recent highs. High risk rating with continued selling pressure.', - }, - { - symbol: 'ADANIPORTS', - company_name: 'Adani Ports and SEZ Ltd', - reason: 'SELL with 12% monthly decline and consistently lower lows. High risk profile.', - }, - ], - }; -} - -// Generate and export sample recommendations (10 days of historical data) -export const sampleRecommendations: DailyRecommendation[] = generateHistoricalRecommendations(); - -// Function to get recommendation for a specific date -export function getRecommendationByDate(date: string): DailyRecommendation | undefined { - return sampleRecommendations.find(r => r.date === date); -} - -// Function to get latest recommendation -export function getLatestRecommendation(): DailyRecommendation | undefined { - return sampleRecommendations[0]; -} - -// Function to get all available dates -export function getAvailableDates(): string[] { - return sampleRecommendations.map(r => r.date); -} - -// Function to get stock history across all dates -export function getStockHistory(symbol: string): { date: string; decision: Decision }[] { - return sampleRecommendations - .filter(r => r.analysis[symbol]) - .map(r => ({ - date: r.date, - decision: r.analysis[symbol].decision as Decision, - })) - .reverse(); -} - -// Get decision counts for charts -export function getDecisionCounts(date: string): { buy: number; sell: number; hold: number } { - const rec = getRecommendationByDate(date); - if (!rec) return { buy: 0, sell: 0, hold: 0 }; - return rec.summary; -} - -// Get extended price history with more data points for charting -// Generates price history ending at the latest recommendation date -export function getExtendedPriceHistory(symbol: string, days: number = 60): PricePoint[] { - // Use getBacktestResult to get data for any stock (including dynamically generated) - const backtest = getBacktestResult(symbol); - const latestRec = sampleRecommendations[0]; - - // Get the end date from the latest recommendation, or use today - const endDate = latestRec ? new Date(latestRec.date) : new Date(); - - const basePrice = backtest ? backtest.price_at_prediction * 0.9 : 1000; - const trend = backtest - ? (backtest.actual_return_1m > 2 ? 'up' : backtest.actual_return_1m < -2 ? 'down' : 'flat') - : 'flat'; - - return generatePriceHistoryWithEndDate(basePrice, trend, days, endDate, symbol); -} - -// Generate price history ending at a specific date with consistent seeding -function generatePriceHistoryWithEndDate( - basePrice: number, - trend: 'up' | 'down' | 'flat', - days: number, - endDate: Date, - symbol?: string -): PricePoint[] { - const history: PricePoint[] = []; - let price = basePrice; - const trendBias = trend === 'up' ? 0.003 : trend === 'down' ? -0.003 : 0; - const baseSeed = symbol ? getSymbolSeed(symbol) : Date.now(); - - for (let i = days; i >= 0; i--) { - const date = new Date(endDate); - date.setDate(date.getDate() - i); - - // Use seeded random for consistent results - const dailyReturn = trendBias + (seededRandom(baseSeed + i * 100) - 0.5) * 0.02; - price = price * (1 + dailyReturn); - - history.push({ - date: date.toISOString().split('T')[0], - price: Math.round(price * 100) / 100, - }); - } - - return history; -} - -// Get prediction points for the chart with actual prices -// Only returns predictions that exist in the actual saved historical recommendations -export function getPredictionPointsWithPrices( - symbol: string, - priceHistory: PricePoint[] -): { date: string; decision: Decision; price: number }[] { - // Get actual historical recommendations for this stock - const stockHistory = getStockHistory(symbol); - - if (stockHistory.length === 0 || priceHistory.length === 0) { - return []; - } - - // Map actual historical recommendations to prediction points - const predictions: { date: string; decision: Decision; price: number }[] = []; - - for (const historyEntry of stockHistory) { - const historyDate = new Date(historyEntry.date).getTime(); - - // Find the closest date in price history - let closestPricePoint = priceHistory[0]; - let closestDiff = Math.abs(new Date(closestPricePoint.date).getTime() - historyDate); - - for (const pricePoint of priceHistory) { - const diff = Math.abs(new Date(pricePoint.date).getTime() - historyDate); - if (diff < closestDiff) { - closestDiff = diff; - closestPricePoint = pricePoint; - } - } - - // Use the closest price point's date (so it aligns with the chart's x-axis) - predictions.push({ - date: closestPricePoint.date, - decision: historyEntry.decision, - price: closestPricePoint.price, - }); - } - - return predictions.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); -} - -// Detailed return breakdown for explanation modal -export interface ReturnBreakdown { - correctPredictions: { - count: number; - totalReturn: number; - avgReturn: number; - stocks: { symbol: string; decision: string; return1d: number }[]; - }; - incorrectPredictions: { - count: number; - totalReturn: number; - avgReturn: number; - stocks: { symbol: string; decision: string; return1d: number }[]; - }; - weightedReturn: number; - formula: string; -} - -// Get statistics for a specific recommendation date -// Uses weighted average: correct predictions contribute positively, incorrect negatively -export function getDateStats(date: string): DateStats | null { - const rec = getRecommendationByDate(date); - if (!rec) return null; - - const symbols = Object.keys(rec.analysis); - let correctCount = 0; - let incorrectCount = 0; - let correctTotalReturn = 0; // Sum of gains from correct predictions - let incorrectTotalReturn = 0; // Sum of losses from incorrect predictions - - for (const symbol of symbols) { - const stockAnalysis = rec.analysis[symbol]; - const backtest = getBacktestResult(symbol); - if (!backtest || !stockAnalysis?.decision) continue; - - const decision = stockAnalysis.decision; - const return1d = backtest.actual_return_1d; - - if (backtest.prediction_correct) { - correctCount++; - // For correct predictions (LONG-ONLY strategy, no short positions): - // - BUY/HOLD that went up: add the positive return (we profited from long position) - // - SELL that went down: add the absolute value (we avoided loss by exiting) - if (decision === 'BUY' || decision === 'HOLD') { - correctTotalReturn += return1d; // Positive return from long position - } else if (decision === 'SELL') { - correctTotalReturn += Math.abs(return1d); // We avoided this loss by exiting - } - } else { - incorrectCount++; - // For incorrect predictions (LONG-ONLY strategy, no short positions): - // - BUY/HOLD that went down: subtract the loss (we lost on long position) - // - SELL that went up: subtract the missed gain (we missed out by exiting) - if (decision === 'BUY' || decision === 'HOLD') { - incorrectTotalReturn += return1d; // Negative return (loss from long position) - } else if (decision === 'SELL') { - incorrectTotalReturn += -Math.abs(return1d); // We missed this gain by exiting - } - } - } - - const totalStocks = correctCount + incorrectCount; - - // Calculate weighted average - // correct_avg * (correct_count/total) + incorrect_avg * (incorrect_count/total) - const correctAvg = correctCount > 0 ? correctTotalReturn / correctCount : 0; - const incorrectAvg = incorrectCount > 0 ? incorrectTotalReturn / incorrectCount : 0; - - const weightedReturn = totalStocks > 0 - ? (correctAvg * (correctCount / totalStocks)) + (incorrectAvg * (incorrectCount / totalStocks)) - : 0; - - return { - date, - avgReturn1d: Math.round(weightedReturn * 10) / 10, - avgReturn1m: 0, // Not used with new calculation - totalStocks, - correctPredictions: correctCount, - accuracy: totalStocks > 0 ? Math.round((correctCount / totalStocks) * 100) : 0, - buyCount: rec.summary.buy, - sellCount: rec.summary.sell, - holdCount: rec.summary.hold, - }; -} - -// Get detailed return breakdown for the explanation modal -export function getReturnBreakdown(date: string): ReturnBreakdown | null { - const rec = getRecommendationByDate(date); - if (!rec) return null; - - const correctStocks: { symbol: string; decision: string; return1d: number }[] = []; - const incorrectStocks: { symbol: string; decision: string; return1d: number }[] = []; - let correctTotalReturn = 0; - let incorrectTotalReturn = 0; - - const symbols = Object.keys(rec.analysis); - for (const symbol of symbols) { - const stockAnalysis = rec.analysis[symbol]; - const backtest = getBacktestResult(symbol); - if (!backtest || !stockAnalysis?.decision) continue; - - const decision = stockAnalysis.decision; - const return1d = backtest.actual_return_1d; - - if (backtest.prediction_correct) { - let effectiveReturn = 0; - if (decision === 'BUY') { - effectiveReturn = return1d; - } else if (decision === 'SELL') { - effectiveReturn = Math.abs(return1d); - } else { - effectiveReturn = Math.abs(return1d) < 2 ? 0.1 : 0; - } - correctTotalReturn += effectiveReturn; - correctStocks.push({ symbol, decision, return1d: effectiveReturn }); - } else { - let effectiveReturn = 0; - if (decision === 'BUY') { - effectiveReturn = return1d; - } else if (decision === 'SELL') { - effectiveReturn = -Math.abs(return1d); - } else { - effectiveReturn = -Math.abs(return1d); - } - incorrectTotalReturn += effectiveReturn; - incorrectStocks.push({ symbol, decision, return1d: effectiveReturn }); - } - } - - const correctCount = correctStocks.length; - const incorrectCount = incorrectStocks.length; - const totalStocks = correctCount + incorrectCount; - - const correctAvg = correctCount > 0 ? correctTotalReturn / correctCount : 0; - const incorrectAvg = incorrectCount > 0 ? incorrectTotalReturn / incorrectCount : 0; - - const weightedReturn = totalStocks > 0 - ? (correctAvg * (correctCount / totalStocks)) + (incorrectAvg * (incorrectCount / totalStocks)) - : 0; - - const formula = totalStocks > 0 - ? `(${correctAvg.toFixed(2)}% × ${correctCount}/${totalStocks}) + (${incorrectAvg.toFixed(2)}% × ${incorrectCount}/${totalStocks}) = ${weightedReturn.toFixed(2)}%` - : 'No data'; - - return { - correctPredictions: { - count: correctCount, - totalReturn: Math.round(correctTotalReturn * 10) / 10, - avgReturn: Math.round(correctAvg * 10) / 10, - stocks: correctStocks.sort((a, b) => b.return1d - a.return1d).slice(0, 5), // Top 5 - }, - incorrectPredictions: { - count: incorrectCount, - totalReturn: Math.round(incorrectTotalReturn * 10) / 10, - avgReturn: Math.round(incorrectAvg * 10) / 10, - stocks: incorrectStocks.sort((a, b) => a.return1d - b.return1d).slice(0, 5), // Bottom 5 - }, - weightedReturn: Math.round(weightedReturn * 10) / 10, - formula, - }; -} - -// Get overall statistics across all recommendation dates -// Uses compound returns (multiplier approach) for realistic portfolio simulation -export function getOverallStats(): OverallStats { - const dates = getAvailableDates(); - let compoundMultiplier = 1; // Start with 1 (100%) - let totalPredictions = 0; - let totalCorrect = 0; - - let bestDay: { date: string; return: number } | null = null; - let worstDay: { date: string; return: number } | null = null; - - // Sort dates chronologically for proper compounding - const sortedDates = [...dates].sort((a, b) => new Date(a).getTime() - new Date(b).getTime()); - - for (const date of sortedDates) { - const stats = getDateStats(date); - if (stats) { - // Compound the daily return: multiply by (1 + daily_return/100) - compoundMultiplier *= (1 + stats.avgReturn1d / 100); - - totalPredictions += stats.totalStocks; - totalCorrect += stats.correctPredictions; - - if (!bestDay || stats.avgReturn1d > bestDay.return) { - bestDay = { date, return: stats.avgReturn1d }; - } - if (!worstDay || stats.avgReturn1d < worstDay.return) { - worstDay = { date, return: stats.avgReturn1d }; - } - } - } - - // Convert multiplier back to percentage: (multiplier - 1) * 100 - const compoundReturn = (compoundMultiplier - 1) * 100; - - return { - totalDays: dates.length, - totalPredictions, - avgDailyReturn: Math.round(compoundReturn * 10) / 10, // This is now the compound return - avgMonthlyReturn: 0, // Not used - overallAccuracy: totalPredictions > 0 ? Math.round((totalCorrect / totalPredictions) * 100) : 0, - bestDay, - worstDay, - }; -} - -// Get detailed breakdown of overall compound return calculation -export function getOverallReturnBreakdown(): { - dailyReturns: { date: string; return: number; multiplier: number; cumulative: number }[]; - finalMultiplier: number; - finalReturn: number; - formula: string; -} { - const dates = getAvailableDates(); - const sortedDates = [...dates].sort((a, b) => new Date(a).getTime() - new Date(b).getTime()); - - const dailyReturns: { date: string; return: number; multiplier: number; cumulative: number }[] = []; - let cumulativeMultiplier = 1; - - for (const date of sortedDates) { - const stats = getDateStats(date); - if (stats) { - const dailyMultiplier = 1 + stats.avgReturn1d / 100; - cumulativeMultiplier *= dailyMultiplier; - dailyReturns.push({ - date, - return: stats.avgReturn1d, - multiplier: Math.round(dailyMultiplier * 10000) / 10000, - cumulative: Math.round((cumulativeMultiplier - 1) * 1000) / 10, // As percentage - }); - } - } - - const finalReturn = (cumulativeMultiplier - 1) * 100; - const multiplierParts = dailyReturns.map(d => `(1 + ${d.return}%)`).join(' × '); - const formula = dailyReturns.length > 0 - ? `${multiplierParts} = ${cumulativeMultiplier.toFixed(4)} → ${finalReturn.toFixed(2)}%` - : 'No data'; - - return { - dailyReturns, - finalMultiplier: Math.round(cumulativeMultiplier * 10000) / 10000, - finalReturn: Math.round(finalReturn * 10) / 10, - formula, - }; -} - -// =============================================== -// NEW FUNCTIONS FOR ENHANCED FEATURES -// =============================================== - -// Get Nifty50 Index historical data -export function getNifty50IndexHistory(): Nifty50IndexPoint[] { - const dates = getAvailableDates(); - const sortedDates = [...dates].sort((a, b) => new Date(a).getTime() - new Date(b).getTime()); - - const indexData: Nifty50IndexPoint[] = []; - let indexValue = 21500; // Starting Nifty value - - for (let i = 0; i < sortedDates.length; i++) { - const date = sortedDates[i]; - const seed = getSymbolSeed(date) + 9999; - - // Generate realistic daily return (-1.5% to +1.5% range) - const dailyReturn = (seededRandom(seed) - 0.5) * 3; - indexValue = indexValue * (1 + dailyReturn / 100); - - indexData.push({ - date, - value: Math.round(indexValue * 100) / 100, - return: Math.round(dailyReturn * 10) / 10, - }); - } - - return indexData; -} - -// Calculate risk metrics for the AI trading strategy -export function calculateRiskMetrics(): RiskMetrics { - const dates = getAvailableDates(); - const dailyReturns: number[] = []; - let wins = 0; - let losses = 0; - let totalWinReturn = 0; - let totalLossReturn = 0; - let totalCorrect = 0; - let totalPredictions = 0; - - // Collect daily returns and win/loss stats - for (const date of dates) { - const stats = getDateStats(date); - if (stats) { - dailyReturns.push(stats.avgReturn1d); - totalCorrect += stats.correctPredictions; - totalPredictions += stats.totalStocks; - - if (stats.avgReturn1d > 0) { - wins++; - totalWinReturn += stats.avgReturn1d; - } else if (stats.avgReturn1d < 0) { - losses++; - totalLossReturn += Math.abs(stats.avgReturn1d); - } - } - } - - // Calculate standard deviation (volatility) - const mean = dailyReturns.length > 0 - ? dailyReturns.reduce((a, b) => a + b, 0) / dailyReturns.length - : 0; - const variance = dailyReturns.length > 0 - ? dailyReturns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / dailyReturns.length - : 0; - const volatility = Math.sqrt(variance); - - // Calculate Sharpe ratio (assuming 0.02% risk-free daily rate, ~5% annual) - const riskFreeRate = 0.02; - const sharpeRatio = volatility > 0 ? (mean - riskFreeRate) / volatility : 0; - - // Calculate max drawdown - let peak = 100; - let maxDrawdown = 0; - let maxDrawdownPeak = 100; - let maxDrawdownTrough = 100; - let currentValue = 100; - for (const ret of dailyReturns) { - currentValue = currentValue * (1 + ret / 100); - if (currentValue > peak) { - peak = currentValue; - } - const drawdown = ((peak - currentValue) / peak) * 100; - if (drawdown > maxDrawdown) { - maxDrawdown = drawdown; - maxDrawdownPeak = peak; - maxDrawdownTrough = currentValue; - } - } - - // Calculate win/loss ratio - const avgWin = wins > 0 ? totalWinReturn / wins : 0; - const avgLoss = losses > 0 ? totalLossReturn / losses : 1; - const winLossRatio = avgLoss > 0 ? avgWin / avgLoss : avgWin; - - // Win rate - const winRate = totalPredictions > 0 ? (totalCorrect / totalPredictions) * 100 : 0; - - return { - sharpeRatio: Math.round(sharpeRatio * 100) / 100, - maxDrawdown: Math.round(maxDrawdown * 10) / 10, - winLossRatio: Math.round(winLossRatio * 100) / 100, - winRate: Math.round(winRate), - volatility: Math.round(volatility * 100) / 100, - totalTrades: totalPredictions, - // Calculation details for showing formulas - meanReturn: Math.round(mean * 100) / 100, - riskFreeRate: riskFreeRate, - winningTrades: totalCorrect, - losingTrades: totalPredictions - totalCorrect, - avgWinReturn: Math.round(avgWin * 100) / 100, - avgLossReturn: Math.round(avgLoss * 100) / 100, - peakValue: Math.round(maxDrawdownPeak * 100) / 100, - troughValue: Math.round(maxDrawdownTrough * 100) / 100, - }; -} - -// Get return distribution histogram -export function getReturnDistribution(): ReturnBucket[] { - const buckets: ReturnBucket[] = [ - { range: '< -3%', min: -Infinity, max: -3, count: 0, stocks: [] }, - { range: '-3% to -2%', min: -3, max: -2, count: 0, stocks: [] }, - { range: '-2% to -1%', min: -2, max: -1, count: 0, stocks: [] }, - { range: '-1% to 0%', min: -1, max: 0, count: 0, stocks: [] }, - { range: '0% to 1%', min: 0, max: 1, count: 0, stocks: [] }, - { range: '1% to 2%', min: 1, max: 2, count: 0, stocks: [] }, - { range: '2% to 3%', min: 2, max: 3, count: 0, stocks: [] }, - { range: '> 3%', min: 3, max: Infinity, count: 0, stocks: [] }, - ]; - - // Get all stocks from latest recommendation and their returns - const latestRec = sampleRecommendations[0]; - if (!latestRec) return buckets; - - for (const symbol of Object.keys(latestRec.analysis)) { - const backtest = getBacktestResult(symbol); - if (!backtest) continue; - - const returnVal = backtest.actual_return_1d; - - for (const bucket of buckets) { - if (returnVal >= bucket.min && returnVal < bucket.max) { - bucket.count++; - bucket.stocks.push(symbol); - break; - } - } - } - - return buckets; -} - -// Get accuracy trend over time -export function getAccuracyTrend(): AccuracyTrendPoint[] { - const dates = getAvailableDates(); - const sortedDates = [...dates].sort((a, b) => new Date(a).getTime() - new Date(b).getTime()); - - return sortedDates.map(date => { - const rec = getRecommendationByDate(date); - if (!rec) { - return { date, overall: 0, buy: 0, sell: 0, hold: 0 }; - } - - let totalBuy = 0, correctBuy = 0; - let totalSell = 0, correctSell = 0; - let totalHold = 0, correctHold = 0; - - for (const symbol of Object.keys(rec.analysis)) { - const stockAnalysis = rec.analysis[symbol]; - const backtest = getBacktestResult(symbol); - if (!backtest || !stockAnalysis?.decision) continue; - - if (stockAnalysis.decision === 'BUY') { - totalBuy++; - if (backtest.prediction_correct) correctBuy++; - } else if (stockAnalysis.decision === 'SELL') { - totalSell++; - if (backtest.prediction_correct) correctSell++; - } else { - totalHold++; - if (backtest.prediction_correct) correctHold++; - } - } - - const total = totalBuy + totalSell + totalHold; - const correct = correctBuy + correctSell + correctHold; - - return { - date, - overall: total > 0 ? Math.round((correct / total) * 100) : 0, - buy: totalBuy > 0 ? Math.round((correctBuy / totalBuy) * 100) : 0, - sell: totalSell > 0 ? Math.round((correctSell / totalSell) * 100) : 0, - hold: totalHold > 0 ? Math.round((correctHold / totalHold) * 100) : 0, - }; - }); -} - -// Get cumulative portfolio data for charting -export function getCumulativeReturns(): { date: string; value: number; aiReturn: number; indexReturn: number }[] { - const dates = getAvailableDates(); - const sortedDates = [...dates].sort((a, b) => new Date(a).getTime() - new Date(b).getTime()); - const indexData = getNifty50IndexHistory(); - - const data: { date: string; value: number; aiReturn: number; indexReturn: number }[] = []; - let aiMultiplier = 1; - let indexMultiplier = 1; - - for (let i = 0; i < sortedDates.length; i++) { - const date = sortedDates[i]; - const stats = getDateStats(date); - const indexPoint = indexData.find(d => d.date === date); - - if (stats) { - aiMultiplier *= (1 + stats.avgReturn1d / 100); - } - if (indexPoint) { - indexMultiplier *= (1 + indexPoint.return / 100); - } - - data.push({ - date, - value: Math.round(aiMultiplier * 10000) / 100, // As percentage of starting value - aiReturn: Math.round((aiMultiplier - 1) * 1000) / 10, - indexReturn: Math.round((indexMultiplier - 1) * 1000) / 10, - }); - } - - return data; -} - -// Get all unique sectors from stocks -export function getAllSectors(): string[] { - const sectors = new Set(); - for (const stock of nifty50List) { - if (stock.sector) { - sectors.add(stock.sector); - } - } - return Array.from(sectors).sort(); -} - -// Get stock history with prediction outcomes (1-day return after each prediction) -export interface StockHistoryWithOutcome { - date: string; - decision: Decision; - outcome: { - return1d: number; - predictionCorrect: boolean; - } | null; -} - -export function getStockHistoryWithOutcomes(symbol: string): StockHistoryWithOutcome[] { - const history = getStockHistory(symbol); - - // Map each historical entry with simulated outcomes based on the decision - return history.map((entry, index) => { - // Use seeded random for consistent outcomes per stock/date combination - const seed = getSymbolSeed(symbol) + getSymbolSeed(entry.date) + index; - - // First, generate the return randomly (with market-like distribution) - // Return can be positive or negative - this is NOT pre-determined by decision - const return1d = (seededRandom(seed) - 0.45) * 6; // -2.7% to +3.3% range (slight positive bias) - - // Determine prediction correctness based on actual return vs decision - // This is the CORRECT logic: BUY/HOLD correct if return > 0, SELL correct if return < 0 - let predictionCorrect: boolean; - if (entry.decision === 'BUY' || entry.decision === 'HOLD') { - // BUY and HOLD are correct if stock price went up - predictionCorrect = return1d > 0; - } else { - // SELL is correct if stock price went down - predictionCorrect = return1d < 0; - } - - return { - date: entry.date, - decision: entry.decision, - outcome: { - return1d: Math.round(return1d * 10) / 10, - predictionCorrect, - }, - }; - }); -} - -// Get prediction accuracy stats for a specific stock -export function getStockPredictionStats(symbol: string): { - totalPredictions: number; - correctPredictions: number; - accuracy: number; - avgReturn: number; - buyAccuracy: number; - sellAccuracy: number; - holdAccuracy: number; -} { - const history = getStockHistoryWithOutcomes(symbol); - - let correct = 0; - let totalReturn = 0; - let buyTotal = 0, buyCorrect = 0; - let sellTotal = 0, sellCorrect = 0; - let holdTotal = 0, holdCorrect = 0; - - for (const entry of history) { - if (entry.outcome) { - totalReturn += entry.outcome.return1d; - if (entry.outcome.predictionCorrect) correct++; - - if (entry.decision === 'BUY') { - buyTotal++; - if (entry.outcome.predictionCorrect) buyCorrect++; - } else if (entry.decision === 'SELL') { - sellTotal++; - if (entry.outcome.predictionCorrect) sellCorrect++; - } else { - holdTotal++; - if (entry.outcome.predictionCorrect) holdCorrect++; - } - } - } - - return { - totalPredictions: history.length, - correctPredictions: correct, - accuracy: history.length > 0 ? Math.round((correct / history.length) * 100) : 0, - avgReturn: history.length > 0 ? Math.round((totalReturn / history.length) * 10) / 10 : 0, - buyAccuracy: buyTotal > 0 ? Math.round((buyCorrect / buyTotal) * 100) : 0, - sellAccuracy: sellTotal > 0 ? Math.round((sellCorrect / sellTotal) * 100) : 0, - holdAccuracy: holdTotal > 0 ? Math.round((holdCorrect / holdTotal) * 100) : 0, - }; -} diff --git a/frontend/src/index.css b/frontend/src/index.css index 16e0c0cf..bfadc180 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,5 +1,8 @@ @import "tailwindcss"; +/* Use class-based dark mode (html.dark) instead of media query */ +@variant dark (&:where(.dark, .dark *)); + @theme { /* Custom colors */ --color-nifty-50: #f0f9ff; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index caafb8b2..a5d1cb57 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -5,8 +5,6 @@ import TopPicks, { StocksToAvoid } from '../components/TopPicks'; import { DecisionBadge, HoldDaysBadge, RankBadge } from '../components/StockCard'; import TerminalModal from '../components/TerminalModal'; import HowItWorks from '../components/HowItWorks'; -import BackgroundSparkline from '../components/BackgroundSparkline'; -import { getLatestRecommendation, getBacktestResult as getStaticBacktestResult } from '../data/recommendations'; import { api } from '../services/api'; import { useSettings } from '../contexts/SettingsContext'; import { useNotification } from '../contexts/NotificationContext'; @@ -19,28 +17,17 @@ export default function Dashboard() { // State for real API data const [recommendation, setRecommendation] = useState(null); const [isLoadingData, setIsLoadingData] = useState(true); - const [isUsingMockData, setIsUsingMockData] = useState(false); - // Fetch real recommendation from API + // Fetch recommendation from API const fetchRecommendation = useCallback(async () => { setIsLoadingData(true); try { const data = await api.getLatestRecommendation(); if (data && data.analysis && Object.keys(data.analysis).length > 0) { setRecommendation(data); - setIsUsingMockData(false); - } else { - // API returned empty data, fall back to mock - const mockData = getLatestRecommendation(); - setRecommendation(mockData || null); - setIsUsingMockData(true); } } catch (error) { console.error('Failed to fetch recommendation from API:', error); - // Fall back to mock data - const mockData = getLatestRecommendation(); - setRecommendation(mockData || null); - setIsUsingMockData(true); } finally { setIsLoadingData(false); } @@ -493,16 +480,6 @@ export default function Dashboard() {
- {/* Mock Data Indicator */} - {isUsingMockData && ( -
- - - Using demo data. Run "Analyze All" or start the backend server for real AI recommendations. - -
- )} - {/* Analysis Progress Banner */} {isAnalyzing && analysisProgress && (
@@ -662,8 +639,6 @@ export default function Dashboard() { {filteredItems.map((item) => { // COMPLETED with analysis data: clickable link if (item.liveState === 'completed' && item.analysis) { - const backtest = isUsingMockData ? getStaticBacktestResult(item.symbol) : null; - const trend = item.analysis.decision === 'BUY' ? 'up' : item.analysis.decision === 'SELL' ? 'down' : 'flat'; return ( - {backtest && ( -
- -
- )}
diff --git a/frontend/src/pages/History.tsx b/frontend/src/pages/History.tsx index af778620..9c4b341d 100644 --- a/frontend/src/pages/History.tsx +++ b/frontend/src/pages/History.tsx @@ -1,11 +1,9 @@ -import { useState, useMemo, useEffect, useCallback } from 'react'; +import { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; -import { Calendar, TrendingUp, TrendingDown, Minus, ChevronRight, BarChart3, Target, HelpCircle, Activity, Calculator, LineChart, PieChart, Shield, Filter, Loader2, AlertCircle } from 'lucide-react'; -import { sampleRecommendations, getBacktestResult as getStaticBacktestResult, calculateAccuracyMetrics as calculateStaticAccuracyMetrics, getDateStats as getStaticDateStats, getOverallStats as getStaticOverallStats, getReturnBreakdown as getStaticReturnBreakdown } from '../data/recommendations'; -import type { ReturnBreakdown } from '../data/recommendations'; +import { Calendar, TrendingUp, TrendingDown, Minus, ChevronRight, ChevronDown, BarChart3, Target, HelpCircle, Activity, Calculator, LineChart, PieChart, Shield, Filter, Clock, Zap, Award, ArrowUpRight, ArrowDownRight, Play, Loader2, FileText, MessageSquare, Search, XCircle, AlertTriangle } from 'lucide-react'; +import type { ReturnBreakdown } from '../types'; import { DecisionBadge, HoldDaysBadge, RankBadge } from '../components/StockCard'; import Sparkline from '../components/Sparkline'; -import AccuracyBadge from '../components/AccuracyBadge'; import AccuracyExplainModal from '../components/AccuracyExplainModal'; import ReturnExplainModal from '../components/ReturnExplainModal'; import OverallReturnModal, { type OverallReturnBreakdown } from '../components/OverallReturnModal'; @@ -16,29 +14,28 @@ import PortfolioSimulator, { type InvestmentMode } from '../components/Portfolio import IndexComparisonChart from '../components/IndexComparisonChart'; import InfoModal from '../components/InfoModal'; import { api } from '../services/api'; +import { useSettings } from '../contexts/SettingsContext'; import type { StockAnalysis, DailyRecommendation, RiskMetrics, ReturnBucket, CumulativeReturnPoint } from '../types'; -// Type for real backtest data -interface RealBacktestData { - symbol: string; +// Type for batch backtest data (per date, per symbol) +type BacktestByDate = Record; -} +}>>; // Helper for consistent positive/negative color classes function getValueColorClass(value: number): string { return value >= 0 - ? 'text-green-600 dark:text-green-400' - : 'text-red-600 dark:text-red-400'; + ? 'text-emerald-600 dark:text-emerald-400' + : 'text-red-500 dark:text-red-400'; } -// Investment Mode Toggle Component for reuse +// Investment Mode Toggle Component function InvestmentModeToggle({ mode, onChange, @@ -49,14 +46,14 @@ function InvestmentModeToggle({ size?: 'sm' | 'md'; }) { const sizeClasses = size === 'sm' - ? 'px-2 py-1 text-[10px]' + ? 'px-2.5 py-1 text-[10px]' : 'px-3 py-1.5 text-xs'; return ( -
+
- {isBacktestDataLoading ? ( -
- {['nifty', 'green', 'red', 'amber'].map(color => ( -
- - + +
+ {/* Circular gauge for Overall accuracy */} +
+
+ + + + +
+ + {(accuracyMetrics.success_rate * 100).toFixed(0)}% + + Overall
- ))} +
+

+ {accuracyMetrics.correct_predictions}/{accuracyMetrics.total_predictions} correct +

- ) : ( -
-
-
- {(accuracyMetrics.success_rate * 100).toFixed(0)}% + + {/* Progress bars for Buy / Sell / Hold */} +
+ {/* Buy */} +
+
+
+ + Buy Accuracy +
+ + {(accuracyMetrics.buy_accuracy * 100).toFixed(0)}% + +
+
+
-
Overall Accuracy
-
-
- {(accuracyMetrics.buy_accuracy * 100).toFixed(0)}% + + {/* Sell */} +
+
+
+ + Sell Accuracy +
+ + {(accuracyMetrics.sell_accuracy * 100).toFixed(0)}% + +
+
+
-
Buy Accuracy
-
-
- {(accuracyMetrics.sell_accuracy * 100).toFixed(0)}% + + {/* Hold */} +
+
+
+ + Hold Accuracy +
+ + {(accuracyMetrics.hold_accuracy * 100).toFixed(0)}% +
-
Sell Accuracy
-
-
-
- {(accuracyMetrics.hold_accuracy * 100).toFixed(0)}% +
+
-
Hold Accuracy
- )} -

- {isBacktestDataLoading - ? 'Fetching backtest data from market...' - : `Based on ${accuracyMetrics.total_predictions} predictions tracked over time` - } -

+
{/* Accuracy Trend Chart */} -
-
-
- -

Accuracy Trend

-
- {isLoadingAccuracyTrend && ( -
- - Loading real data... -
- )} -
- {isBacktestDataLoading && !isUsingMockData ? ( - - ) : ( - <> - 0 ? accuracyTrendData : undefined) - : accuracyTrendData - } - /> -

- {accuracyTrendData.length > 0 ? ( - <>Prediction accuracy from real backtest data over {accuracyTrendData.length} trading days - ) : isUsingMockData ? ( - <>Demo data - Start backend for real accuracy tracking - ) : ( - <>Prediction accuracy over the past {dates.length} trading days - )} -

- - )} +
+ } + title="Accuracy Trend" + subtitle={accuracyTrendData.length > 0 ? `${accuracyTrendData.length} trading days tracked` : undefined} + /> +
{/* Risk Metrics */} -
-
-
- -

Risk Metrics

-
- {isLoadingRiskMetrics && ( -
- - Loading real data... -
- )} -
- {isBacktestDataLoading && !isUsingMockData ? ( - - ) : ( - <> - -

- {realRiskMetrics ? ( - <>Risk-adjusted performance from real backtest data ({realRiskMetrics.totalTrades} trades) - ) : isUsingMockData ? ( - <>Demo data - Start backend for real risk metrics - ) : ( - <>Risk-adjusted performance metrics for the AI trading strategy - )} -

- - )} +
+ } + title="Risk Metrics" + subtitle={chartData.riskMetrics ? `${chartData.riskMetrics.totalTrades} trades analyzed` : undefined} + /> +
{/* Portfolio Simulator */} - {/* Date Selector with Mode Toggle */} -
-
-
- -

Select Date

-
-
- - -
+ {/* Date Selector */} +
+ } + title="Select Date" + right={ +
+ + +
+ } + /> + + {/* Backtest Date Input */} +
+ + setBacktestDateInput(e.target.value)} + className="flex-1 px-3 py-1.5 text-sm bg-white dark:bg-slate-600 border border-gray-200 dark:border-slate-500 rounded-lg focus:ring-2 focus:ring-nifty-500 focus:border-transparent outline-none text-gray-900 dark:text-gray-100" + max={new Date().toISOString().split('T')[0]} + /> +
+ + {/* Backtest feedback message */} + {backtestMessage && ( +
+
+ {backtestMessage.type === 'progress' && } + {backtestMessage.text} + {backtestMessage.type === 'progress' ? ( + + ) : ( + + )} +
+ {backtestMessage.type === 'progress' && ( +
+ + Note: Price data & technical indicators use historical data for the selected date. News & analyst ratings reflect current data (yfinance limitation). +
+ )} +
+ )} +
{dates.map((date) => { const rec = getRecommendation(date); const stats = dateStatsMap[date]; const avgReturn = stats?.avgReturn1d ?? 0; - const hasBacktestData = !isUsingMockData ? (realDateReturns[date] !== undefined) : true; + const hasData = chartData.dateReturns[date] !== undefined; const isPositive = avgReturn >= 0; - // Calculate filtered summary for this date const filteredSummary = dateFilterMode === 'topPicks' ? { buy: rec?.top_picks.length || 0, sell: 0, hold: 0 } : rec?.summary || { buy: 0, sell: 0, hold: 0 }; @@ -1131,18 +1054,14 @@ export default function History() {
- {/* Help button for return explanation */} -
{/* Selected Date Details */} {selectedDate && ( -
-
+
+
-

+
+ +
+

{new Date(selectedDate).toLocaleDateString('en-IN', { - weekday: 'short', - month: 'short', - day: 'numeric', - year: 'numeric', + weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', })}

@@ -1223,21 +1127,21 @@ export default function History() {
{dateFilterMode === 'all50' ? ( <> - - + + {getRecommendation(selectedDate)?.summary.buy} Buy - - + + {getRecommendation(selectedDate)?.summary.sell} Sell - + {getRecommendation(selectedDate)?.summary.hold} Hold ) : ( - + {getRecommendation(selectedDate)?.top_picks.length} Top Picks (BUY) @@ -1246,85 +1150,252 @@ export default function History() {
- {isLoadingBacktest ? ( -
- -

Fetching real market data...

+ {/* Detailed Backtest Action Bar */} +
+ + {detailedBacktest?.date === selectedDate + ? `Detailed backtest: ${detailedBacktest.total_stocks} stocks with live P&L` + : 'View live P&L with full explainability'} + + +
+ + {detailedBacktest && detailedBacktest.date === selectedDate ? ( + /* Detailed expandable view */ +
+ {detailedBacktest.stocks.map((stock) => { + const isExpanded = expandedStock === stock.symbol; + const returnVal = stock.return_at_hold ?? stock.return_current; + const isPositiveReturn = returnVal !== null && returnVal >= 0; + + return ( +
+ {/* Stock Summary Row */} + + + {/* Expanded Detail */} + {isExpanded && ( +
+ + {/* P&L Formula */} +
+
+ + P&L Formula +
+
+                            {stock.formula || 'No formula available'}
+                          
+
+ + {/* Hold Period Progress */} +
+
+
+ + Hold Period +
+ + {stock.hold_period_active ? 'Active' : 'Completed'} + +
+
+
+
+
+ {stock.hold_days_elapsed} / {stock.hold_days} days elapsed + {stock.prediction_correct !== null && ( + + Prediction {stock.prediction_correct ? 'Correct' : 'Incorrect'} + + )} +
+
+ + {/* Decision Reasoning */} +
+
+ + Decision Reasoning +
+
+ + + {stock.confidence} confidence, {stock.risk} risk + +
+

+ {stock.raw_analysis || 'No analysis text available'} +

+
+ + {/* Agent Reports */} + {Object.keys(stock.agent_summary).length > 0 && ( +
+
+ + Agent Reports +
+
+ {Object.keys(stock.agent_summary).map((key) => ( + + ))} +
+

+ {stock.agent_summary[activeAgentTab] || 'No report available for this agent'} +

+
+ )} + + {/* Debate Summary */} + {Object.keys(stock.debate_summary).length > 0 && ( +
+
+ + Debate Summary +
+
+ {Object.entries(stock.debate_summary).map(([type, summary]) => ( +
+ {type}:{' '} + {summary} +
+ ))} +
+
+ )} + + {/* Link to full stock detail */} + + View full stock detail + + +
+ )} +
+ ); + })}
) : ( -
+ /* Simple stock list */ +
{getFilteredStocks(selectedDate).map((stock: StockAnalysis) => { - const realData = realBacktestData[stock.symbol]; - - let nextDayReturn: number | null; - let priceHistory: Array<{ date: string; price: number }> | undefined; + const bt = batchBacktestByDate[selectedDate]?.[stock.symbol]; + let nextDayReturn: number | null = null; let predictionCorrect: boolean | null = null; - if (!isUsingMockData) { - // Real data mode: use hold-period return when available - nextDayReturn = realData?.primaryReturn ?? realData?.return1d ?? null; - priceHistory = realData?.priceHistory; - if (realData?.predictionCorrect !== undefined) { - predictionCorrect = realData.predictionCorrect; - } - } else { - // Mock data mode: use real if available, fall back to mock - const mockBacktest = getStaticBacktestResult(stock.symbol); - nextDayReturn = realData?.primaryReturn ?? realData?.return1d ?? mockBacktest?.actual_return_1d ?? 0; - priceHistory = realData?.priceHistory ?? mockBacktest?.price_history; - if (realData?.predictionCorrect !== undefined) { - predictionCorrect = realData.predictionCorrect; - } else if (mockBacktest && stock.decision) { - if (stock.decision === 'BUY' || stock.decision === 'HOLD') { - predictionCorrect = nextDayReturn > 0; - } else if (stock.decision === 'SELL') { - predictionCorrect = nextDayReturn < 0; - } + if (bt) { + nextDayReturn = bt.return_at_hold ?? bt.return_1d ?? null; + if (nextDayReturn !== null) { + predictionCorrect = (stock.decision === 'BUY' || stock.decision === 'HOLD') + ? nextDayReturn > 0 + : nextDayReturn < 0; } } - const isPositive = (nextDayReturn ?? 0) >= 0; return (
- {stock.symbol} + {stock.symbol} {stock.company_name} - {realData && ( - - Real - - )}
{nextDayReturn !== null && ( - + {nextDayReturn >= 0 ? '+' : ''}{nextDayReturn.toFixed(1)}% - {realData?.holdDays && /{realData.holdDays}d} + {bt?.hold_days && /{bt.hold_days}d} )} - {predictionCorrect !== null && ( - - )} - {priceHistory && ( - - )} - +
); @@ -1334,195 +1405,100 @@ export default function History() {
)} - {/* Performance Summary Cards with Mode Toggle */} -
-
-
- -

Performance Summary

-
- + {/* Performance Summary */} +
+ } + title="Performance Summary" + right={} + /> +
+ {[ + { label: 'Days Tracked', value: filteredStats.totalDays.toString(), icon: , color: 'nifty', modal: 'daysTracked' as SummaryModalType }, + { label: 'Avg Return', value: `${filteredStats.avgDailyReturn >= 0 ? '+' : ''}${filteredStats.avgDailyReturn.toFixed(1)}%`, icon: , color: filteredStats.avgDailyReturn >= 0 ? 'emerald' : 'red', modal: 'avgReturn' as SummaryModalType }, + { label: summaryMode === 'topPicks' ? 'Top Picks' : 'Buy Signals', value: filteredStats.buySignals.toString(), icon: , color: 'emerald', modal: 'buySignals' as SummaryModalType }, + { label: 'Sell Signals', value: filteredStats.sellSignals.toString(), icon: , color: 'red', modal: 'sellSignals' as SummaryModalType }, + ].map(({ label, value, icon, color, modal }) => ( +
setActiveSummaryModal(modal)} + > +
{value}
+
+ {icon} + {label} + +
+
+ ))}
- {isBacktestDataLoading && !isUsingMockData ? ( -
- {[1, 2, 3, 4].map(i => ( -
- - -
- ))} -
- ) : ( -
-
setActiveSummaryModal('daysTracked')} - > -
{filteredStats.totalDays}
-
- Days Tracked -
-
-
setActiveSummaryModal('avgReturn')} - > -
- {filteredStats.avgDailyReturn >= 0 ? '+' : ''}{filteredStats.avgDailyReturn.toFixed(1)}% -
-
- Avg Return -
-
-
setActiveSummaryModal('buySignals')} - > -
- {filteredStats.buySignals} -
-
- {summaryMode === 'topPicks' ? 'Top Pick Signals' : 'Buy Signals'} -
-
-
setActiveSummaryModal('sellSignals')} - > -
- {filteredStats.sellSignals} -
-
- Sell Signals -
-
-
- )}

- {isBacktestDataLoading && !isUsingMockData - ? 'Loading performance data from market...' - : summaryMode === 'topPicks' - ? 'Performance based on Top Picks recommendations only (3 stocks per day)' - : 'Returns measured over hold period (or 1-day when no hold period specified)' - } + {summaryMode === 'topPicks' + ? 'Performance based on Top Picks recommendations only (3 stocks per day)' + : 'Returns measured over hold period (or 1-day when no hold period specified)'}

{/* AI vs Nifty50 Index Comparison */} -
-
-
- -

AI Strategy vs Nifty50 Index

-
-
- {isLoadingCumulativeReturns && ( -
- - Loading... -
- )} - -
-
- {isBacktestDataLoading && !isUsingMockData ? ( - - ) : ( - <> - -

- {(indexChartMode === 'topPicks' ? topPicksCumulativeReturns : realCumulativeReturns)?.length ? ( - <> - Cumulative returns for {indexChartMode === 'topPicks' ? 'Top Picks' : 'All 50 stocks'} over{' '} - {(indexChartMode === 'topPicks' ? topPicksCumulativeReturns : realCumulativeReturns)?.length} trading days - - ) : isUsingMockData ? ( - <>Demo data - Start backend for real performance comparison - ) : ( - <>Comparison of cumulative returns between AI strategy and Nifty50 index - )} -

- - )} +
+ } + title="AI Strategy vs Nifty50 Index" + right={} + /> + +

+ {(indexChartMode === 'topPicks' ? chartData.topPicksCumulativeReturns : chartData.cumulativeReturns)?.length ? ( + <>Cumulative returns for {indexChartMode === 'topPicks' ? 'Top Picks' : 'All 50 stocks'} over {(indexChartMode === 'topPicks' ? chartData.topPicksCumulativeReturns : chartData.cumulativeReturns)?.length} trading days + ) : ( + <>AI strategy vs Nifty50 index cumulative returns + )} +

{/* Return Distribution */} -
-
-
- -

Return Distribution

-
-
- {isLoadingReturnDistribution && ( -
- - Loading... -
- )} - -
-
- {isBacktestDataLoading && !isUsingMockData ? ( - - ) : ( - <> - -

- {(distributionMode === 'topPicks' ? topPicksReturnDistribution : realReturnDistribution) ? ( - <>Distribution of {distributionMode === 'topPicks' ? 'Top Picks' : 'all 50 stocks'} hold-period returns. Click bars to see stocks. - ) : isUsingMockData ? ( - <>Demo data - Start backend for real return distribution - ) : ( - <>Distribution of hold-period returns across all predictions. Click bars to see stocks. - )} -

- - )} +
+ } + title="Return Distribution" + right={} + /> + +

+ {(distributionMode === 'topPicks' ? chartData.topPicksReturnDistribution : chartData.returnDistribution)?.some(b => b.count > 0) ? ( + <>Distribution of {distributionMode === 'topPicks' ? 'Top Picks' : 'all 50 stocks'} hold-period returns. Click bars to see stocks. + ) : ( + <>Distribution of hold-period returns across all predictions + )} +

- {/* Accuracy Explanation Modal */} - setShowAccuracyModal(false)} - metrics={accuracyMetrics} - /> + {/* Modals */} + setShowAccuracyModal(false)} metrics={accuracyMetrics} /> - {/* Return Calculation Modal */} setShowReturnModal(false)} - breakdown={returnModalDate ? (isUsingMockData ? getStaticReturnBreakdown(returnModalDate) : buildReturnBreakdown(returnModalDate)) : null} + breakdown={returnModalDate ? buildReturnBreakdown(returnModalDate) : null} date={returnModalDate || ''} /> - {/* Overall Return Modal */} setShowOverallModal(false)} - breakdown={realOverallBreakdown} - cumulativeData={realCumulativeReturns} + breakdown={chartData.overallBreakdown} + cumulativeData={chartData.cumulativeReturns} /> {/* Performance Summary Modals */} - setActiveSummaryModal(null)} - title="Days Tracked" - icon={} - > + setActiveSummaryModal(null)} title="Days Tracked" icon={}>

Days Tracked shows the total number of trading days where AI recommendations have been recorded and analyzed.

@@ -1533,23 +1509,17 @@ export default function History() {
- setActiveSummaryModal(null)} - title="Average Return" - icon={} - > + setActiveSummaryModal(null)} title="Average Return" icon={}>

Average Return measures the mean percentage price change over each stock's recommended hold period.

How it's calculated:
  1. Record stock price at recommendation time
  2. -
  3. Record price after the recommended hold period (e.g. 15 days)
  4. -
  5. Calculate: (Exit Price - Entry Price) / Entry Price × 100
  6. -
  7. Average all these returns across stocks
  8. +
  9. Record price after the recommended hold period
  10. +
  11. Calculate: (Exit - Entry) / Entry x 100
  12. +
  13. Average all returns across stocks
-

If no hold period is specified, falls back to 1-day return.

= 0 ? 'bg-green-50 dark:bg-green-900/20' : 'bg-red-50 dark:bg-red-900/20'} rounded-lg`}>
Current Average:
@@ -1560,70 +1530,26 @@ export default function History() {
- setActiveSummaryModal(null)} - title={summaryMode === 'topPicks' ? 'Top Pick Signals' : 'Buy Signals'} - icon={} - > + setActiveSummaryModal(null)} title={summaryMode === 'topPicks' ? 'Top Pick Signals' : 'Buy Signals'} icon={}>
- {summaryMode === 'topPicks' ? ( - <> -

Top Pick Signals counts all stocks that were selected as "Top Picks" across all tracked days.

-
-
What makes a Top Pick?
-
    -
  • Strong bullish momentum indicators
  • -
  • Positive technical analysis signals
  • -
  • Favorable risk-reward ratio
  • -
  • High confidence BUY recommendation
  • -
-
- - ) : ( - <> -

Buy Signals counts every BUY recommendation issued by the AI across all tracked days and all 50 stocks.

-
-
When is BUY recommended?
-
    -
  • Technical indicators show bullish momentum
  • -
  • Positive sentiment in news/fundamentals
  • -
  • Expected price appreciation in short term
  • -
-
- - )} +

{summaryMode === 'topPicks' ? 'Top Pick Signals' : 'Buy Signals'} counts all {summaryMode === 'topPicks' ? 'Top Picks' : 'BUY recommendations'} across all tracked days.

- Total {summaryMode === 'topPicks' ? 'Top Pick' : 'Buy'} Signals: + Total: {filteredStats.buySignals}
- setActiveSummaryModal(null)} - title="Sell Signals" - icon={} - > + setActiveSummaryModal(null)} title="Sell Signals" icon={}>
-

Sell Signals counts every SELL recommendation issued by the AI across all tracked days.

-
-
When is SELL recommended?
-
    -
  • Technical indicators show bearish momentum
  • -
  • Negative sentiment in news/fundamentals
  • -
  • Expected price decline in short term
  • -
  • Risk level exceeds acceptable threshold
  • -
-
+

Sell Signals counts every SELL recommendation issued across all tracked days.

{summaryMode === 'topPicks' ? (
Note: Top Picks mode only shows BUY recommendations, so sell signals are 0.
) : (
- Total Sell Signals: + Total: {filteredStats.sellSignals}
)} diff --git a/frontend/src/pages/StockDetail.tsx b/frontend/src/pages/StockDetail.tsx index 24094322..a7100a43 100644 --- a/frontend/src/pages/StockDetail.tsx +++ b/frontend/src/pages/StockDetail.tsx @@ -7,7 +7,6 @@ import { } from 'lucide-react'; import { NIFTY_50_STOCKS } from '../types'; import type { DailyRecommendation, StockAnalysis } from '../types'; -import { sampleRecommendations, getStockHistory as getStaticStockHistory, getRawAnalysis } from '../data/recommendations'; import { DecisionBadge, ConfidenceBadge, RiskBadge, HoldDaysBadge, RankBadge } from '../components/StockCard'; import AIAnalysisPanel from '../components/AIAnalysisPanel'; import StockPriceChart from '../components/StockPriceChart'; @@ -79,17 +78,9 @@ export default function StockDetail() { if (rec && rec.analysis && Object.keys(rec.analysis).length > 0) { setLatestRecommendation(rec); setAnalysis(rec.analysis[symbol || '']); - } else { - // Fallback to static data - const mockRec = sampleRecommendations[0]; - setLatestRecommendation(mockRec); - setAnalysis(mockRec?.analysis[symbol || '']); } - } catch { - // Fallback to static data - const mockRec = sampleRecommendations[0]; - setLatestRecommendation(mockRec); - setAnalysis(mockRec?.analysis[symbol || '']); + } catch (err) { + console.error('Failed to fetch recommendation:', err); } try { @@ -97,13 +88,9 @@ export default function StockDetail() { const historyData = await api.getStockHistory(symbol || ''); if (historyData && historyData.history && historyData.history.length > 0) { setHistory(historyData.history); - } else { - // Fallback to static data - setHistory(symbol ? getStaticStockHistory(symbol) : []); } - } catch { - // Fallback to static data - setHistory(symbol ? getStaticStockHistory(symbol) : []); + } catch (err) { + console.error('Failed to fetch stock history:', err); } }; @@ -824,9 +811,9 @@ export default function StockDetail() {
{/* AI Analysis Panel */} - {analysis && (analysis.raw_analysis || getRawAnalysis(symbol || '')) && ( + {analysis?.raw_analysis && ( )} diff --git a/frontend/src/pages/Stocks.tsx b/frontend/src/pages/Stocks.tsx index 0b22f4c7..88868191 100644 --- a/frontend/src/pages/Stocks.tsx +++ b/frontend/src/pages/Stocks.tsx @@ -1,15 +1,29 @@ -import { useState, useMemo } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { Search, Building2 } from 'lucide-react'; import { NIFTY_50_STOCKS } from '../types'; -import { getLatestRecommendation } from '../data/recommendations'; +import type { DailyRecommendation } from '../types'; import { DecisionBadge, ConfidenceBadge } from '../components/StockCard'; +import { api } from '../services/api'; export default function Stocks() { const [search, setSearch] = useState(''); const [sectorFilter, setSectorFilter] = useState('ALL'); + const [recommendation, setRecommendation] = useState(null); - const recommendation = getLatestRecommendation(); + useEffect(() => { + const fetchData = async () => { + try { + const rec = await api.getLatestRecommendation(); + if (rec && rec.analysis && Object.keys(rec.analysis).length > 0) { + setRecommendation(rec); + } + } catch (err) { + console.error('Failed to fetch recommendations:', err); + } + }; + fetchData(); + }, []); const sectors = useMemo(() => { const sectorSet = new Set(NIFTY_50_STOCKS.map(s => s.sector).filter(Boolean)); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index d8d7ef0e..8d85a774 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -349,6 +349,35 @@ class ApiService { return this.fetch('/analyze/all/cancel', { method: 'POST', noCache: true }); } + // ============== Schedule Methods ============== + + /** + * Set the auto-analyze schedule + */ + async setSchedule(config: { + enabled: boolean; + time: string; + config: Record; + }): Promise<{ status: string; message: string }> { + return this.fetch('/settings/schedule', { + method: 'POST', + body: JSON.stringify(config), + noCache: true, + }); + } + + /** + * Get the current auto-analyze schedule + */ + async getSchedule(): Promise<{ + enabled: boolean; + time: string; + config: Record; + last_run_date: string | null; + }> { + return this.fetch('/settings/schedule', { noCache: true }); + } + // ============== Stock Price History Methods ============== /** @@ -375,6 +404,35 @@ class ApiService { return this.fetch('/nifty50/history'); } + // ============== History Bundle ============== + + /** + * Get all data the History page needs in a single call. + * Returns recommendations + all backtest results + accuracy metrics + nifty50 prices. + */ + async getHistoryBundle(): Promise<{ + recommendations: DailyRecommendation[]; + backtest_by_date: Record>; + accuracy: { + overall_accuracy: number; + total_predictions: number; + correct_predictions: number; + by_decision: Record; + by_confidence: Record; + }; + nifty50_prices: Record; + }> { + return this.fetch('/history/bundle'); + } + // ============== Backtest Methods ============== /** @@ -416,6 +474,37 @@ class ApiService { return this.fetch(`/backtest/${date}`); } + /** + * Get detailed backtest data with live prices, formulas, agent reports + */ + async getDetailedBacktest(date: string): Promise<{ + date: string; + total_stocks: number; + stocks: Array<{ + symbol: string; + company_name: string; + rank?: number; + decision: string; + confidence: string; + risk: string; + hold_days: number; + hold_days_elapsed: number; + hold_period_active: boolean; + price_at_prediction: number | null; + price_current: number | null; + price_at_hold_end: number | null; + return_current: number | null; + return_at_hold: number | null; + prediction_correct: boolean | null; + formula: string; + raw_analysis: string; + agent_summary: Record; + debate_summary: Record; + }>; + }> { + return this.fetch(`/backtest/${date}/detailed`, { noCache: true }); + } + /** * Calculate backtest for all recommendations on a date */ diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index d091ac13..b3694f1b 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -2,6 +2,23 @@ export type Decision = 'BUY' | 'SELL' | 'HOLD'; export type Confidence = 'HIGH' | 'MEDIUM' | 'LOW'; export type Risk = 'HIGH' | 'MEDIUM' | 'LOW'; +export interface ReturnBreakdown { + correctPredictions: { + count: number; + totalReturn: number; + avgReturn: number; + stocks: { symbol: string; decision: string; return1d: number }[]; + }; + incorrectPredictions: { + count: number; + totalReturn: number; + avgReturn: number; + stocks: { symbol: string; decision: string; return1d: number }[]; + }; + weightedReturn: number; + formula: string; +} + // Backtest Types export interface PricePoint { date: string; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 7daa3122..0a53969b 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,5 +1,6 @@ /** @type {import('tailwindcss').Config} */ export default { + darkMode: 'class', content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", diff --git a/package.json b/package.json index dc0ba8ba..da802f2e 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,22 @@ { "name": "tradingagents", "version": "1.0.0", - "description": "

", + "description": "Multi-Agent LLM Financial Trading Framework", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "git+https://github.com/TauricResearch/TradingAgents.git" + "url": "git+https://github.com/hjlabs/TradingAgents.git" }, "keywords": [], "author": "", "license": "ISC", "bugs": { - "url": "https://github.com/TauricResearch/TradingAgents/issues" + "url": "https://github.com/hjlabs/TradingAgents/issues" }, - "homepage": "https://github.com/TauricResearch/TradingAgents#readme", + "homepage": "https://github.com/hjlabs/TradingAgents#readme", "dependencies": { "playwright": "^1.58.1" } diff --git a/prediction-accuracy-new.png b/prediction-accuracy-new.png new file mode 100644 index 00000000..1c0db1eb Binary files /dev/null and b/prediction-accuracy-new.png differ diff --git a/setup.py b/setup.py index 793df3e6..ba5c224b 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( description="Multi-Agents LLM Financial Trading Framework", author="TradingAgents Team", author_email="yijia.xiao@cs.ucla.edu", - url="https://github.com/TauricResearch", + url="https://github.com/hjlabs/TradingAgents", packages=find_packages(), install_requires=[ "langchain>=0.1.0", diff --git a/test-dashboard.png b/test-dashboard.png new file mode 100644 index 00000000..e1e95fdf Binary files /dev/null and b/test-dashboard.png differ diff --git a/test-history-auto-backtest.png b/test-history-auto-backtest.png new file mode 100644 index 00000000..e1db8c74 Binary files /dev/null and b/test-history-auto-backtest.png differ diff --git a/test-history-cancel-button.png b/test-history-cancel-button.png new file mode 100644 index 00000000..0845f4d6 Binary files /dev/null and b/test-history-cancel-button.png differ diff --git a/test-history-feb9-fix.png b/test-history-feb9-fix.png new file mode 100644 index 00000000..2a492f15 Binary files /dev/null and b/test-history-feb9-fix.png differ diff --git a/test-history.png b/test-history.png new file mode 100644 index 00000000..b60e1b48 Binary files /dev/null and b/test-history.png differ diff --git a/test-pipeline.png b/test-pipeline.png new file mode 100644 index 00000000..23905214 Binary files /dev/null and b/test-pipeline.png differ diff --git a/test-stockdetail.png b/test-stockdetail.png new file mode 100644 index 00000000..1016ab4e Binary files /dev/null and b/test-stockdetail.png differ diff --git a/tradingagents/claude_max_llm.py b/tradingagents/claude_max_llm.py index 4cf6c329..ccd4ac65 100644 --- a/tradingagents/claude_max_llm.py +++ b/tradingagents/claude_max_llm.py @@ -162,8 +162,10 @@ class ClaudeMaxLLM(BaseChatModel): user_prompt: The user prompt/query """ # Create environment without ANTHROPIC_API_KEY to force subscription auth + # Also remove CLAUDECODE to allow nested CLI calls from within Claude Code sessions env = os.environ.copy() env.pop("ANTHROPIC_API_KEY", None) + env.pop("CLAUDECODE", None) # Build the command with --system-prompt to override Claude Code's default behavior cmd = [ diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py index f883771d..625cc8a5 100644 --- a/tradingagents/dataflows/y_finance.py +++ b/tradingagents/dataflows/y_finance.py @@ -694,22 +694,19 @@ def get_yfinance_news( news = ticker_obj.news if news and len(news) > 0: for i, article in enumerate(news[:10]): - title = article.get("title", "No title") - publisher = article.get("publisher", "Unknown") - publish_time = article.get("providerPublishTime", "") - if publish_time: - try: - from datetime import datetime as _dt - publish_time = _dt.fromtimestamp(publish_time).strftime("%Y-%m-%d %H:%M") - except Exception: - pass - related = article.get("relatedTickers", []) - related_str = ", ".join(related) if related else "N/A" + # yfinance news has nested 'content' structure + content = article.get("content", article) + title = content.get("title", article.get("title", "No title")) + provider = content.get("provider", {}) + publisher = provider.get("displayName", article.get("publisher", "Unknown")) if isinstance(provider, dict) else "Unknown" + publish_time = content.get("pubDate", article.get("providerPublishTime", "")) + summary = content.get("summary", "") sections.append(f"## Article {i+1}: {title}") sections.append(f" Publisher: {publisher}") sections.append(f" Published: {publish_time}") - sections.append(f" Related Tickers: {related_str}") + if summary: + sections.append(f" Summary: {summary[:200]}") sections.append("") else: sections.append("No news articles available from Yahoo Finance.\n")