|
|
@ -15,3 +15,6 @@ node_modules/
|
|||
|
||||
# Frontend dev artifacts
|
||||
.frontend-dev/
|
||||
|
||||
# Runtime config
|
||||
schedule_config.json
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
#### Date Selection & Stock List
|
||||
Select any date to view all 50 ranked stocks with decisions, hold periods, and returns:
|
||||
|
||||

|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Frontend**: React 18 + TypeScript + Vite
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 512 KiB After Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 381 KiB After Width: | Height: | Size: 381 KiB |
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 77 KiB |
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 }> = [
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.'}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 49 KiB |
2
setup.py
|
|
@ -10,7 +10,7 @@ setup(
|
|||
description="Multi-Agents LLM Financial Trading Framework",
|
||||
author="TradingAgents Team",
|
||||
author_email="yijia.xiao@cs.ucla.edu",
|
||||
url="https://github.com/TauricResearch",
|
||||
url="https://github.com/hjlabs/TradingAgents",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
"langchain>=0.1.0",
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||