This commit is contained in:
hemangjoshi37a 2026-02-14 14:24:18 +11:00
parent b79179cea2
commit 7619a7f9bb
68 changed files with 1872 additions and 2875 deletions

3
.gitignore vendored
View File

@ -15,3 +15,6 @@ node_modules/
# Frontend dev artifacts
.frontend-dev/
# Runtime config
schedule_config.json

BIN
01-dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

BIN
02-settings-modal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
04-analysis-pipeline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

BIN
05-debates-tab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

BIN
07-data-sources-tab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
08-dashboard-dark-mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
09-how-it-works.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

BIN
10-history-page.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -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 |

View File

@ -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

BIN
debug-dark-after-fix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
debug-light-after-fix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -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

View File

@ -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()

Binary file not shown.

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 KiB

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -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(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
@ -172,6 +173,7 @@ export default function AccuracyExplainModal({ isOpen, onClose, metrics }: Accur
</button>
</div>
</div>
</div>
</div>,
document.body
);
}

View File

@ -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 (

View File

@ -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 (

View File

@ -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<FilterState['decision']> = ['ALL', 'BUY', 'SELL', 'HOLD'];
const sortOptions: Array<{ value: FilterState['sortBy']; label: string }> = [

View File

@ -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 (

View File

@ -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(
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div
@ -38,7 +39,7 @@ export default function InfoModal({ isOpen, onClose, title, children, icon }: In
</div>
{/* Content */}
<div className="p-4">
<div className="p-4 max-h-[70vh] overflow-y-auto">
{children}
</div>
@ -53,7 +54,8 @@ export default function InfoModal({ isOpen, onClose, title, children, icon }: In
</div>
</div>
</div>
</div>
</div>,
document.body
);
}

View File

@ -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(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
@ -246,6 +245,7 @@ export default function OverallReturnModal({ isOpen, onClose, breakdown: propBre
</button>
</div>
</div>
</div>
</div>,
document.body
);
}

View File

@ -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<string, number>;
allBacktestData?: Record<string, Record<string, number>>;
}
@ -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<string, number>,
@ -48,7 +46,6 @@ function calculateSmartTrades(
openPositions: Record<string, { entryDate: string; entryPrice: number; decision: Decision }>;
} {
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({
</div>
)}
{/* Demo Data Notice */}
{isUsingMockData && (
<div className="flex items-center gap-2 px-3 py-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg mt-3">
<AlertCircle className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0" />
<span className="text-[10px] text-amber-700 dark:text-amber-300">
Simulation uses demo data. Results are illustrative only.
</span>
</div>
)}
<p className="text-[10px] text-gray-400 dark:text-gray-500 mt-3 text-center">
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.'}

View File

@ -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 (

View File

@ -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(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
@ -214,6 +215,7 @@ export default function ReturnExplainModal({ isOpen, onClose, breakdown, date }:
</button>
</div>
</div>
</div>
</div>,
document.body
);
}

View File

@ -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<MetricModal>(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';

View File

@ -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(
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div
@ -296,6 +297,102 @@ export default function SettingsModal() {
</p>
</div>
</section>
{/* Auto-Analyze Schedule */}
<section>
<h3 className="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">
<Clock className="w-4 h-4 text-indigo-500" />
Auto-Analyze Schedule
</h3>
{/* Enable Toggle */}
<div className="flex items-center justify-between mb-4">
<div>
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">
Daily Auto-Analyze
</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">
Automatically run Analyze All at the scheduled time
</div>
</div>
<button
onClick={() => updateSettings({ autoAnalyzeEnabled: !settings.autoAnalyzeEnabled })}
className={`relative w-10 h-5 rounded-full transition-colors ${
settings.autoAnalyzeEnabled
? 'bg-nifty-600'
: 'bg-gray-300 dark:bg-slate-600'
}`}
>
<span className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${
settings.autoAnalyzeEnabled ? 'translate-x-5' : 'translate-x-0'
}`} />
</button>
</div>
{/* Timezone */}
<div className={`mb-3 ${!settings.autoAnalyzeEnabled ? 'opacity-40 pointer-events-none' : ''}`}>
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400 mb-1 block">Timezone</label>
<select
value={settings.autoAnalyzeTimezone}
onChange={(e) => updateSettings({ autoAnalyzeTimezone: e.target.value as TimezoneId })}
className="w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-nifty-500"
>
{TIMEZONES.map(tz => (
<option key={tz.id} value={tz.id}>
{tz.label} (UTC{tz.offset})
</option>
))}
</select>
</div>
{/* Time Picker */}
<div className={`flex items-center gap-3 ${!settings.autoAnalyzeEnabled ? 'opacity-40 pointer-events-none' : ''}`}>
<div className="flex-1">
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400 mb-1 block">Hour</label>
<select
value={settings.autoAnalyzeTime.split(':')[0]}
onChange={(e) => {
const minute = settings.autoAnalyzeTime.split(':')[1];
updateSettings({ autoAnalyzeTime: `${e.target.value}:${minute}` });
}}
className="w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-nifty-500"
>
{Array.from({ length: 24 }, (_, i) => (
<option key={i} value={String(i).padStart(2, '0')}>
{String(i).padStart(2, '0')}
</option>
))}
</select>
</div>
<span className="text-lg font-bold text-gray-400 dark:text-gray-500 mt-4">:</span>
<div className="flex-1">
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400 mb-1 block">Minute</label>
<select
value={settings.autoAnalyzeTime.split(':')[1]}
onChange={(e) => {
const hour = settings.autoAnalyzeTime.split(':')[0];
updateSettings({ autoAnalyzeTime: `${hour}:${e.target.value}` });
}}
className="w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-nifty-500"
>
{Array.from({ length: 12 }, (_, i) => (
<option key={i} value={String(i * 5).padStart(2, '0')}>
{String(i * 5).padStart(2, '0')}
</option>
))}
</select>
</div>
</div>
{/* Preview */}
{settings.autoAnalyzeEnabled && (
<div className="mt-3 p-2.5 rounded-lg bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-100 dark:border-indigo-800">
<p className="text-xs text-indigo-700 dark:text-indigo-300 font-medium">
Runs daily at {settings.autoAnalyzeTime} {TIMEZONES.find(tz => tz.id === settings.autoAnalyzeTimezone)?.label || settings.autoAnalyzeTimezone} when the backend is running
</p>
</div>
)}
</section>
</div>
{/* Footer */}
@ -315,6 +412,7 @@ export default function SettingsModal() {
</div>
</div>
</div>
</div>
</div>,
document.body
);
}

View File

@ -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(
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center sm:p-4">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
@ -407,6 +408,7 @@ export default function TerminalModal({ isOpen, onClose, isAnalyzing }: Terminal
</span>
</div>
</div>
</div>
</div>,
document.body
);
}

View File

@ -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) {
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
{picks.map((pick, index) => {
const backtest = getBacktestResult(pick.symbol);
return (
<Link
key={pick.symbol}
@ -36,11 +33,6 @@ export default function TopPicks({ picks }: TopPicksProps) {
: 'linear-gradient(135deg, rgba(16,185,129,0.04), rgba(5,150,105,0.01))',
}}
>
{backtest && (
<div className="absolute inset-0 opacity-[0.06]">
<BackgroundSparkline data={backtest.price_history} trend="up" />
</div>
)}
<div className="relative z-10">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
@ -91,7 +83,6 @@ export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{stocks.map((stock) => {
const backtest = getBacktestResult(stock.symbol);
return (
<Link
key={stock.symbol}
@ -99,11 +90,6 @@ export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
className="group relative overflow-hidden rounded-xl border border-red-200/40 dark:border-red-800/25 p-3 transition-all hover:border-red-300 dark:hover:border-red-700/40"
style={{ background: 'linear-gradient(135deg, rgba(239,68,68,0.04), rgba(220,38,38,0.01))' }}
>
{backtest && (
<div className="absolute inset-0 opacity-[0.06]">
<BackgroundSparkline data={backtest.price_history} trend="down" />
</div>
)}
<div className="relative z-10">
<div className="flex items-center justify-between mb-2">
<span className="font-bold text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>

View File

@ -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<Settings>) => {
setSettings(prev => ({ ...prev, ...newSettings }));
};

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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<DailyRecommendation | null>(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() {
</div>
</div>
{/* Mock Data Indicator */}
{isUsingMockData && (
<div className="mt-3 flex items-center gap-2 px-3 py-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<AlertCircle className="w-4 h-4 text-amber-600 dark:text-amber-400 flex-shrink-0" />
<span className="text-xs text-amber-700 dark:text-amber-300">
Using demo data. Run "Analyze All" or start the backend server for real AI recommendations.
</span>
</div>
)}
{/* Analysis Progress Banner */}
{isAnalyzing && analysisProgress && (
<div className="mt-3 p-3 bg-blue-50 dark:bg-blue-900/30 rounded-lg border border-blue-200 dark:border-blue-800">
@ -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 (
<Link
key={item.symbol}
@ -671,11 +646,6 @@ export default function Dashboard() {
className="card-hover p-2 group relative overflow-hidden"
role="listitem"
>
{backtest && (
<div className="absolute inset-0 opacity-[0.06]">
<BackgroundSparkline data={backtest.price_history.slice(-15)} trend={trend} />
</div>
)}
<div className="relative z-10">
<div className="flex items-center gap-1.5 mb-0.5">
<RankBadge rank={item.analysis.rank} size="small" />

File diff suppressed because it is too large Load Diff

View File

@ -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() {
</section>
{/* AI Analysis Panel */}
{analysis && (analysis.raw_analysis || getRawAnalysis(symbol || '')) && (
{analysis?.raw_analysis && (
<AIAnalysisPanel
analysis={analysis.raw_analysis || getRawAnalysis(symbol || '') || ''}
analysis={analysis.raw_analysis}
decision={analysis.decision}
/>
)}

View File

@ -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<string>('ALL');
const [recommendation, setRecommendation] = useState<DailyRecommendation | null>(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));

View File

@ -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<string, unknown>;
}): 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<string, unknown>;
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<string, Record<string, {
return_1d?: number;
return_1w?: number;
return_1m?: number;
return_at_hold?: number;
hold_days?: number;
prediction_correct?: boolean;
decision: string;
}>>;
accuracy: {
overall_accuracy: number;
total_predictions: number;
correct_predictions: number;
by_decision: Record<string, { accuracy: number; total: number; correct: number }>;
by_confidence: Record<string, { accuracy: number; total: number; correct: number }>;
};
nifty50_prices: Record<string, number>;
}> {
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<string, string>;
debate_summary: Record<string, string>;
}>;
}> {
return this.fetch(`/backtest/${date}/detailed`, { noCache: true });
}
/**
* Calculate backtest for all recommendations on a date
*/

View File

@ -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;

View File

@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",

View File

@ -1,22 +1,22 @@
{
"name": "tradingagents",
"version": "1.0.0",
"description": "<p align=\"center\"> <img src=\"assets/TauricResearch.png\" style=\"width: 60%; height: auto;\"> </p>",
"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"
}

BIN
prediction-accuracy-new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -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",

BIN
test-dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
test-history-feb9-fix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
test-history.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
test-pipeline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
test-stockdetail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

View File

@ -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 = [

View File

@ -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")