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 artifacts
.frontend-dev/ .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 | | **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 | | **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 | | **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 | | **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 | | **Settings Panel** | Configure LLM provider (Claude/OpenAI), model tiers, debate rounds, parallel workers |
| **Dark Mode** | Automatic system theme detection with manual toggle | | **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( layout["header"].update(
Panel( Panel(
"[bold green]Welcome to TradingAgents CLI[/bold green]\n" "[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", title="Welcome to TradingAgents",
border_style="green", border_style="green",
padding=(1, 2), padding=(1, 2),
@ -408,7 +408,7 @@ def get_user_selections():
welcome_content += "[bold]Workflow Steps:[/bold]\n" 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 += "I. Analyst Team → II. Research Team → III. Trader → IV. Risk Management → V. Portfolio Management\n\n"
welcome_content += ( 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 # 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 - **Prediction Accuracy**: Overall and per-recommendation-type accuracy
- **Accuracy Trend**: Visualize accuracy over time - **Accuracy Trend**: Visualize accuracy over time
- **Risk Metrics**: Sharpe ratio, max drawdown, win rate - **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 - **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) ![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 ## Tech Stack
- **Frontend**: React 18 + TypeScript + Vite - **Frontend**: React 18 + TypeScript + Vite

View File

@ -1219,6 +1219,42 @@ def get_backtest_results_by_date(date: str) -> list:
conn.close() 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: def get_all_backtest_results() -> list:
"""Get all backtest results for accuracy calculation.""" """Get all backtest results for accuracy calculation."""
conn = get_connection() conn = get_connection()

Binary file not shown.

View File

@ -8,7 +8,7 @@ import database as db
import sys import sys
import os import os
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime, timedelta
import threading import threading
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
import asyncio import asyncio
@ -549,6 +549,29 @@ bulk_analysis_state = {
"cancelled": False # Flag to signal cancellation "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 # List of Nifty 50 stocks
NIFTY_50_SYMBOLS = [ NIFTY_50_SYMBOLS = [
"RELIANCE", "TCS", "HDFCBANK", "INFY", "ICICIBANK", "HINDUNILVR", "ITC", "SBIN", "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 ============== # ============== Backtest Endpoints ==============
# NOTE: Static routes must come BEFORE parameterized routes to avoid # NOTE: Static routes must come BEFORE parameterized routes to avoid
# "accuracy" being matched as a {date} parameter. # "accuracy" being matched as a {date} parameter.
@ -930,6 +1016,149 @@ async def get_accuracy_metrics():
return 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}") @app.get("/backtest/{date}/{symbol}")
async def get_backtest_result(date: str, symbol: str): async def get_backtest_result(date: str, symbol: str):
"""Get backtest result for a specific stock and date. """Get backtest result for a specific stock and date.
@ -1045,11 +1274,198 @@ async def get_nifty50_history():
return {"dates": [], "prices": {}, "error": str(e)} 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") @app.on_event("startup")
async def startup_event(): async def startup_event():
"""Rebuild daily_recommendations and trigger backtest calculations at startup.""" """Rebuild daily_recommendations and trigger backtest calculations at startup."""
db.rebuild_all_daily_recommendations() 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 # Trigger backtest calculation for all dates in background
def startup_backtest(): def startup_backtest():
import backtest_service as bt 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 { X, HelpCircle, TrendingUp, TrendingDown, Minus, CheckCircle } from 'lucide-react';
import { createPortal } from 'react-dom';
import type { AccuracyMetrics } from '../types'; import type { AccuracyMetrics } from '../types';
interface AccuracyExplainModalProps { 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 holdCorrect = Math.round(metrics.hold_accuracy * metrics.total_predictions * 0.66); // ~33 hold signals
const holdTotal = Math.round(metrics.total_predictions * 0.66); 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"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */} {/* Backdrop */}
<div <div
@ -172,6 +173,7 @@ export default function AccuracyExplainModal({ isOpen, onClose, metrics }: Accur
</button> </button>
</div> </div>
</div> </div>
</div> </div>,
document.body
); );
} }

View File

@ -1,5 +1,4 @@
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { getAccuracyTrend } from '../data/recommendations';
export interface AccuracyTrendPoint { export interface AccuracyTrendPoint {
date: string; date: string;
@ -12,12 +11,11 @@ export interface AccuracyTrendPoint {
interface AccuracyTrendChartProps { interface AccuracyTrendChartProps {
height?: number; height?: number;
className?: string; className?: string;
data?: AccuracyTrendPoint[]; // Optional prop for real data data?: AccuracyTrendPoint[];
} }
export default function AccuracyTrendChart({ height = 200, className = '', data: propData }: AccuracyTrendChartProps) { export default function AccuracyTrendChart({ height = 200, className = '', data: propData }: AccuracyTrendChartProps) {
// Use provided data or fall back to mock data const data = propData || [];
const data = propData || getAccuracyTrend();
if (data.length === 0) { if (data.length === 0) {
return ( return (

View File

@ -1,16 +1,14 @@
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
import { getCumulativeReturns } from '../data/recommendations';
import type { CumulativeReturnPoint } from '../types'; import type { CumulativeReturnPoint } from '../types';
interface CumulativeReturnChartProps { interface CumulativeReturnChartProps {
height?: number; height?: number;
className?: string; className?: string;
data?: CumulativeReturnPoint[]; // Optional prop for real data data?: CumulativeReturnPoint[];
} }
export default function CumulativeReturnChart({ height = 160, className = '', data: propData }: CumulativeReturnChartProps) { export default function CumulativeReturnChart({ height = 160, className = '', data: propData }: CumulativeReturnChartProps) {
// Use provided data or fall back to mock data const data = propData || [];
const data = propData || getCumulativeReturns();
if (data.length === 0) { if (data.length === 0) {
return ( return (

View File

@ -1,5 +1,5 @@
import { SlidersHorizontal, ArrowUpDown } from 'lucide-react'; import { SlidersHorizontal, ArrowUpDown } from 'lucide-react';
import { getAllSectors } from '../data/recommendations'; import { NIFTY_50_STOCKS } from '../types';
import type { FilterState } from '../types'; import type { FilterState } from '../types';
interface FilterPanelProps { interface FilterPanelProps {
@ -9,7 +9,7 @@ interface FilterPanelProps {
} }
export default function FilterPanel({ filters, onFilterChange, className = '' }: 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 decisions: Array<FilterState['decision']> = ['ALL', 'BUY', 'SELL', 'HOLD'];
const sortOptions: Array<{ value: FilterState['sortBy']; label: string }> = [ 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 { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, ReferenceLine } from 'recharts';
import { TrendingUp, TrendingDown } from 'lucide-react'; import { TrendingUp, TrendingDown } from 'lucide-react';
import { getCumulativeReturns } from '../data/recommendations';
import type { CumulativeReturnPoint } from '../types'; import type { CumulativeReturnPoint } from '../types';
export interface IndexComparisonChartProps { export interface IndexComparisonChartProps {
height?: number; height?: number;
className?: string; className?: string;
data?: CumulativeReturnPoint[]; // Optional prop for real data data?: CumulativeReturnPoint[];
} }
export default function IndexComparisonChart({ height = 220, className = '', data: propData }: IndexComparisonChartProps) { export default function IndexComparisonChart({ height = 220, className = '', data: propData }: IndexComparisonChartProps) {
// Use provided data or fall back to mock data const data = propData || [];
const data = propData || getCumulativeReturns();
if (data.length === 0) { if (data.length === 0) {
return ( return (

View File

@ -1,4 +1,5 @@
import { X, Info } from 'lucide-react'; import { X, Info } from 'lucide-react';
import { createPortal } from 'react-dom';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
interface InfoModalProps { interface InfoModalProps {
@ -12,7 +13,7 @@ interface InfoModalProps {
export default function InfoModal({ isOpen, onClose, title, children, icon }: InfoModalProps) { export default function InfoModal({ isOpen, onClose, title, children, icon }: InfoModalProps) {
if (!isOpen) return null; if (!isOpen) return null;
return ( return createPortal(
<div className="fixed inset-0 z-50 overflow-y-auto"> <div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */} {/* Backdrop */}
<div <div
@ -38,7 +39,7 @@ export default function InfoModal({ isOpen, onClose, title, children, icon }: In
</div> </div>
{/* Content */} {/* Content */}
<div className="p-4"> <div className="p-4 max-h-[70vh] overflow-y-auto">
{children} {children}
</div> </div>
@ -53,7 +54,8 @@ export default function InfoModal({ isOpen, onClose, title, children, icon }: In
</div> </div>
</div> </div>
</div> </div>
</div> </div>,
document.body
); );
} }

View File

@ -1,5 +1,5 @@
import { X, Activity } from 'lucide-react'; import { X, Activity } from 'lucide-react';
import { getOverallReturnBreakdown } from '../data/recommendations'; import { createPortal } from 'react-dom';
import CumulativeReturnChart from './CumulativeReturnChart'; import CumulativeReturnChart from './CumulativeReturnChart';
import type { CumulativeReturnPoint } from '../types'; import type { CumulativeReturnPoint } from '../types';
@ -20,10 +20,9 @@ interface OverallReturnModalProps {
export default function OverallReturnModal({ isOpen, onClose, breakdown: propBreakdown, cumulativeData }: OverallReturnModalProps) { export default function OverallReturnModal({ isOpen, onClose, breakdown: propBreakdown, cumulativeData }: OverallReturnModalProps) {
if (!isOpen) return null; if (!isOpen) return null;
// Use provided breakdown or fall back to mock data const breakdown = propBreakdown || { dailyReturns: [], finalMultiplier: 1, finalReturn: 0, formula: '' };
const breakdown = propBreakdown || getOverallReturnBreakdown();
return ( return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */} {/* Backdrop */}
<div <div
@ -246,6 +245,7 @@ export default function OverallReturnModal({ isOpen, onClose, breakdown: propBre
</button> </button>
</div> </div>
</div> </div>
</div> </div>,
document.body
); );
} }

View File

@ -1,7 +1,6 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Legend, BarChart, Bar, Cell, LabelList } from 'recharts'; 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 { Calculator, ChevronDown, ChevronUp, IndianRupee, Settings2, BarChart3, Info, TrendingUp, TrendingDown, ArrowRightLeft, Wallet, PiggyBank, Receipt, HelpCircle } from 'lucide-react';
import { sampleRecommendations, getNifty50IndexHistory, getBacktestResult } from '../data/recommendations';
import { calculateBrokerage, formatINR, type BrokerageBreakdown } from '../utils/brokerageCalculator'; import { calculateBrokerage, formatINR, type BrokerageBreakdown } from '../utils/brokerageCalculator';
import InfoModal, { InfoButton } from './InfoModal'; import InfoModal, { InfoButton } from './InfoModal';
import type { Decision, DailyRecommendation } from '../types'; import type { Decision, DailyRecommendation } from '../types';
@ -9,7 +8,6 @@ import type { Decision, DailyRecommendation } from '../types';
interface PortfolioSimulatorProps { interface PortfolioSimulatorProps {
className?: string; className?: string;
recommendations?: DailyRecommendation[]; recommendations?: DailyRecommendation[];
isUsingMockData?: boolean;
nifty50Prices?: Record<string, number>; nifty50Prices?: Record<string, number>;
allBacktestData?: Record<string, 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 // Smart trade counting logic using Zerodha brokerage for Equity Delivery
function calculateSmartTrades( function calculateSmartTrades(
recommendations: typeof sampleRecommendations, recommendations: DailyRecommendation[],
mode: InvestmentMode, mode: InvestmentMode,
startingAmount: number, startingAmount: number,
nifty50Prices?: Record<string, number>, nifty50Prices?: Record<string, number>,
@ -48,7 +46,6 @@ function calculateSmartTrades(
openPositions: Record<string, { entryDate: string; entryPrice: number; decision: Decision }>; openPositions: Record<string, { entryDate: string; entryPrice: number; decision: Decision }>;
} { } {
const hasRealNifty = nifty50Prices && Object.keys(nifty50Prices).length > 0; 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()); const sortedRecs = [...recommendations].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
// Precompute real Nifty start price for comparison // Precompute real Nifty start price for comparison
@ -75,7 +72,6 @@ function calculateSmartTrades(
let portfolioValue = startingAmount; let portfolioValue = startingAmount;
let niftyValue = startingAmount; let niftyValue = startingAmount;
const niftyStartValue = niftyHistory?.[0]?.value || 21500;
const portfolioData = sortedRecs.map((rec) => { const portfolioData = sortedRecs.map((rec) => {
const stocks = getStocksToTrack(rec); const stocks = getStocksToTrack(rec);
@ -89,8 +85,7 @@ function calculateSmartTrades(
const decision = analysis.decision; const decision = analysis.decision;
const prevPosition = openPositions[symbol]; const prevPosition = openPositions[symbol];
const backtest = getBacktestResult(symbol); const currentPrice = 1000; // Nominal price for position sizing
const currentPrice = backtest?.current_price || 1000;
const quantity = Math.floor(investmentPerStock / currentPrice); const quantity = Math.floor(investmentPerStock / currentPrice);
if (decision === 'BUY') { if (decision === 'BUY') {
@ -160,11 +155,6 @@ function calculateSmartTrades(
if (closestDate && nifty50Prices[closestDate]) { if (closestDate && nifty50Prices[closestDate]) {
niftyValue = startingAmount * (nifty50Prices[closestDate] / niftyStartPrice); 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 { return {
@ -214,8 +204,7 @@ function getValueColorClass(value: number): string {
export default function PortfolioSimulator({ export default function PortfolioSimulator({
className = '', className = '',
recommendations = sampleRecommendations, recommendations = [],
isUsingMockData = true, // Default to true since this uses simulated returns
nifty50Prices, nifty50Prices,
allBacktestData, allBacktestData,
}: PortfolioSimulatorProps) { }: PortfolioSimulatorProps) {
@ -705,16 +694,6 @@ export default function PortfolioSimulator({
</div> </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"> <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%). 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.'} {investmentMode === 'topPicks' ? ' Investing in Top Picks only.' : ' Investing in all 50 stocks.'}

View File

@ -1,19 +1,17 @@
import { useState } from 'react'; import { useState } from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { getReturnDistribution } from '../data/recommendations';
import type { ReturnBucket } from '../types'; import type { ReturnBucket } from '../types';
export interface ReturnDistributionChartProps { export interface ReturnDistributionChartProps {
height?: number; height?: number;
className?: string; className?: string;
data?: ReturnBucket[]; // Optional prop for real data data?: ReturnBucket[];
} }
export default function ReturnDistributionChart({ height = 200, className = '', data: propData }: ReturnDistributionChartProps) { export default function ReturnDistributionChart({ height = 200, className = '', data: propData }: ReturnDistributionChartProps) {
const [selectedBucket, setSelectedBucket] = useState<{ range: string; stocks: string[] } | null>(null); const [selectedBucket, setSelectedBucket] = useState<{ range: string; stocks: string[] } | null>(null);
// Use provided data or fall back to mock data const data = propData || [];
const data = propData || getReturnDistribution();
if (data.every(d => d.count === 0)) { if (data.every(d => d.count === 0)) {
return ( return (

View File

@ -1,5 +1,6 @@
import { X, CheckCircle, XCircle, Calculator } from 'lucide-react'; 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 { interface ReturnExplainModalProps {
isOpen: boolean; isOpen: boolean;
@ -18,7 +19,7 @@ export default function ReturnExplainModal({ isOpen, onClose, breakdown, date }:
year: 'numeric', year: 'numeric',
}); });
return ( return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */} {/* Backdrop */}
<div <div
@ -214,6 +215,7 @@ export default function ReturnExplainModal({ isOpen, onClose, breakdown, date }:
</button> </button>
</div> </div>
</div> </div>
</div> </div>,
document.body
); );
} }

View File

@ -1,20 +1,22 @@
import { TrendingUp, TrendingDown, Activity, Target } from 'lucide-react'; import { TrendingUp, TrendingDown, Activity, Target } from 'lucide-react';
import { calculateRiskMetrics } from '../data/recommendations';
import { useState } from 'react'; import { useState } from 'react';
import InfoModal, { InfoButton } from './InfoModal'; import InfoModal, { InfoButton } from './InfoModal';
import type { RiskMetrics } from '../types'; import type { RiskMetrics } from '../types';
export interface RiskMetricsCardProps { export interface RiskMetricsCardProps {
className?: string; className?: string;
metrics?: RiskMetrics; // Optional prop for real data metrics?: RiskMetrics;
} }
type MetricModal = 'sharpe' | 'drawdown' | 'winloss' | 'winrate' | null; 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) { export default function RiskMetricsCard({ className = '', metrics: propMetrics }: RiskMetricsCardProps) {
const [activeModal, setActiveModal] = useState<MetricModal>(null); const [activeModal, setActiveModal] = useState<MetricModal>(null);
// Use provided metrics or fall back to mock data const metrics = propMetrics || defaultMetrics;
const metrics = propMetrics || calculateRiskMetrics();
// Color classes for metric values // Color classes for metric values
const COLOR_GOOD = 'text-green-600 dark:text-green-400'; const COLOR_GOOD = 'text-green-600 dark:text-green-400';

View File

@ -1,10 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { createPortal } from 'react-dom';
import { import {
X, Settings, Cpu, Key, Zap, Brain, Sparkles, X, Settings, Cpu, Key, Zap, Brain, Sparkles,
Eye, EyeOff, Check, AlertCircle, RefreshCw Eye, EyeOff, Check, AlertCircle, RefreshCw, Clock
} from 'lucide-react'; } from 'lucide-react';
import { useSettings, MODELS, PROVIDERS } from '../contexts/SettingsContext'; import { useSettings, MODELS, PROVIDERS, TIMEZONES } from '../contexts/SettingsContext';
import type { ModelId, ProviderId } from '../contexts/SettingsContext'; import type { ModelId, ProviderId, TimezoneId } from '../contexts/SettingsContext';
export default function SettingsModal() { export default function SettingsModal() {
const { settings, updateSettings, resetSettings, isSettingsOpen, closeSettings } = useSettings(); const { settings, updateSettings, resetSettings, isSettingsOpen, closeSettings } = useSettings();
@ -52,7 +53,7 @@ export default function SettingsModal() {
const selectedProvider = PROVIDERS[settings.provider]; const selectedProvider = PROVIDERS[settings.provider];
return ( return createPortal(
<div className="fixed inset-0 z-50 overflow-y-auto"> <div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */} {/* Backdrop */}
<div <div
@ -296,6 +297,102 @@ export default function SettingsModal() {
</p> </p>
</div> </div>
</section> </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> </div>
{/* Footer */} {/* Footer */}
@ -315,6 +412,7 @@ export default function SettingsModal() {
</div> </div>
</div> </div>
</div> </div>
</div> </div>,
document.body
); );
} }

View File

@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useCallback } from 'react'; 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'; import { X, Terminal, Trash2, Download, Pause, Play, ChevronDown, Plus, Minus } from 'lucide-react';
interface LogEntry { interface LogEntry {
@ -224,7 +225,7 @@ export default function TerminalModal({ isOpen, onClose, isAnalyzing }: Terminal
if (!isOpen) return null; 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"> <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center sm:p-4">
{/* Backdrop */} {/* Backdrop */}
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} /> <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> </span>
</div> </div>
</div> </div>
</div> </div>,
document.body
); );
} }

View File

@ -1,9 +1,7 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Trophy, AlertTriangle, TrendingUp, TrendingDown, ChevronRight } from 'lucide-react'; import { Trophy, AlertTriangle, TrendingUp, TrendingDown, ChevronRight } from 'lucide-react';
import type { TopPick, StockToAvoid } from '../types'; import type { TopPick, StockToAvoid } from '../types';
import BackgroundSparkline from './BackgroundSparkline';
import { RankBadge } from './StockCard'; import { RankBadge } from './StockCard';
import { getBacktestResult } from '../data/recommendations';
interface TopPicksProps { interface TopPicksProps {
picks: TopPick[]; picks: TopPick[];
@ -24,7 +22,6 @@ export default function TopPicks({ picks }: TopPicksProps) {
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
{picks.map((pick, index) => { {picks.map((pick, index) => {
const backtest = getBacktestResult(pick.symbol);
return ( return (
<Link <Link
key={pick.symbol} 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))', : '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="relative z-10">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-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"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{stocks.map((stock) => { {stocks.map((stock) => {
const backtest = getBacktestResult(stock.symbol);
return ( return (
<Link <Link
key={stock.symbol} 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" 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))' }} 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="relative z-10">
<div className="flex items-center justify-between mb-2"> <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> <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 ModelId = keyof typeof MODELS;
export type ProviderId = keyof typeof PROVIDERS; 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 { interface Settings {
// Model settings // Model settings
deepThinkModel: ModelId; deepThinkModel: ModelId;
@ -41,6 +65,11 @@ interface Settings {
// Analysis settings // Analysis settings
maxDebateRounds: number; maxDebateRounds: number;
parallelWorkers: number; parallelWorkers: number;
// Auto-analyze schedule
autoAnalyzeEnabled: boolean;
autoAnalyzeTime: string; // "HH:MM" in 24hr format
autoAnalyzeTimezone: TimezoneId;
} }
interface SettingsContextType { interface SettingsContextType {
@ -59,6 +88,9 @@ const DEFAULT_SETTINGS: Settings = {
anthropicApiKey: '', anthropicApiKey: '',
maxDebateRounds: 1, maxDebateRounds: 1,
parallelWorkers: 3, parallelWorkers: 3,
autoAnalyzeEnabled: false,
autoAnalyzeTime: '09:00',
autoAnalyzeTimezone: 'Asia/Kolkata',
}; };
const STORAGE_KEY = 'nifty50ai_settings'; const STORAGE_KEY = 'nifty50ai_settings';
@ -92,6 +124,34 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
} }
}, [settings]); }, [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>) => { const updateSettings = (newSettings: Partial<Settings>) => {
setSettings(prev => ({ ...prev, ...newSettings })); setSettings(prev => ({ ...prev, ...newSettings }));
}; };

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,8 @@
@import "tailwindcss"; @import "tailwindcss";
/* Use class-based dark mode (html.dark) instead of media query */
@variant dark (&:where(.dark, .dark *));
@theme { @theme {
/* Custom colors */ /* Custom colors */
--color-nifty-50: #f0f9ff; --color-nifty-50: #f0f9ff;

View File

@ -5,8 +5,6 @@ import TopPicks, { StocksToAvoid } from '../components/TopPicks';
import { DecisionBadge, HoldDaysBadge, RankBadge } from '../components/StockCard'; import { DecisionBadge, HoldDaysBadge, RankBadge } from '../components/StockCard';
import TerminalModal from '../components/TerminalModal'; import TerminalModal from '../components/TerminalModal';
import HowItWorks from '../components/HowItWorks'; import HowItWorks from '../components/HowItWorks';
import BackgroundSparkline from '../components/BackgroundSparkline';
import { getLatestRecommendation, getBacktestResult as getStaticBacktestResult } from '../data/recommendations';
import { api } from '../services/api'; import { api } from '../services/api';
import { useSettings } from '../contexts/SettingsContext'; import { useSettings } from '../contexts/SettingsContext';
import { useNotification } from '../contexts/NotificationContext'; import { useNotification } from '../contexts/NotificationContext';
@ -19,28 +17,17 @@ export default function Dashboard() {
// State for real API data // State for real API data
const [recommendation, setRecommendation] = useState<DailyRecommendation | null>(null); const [recommendation, setRecommendation] = useState<DailyRecommendation | null>(null);
const [isLoadingData, setIsLoadingData] = useState(true); const [isLoadingData, setIsLoadingData] = useState(true);
const [isUsingMockData, setIsUsingMockData] = useState(false);
// Fetch real recommendation from API // Fetch recommendation from API
const fetchRecommendation = useCallback(async () => { const fetchRecommendation = useCallback(async () => {
setIsLoadingData(true); setIsLoadingData(true);
try { try {
const data = await api.getLatestRecommendation(); const data = await api.getLatestRecommendation();
if (data && data.analysis && Object.keys(data.analysis).length > 0) { if (data && data.analysis && Object.keys(data.analysis).length > 0) {
setRecommendation(data); setRecommendation(data);
setIsUsingMockData(false);
} else {
// API returned empty data, fall back to mock
const mockData = getLatestRecommendation();
setRecommendation(mockData || null);
setIsUsingMockData(true);
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch recommendation from API:', error); console.error('Failed to fetch recommendation from API:', error);
// Fall back to mock data
const mockData = getLatestRecommendation();
setRecommendation(mockData || null);
setIsUsingMockData(true);
} finally { } finally {
setIsLoadingData(false); setIsLoadingData(false);
} }
@ -493,16 +480,6 @@ export default function Dashboard() {
</div> </div>
</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 */} {/* Analysis Progress Banner */}
{isAnalyzing && analysisProgress && ( {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"> <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) => { {filteredItems.map((item) => {
// COMPLETED with analysis data: clickable link // COMPLETED with analysis data: clickable link
if (item.liveState === 'completed' && item.analysis) { 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 ( return (
<Link <Link
key={item.symbol} key={item.symbol}
@ -671,11 +646,6 @@ export default function Dashboard() {
className="card-hover p-2 group relative overflow-hidden" className="card-hover p-2 group relative overflow-hidden"
role="listitem" 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="relative z-10">
<div className="flex items-center gap-1.5 mb-0.5"> <div className="flex items-center gap-1.5 mb-0.5">
<RankBadge rank={item.analysis.rank} size="small" /> <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'; } from 'lucide-react';
import { NIFTY_50_STOCKS } from '../types'; import { NIFTY_50_STOCKS } from '../types';
import type { DailyRecommendation, StockAnalysis } 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 { DecisionBadge, ConfidenceBadge, RiskBadge, HoldDaysBadge, RankBadge } from '../components/StockCard';
import AIAnalysisPanel from '../components/AIAnalysisPanel'; import AIAnalysisPanel from '../components/AIAnalysisPanel';
import StockPriceChart from '../components/StockPriceChart'; import StockPriceChart from '../components/StockPriceChart';
@ -79,17 +78,9 @@ export default function StockDetail() {
if (rec && rec.analysis && Object.keys(rec.analysis).length > 0) { if (rec && rec.analysis && Object.keys(rec.analysis).length > 0) {
setLatestRecommendation(rec); setLatestRecommendation(rec);
setAnalysis(rec.analysis[symbol || '']); setAnalysis(rec.analysis[symbol || '']);
} else {
// Fallback to static data
const mockRec = sampleRecommendations[0];
setLatestRecommendation(mockRec);
setAnalysis(mockRec?.analysis[symbol || '']);
} }
} catch { } catch (err) {
// Fallback to static data console.error('Failed to fetch recommendation:', err);
const mockRec = sampleRecommendations[0];
setLatestRecommendation(mockRec);
setAnalysis(mockRec?.analysis[symbol || '']);
} }
try { try {
@ -97,13 +88,9 @@ export default function StockDetail() {
const historyData = await api.getStockHistory(symbol || ''); const historyData = await api.getStockHistory(symbol || '');
if (historyData && historyData.history && historyData.history.length > 0) { if (historyData && historyData.history && historyData.history.length > 0) {
setHistory(historyData.history); setHistory(historyData.history);
} else {
// Fallback to static data
setHistory(symbol ? getStaticStockHistory(symbol) : []);
} }
} catch { } catch (err) {
// Fallback to static data console.error('Failed to fetch stock history:', err);
setHistory(symbol ? getStaticStockHistory(symbol) : []);
} }
}; };
@ -824,9 +811,9 @@ export default function StockDetail() {
</section> </section>
{/* AI Analysis Panel */} {/* AI Analysis Panel */}
{analysis && (analysis.raw_analysis || getRawAnalysis(symbol || '')) && ( {analysis?.raw_analysis && (
<AIAnalysisPanel <AIAnalysisPanel
analysis={analysis.raw_analysis || getRawAnalysis(symbol || '') || ''} analysis={analysis.raw_analysis}
decision={analysis.decision} 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 { Link } from 'react-router-dom';
import { Search, Building2 } from 'lucide-react'; import { Search, Building2 } from 'lucide-react';
import { NIFTY_50_STOCKS } from '../types'; import { NIFTY_50_STOCKS } from '../types';
import { getLatestRecommendation } from '../data/recommendations'; import type { DailyRecommendation } from '../types';
import { DecisionBadge, ConfidenceBadge } from '../components/StockCard'; import { DecisionBadge, ConfidenceBadge } from '../components/StockCard';
import { api } from '../services/api';
export default function Stocks() { export default function Stocks() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [sectorFilter, setSectorFilter] = useState<string>('ALL'); 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 sectors = useMemo(() => {
const sectorSet = new Set(NIFTY_50_STOCKS.map(s => s.sector).filter(Boolean)); 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 }); 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 ============== // ============== Stock Price History Methods ==============
/** /**
@ -375,6 +404,35 @@ class ApiService {
return this.fetch('/nifty50/history'); 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 ============== // ============== Backtest Methods ==============
/** /**
@ -416,6 +474,37 @@ class ApiService {
return this.fetch(`/backtest/${date}`); 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 * 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 Confidence = 'HIGH' | 'MEDIUM' | 'LOW';
export type Risk = '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 // Backtest Types
export interface PricePoint { export interface PricePoint {
date: string; date: string;

View File

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

View File

@ -1,22 +1,22 @@
{ {
"name": "tradingagents", "name": "tradingagents",
"version": "1.0.0", "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", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/TauricResearch/TradingAgents.git" "url": "git+https://github.com/hjlabs/TradingAgents.git"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"bugs": { "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": { "dependencies": {
"playwright": "^1.58.1" "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", description="Multi-Agents LLM Financial Trading Framework",
author="TradingAgents Team", author="TradingAgents Team",
author_email="yijia.xiao@cs.ucla.edu", author_email="yijia.xiao@cs.ucla.edu",
url="https://github.com/TauricResearch", url="https://github.com/hjlabs/TradingAgents",
packages=find_packages(), packages=find_packages(),
install_requires=[ install_requires=[
"langchain>=0.1.0", "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 user_prompt: The user prompt/query
""" """
# Create environment without ANTHROPIC_API_KEY to force subscription auth # 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 = os.environ.copy()
env.pop("ANTHROPIC_API_KEY", None) env.pop("ANTHROPIC_API_KEY", None)
env.pop("CLAUDECODE", None)
# Build the command with --system-prompt to override Claude Code's default behavior # Build the command with --system-prompt to override Claude Code's default behavior
cmd = [ cmd = [

View File

@ -694,22 +694,19 @@ def get_yfinance_news(
news = ticker_obj.news news = ticker_obj.news
if news and len(news) > 0: if news and len(news) > 0:
for i, article in enumerate(news[:10]): for i, article in enumerate(news[:10]):
title = article.get("title", "No title") # yfinance news has nested 'content' structure
publisher = article.get("publisher", "Unknown") content = article.get("content", article)
publish_time = article.get("providerPublishTime", "") title = content.get("title", article.get("title", "No title"))
if publish_time: provider = content.get("provider", {})
try: publisher = provider.get("displayName", article.get("publisher", "Unknown")) if isinstance(provider, dict) else "Unknown"
from datetime import datetime as _dt publish_time = content.get("pubDate", article.get("providerPublishTime", ""))
publish_time = _dt.fromtimestamp(publish_time).strftime("%Y-%m-%d %H:%M") summary = content.get("summary", "")
except Exception:
pass
related = article.get("relatedTickers", [])
related_str = ", ".join(related) if related else "N/A"
sections.append(f"## Article {i+1}: {title}") sections.append(f"## Article {i+1}: {title}")
sections.append(f" Publisher: {publisher}") sections.append(f" Publisher: {publisher}")
sections.append(f" Published: {publish_time}") sections.append(f" Published: {publish_time}")
sections.append(f" Related Tickers: {related_str}") if summary:
sections.append(f" Summary: {summary[:200]}")
sections.append("") sections.append("")
else: else:
sections.append("No news articles available from Yahoo Finance.\n") sections.append("No news articles available from Yahoo Finance.\n")