|
|
@ -15,3 +15,6 @@ node_modules/
|
||||||
|
|
||||||
# Frontend dev artifacts
|
# Frontend dev artifacts
|
||||||
.frontend-dev/
|
.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 |
|
| **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 |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
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
|
- **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
|
||||||
|
|
||||||

|

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

|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Frontend**: React 18 + TypeScript + Vite
|
- **Frontend**: React 18 + TypeScript + Vite
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
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 { 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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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 }> = [
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.'}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 }));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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}",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 49 KiB |
2
setup.py
|
|
@ -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",
|
||||||
|
|
|
||||||
|
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
|
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 = [
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||