Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5837a9a2f5 | ||
|
|
e82cf3a5ee | ||
|
|
754b4a62c0 | ||
|
|
3f7b69b52c | ||
|
|
e3f1a8079c |
@@ -0,0 +1,444 @@
|
|||||||
|
"""
|
||||||
|
Phase 5: Real-time AI Trading Coach
|
||||||
|
AI-powered real-time trading assistance and guidance
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/ai-coach", tags=["AI Trading Coach"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/coaching-session")
|
||||||
|
async def start_coaching_session(
|
||||||
|
trading_style: str = Query("swing", regex="^(scalping|swing|position)$"),
|
||||||
|
experience_level: str = Query("intermediate", regex="^(beginner|intermediate|advanced)$"),
|
||||||
|
):
|
||||||
|
"""Start an AI coaching session with personalized guidance"""
|
||||||
|
guidance = {
|
||||||
|
"beginner": {
|
||||||
|
"focus_points": [
|
||||||
|
"Risk management is paramount - never risk more than 1% per trade",
|
||||||
|
"Keep trade journal to track mistakes and improve",
|
||||||
|
"Start with one strategy and master it",
|
||||||
|
"Understand support/resistance before entering trades",
|
||||||
|
"Use stop losses on every single trade",
|
||||||
|
],
|
||||||
|
"common_mistakes": [
|
||||||
|
"Over-leveraging accounts",
|
||||||
|
"Trading without a plan",
|
||||||
|
"Revenge trading after losses",
|
||||||
|
"Ignoring risk management rules",
|
||||||
|
"Chasing losses",
|
||||||
|
],
|
||||||
|
"daily_routine": [
|
||||||
|
"Review previous day trades (15 min)",
|
||||||
|
"Check economic calendar for events (5 min)",
|
||||||
|
"Plan setups for today (10 min)",
|
||||||
|
"Trade with discipline (pre-planned stops/targets)",
|
||||||
|
"End-of-day review and journal (10 min)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"intermediate": {
|
||||||
|
"focus_points": [
|
||||||
|
"Develop multiple strategies for different market conditions",
|
||||||
|
"Focus on win rate AND risk/reward optimization",
|
||||||
|
"Use advanced technical analysis effectively",
|
||||||
|
"Understand market correlations (gold/USD/bonds)",
|
||||||
|
"Build robust trading systems",
|
||||||
|
],
|
||||||
|
"common_mistakes": [
|
||||||
|
"Over-optimization of strategies",
|
||||||
|
"Ignoring current market regime",
|
||||||
|
"Not adapting to changing conditions",
|
||||||
|
"Trading too many timeframes simultaneously",
|
||||||
|
"Revenge trading",
|
||||||
|
],
|
||||||
|
"daily_routine": [
|
||||||
|
"Multi-timeframe analysis (20 min)",
|
||||||
|
"Economic calendar review (5 min)",
|
||||||
|
"Identify 3-5 key setups (15 min)",
|
||||||
|
"Execute with high probability setups only (pre-market to close)",
|
||||||
|
"Full session review and optimization (20 min)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"focus_points": [
|
||||||
|
"Develop proprietary edge and algorithms",
|
||||||
|
"Statistical edge validation and backtesting",
|
||||||
|
"Portfolio optimization and diversification",
|
||||||
|
"Advanced risk metrics (Sharpe, Sortino, Calmar ratios)",
|
||||||
|
"Systematic execution with automation",
|
||||||
|
],
|
||||||
|
"common_mistakes": [
|
||||||
|
"Over-fitting strategies to historical data",
|
||||||
|
"Ignoring black swan events",
|
||||||
|
"Negligent risk monitoring",
|
||||||
|
"Insufficient position sizing",
|
||||||
|
"Emotional override of systems",
|
||||||
|
],
|
||||||
|
"daily_routine": [
|
||||||
|
"Pre-market algorithmic analysis (15 min)",
|
||||||
|
"Monitor system performance metrics (10 min)",
|
||||||
|
"Execute systematic trades (monitoring only)",
|
||||||
|
"Real-time risk management (ongoing)",
|
||||||
|
"Post-market data analysis and optimization (20 min)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy_focus = {
|
||||||
|
"scalping": {
|
||||||
|
"holding_period": "Seconds to 5 minutes",
|
||||||
|
"best_indicators": "Fast MA, RSI(14), MACD",
|
||||||
|
"position_sizing": "0.5-1% per trade",
|
||||||
|
"daily_goal": "5-10 trades, 0.5-1% daily return",
|
||||||
|
"key_rule": "Get in, get out quickly with defined exit",
|
||||||
|
},
|
||||||
|
"swing": {
|
||||||
|
"holding_period": "Minutes to hours",
|
||||||
|
"best_indicators": "EMA(12/26), RSI(14), Pivot Points",
|
||||||
|
"position_sizing": "1-2% per trade",
|
||||||
|
"daily_goal": "2-5 trades, 1-3% daily return",
|
||||||
|
"key_rule": "Let winners run, cut losers quickly",
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"holding_period": "Hours to days",
|
||||||
|
"best_indicators": "SMA(50/200), Support/Resistance, Trends",
|
||||||
|
"position_sizing": "2-5% per trade",
|
||||||
|
"daily_goal": "0-2 trades, 2-5% weekly return",
|
||||||
|
"key_rule": "Focus on trend direction, ignore noise",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"session_id": f"coach_{datetime.now().timestamp()}",
|
||||||
|
"trading_style": trading_style,
|
||||||
|
"experience_level": experience_level,
|
||||||
|
"guidance": guidance[experience_level],
|
||||||
|
"strategy_focus": strategy_focus[trading_style],
|
||||||
|
"coaching_tips": f"Welcome to AI Coach! As a {experience_level} trader using {trading_style} strategy, focus on: {', '.join(guidance[experience_level]['focus_points'][:3])}",
|
||||||
|
"session_started": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/real-time-advice")
|
||||||
|
async def get_real_time_advice(
|
||||||
|
current_price: float = Query(...),
|
||||||
|
high_24h: float = Query(...),
|
||||||
|
low_24h: float = Query(...),
|
||||||
|
rsi: float = Query(..., ge=0, le=100),
|
||||||
|
macd_signal: str = Query("neutral", regex="^(bullish|bearish|neutral)$"),
|
||||||
|
market_condition: str = Query("normal", regex="^(trending_up|trending_down|ranging|volatile)$"),
|
||||||
|
):
|
||||||
|
"""Get real-time AI coaching advice based on current market conditions"""
|
||||||
|
advice_pieces = []
|
||||||
|
confidence = 0.5
|
||||||
|
|
||||||
|
# RSI analysis
|
||||||
|
if rsi > 70:
|
||||||
|
advice_pieces.append({
|
||||||
|
"indicator": "RSI",
|
||||||
|
"signal": "OVERBOUGHT",
|
||||||
|
"advice": "Consider taking profits on long positions. Watch for reversal signals.",
|
||||||
|
"weight": 0.7,
|
||||||
|
})
|
||||||
|
confidence = min(0.9, confidence + 0.2)
|
||||||
|
elif rsi < 30:
|
||||||
|
advice_pieces.append({
|
||||||
|
"indicator": "RSI",
|
||||||
|
"signal": "OVERSOLD",
|
||||||
|
"advice": "Look for buy signals. Market is stretched lower with bounce potential.",
|
||||||
|
"weight": 0.7,
|
||||||
|
})
|
||||||
|
confidence = min(0.9, confidence + 0.2)
|
||||||
|
else:
|
||||||
|
advice_pieces.append({
|
||||||
|
"indicator": "RSI",
|
||||||
|
"signal": "NEUTRAL",
|
||||||
|
"advice": "RSI is in neutral zone. Confirm with other indicators.",
|
||||||
|
"weight": 0.3,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Market condition analysis
|
||||||
|
if market_condition == "trending_up":
|
||||||
|
advice_pieces.append({
|
||||||
|
"indicator": "Market Trend",
|
||||||
|
"signal": "BULLISH",
|
||||||
|
"advice": "Market in uptrend. Favor long positions. Avoid shorts.",
|
||||||
|
"weight": 0.9,
|
||||||
|
})
|
||||||
|
confidence = min(1.0, confidence + 0.3)
|
||||||
|
elif market_condition == "trending_down":
|
||||||
|
advice_pieces.append({
|
||||||
|
"indicator": "Market Trend",
|
||||||
|
"signal": "BEARISH",
|
||||||
|
"advice": "Market in downtrend. Favor short positions. Avoid longs.",
|
||||||
|
"weight": 0.9,
|
||||||
|
})
|
||||||
|
confidence = min(1.0, confidence + 0.3)
|
||||||
|
else:
|
||||||
|
advice_pieces.append({
|
||||||
|
"indicator": "Market Trend",
|
||||||
|
"signal": "RANGING/VOLATILE",
|
||||||
|
"advice": "No clear trend. Focus on support/resistance bounces.",
|
||||||
|
"weight": 0.6,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Price action
|
||||||
|
price_range = high_24h - low_24h
|
||||||
|
price_from_low = current_price - low_24h
|
||||||
|
range_pct = (price_from_low / price_range * 100) if price_range > 0 else 50
|
||||||
|
|
||||||
|
if range_pct > 75:
|
||||||
|
advice_pieces.append({
|
||||||
|
"indicator": "Price Action",
|
||||||
|
"signal": "NEAR HIGH",
|
||||||
|
"advice": "Price near 24h high. Be cautious with new longs. Watch for reversals.",
|
||||||
|
"weight": 0.6,
|
||||||
|
})
|
||||||
|
elif range_pct < 25:
|
||||||
|
advice_pieces.append({
|
||||||
|
"indicator": "Price Action",
|
||||||
|
"signal": "NEAR LOW",
|
||||||
|
"advice": "Price near 24h low. Good bounce opportunity if conditions align.",
|
||||||
|
"weight": 0.6,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Overall recommendation
|
||||||
|
if confidence >= 0.8:
|
||||||
|
recommendation = "STRONG BUY" if market_condition == "trending_up" and rsi < 50 else "STRONG SELL" if market_condition == "trending_down" and rsi > 50 else "WAIT FOR CONFIRMATION"
|
||||||
|
elif confidence >= 0.6:
|
||||||
|
recommendation = "BUY" if market_condition == "trending_up" else "SELL" if market_condition == "trending_down" else "NEUTRAL"
|
||||||
|
else:
|
||||||
|
recommendation = "WAIT FOR BETTER SETUP"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"current_price": current_price,
|
||||||
|
"market_condition": market_condition,
|
||||||
|
"rsi_level": rsi,
|
||||||
|
"macd_signal": macd_signal,
|
||||||
|
"advice_pieces": advice_pieces,
|
||||||
|
"overall_recommendation": recommendation,
|
||||||
|
"confidence_level": round(confidence, 2),
|
||||||
|
"suggested_action": {
|
||||||
|
"action": recommendation.split()[0],
|
||||||
|
"entry": current_price * (1 - 0.003) if "BUY" in recommendation else current_price * (1 + 0.003),
|
||||||
|
"take_profit": current_price * (1 + 0.015) if "BUY" in recommendation else current_price * (1 - 0.015),
|
||||||
|
"stop_loss": current_price * (1 - 0.008) if "BUY" in recommendation else current_price * (1 + 0.008),
|
||||||
|
},
|
||||||
|
"risk_assessment": "HIGH" if "STRONG" not in recommendation else "MEDIUM" if confidence < 0.85 else "LOW",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/trade-review/{trade_id}")
|
||||||
|
async def review_trade(
|
||||||
|
trade_id: str,
|
||||||
|
entry_price: float = Query(...),
|
||||||
|
exit_price: float = Query(...),
|
||||||
|
quantity: float = Query(...),
|
||||||
|
hold_time_minutes: int = Query(..., ge=1),
|
||||||
|
win_loss: str = Query(..., regex="^(win|loss)$"),
|
||||||
|
):
|
||||||
|
"""AI coach reviews a completed trade and provides feedback"""
|
||||||
|
pnl = (exit_price - entry_price) * quantity
|
||||||
|
return_pct = ((exit_price - entry_price) / entry_price) * 100
|
||||||
|
|
||||||
|
feedback = []
|
||||||
|
score = 50
|
||||||
|
|
||||||
|
# Entry analysis
|
||||||
|
if abs(return_pct) > 2:
|
||||||
|
feedback.append("✓ Good risk/reward ratio achieved")
|
||||||
|
score += 15
|
||||||
|
elif abs(return_pct) > 1:
|
||||||
|
feedback.append("✓ Decent risk/reward ratio")
|
||||||
|
score += 5
|
||||||
|
else:
|
||||||
|
feedback.append("⚠ Small return - may need better entry timing")
|
||||||
|
|
||||||
|
# Hold time analysis
|
||||||
|
if hold_time_minutes < 30 and win_loss == "win":
|
||||||
|
feedback.append("✓ Executed quickly - good scalping")
|
||||||
|
score += 10
|
||||||
|
elif hold_time_minutes > 120 and win_loss == "win":
|
||||||
|
feedback.append("✓ Allowed winner to run - good discipline")
|
||||||
|
score += 15
|
||||||
|
elif hold_time_minutes > 120 and win_loss == "loss":
|
||||||
|
feedback.append("⚠ Held losing trade too long - cut losses faster")
|
||||||
|
score -= 15
|
||||||
|
|
||||||
|
# Trade size
|
||||||
|
if abs(return_pct) <= 3:
|
||||||
|
feedback.append("✓ Conservative position sizing managed risk")
|
||||||
|
score += 5
|
||||||
|
|
||||||
|
# Consistency
|
||||||
|
if win_loss == "win":
|
||||||
|
feedback.append("✓ Won trade - well executed!")
|
||||||
|
score += 20
|
||||||
|
else:
|
||||||
|
feedback.append("⚠ Lost trade - learn from mistakes, don't revenge trade")
|
||||||
|
score = max(10, score - 20)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"trade_id": trade_id,
|
||||||
|
"entry_price": entry_price,
|
||||||
|
"exit_price": exit_price,
|
||||||
|
"pnl": round(pnl, 2),
|
||||||
|
"return_percentage": round(return_pct, 2),
|
||||||
|
"hold_time_minutes": hold_time_minutes,
|
||||||
|
"result": win_loss,
|
||||||
|
"trade_score": score,
|
||||||
|
"feedback": feedback,
|
||||||
|
"overall_assessment": "EXCELLENT TRADE" if score >= 80 else "GOOD TRADE" if score >= 60 else "ACCEPTABLE" if score >= 40 else "IMPROVE NEXT TIME",
|
||||||
|
"next_steps": [
|
||||||
|
"Review your entry signal - was it clear?",
|
||||||
|
"Check your exit - was it based on plan or emotion?",
|
||||||
|
"Journal this trade with conditions and setup",
|
||||||
|
"Identify the pattern/cluster this belongs to",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/performance-coach")
|
||||||
|
async def performance_coaching(
|
||||||
|
total_trades: int = Query(..., ge=1),
|
||||||
|
winning_trades: int = Query(..., ge=0),
|
||||||
|
total_pnl: float = Query(...),
|
||||||
|
avg_win: float = Query(...),
|
||||||
|
avg_loss: float = Query(...),
|
||||||
|
):
|
||||||
|
"""AI coach analyzes overall performance and provides improvement suggestions"""
|
||||||
|
win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
|
||||||
|
profit_factor = (avg_win * winning_trades / (avg_loss * (total_trades - winning_trades))) if (total_trades - winning_trades) > 0 and avg_loss > 0 else 0
|
||||||
|
|
||||||
|
coaching_notes = []
|
||||||
|
priority_areas = []
|
||||||
|
|
||||||
|
# Win rate analysis
|
||||||
|
if win_rate < 40:
|
||||||
|
coaching_notes.append("⚠ Low win rate (<40%). Focus on entry signal quality.")
|
||||||
|
priority_areas.append("Improve Entry Signals")
|
||||||
|
elif win_rate > 70:
|
||||||
|
coaching_notes.append("✓ Excellent win rate (>70%)! Keep this up.")
|
||||||
|
elif win_rate > 55:
|
||||||
|
coaching_notes.append("✓ Good win rate (>55%). This is solid.")
|
||||||
|
else:
|
||||||
|
coaching_notes.append("⚠ Win rate below 50%. Work on strategy validation.")
|
||||||
|
priority_areas.append("Validate Strategy Edge")
|
||||||
|
|
||||||
|
# Profit factor analysis
|
||||||
|
if profit_factor > 2:
|
||||||
|
coaching_notes.append("✓ Excellent profit factor (>2). Great risk/reward management.")
|
||||||
|
elif profit_factor > 1.5:
|
||||||
|
coaching_notes.append("✓ Good profit factor (>1.5). Continue this discipline.")
|
||||||
|
elif profit_factor > 1:
|
||||||
|
coaching_notes.append("⚠ Profit factor at 1:1. Improve risk/reward or exits.")
|
||||||
|
priority_areas.append("Optimize Risk/Reward")
|
||||||
|
else:
|
||||||
|
coaching_notes.append("⚠ Losses exceed gains. Immediate action needed.")
|
||||||
|
priority_areas.append("Fix Risk Management")
|
||||||
|
|
||||||
|
# Trade count
|
||||||
|
if total_trades < 30:
|
||||||
|
coaching_notes.append("⚠ Low sample size (<30 trades). Need more data for analysis.")
|
||||||
|
priority_areas.append("Increase Sample Size")
|
||||||
|
elif total_trades > 200:
|
||||||
|
coaching_notes.append("✓ Large sample size (>200). Statistics are reliable.")
|
||||||
|
|
||||||
|
# PnL assessment
|
||||||
|
daily_avg = total_pnl / max(1, total_trades)
|
||||||
|
if daily_avg > avg_win * 0.5:
|
||||||
|
coaching_notes.append(f"✓ Good average trade profit: ${daily_avg:.2f}")
|
||||||
|
elif daily_avg > 0:
|
||||||
|
coaching_notes.append(f"⚠ Average profit is low: ${daily_avg:.2f}. Look for better setups.")
|
||||||
|
priority_areas.append("Select Higher Probability Trades")
|
||||||
|
else:
|
||||||
|
coaching_notes.append("⚠ Negative average trade. Review your entire system.")
|
||||||
|
priority_areas.append("Complete System Review")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"performance_summary": {
|
||||||
|
"total_trades": total_trades,
|
||||||
|
"winning_trades": winning_trades,
|
||||||
|
"losing_trades": total_trades - winning_trades,
|
||||||
|
"win_rate": round(win_rate, 1),
|
||||||
|
"total_pnl": round(total_pnl, 2),
|
||||||
|
"avg_winning_trade": round(avg_win, 2),
|
||||||
|
"avg_losing_trade": round(avg_loss, 2),
|
||||||
|
"profit_factor": round(profit_factor, 2),
|
||||||
|
"avg_trade_profit": round(daily_avg, 2),
|
||||||
|
},
|
||||||
|
"coaching_analysis": coaching_notes,
|
||||||
|
"priority_improvement_areas": priority_areas,
|
||||||
|
"action_plan": {
|
||||||
|
"immediate": priority_areas[:2] if priority_areas else ["Continue current strategy"],
|
||||||
|
"short_term": [
|
||||||
|
"Keep detailed trade journal with reasons for each trade",
|
||||||
|
"Identify your best performing trade patterns",
|
||||||
|
"Eliminate your worst performing patterns",
|
||||||
|
],
|
||||||
|
"long_term": [
|
||||||
|
"Develop multiple strategies for different market conditions",
|
||||||
|
"Backtest strategies thoroughly before live trading",
|
||||||
|
"Track and analyze all statistics systematically",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"encouragement": "You're on the right track!" if win_rate > 50 and profit_factor > 1 else "Every successful trader started where you are. Keep improving!",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/decision-helper")
|
||||||
|
async def get_decision_help(
|
||||||
|
trade_setup: str = Query(...),
|
||||||
|
risk_per_trade_pct: float = Query(1.0, ge=0.1, le=5),
|
||||||
|
account_size: float = Query(10000),
|
||||||
|
current_streak: str = Query("neutral", regex="^(winning|losing|neutral)$"),
|
||||||
|
):
|
||||||
|
"""AI coach helps with specific trade decisions"""
|
||||||
|
max_loss = account_size * (risk_per_trade_pct / 100)
|
||||||
|
|
||||||
|
decision_factors = {
|
||||||
|
"winning": {
|
||||||
|
"advice": "Great! You're in a winning streak. Stay disciplined and don't over-trade.",
|
||||||
|
"risk_adjustment": "Keep position size normal",
|
||||||
|
"caution": "Over-confidence risk. Stick to your plan.",
|
||||||
|
},
|
||||||
|
"losing": {
|
||||||
|
"advice": "In a losing streak? Take a break or reduce position size.",
|
||||||
|
"risk_adjustment": "Consider dropping to 0.5% risk temporarily",
|
||||||
|
"caution": "Revenge trading risk. Your plan is still valid.",
|
||||||
|
},
|
||||||
|
"neutral": {
|
||||||
|
"advice": "Neutral momentum. Trade only high probability setups.",
|
||||||
|
"risk_adjustment": "Keep position size at plan",
|
||||||
|
"caution": "None - stay focused on setup quality",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"trade_setup": trade_setup,
|
||||||
|
"account_analysis": {
|
||||||
|
"account_size": account_size,
|
||||||
|
"risk_per_trade_pct": risk_per_trade_pct,
|
||||||
|
"max_loss_per_trade": round(max_loss, 2),
|
||||||
|
"trades_before_account_ruin": round(account_size / max_loss / 10),
|
||||||
|
},
|
||||||
|
"trading_streak": current_streak,
|
||||||
|
"streak_guidance": decision_factors[current_streak],
|
||||||
|
"recommendation": "TAKE THIS SETUP" if "high" in trade_setup.lower() else "PASS - WAIT FOR BETTER" if "low" in trade_setup.lower() else "PROCEED WITH CAUTION",
|
||||||
|
"risk_management": {
|
||||||
|
"suggested_entry": "Execute at pre-defined level",
|
||||||
|
"suggested_stop_loss": f"${max_loss:.2f} maximum loss",
|
||||||
|
"position_size": f"{round(max_loss / 50, 2)} contracts or shares",
|
||||||
|
"profit_target": f"2:1 risk/reward = ${max_loss * 2:.2f} profit target",
|
||||||
|
},
|
||||||
|
"emotional_check": [
|
||||||
|
"Are you making this trade for the right reason?",
|
||||||
|
"Does this fit your written trading plan?",
|
||||||
|
"Have you seen this setup before successfully?",
|
||||||
|
"Can you afford the risk on this trade?",
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
"""
|
||||||
|
Phase 3: Advanced Analytics API Endpoints
|
||||||
|
Performance tracking, pattern analysis, and reporting
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import func
|
||||||
|
from datetime import datetime, date, timedelta
|
||||||
|
from typing import List, Optional
|
||||||
|
from app.db.database import get_db
|
||||||
|
from app.models.models import (
|
||||||
|
PerformanceSnapshot, TradePattern, LessonLearned, MonthlyReview, Trade
|
||||||
|
)
|
||||||
|
from app.schemas.schemas import (
|
||||||
|
PerformanceSnapshotCreate, PerformanceSnapshotResponse,
|
||||||
|
TradePatternCreate, TradePatternResponse,
|
||||||
|
LessonLearnedCreate, LessonLearnedResponse,
|
||||||
|
MonthlyReviewCreate, MonthlyReviewResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/analytics", tags=["Advanced Analytics"])
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PERFORMANCE SNAPSHOTS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@router.post("/snapshots", response_model=PerformanceSnapshotResponse, status_code=201)
|
||||||
|
async def create_performance_snapshot(
|
||||||
|
snapshot: PerformanceSnapshotCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Create a performance snapshot"""
|
||||||
|
db_snapshot = PerformanceSnapshot(**snapshot.dict())
|
||||||
|
db.add(db_snapshot)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_snapshot)
|
||||||
|
return db_snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/snapshots", response_model=List[PerformanceSnapshotResponse])
|
||||||
|
async def list_performance_snapshots(
|
||||||
|
start_date: Optional[str] = Query(None),
|
||||||
|
end_date: Optional[str] = Query(None),
|
||||||
|
limit: int = Query(30, ge=1, le=365),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""List performance snapshots with optional date range"""
|
||||||
|
query = db.query(PerformanceSnapshot)
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
start = datetime.fromisoformat(start_date).date()
|
||||||
|
query = query.filter(PerformanceSnapshot.snapshot_date >= start)
|
||||||
|
|
||||||
|
if end_date:
|
||||||
|
end = datetime.fromisoformat(end_date).date()
|
||||||
|
query = query.filter(PerformanceSnapshot.snapshot_date <= end)
|
||||||
|
|
||||||
|
return query.order_by(PerformanceSnapshot.snapshot_date.desc()).limit(limit).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/snapshots/stats/monthly")
|
||||||
|
async def get_monthly_stats(
|
||||||
|
year: int = Query(...),
|
||||||
|
month: int = Query(..., ge=1, le=12),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Get monthly aggregate statistics"""
|
||||||
|
snapshots = db.query(PerformanceSnapshot).filter(
|
||||||
|
func.extract('year', PerformanceSnapshot.snapshot_date) == year,
|
||||||
|
func.extract('month', PerformanceSnapshot.snapshot_date) == month
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if not snapshots:
|
||||||
|
return {
|
||||||
|
"year": year,
|
||||||
|
"month": month,
|
||||||
|
"trading_days": 0,
|
||||||
|
"total_pnl": 0.0,
|
||||||
|
"avg_daily_pnl": 0.0,
|
||||||
|
"best_day_pnl": 0.0,
|
||||||
|
"worst_day_pnl": 0.0,
|
||||||
|
"win_rate": 0.0,
|
||||||
|
"total_trades": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
total_pnl = sum(s.daily_pnl for s in snapshots)
|
||||||
|
total_trades = sum(s.total_trades for s in snapshots)
|
||||||
|
winning_days = sum(1 for s in snapshots if s.daily_pnl > 0)
|
||||||
|
trading_days = len(snapshots)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"year": year,
|
||||||
|
"month": month,
|
||||||
|
"trading_days": trading_days,
|
||||||
|
"total_pnl": total_pnl,
|
||||||
|
"avg_daily_pnl": total_pnl / trading_days if trading_days > 0 else 0,
|
||||||
|
"best_day_pnl": max((s.daily_pnl for s in snapshots), default=0),
|
||||||
|
"worst_day_pnl": min((s.daily_pnl for s in snapshots), default=0),
|
||||||
|
"win_rate": (winning_days / trading_days * 100) if trading_days > 0 else 0,
|
||||||
|
"total_trades": total_trades,
|
||||||
|
"winning_days": winning_days,
|
||||||
|
"losing_days": trading_days - winning_days
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/snapshots/stats/yearly")
|
||||||
|
async def get_yearly_stats(
|
||||||
|
year: int = Query(...),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Get yearly aggregate statistics"""
|
||||||
|
snapshots = db.query(PerformanceSnapshot).filter(
|
||||||
|
func.extract('year', PerformanceSnapshot.snapshot_date) == year
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if not snapshots:
|
||||||
|
return {"year": year, "message": "No data for this year"}
|
||||||
|
|
||||||
|
total_pnl = sum(s.daily_pnl for s in snapshots)
|
||||||
|
total_trades = sum(s.total_trades for s in snapshots)
|
||||||
|
winning_days = sum(1 for s in snapshots if s.daily_pnl > 0)
|
||||||
|
trading_days = len(snapshots)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"year": year,
|
||||||
|
"trading_days": trading_days,
|
||||||
|
"total_pnl": total_pnl,
|
||||||
|
"avg_daily_pnl": total_pnl / trading_days if trading_days > 0 else 0,
|
||||||
|
"best_day": max((s.daily_pnl for s in snapshots), default=0),
|
||||||
|
"worst_day": min((s.daily_pnl for s in snapshots), default=0),
|
||||||
|
"win_rate": (winning_days / trading_days * 100) if trading_days > 0 else 0,
|
||||||
|
"total_trades": total_trades,
|
||||||
|
"best_month": None, # Can be calculated from monthly stats
|
||||||
|
"worst_month": None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TRADE PATTERNS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@router.post("/patterns", response_model=TradePatternResponse, status_code=201)
|
||||||
|
async def create_pattern(
|
||||||
|
pattern: TradePatternCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Identify and create a new trade pattern"""
|
||||||
|
db_pattern = TradePattern(**pattern.dict())
|
||||||
|
db.add(db_pattern)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_pattern)
|
||||||
|
return db_pattern
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/patterns", response_model=List[TradePatternResponse])
|
||||||
|
async def list_patterns(
|
||||||
|
min_confidence: float = Query(0, ge=0, le=100),
|
||||||
|
min_sample_count: int = Query(3, ge=1),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""List identified trade patterns"""
|
||||||
|
patterns = db.query(TradePattern).filter(
|
||||||
|
TradePattern.confidence_score >= min_confidence,
|
||||||
|
TradePattern.sample_count >= min_sample_count
|
||||||
|
).order_by(TradePattern.confidence_score.desc()).all()
|
||||||
|
|
||||||
|
return patterns
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/patterns/{pattern_id}", response_model=TradePatternResponse)
|
||||||
|
async def get_pattern(
|
||||||
|
pattern_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Get specific pattern details"""
|
||||||
|
pattern = db.query(TradePattern).filter(TradePattern.id == pattern_id).first()
|
||||||
|
if not pattern:
|
||||||
|
raise HTTPException(status_code=404, detail="Pattern not found")
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/patterns/stats/best")
|
||||||
|
async def get_best_patterns(
|
||||||
|
limit: int = Query(5, ge=1, le=20),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Get your top performing patterns"""
|
||||||
|
patterns = db.query(TradePattern).order_by(
|
||||||
|
TradePattern.confidence_score.desc()
|
||||||
|
).limit(limit).all()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"pattern": p.pattern_name,
|
||||||
|
"win_rate": p.win_rate,
|
||||||
|
"confidence": p.confidence_score,
|
||||||
|
"sample_size": p.sample_count,
|
||||||
|
"total_profit": p.total_profit,
|
||||||
|
"best_timeframe": p.best_timeframe,
|
||||||
|
"best_time": p.best_time_of_day
|
||||||
|
}
|
||||||
|
for p in patterns
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LESSONS LEARNED
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@router.post("/lessons", response_model=LessonLearnedResponse, status_code=201)
|
||||||
|
async def create_lesson(
|
||||||
|
lesson: LessonLearnedCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Log a lesson learned"""
|
||||||
|
db_lesson = LessonLearned(**lesson.dict())
|
||||||
|
db.add(db_lesson)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_lesson)
|
||||||
|
return db_lesson
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/lessons", response_model=List[LessonLearnedResponse])
|
||||||
|
async def list_lessons(
|
||||||
|
category: Optional[str] = Query(None),
|
||||||
|
importance: Optional[str] = Query(None),
|
||||||
|
tag: Optional[str] = Query(None),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""List lessons learned with optional filters"""
|
||||||
|
query = db.query(LessonLearned).filter(LessonLearned.status == "active")
|
||||||
|
|
||||||
|
if category:
|
||||||
|
query = query.filter(LessonLearned.category == category)
|
||||||
|
if importance:
|
||||||
|
query = query.filter(LessonLearned.importance == importance)
|
||||||
|
|
||||||
|
lessons = query.order_by(LessonLearned.date_learned.desc()).limit(limit).all()
|
||||||
|
|
||||||
|
# Filter by tag if specified
|
||||||
|
if tag:
|
||||||
|
lessons = [l for l in lessons if tag in l.tags]
|
||||||
|
|
||||||
|
return lessons
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/lessons/categories")
|
||||||
|
async def get_lesson_categories(db: Session = Depends(get_db)):
|
||||||
|
"""Get available lesson categories"""
|
||||||
|
categories = db.query(LessonLearned.category).distinct().all()
|
||||||
|
return {
|
||||||
|
"categories": [c[0] for c in categories if c[0]],
|
||||||
|
"available": ["entry", "exit", "risk", "psychology", "market"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/lessons/recurring-mistakes")
|
||||||
|
async def get_recurring_mistakes(
|
||||||
|
limit: int = Query(10, ge=1, le=20),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Identify recurring mistakes from lessons"""
|
||||||
|
negative_lessons = db.query(LessonLearned).filter(
|
||||||
|
LessonLearned.impact == "negative"
|
||||||
|
).order_by(LessonLearned.date_learned.desc()).all()
|
||||||
|
|
||||||
|
# Count tag occurrences
|
||||||
|
tag_counts = {}
|
||||||
|
for lesson in negative_lessons:
|
||||||
|
for tag in lesson.tags:
|
||||||
|
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
||||||
|
|
||||||
|
# Sort by frequency
|
||||||
|
recurring = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"recurring_mistakes": recurring[:limit],
|
||||||
|
"total_negative_lessons": len(negative_lessons),
|
||||||
|
"recommendation": "Focus on preventing these recurring mistakes"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MONTHLY REVIEWS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@router.post("/reviews/monthly", response_model=MonthlyReviewResponse, status_code=201)
|
||||||
|
async def create_monthly_review(
|
||||||
|
review: MonthlyReviewCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Create a monthly performance review"""
|
||||||
|
# Check if review already exists
|
||||||
|
existing = db.query(MonthlyReview).filter(
|
||||||
|
MonthlyReview.year == review.year,
|
||||||
|
MonthlyReview.month == review.month
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Monthly review for {review.year}-{review.month} already exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
db_review = MonthlyReview(**review.dict())
|
||||||
|
db.add(db_review)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_review)
|
||||||
|
return db_review
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/reviews/monthly", response_model=List[MonthlyReviewResponse])
|
||||||
|
async def list_monthly_reviews(
|
||||||
|
year: Optional[int] = Query(None),
|
||||||
|
limit: int = Query(12, ge=1, le=60),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""List monthly reviews"""
|
||||||
|
query = db.query(MonthlyReview)
|
||||||
|
|
||||||
|
if year:
|
||||||
|
query = query.filter(MonthlyReview.year == year)
|
||||||
|
|
||||||
|
return query.order_by(
|
||||||
|
MonthlyReview.year.desc(),
|
||||||
|
MonthlyReview.month.desc()
|
||||||
|
).limit(limit).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/reviews/quarterly")
|
||||||
|
async def get_quarterly_review(
|
||||||
|
year: int = Query(...),
|
||||||
|
quarter: int = Query(..., ge=1, le=4),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Get quarterly performance review"""
|
||||||
|
months = {
|
||||||
|
1: [1, 2, 3],
|
||||||
|
2: [4, 5, 6],
|
||||||
|
3: [7, 8, 9],
|
||||||
|
4: [10, 11, 12]
|
||||||
|
}
|
||||||
|
|
||||||
|
month_list = months[quarter]
|
||||||
|
reviews = db.query(MonthlyReview).filter(
|
||||||
|
MonthlyReview.year == year,
|
||||||
|
MonthlyReview.month.in_(month_list)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if not reviews:
|
||||||
|
return {"quarter": quarter, "year": year, "message": "No data"}
|
||||||
|
|
||||||
|
total_pnl = sum(r.total_pnl for r in reviews)
|
||||||
|
total_trades = sum(r.total_trades for r in reviews)
|
||||||
|
avg_win_rate = sum(r.win_rate for r in reviews) / len(reviews) if reviews else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"quarter": quarter,
|
||||||
|
"year": year,
|
||||||
|
"months_covered": month_list,
|
||||||
|
"total_pnl": total_pnl,
|
||||||
|
"total_trades": total_trades,
|
||||||
|
"avg_win_rate": avg_win_rate,
|
||||||
|
"best_month": max((r.total_pnl for r in reviews), default=0),
|
||||||
|
"worst_month": min((r.total_pnl for r in reviews), default=0),
|
||||||
|
"monthly_reviews": [
|
||||||
|
{
|
||||||
|
"month": r.month,
|
||||||
|
"pnl": r.total_pnl,
|
||||||
|
"win_rate": r.win_rate,
|
||||||
|
"trades": r.total_trades
|
||||||
|
}
|
||||||
|
for r in reviews
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# COMPREHENSIVE ANALYTICS DASHBOARD
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@router.get("/dashboard")
|
||||||
|
async def get_analytics_dashboard(
|
||||||
|
period: str = Query("month", regex="^(week|month|quarter|year)$"),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Get comprehensive analytics dashboard"""
|
||||||
|
today = date.today()
|
||||||
|
|
||||||
|
# Determine date range
|
||||||
|
if period == "week":
|
||||||
|
start_date = today - timedelta(days=7)
|
||||||
|
elif period == "month":
|
||||||
|
start_date = today - timedelta(days=30)
|
||||||
|
elif period == "quarter":
|
||||||
|
start_date = today - timedelta(days=90)
|
||||||
|
else: # year
|
||||||
|
start_date = today - timedelta(days=365)
|
||||||
|
|
||||||
|
# Get snapshots for period
|
||||||
|
snapshots = db.query(PerformanceSnapshot).filter(
|
||||||
|
PerformanceSnapshot.snapshot_date >= start_date
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Get patterns
|
||||||
|
patterns = db.query(TradePattern).order_by(
|
||||||
|
TradePattern.confidence_score.desc()
|
||||||
|
).limit(5).all()
|
||||||
|
|
||||||
|
# Get recent lessons
|
||||||
|
lessons = db.query(LessonLearned).filter(
|
||||||
|
LessonLearned.status == "active"
|
||||||
|
).order_by(LessonLearned.date_learned.desc()).limit(5).all()
|
||||||
|
|
||||||
|
# Calculate metrics
|
||||||
|
total_pnl = sum(s.daily_pnl for s in snapshots)
|
||||||
|
total_trades = sum(s.total_trades for s in snapshots)
|
||||||
|
winning_days = sum(1 for s in snapshots if s.daily_pnl > 0)
|
||||||
|
avg_win_rate = sum(s.win_rate for s in snapshots) / len(snapshots) if snapshots else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"period": period,
|
||||||
|
"snapshot_count": len(snapshots),
|
||||||
|
"performance": {
|
||||||
|
"total_pnl": total_pnl,
|
||||||
|
"avg_daily_pnl": total_pnl / len(snapshots) if snapshots else 0,
|
||||||
|
"total_trades": total_trades,
|
||||||
|
"winning_days": winning_days,
|
||||||
|
"losing_days": len(snapshots) - winning_days,
|
||||||
|
"avg_win_rate": avg_win_rate,
|
||||||
|
"best_day": max((s.daily_pnl for s in snapshots), default=0),
|
||||||
|
"worst_day": min((s.daily_pnl for s in snapshots), default=0)
|
||||||
|
},
|
||||||
|
"top_patterns": [
|
||||||
|
{
|
||||||
|
"name": p.pattern_name,
|
||||||
|
"confidence": p.confidence_score,
|
||||||
|
"win_rate": p.win_rate,
|
||||||
|
"samples": p.sample_count
|
||||||
|
}
|
||||||
|
for p in patterns
|
||||||
|
],
|
||||||
|
"recent_lessons": [
|
||||||
|
{
|
||||||
|
"category": l.category,
|
||||||
|
"lesson": l.lesson_text[:100],
|
||||||
|
"importance": l.importance,
|
||||||
|
"date": l.date_learned.isoformat()
|
||||||
|
}
|
||||||
|
for l in lessons
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
"""
|
||||||
|
Phase 4: Economic Calendar API Integration
|
||||||
|
Real-time economic events and market-moving indicators
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Optional
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/economic-calendar", tags=["Economic Calendar"])
|
||||||
|
|
||||||
|
|
||||||
|
# Mock economic calendar data (in production, integrate with real APIs)
|
||||||
|
# Popular APIs: Trading Economics, Forexfactory, Economic Calendar Pro, etc.
|
||||||
|
SAMPLE_EVENTS = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Non-Farm Payroll",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=1)).isoformat(),
|
||||||
|
"time": "08:30",
|
||||||
|
"impact": "high",
|
||||||
|
"forecast": "230000",
|
||||||
|
"previous": "227000",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Employment change in the non-agricultural sector",
|
||||||
|
"importance": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Unemployment Rate",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=1)).isoformat(),
|
||||||
|
"time": "08:30",
|
||||||
|
"impact": "high",
|
||||||
|
"forecast": "3.8%",
|
||||||
|
"previous": "3.8%",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Percentage of the labor force that is jobless",
|
||||||
|
"importance": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Consumer Price Index",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=5)).isoformat(),
|
||||||
|
"time": "12:30",
|
||||||
|
"impact": "high",
|
||||||
|
"forecast": "3.4%",
|
||||||
|
"previous": "3.4%",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Inflation rate measurement",
|
||||||
|
"importance": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Federal Funds Rate Decision",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=8)).isoformat(),
|
||||||
|
"time": "18:00",
|
||||||
|
"impact": "high",
|
||||||
|
"forecast": "5.33%",
|
||||||
|
"previous": "5.33%",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Federal Reserve interest rate decision",
|
||||||
|
"importance": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"country": "EUR",
|
||||||
|
"indicator": "ECB Interest Rate Decision",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=10)).isoformat(),
|
||||||
|
"time": "12:45",
|
||||||
|
"impact": "high",
|
||||||
|
"forecast": "4.50%",
|
||||||
|
"previous": "4.50%",
|
||||||
|
"actual": None,
|
||||||
|
"description": "European Central Bank rate decision",
|
||||||
|
"importance": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "ISM Manufacturing PMI",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=2)).isoformat(),
|
||||||
|
"time": "09:00",
|
||||||
|
"impact": "medium",
|
||||||
|
"forecast": "49.5",
|
||||||
|
"previous": "49.0",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Manufacturing sector activity indicator",
|
||||||
|
"importance": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Initial Jobless Claims",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=3)).isoformat(),
|
||||||
|
"time": "08:30",
|
||||||
|
"impact": "medium",
|
||||||
|
"forecast": "215000",
|
||||||
|
"previous": "216000",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Weekly unemployment benefit applications",
|
||||||
|
"importance": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Retail Sales",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=7)).isoformat(),
|
||||||
|
"time": "12:30",
|
||||||
|
"impact": "medium",
|
||||||
|
"forecast": "0.4%",
|
||||||
|
"previous": "0.7%",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Consumer spending and retail activity",
|
||||||
|
"importance": 2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/events")
|
||||||
|
async def get_economic_events(
|
||||||
|
days_ahead: int = Query(30, ge=1, le=180),
|
||||||
|
countries: Optional[str] = Query(None),
|
||||||
|
impact: Optional[str] = Query(None, regex="^(high|medium|low)$"),
|
||||||
|
sort_by: str = Query("date", regex="^(date|importance|impact)$"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get upcoming economic calendar events
|
||||||
|
|
||||||
|
- **days_ahead**: Number of days to look ahead (1-180)
|
||||||
|
- **countries**: Comma-separated country codes (US, EUR, GBP, JPY, etc.)
|
||||||
|
- **impact**: Filter by impact level (high, medium, low)
|
||||||
|
- **sort_by**: Sort results by date, importance, or impact
|
||||||
|
"""
|
||||||
|
events = SAMPLE_EVENTS.copy()
|
||||||
|
|
||||||
|
# Filter by countries
|
||||||
|
if countries:
|
||||||
|
country_list = [c.strip() for c in countries.split(",")]
|
||||||
|
events = [e for e in events if e["country"] in country_list]
|
||||||
|
|
||||||
|
# Filter by impact
|
||||||
|
if impact:
|
||||||
|
impact_map = {"high": 3, "medium": 2, "low": 1}
|
||||||
|
events = [e for e in events if e["importance"] == impact_map.get(impact, 2)]
|
||||||
|
|
||||||
|
# Filter by days ahead
|
||||||
|
cutoff_date = datetime.now() + timedelta(days=days_ahead)
|
||||||
|
events = [
|
||||||
|
e
|
||||||
|
for e in events
|
||||||
|
if datetime.fromisoformat(e["event_date"]) <= cutoff_date
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sort
|
||||||
|
if sort_by == "importance":
|
||||||
|
events.sort(key=lambda x: x["importance"], reverse=True)
|
||||||
|
elif sort_by == "impact":
|
||||||
|
impact_order = {"high": 3, "medium": 2, "low": 1}
|
||||||
|
events.sort(key=lambda x: impact_order.get(x["impact"], 1), reverse=True)
|
||||||
|
else: # date
|
||||||
|
events.sort(key=lambda x: x["event_date"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(events),
|
||||||
|
"events": events,
|
||||||
|
"filter_applied": {
|
||||||
|
"days_ahead": days_ahead,
|
||||||
|
"countries": countries,
|
||||||
|
"impact": impact,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/today")
|
||||||
|
async def get_today_events():
|
||||||
|
"""Get economic events scheduled for today"""
|
||||||
|
today = datetime.now().date()
|
||||||
|
today_start = datetime.combine(today, datetime.min.time()).isoformat()
|
||||||
|
today_end = datetime.combine(today, datetime.max.time()).isoformat()
|
||||||
|
|
||||||
|
events = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if today_start <= e["event_date"] <= today_end
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"date": today.isoformat(),
|
||||||
|
"total": len(events),
|
||||||
|
"events": events,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/upcoming")
|
||||||
|
async def get_upcoming_events(hours: int = Query(24, ge=1, le=168)):
|
||||||
|
"""
|
||||||
|
Get upcoming events within specified hours
|
||||||
|
|
||||||
|
- **hours**: Number of hours ahead to check (1-168 hours = 1-7 days)
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
cutoff = now + timedelta(hours=hours)
|
||||||
|
|
||||||
|
events = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if now <= datetime.fromisoformat(e["event_date"]) <= cutoff
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sort by time
|
||||||
|
events.sort(key=lambda x: x["event_date"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"now": now.isoformat(),
|
||||||
|
"hours_ahead": hours,
|
||||||
|
"total": len(events),
|
||||||
|
"events": events,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/high-impact")
|
||||||
|
async def get_high_impact_events():
|
||||||
|
"""Get only high-impact economic events for the next 30 days"""
|
||||||
|
cutoff = datetime.now() + timedelta(days=30)
|
||||||
|
events = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if e["importance"] == 3
|
||||||
|
and datetime.fromisoformat(e["event_date"]) <= cutoff
|
||||||
|
]
|
||||||
|
|
||||||
|
events.sort(key=lambda x: x["event_date"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(events),
|
||||||
|
"events": events,
|
||||||
|
"note": "Only high-impact events that could significantly move gold prices",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/by-country/{country}")
|
||||||
|
async def get_country_events(
|
||||||
|
country: str, days: int = Query(30, ge=1, le=180)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get economic events for a specific country
|
||||||
|
|
||||||
|
- **country**: Country code (US, EUR, GBP, JPY, CHF, CAD, AUD, NZD, etc.)
|
||||||
|
- **days**: Days to look ahead
|
||||||
|
"""
|
||||||
|
cutoff = datetime.now() + timedelta(days=days)
|
||||||
|
events = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if e["country"].upper() == country.upper()
|
||||||
|
and datetime.fromisoformat(e["event_date"]) <= cutoff
|
||||||
|
]
|
||||||
|
|
||||||
|
if not events:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No events found for country: {country}"
|
||||||
|
)
|
||||||
|
|
||||||
|
events.sort(key=lambda x: x["event_date"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"country": country.upper(),
|
||||||
|
"days": days,
|
||||||
|
"total": len(events),
|
||||||
|
"events": events,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/impact-analysis")
|
||||||
|
async def get_impact_analysis():
|
||||||
|
"""
|
||||||
|
Analyze economic impact on gold prices
|
||||||
|
|
||||||
|
Returns analysis of how different economic indicators
|
||||||
|
typically affect gold trading
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"gold_trading_impact": {
|
||||||
|
"high_impact": {
|
||||||
|
"indicators": [
|
||||||
|
"Interest Rate Decisions",
|
||||||
|
"Inflation Data",
|
||||||
|
"Employment Reports",
|
||||||
|
"GDP Growth",
|
||||||
|
],
|
||||||
|
"typical_response": "Gold typically moves 100-200 pips on high-impact events",
|
||||||
|
"best_time": "Around event release time",
|
||||||
|
},
|
||||||
|
"medium_impact": {
|
||||||
|
"indicators": [
|
||||||
|
"PMI Indices",
|
||||||
|
"Consumer Confidence",
|
||||||
|
"Retail Sales",
|
||||||
|
"Producer Prices",
|
||||||
|
],
|
||||||
|
"typical_response": "Gold typically moves 50-100 pips",
|
||||||
|
"best_time": "Watch 5-30 mins after release",
|
||||||
|
},
|
||||||
|
"low_impact": {
|
||||||
|
"indicators": [
|
||||||
|
"Housing Starts",
|
||||||
|
"Factory Orders",
|
||||||
|
"Building Permits",
|
||||||
|
],
|
||||||
|
"typical_response": "Gold rarely moves significantly",
|
||||||
|
"best_time": "Usually skipped by day traders",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"inverse_correlation": {
|
||||||
|
"US_Dollar_Strength": "Strong dollar typically weakens gold (inverse correlation)",
|
||||||
|
"Interest_Rates": "Higher rates reduce gold appeal (inverse correlation)",
|
||||||
|
"Risk_Appetite": "Risk-on environment weakens gold demand",
|
||||||
|
"Inflation": "High inflation supports higher gold prices",
|
||||||
|
},
|
||||||
|
"trading_tips": [
|
||||||
|
"Trade 30 mins after high-impact events when volatility settles",
|
||||||
|
"Avoid trading during overlapping Fed/ECB announcements",
|
||||||
|
"Watch preliminary indicators before main events",
|
||||||
|
"Check gold correlation with USD index and bond yields",
|
||||||
|
"Set wider stops during high-impact event windows",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/calendar-view")
|
||||||
|
async def get_calendar_view(
|
||||||
|
month: Optional[int] = Query(None, ge=1, le=12),
|
||||||
|
year: Optional[int] = Query(None),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get economic calendar in calendar view format
|
||||||
|
|
||||||
|
- **month**: Specific month (1-12), defaults to current month
|
||||||
|
- **year**: Specific year, defaults to current year
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
view_month = month or now.month
|
||||||
|
view_year = year or now.year
|
||||||
|
|
||||||
|
calendar_events = {}
|
||||||
|
for event in SAMPLE_EVENTS:
|
||||||
|
event_date = datetime.fromisoformat(event["event_date"])
|
||||||
|
if (
|
||||||
|
event_date.month == view_month
|
||||||
|
and event_date.year == view_year
|
||||||
|
):
|
||||||
|
day = event_date.day
|
||||||
|
if day not in calendar_events:
|
||||||
|
calendar_events[day] = []
|
||||||
|
calendar_events[day].append(
|
||||||
|
{
|
||||||
|
"indicator": event["indicator"],
|
||||||
|
"time": event["time"],
|
||||||
|
"impact": event["impact"],
|
||||||
|
"country": event["country"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"month": view_month,
|
||||||
|
"year": view_year,
|
||||||
|
"calendar": calendar_events,
|
||||||
|
"month_name": datetime(view_year, view_month, 1).strftime("%B"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/events/{event_id}/notify")
|
||||||
|
async def set_event_notification(event_id: int, minutes_before: int = Query(30)):
|
||||||
|
"""
|
||||||
|
Set a notification reminder for an economic event
|
||||||
|
|
||||||
|
- **event_id**: ID of the economic event
|
||||||
|
- **minutes_before**: Notify X minutes before event (15-120)
|
||||||
|
"""
|
||||||
|
event = next((e for e in SAMPLE_EVENTS if e["id"] == event_id), None)
|
||||||
|
if not event:
|
||||||
|
raise HTTPException(status_code=404, detail="Event not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "notification_set",
|
||||||
|
"event": event["indicator"],
|
||||||
|
"notify_minutes_before": minutes_before,
|
||||||
|
"event_time": event["event_date"],
|
||||||
|
"notification_time": (
|
||||||
|
datetime.fromisoformat(event["event_date"])
|
||||||
|
- timedelta(minutes=minutes_before)
|
||||||
|
).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats")
|
||||||
|
async def get_economic_calendar_stats():
|
||||||
|
"""Get statistics about upcoming economic events"""
|
||||||
|
now = datetime.now()
|
||||||
|
next_7_days = now + timedelta(days=7)
|
||||||
|
next_30_days = now + timedelta(days=30)
|
||||||
|
|
||||||
|
events_7 = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if now <= datetime.fromisoformat(e["event_date"]) <= next_7_days
|
||||||
|
]
|
||||||
|
events_30 = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if now <= datetime.fromisoformat(e["event_date"]) <= next_30_days
|
||||||
|
]
|
||||||
|
|
||||||
|
high_impact = [e for e in events_30 if e["importance"] == 3]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"summary": {
|
||||||
|
"total_events_30_days": len(events_30),
|
||||||
|
"total_events_7_days": len(events_7),
|
||||||
|
"high_impact_events": len(high_impact),
|
||||||
|
"total_countries": len(set(e["country"] for e in events_30)),
|
||||||
|
},
|
||||||
|
"by_impact": {
|
||||||
|
"high": len([e for e in events_30 if e["importance"] == 3]),
|
||||||
|
"medium": len([e for e in events_30 if e["importance"] == 2]),
|
||||||
|
"low": len([e for e in events_30 if e["importance"] == 1]),
|
||||||
|
},
|
||||||
|
"busiest_days": sorted(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
e["event_date"].split("T")[0],
|
||||||
|
len(
|
||||||
|
[
|
||||||
|
x
|
||||||
|
for x in events_30
|
||||||
|
if x["event_date"].split("T")[0] == e["event_date"].split("T")[0]
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for e in events_30
|
||||||
|
],
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True,
|
||||||
|
)[:5],
|
||||||
|
}
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
"""
|
||||||
|
Phase 4: Advanced Indicators Management
|
||||||
|
Technical analysis indicators configuration and management
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/indicators", tags=["Technical Indicators"])
|
||||||
|
|
||||||
|
|
||||||
|
# Available indicators with their parameters
|
||||||
|
AVAILABLE_INDICATORS = {
|
||||||
|
"moving_averages": {
|
||||||
|
"name": "Moving Averages",
|
||||||
|
"description": "SMA, EMA, DEMA, TEMA, WMA",
|
||||||
|
"indicators": [
|
||||||
|
{
|
||||||
|
"id": "sma",
|
||||||
|
"name": "Simple Moving Average",
|
||||||
|
"periods": [5, 10, 20, 50, 100, 200],
|
||||||
|
"default_period": 20,
|
||||||
|
"type": "trend",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ema",
|
||||||
|
"name": "Exponential Moving Average",
|
||||||
|
"periods": [5, 10, 20, 50, 100, 200],
|
||||||
|
"default_period": 12,
|
||||||
|
"type": "trend",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wma",
|
||||||
|
"name": "Weighted Moving Average",
|
||||||
|
"periods": [5, 10, 20, 50],
|
||||||
|
"default_period": 20,
|
||||||
|
"type": "trend",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"oscillators": {
|
||||||
|
"name": "Oscillators",
|
||||||
|
"description": "RSI, Stochastic, MACD, KDJ",
|
||||||
|
"indicators": [
|
||||||
|
{
|
||||||
|
"id": "rsi",
|
||||||
|
"name": "Relative Strength Index",
|
||||||
|
"periods": [14],
|
||||||
|
"default_period": 14,
|
||||||
|
"bounds": [0, 100],
|
||||||
|
"overbought": 70,
|
||||||
|
"oversold": 30,
|
||||||
|
"type": "momentum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "stochastic",
|
||||||
|
"name": "Stochastic Oscillator",
|
||||||
|
"periods": [14],
|
||||||
|
"smoothing": [3, 5, 7],
|
||||||
|
"default_period": 14,
|
||||||
|
"bounds": [0, 100],
|
||||||
|
"overbought": 80,
|
||||||
|
"oversold": 20,
|
||||||
|
"type": "momentum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "macd",
|
||||||
|
"name": "MACD",
|
||||||
|
"fast_period": 12,
|
||||||
|
"slow_period": 26,
|
||||||
|
"signal_period": 9,
|
||||||
|
"type": "momentum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kdj",
|
||||||
|
"name": "KDJ Index",
|
||||||
|
"periods": [9, 14],
|
||||||
|
"default_period": 9,
|
||||||
|
"bounds": [0, 100],
|
||||||
|
"type": "momentum",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"volatility": {
|
||||||
|
"name": "Volatility Indicators",
|
||||||
|
"description": "Bollinger Bands, ATR, Keltner Channel",
|
||||||
|
"indicators": [
|
||||||
|
{
|
||||||
|
"id": "bb",
|
||||||
|
"name": "Bollinger Bands",
|
||||||
|
"periods": [20],
|
||||||
|
"default_period": 20,
|
||||||
|
"std_dev": 2,
|
||||||
|
"type": "volatility",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "atr",
|
||||||
|
"name": "Average True Range",
|
||||||
|
"periods": [14],
|
||||||
|
"default_period": 14,
|
||||||
|
"type": "volatility",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kc",
|
||||||
|
"name": "Keltner Channel",
|
||||||
|
"periods": [20],
|
||||||
|
"default_period": 20,
|
||||||
|
"atr_mult": 2,
|
||||||
|
"type": "volatility",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"support_resistance": {
|
||||||
|
"name": "Support & Resistance",
|
||||||
|
"description": "Pivot Points, Fibonacci, Trend Lines",
|
||||||
|
"indicators": [
|
||||||
|
{
|
||||||
|
"id": "pivot",
|
||||||
|
"name": "Pivot Points",
|
||||||
|
"types": ["Classic", "Camarilla", "Woodie"],
|
||||||
|
"default_type": "Classic",
|
||||||
|
"type": "level",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fibonacci",
|
||||||
|
"name": "Fibonacci Retracement",
|
||||||
|
"levels": [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0],
|
||||||
|
"type": "level",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"volume": {
|
||||||
|
"name": "Volume Indicators",
|
||||||
|
"description": "OBV, Volume Profile, CMF",
|
||||||
|
"indicators": [
|
||||||
|
{
|
||||||
|
"id": "obv",
|
||||||
|
"name": "On-Balance Volume",
|
||||||
|
"periods": [20],
|
||||||
|
"default_period": 20,
|
||||||
|
"type": "volume",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmf",
|
||||||
|
"name": "Chaikin Money Flow",
|
||||||
|
"periods": [20],
|
||||||
|
"default_period": 20,
|
||||||
|
"type": "volume",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default indicator configuration for gold trading
|
||||||
|
DEFAULT_INDICATORS = {
|
||||||
|
"trend": ["ema_12", "ema_26"],
|
||||||
|
"momentum": ["rsi_14", "macd"],
|
||||||
|
"volatility": ["bb_20", "atr_14"],
|
||||||
|
"support_resistance": ["pivot_classic"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock user configurations
|
||||||
|
USER_INDICATORS = {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/available")
|
||||||
|
async def get_available_indicators():
|
||||||
|
"""Get all available technical indicators"""
|
||||||
|
return {
|
||||||
|
"total_categories": len(AVAILABLE_INDICATORS),
|
||||||
|
"categories": AVAILABLE_INDICATORS,
|
||||||
|
"total_indicators": sum(
|
||||||
|
len(cat.get("indicators", [])) for cat in AVAILABLE_INDICATORS.values()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/categories")
|
||||||
|
async def get_indicator_categories():
|
||||||
|
"""Get indicator categories"""
|
||||||
|
return {
|
||||||
|
"categories": [
|
||||||
|
{"key": key, "name": value["name"], "description": value["description"]}
|
||||||
|
for key, value in AVAILABLE_INDICATORS.items()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/category/{category}")
|
||||||
|
async def get_category_indicators(category: str):
|
||||||
|
"""Get indicators in a specific category"""
|
||||||
|
if category not in AVAILABLE_INDICATORS:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Category '{category}' not found")
|
||||||
|
|
||||||
|
return AVAILABLE_INDICATORS[category]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{indicator_id}")
|
||||||
|
async def get_indicator_details(indicator_id: str):
|
||||||
|
"""Get detailed information about a specific indicator"""
|
||||||
|
for category in AVAILABLE_INDICATORS.values():
|
||||||
|
for indicator in category.get("indicators", []):
|
||||||
|
if indicator["id"] == indicator_id:
|
||||||
|
return indicator
|
||||||
|
|
||||||
|
raise HTTPException(status_code=404, detail=f"Indicator '{indicator_id}' not found")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/default")
|
||||||
|
async def get_default_configuration():
|
||||||
|
"""Get recommended indicator configuration for gold trading"""
|
||||||
|
return {
|
||||||
|
"name": "Gold Trading Starter Pack",
|
||||||
|
"description": "Recommended indicators for gold day trading",
|
||||||
|
"configuration": DEFAULT_INDICATORS,
|
||||||
|
"explanation": {
|
||||||
|
"trend": "EMAs help identify trend direction",
|
||||||
|
"momentum": "RSI and MACD identify overbought/oversold conditions",
|
||||||
|
"volatility": "Bollinger Bands and ATR help with entry/exit zones",
|
||||||
|
"support_resistance": "Pivot points identify key support/resistance levels",
|
||||||
|
},
|
||||||
|
"best_practices": [
|
||||||
|
"Use 12/26 EMA crossover for trend confirmation",
|
||||||
|
"RSI above 70 = potential sell, below 30 = potential buy",
|
||||||
|
"MACD crossovers signal momentum changes",
|
||||||
|
"Bollinger Band squeeze precedes volatility expansion",
|
||||||
|
"Trade ATR breakouts for high probability moves",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/presets")
|
||||||
|
async def get_indicator_presets():
|
||||||
|
"""Get pre-configured indicator setups"""
|
||||||
|
return {
|
||||||
|
"presets": [
|
||||||
|
{
|
||||||
|
"id": "scalping",
|
||||||
|
"name": "Scalping Setup (1-5 min)",
|
||||||
|
"indicators": [
|
||||||
|
"ema_5",
|
||||||
|
"ema_10",
|
||||||
|
"rsi_14",
|
||||||
|
"macd",
|
||||||
|
"bb_20",
|
||||||
|
],
|
||||||
|
"description": "Fast indicators for quick trade entries/exits",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "swing",
|
||||||
|
"name": "Swing Trading Setup (4h-1D)",
|
||||||
|
"indicators": [
|
||||||
|
"sma_50",
|
||||||
|
"ema_200",
|
||||||
|
"rsi_14",
|
||||||
|
"macd",
|
||||||
|
"pivot_classic",
|
||||||
|
],
|
||||||
|
"description": "Medium-term trend and momentum indicators",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "position",
|
||||||
|
"name": "Position Trading Setup (1D+)",
|
||||||
|
"indicators": [
|
||||||
|
"sma_50",
|
||||||
|
"sma_200",
|
||||||
|
"rsi_14",
|
||||||
|
"bb_20",
|
||||||
|
"fibonacci",
|
||||||
|
],
|
||||||
|
"description": "Long-term trend and support/resistance levels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "volatility",
|
||||||
|
"name": "Volatility Focus Setup",
|
||||||
|
"indicators": [
|
||||||
|
"bb_20",
|
||||||
|
"atr_14",
|
||||||
|
"kc_20",
|
||||||
|
"obv_20",
|
||||||
|
],
|
||||||
|
"description": "For high volatility market conditions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "momentum",
|
||||||
|
"name": "Momentum Focus Setup",
|
||||||
|
"indicators": [
|
||||||
|
"rsi_14",
|
||||||
|
"stochastic_14",
|
||||||
|
"macd",
|
||||||
|
"kdj_9",
|
||||||
|
],
|
||||||
|
"description": "For momentum-driven market moves",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/preset/{preset_id}/apply")
|
||||||
|
async def apply_preset(preset_id: str, user_id: Optional[str] = Query(None)):
|
||||||
|
"""Apply a pre-configured indicator preset"""
|
||||||
|
presets = await get_indicator_presets()
|
||||||
|
preset = next((p for p in presets["presets"] if p["id"] == preset_id), None)
|
||||||
|
|
||||||
|
if not preset:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Preset '{preset_id}' not found")
|
||||||
|
|
||||||
|
# Store user configuration
|
||||||
|
if user_id:
|
||||||
|
USER_INDICATORS[user_id] = preset.copy()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "preset_applied",
|
||||||
|
"preset": preset,
|
||||||
|
"applied_at": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/custom")
|
||||||
|
async def create_custom_configuration(
|
||||||
|
indicators_list: List[str], config_name: str, user_id: Optional[str] = Query(None)
|
||||||
|
):
|
||||||
|
"""Create a custom indicator configuration"""
|
||||||
|
# Validate all requested indicators exist
|
||||||
|
valid_indicators = []
|
||||||
|
for cat in AVAILABLE_INDICATORS.values():
|
||||||
|
for ind in cat.get("indicators", []):
|
||||||
|
valid_indicators.append(ind["id"])
|
||||||
|
|
||||||
|
invalid = [i for i in indicators_list if i not in valid_indicators]
|
||||||
|
if invalid:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Invalid indicators: {invalid}",
|
||||||
|
)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"name": config_name,
|
||||||
|
"indicators": indicators_list,
|
||||||
|
"created_at": datetime.now().isoformat(),
|
||||||
|
"indicator_count": len(indicators_list),
|
||||||
|
}
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
USER_INDICATORS[user_id] = config
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "configuration_created",
|
||||||
|
"configuration": config,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/recommendations")
|
||||||
|
async def get_indicator_recommendations(
|
||||||
|
market_condition: str = Query("normal", regex="^(trending|ranging|volatile|calm)$"),
|
||||||
|
trading_style: str = Query("swing", regex="^(scalping|swing|position)$"),
|
||||||
|
):
|
||||||
|
"""Get recommended indicators based on market conditions"""
|
||||||
|
recommendations = {
|
||||||
|
"trending": {
|
||||||
|
"best": ["ema_12_26_crossover", "atr_14", "obv_20"],
|
||||||
|
"supporting": ["pivot_points", "fibonacci"],
|
||||||
|
"avoid": ["stochastic", "rsi_only"],
|
||||||
|
"reasoning": "Use trend-following indicators in trending markets",
|
||||||
|
},
|
||||||
|
"ranging": {
|
||||||
|
"best": ["rsi_14", "stochastic_14", "bb_20"],
|
||||||
|
"supporting": ["pivot_points"],
|
||||||
|
"avoid": ["moving_average_crossovers"],
|
||||||
|
"reasoning": "Use oscillators for overbought/oversold in ranging markets",
|
||||||
|
},
|
||||||
|
"volatile": {
|
||||||
|
"best": ["atr_14", "bb_20", "kc_20"],
|
||||||
|
"supporting": ["ema_12_26"],
|
||||||
|
"avoid": ["simple_moving_averages"],
|
||||||
|
"reasoning": "Track volatility expansion with volatility indicators",
|
||||||
|
},
|
||||||
|
"calm": {
|
||||||
|
"best": ["pivot_points", "fibonacci", "volume_profile"],
|
||||||
|
"supporting": ["rsi_14", "macd"],
|
||||||
|
"avoid": ["atr"],
|
||||||
|
"reasoning": "Focus on support/resistance levels when volatility is low",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
timeframe_recommendations = {
|
||||||
|
"scalping": {
|
||||||
|
"periods": ["1m", "5m"],
|
||||||
|
"indicators": ["ema_5_10", "rsi_14", "macd"],
|
||||||
|
"setup": "Fast indicators for quick entries",
|
||||||
|
},
|
||||||
|
"swing": {
|
||||||
|
"periods": ["4h", "1D"],
|
||||||
|
"indicators": ["ema_12_26", "rsi_14", "bb_20", "pivot_points"],
|
||||||
|
"setup": "Balanced trend and momentum",
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"periods": ["1D", "1W"],
|
||||||
|
"indicators": ["sma_50_200", "rsi_14", "fibonacci"],
|
||||||
|
"setup": "Long-term trend following",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"market_condition": market_condition,
|
||||||
|
"trading_style": trading_style,
|
||||||
|
"recommended_indicators": recommendations.get(
|
||||||
|
market_condition, recommendations["normal"]
|
||||||
|
),
|
||||||
|
"timeframe_setup": timeframe_recommendations.get(trading_style),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/calculate/{indicator}")
|
||||||
|
async def calculate_indicator(
|
||||||
|
indicator: str,
|
||||||
|
price_data: List[float],
|
||||||
|
period: int = Query(14, ge=2, le=200),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Calculate indicator values (for testing/visualization)
|
||||||
|
|
||||||
|
This would typically be called for real calculations
|
||||||
|
"""
|
||||||
|
if indicator == "rsi":
|
||||||
|
# Simplified RSI calculation
|
||||||
|
if len(price_data) < period:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Need at least {period} data points",
|
||||||
|
)
|
||||||
|
|
||||||
|
changes = [price_data[i] - price_data[i - 1] for i in range(1, len(price_data))]
|
||||||
|
gains = [max(0, c) for c in changes]
|
||||||
|
losses = [abs(min(0, c)) for c in changes]
|
||||||
|
|
||||||
|
avg_gain = sum(gains[-period:]) / period
|
||||||
|
avg_loss = sum(losses[-period:]) / period
|
||||||
|
|
||||||
|
rsi = 100 - (100 / (1 + (avg_gain / avg_loss if avg_loss != 0 else 1)))
|
||||||
|
return {"indicator": indicator, "period": period, "value": rsi}
|
||||||
|
|
||||||
|
raise HTTPException(status_code=400, detail=f"Indicator '{indicator}' calculation not implemented")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/alerts/golden-cross")
|
||||||
|
async def get_golden_cross_alerts():
|
||||||
|
"""Get alerts for golden cross (50-day SMA crosses above 200-day SMA)"""
|
||||||
|
return {
|
||||||
|
"alert_type": "golden_cross",
|
||||||
|
"description": "50-day SMA crosses above 200-day SMA (bullish signal)",
|
||||||
|
"current_status": "monitoring",
|
||||||
|
"last_occurrence": "2024-11-10",
|
||||||
|
"signal_strength": "strong",
|
||||||
|
"recommended_action": "Consider long positions",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/alerts/death-cross")
|
||||||
|
async def get_death_cross_alerts():
|
||||||
|
"""Get alerts for death cross (50-day SMA crosses below 200-day SMA)"""
|
||||||
|
return {
|
||||||
|
"alert_type": "death_cross",
|
||||||
|
"description": "50-day SMA crosses below 200-day SMA (bearish signal)",
|
||||||
|
"current_status": "monitoring",
|
||||||
|
"last_occurrence": None,
|
||||||
|
"signal_strength": None,
|
||||||
|
"recommended_action": "Monitor for potential bearish reversal",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/alerts/divergence")
|
||||||
|
async def get_divergence_alerts():
|
||||||
|
"""Get alerts for price/indicator divergences"""
|
||||||
|
return {
|
||||||
|
"divergence_alerts": [
|
||||||
|
{
|
||||||
|
"type": "bullish_divergence",
|
||||||
|
"indicator": "rsi",
|
||||||
|
"description": "Price makes lower low but RSI makes higher low",
|
||||||
|
"signal": "potential_uptrend_reversal",
|
||||||
|
"strength": "medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bearish_divergence",
|
||||||
|
"indicator": "macd",
|
||||||
|
"description": "Price makes higher high but MACD makes lower high",
|
||||||
|
"signal": "potential_downtrend_reversal",
|
||||||
|
"strength": "high",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/cheat-sheet")
|
||||||
|
async def get_indicator_cheat_sheet():
|
||||||
|
"""Get quick reference guide for all indicators"""
|
||||||
|
return {
|
||||||
|
"moving_averages": {
|
||||||
|
"ema_crossover": "Golden Cross (50 > 200) = bullish, Death Cross = bearish",
|
||||||
|
"price_cross_ma": "Price above MA = uptrend, Below = downtrend",
|
||||||
|
"ma_bounce": "Price bounces off MA = trend continuation",
|
||||||
|
},
|
||||||
|
"oscillators": {
|
||||||
|
"rsi_above_70": "Overbought - look for reversals",
|
||||||
|
"rsi_below_30": "Oversold - look for bounces",
|
||||||
|
"rsi_divergence": "Price higher but RSI lower = bearish signal",
|
||||||
|
"macd_cross": "MACD above signal line = bullish",
|
||||||
|
},
|
||||||
|
"volatility": {
|
||||||
|
"bb_squeeze": "Low volatility - breakout coming soon",
|
||||||
|
"bb_expansion": "High volatility - expect big moves",
|
||||||
|
"atr_low": "Low volatility period",
|
||||||
|
"atr_high": "High volatility period",
|
||||||
|
},
|
||||||
|
"support_resistance": {
|
||||||
|
"pivot_s1": "First support level",
|
||||||
|
"pivot_r1": "First resistance level",
|
||||||
|
"fibonacci_618": "Most important retracement level",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
"""
|
||||||
|
Phase 5: ML Pattern Recognition and Clustering
|
||||||
|
Machine learning-based trade pattern analysis and clustering
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import random
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/ml-patterns", tags=["ML Pattern Recognition"])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TradeCluster:
|
||||||
|
"""Represents a cluster of similar trades"""
|
||||||
|
|
||||||
|
cluster_id: int
|
||||||
|
name: str
|
||||||
|
size: int
|
||||||
|
avg_win_rate: float
|
||||||
|
avg_profit: float
|
||||||
|
confidence: float
|
||||||
|
characteristics: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
# Mock ML model results
|
||||||
|
SAMPLE_CLUSTERS = [
|
||||||
|
{
|
||||||
|
"cluster_id": 1,
|
||||||
|
"name": "Morning Golden Cross Strategy",
|
||||||
|
"size": 12,
|
||||||
|
"avg_win_rate": 72.5,
|
||||||
|
"avg_profit": 245.50,
|
||||||
|
"confidence": 0.89,
|
||||||
|
"characteristics": {
|
||||||
|
"entry_condition": "EMA(12) crosses above EMA(26)",
|
||||||
|
"exit_condition": "RSI > 70 or price closes below EMA(12)",
|
||||||
|
"best_timeframe": "15m",
|
||||||
|
"best_hour": "09:00-11:00",
|
||||||
|
"avg_hold_time": "45 minutes",
|
||||||
|
"risk_reward_ratio": 1.8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cluster_id": 2,
|
||||||
|
"name": "Bollinger Band Breakout",
|
||||||
|
"size": 8,
|
||||||
|
"avg_win_rate": 65.0,
|
||||||
|
"avg_profit": 180.25,
|
||||||
|
"confidence": 0.76,
|
||||||
|
"characteristics": {
|
||||||
|
"entry_condition": "Price breaks above BB Upper band",
|
||||||
|
"exit_condition": "Close inside BB or move stops to breakeven",
|
||||||
|
"best_timeframe": "5m",
|
||||||
|
"best_hour": "10:00-15:00",
|
||||||
|
"avg_hold_time": "30 minutes",
|
||||||
|
"risk_reward_ratio": 1.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cluster_id": 3,
|
||||||
|
"name": "RSI Oversold Bounce",
|
||||||
|
"size": 15,
|
||||||
|
"avg_win_rate": 58.0,
|
||||||
|
"avg_profit": 120.75,
|
||||||
|
"confidence": 0.71,
|
||||||
|
"characteristics": {
|
||||||
|
"entry_condition": "RSI < 30 + price bounces off support",
|
||||||
|
"exit_condition": "RSI > 70 or initial stop loss",
|
||||||
|
"best_timeframe": "15m",
|
||||||
|
"best_hour": "All hours",
|
||||||
|
"avg_hold_time": "60 minutes",
|
||||||
|
"risk_reward_ratio": 1.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cluster_id": 4,
|
||||||
|
"name": "MACD Divergence Setup",
|
||||||
|
"size": 6,
|
||||||
|
"avg_win_rate": 83.0,
|
||||||
|
"avg_profit": 320.50,
|
||||||
|
"confidence": 0.92,
|
||||||
|
"characteristics": {
|
||||||
|
"entry_condition": "Price lower high but MACD higher high (bullish)",
|
||||||
|
"exit_condition": "MACD crosses below signal line",
|
||||||
|
"best_timeframe": "1h",
|
||||||
|
"best_hour": "09:00-17:00",
|
||||||
|
"avg_hold_time": "2-4 hours",
|
||||||
|
"risk_reward_ratio": 2.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cluster_id": 5,
|
||||||
|
"name": "Support Bounce Pattern",
|
||||||
|
"size": 20,
|
||||||
|
"avg_win_rate": 62.0,
|
||||||
|
"avg_profit": 95.30,
|
||||||
|
"confidence": 0.68,
|
||||||
|
"characteristics": {
|
||||||
|
"entry_condition": "Price touches pivot point or key support",
|
||||||
|
"exit_condition": "Next resistance or predetermined TP",
|
||||||
|
"best_timeframe": "5m-15m",
|
||||||
|
"best_hour": "09:00-16:00",
|
||||||
|
"avg_hold_time": "20-45 minutes",
|
||||||
|
"risk_reward_ratio": 1.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mock market condition analysis
|
||||||
|
MARKET_CONDITIONS = {
|
||||||
|
"trending_up": {
|
||||||
|
"name": "Strong Uptrend",
|
||||||
|
"description": "Market in clear uptrend with higher highs and higher lows",
|
||||||
|
"best_clusters": [1, 4],
|
||||||
|
"confidence": 0.87,
|
||||||
|
"recommendation": "Trade breakouts and continuations, avoid shorting",
|
||||||
|
},
|
||||||
|
"trending_down": {
|
||||||
|
"name": "Strong Downtrend",
|
||||||
|
"description": "Market in clear downtrend with lower highs and lower lows",
|
||||||
|
"best_clusters": [3, 5],
|
||||||
|
"confidence": 0.84,
|
||||||
|
"recommendation": "Trade support bounces, avoid breakout trades",
|
||||||
|
},
|
||||||
|
"ranging": {
|
||||||
|
"name": "Range-Bound Market",
|
||||||
|
"description": "Market oscillating between support and resistance",
|
||||||
|
"best_clusters": [2, 3, 5],
|
||||||
|
"confidence": 0.72,
|
||||||
|
"recommendation": "Trade bounces off support/resistance, avoid breakouts",
|
||||||
|
},
|
||||||
|
"volatile": {
|
||||||
|
"name": "High Volatility",
|
||||||
|
"description": "Large price swings with low predictability",
|
||||||
|
"best_clusters": [2, 4],
|
||||||
|
"confidence": 0.65,
|
||||||
|
"recommendation": "Use wider stops, trade divergences, avoid scalping",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/clusters")
|
||||||
|
async def get_trade_clusters(
|
||||||
|
min_size: int = Query(5, ge=1),
|
||||||
|
min_confidence: float = Query(0.6, ge=0, le=1),
|
||||||
|
sort_by: str = Query("win_rate", regex="^(win_rate|profit|confidence|size)$"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get ML-discovered trade clusters
|
||||||
|
|
||||||
|
- **min_size**: Minimum trades in cluster
|
||||||
|
- **min_confidence**: Minimum confidence score (0-1)
|
||||||
|
- **sort_by**: Sort by win_rate, profit, confidence, or size
|
||||||
|
"""
|
||||||
|
filtered = [c for c in SAMPLE_CLUSTERS if c["size"] >= min_size and c["confidence"] >= min_confidence]
|
||||||
|
|
||||||
|
# Sort results
|
||||||
|
sort_key = {
|
||||||
|
"win_rate": lambda x: x["avg_win_rate"],
|
||||||
|
"profit": lambda x: x["avg_profit"],
|
||||||
|
"confidence": lambda x: x["confidence"],
|
||||||
|
"size": lambda x: x["size"],
|
||||||
|
}[sort_by]
|
||||||
|
|
||||||
|
filtered.sort(key=sort_key, reverse=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_clusters": len(filtered),
|
||||||
|
"filters_applied": {
|
||||||
|
"min_size": min_size,
|
||||||
|
"min_confidence": min_confidence,
|
||||||
|
},
|
||||||
|
"clusters": filtered,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/cluster/{cluster_id}")
|
||||||
|
async def get_cluster_details(cluster_id: int):
|
||||||
|
"""Get detailed analysis of a specific cluster"""
|
||||||
|
cluster = next((c for c in SAMPLE_CLUSTERS if c["cluster_id"] == cluster_id), None)
|
||||||
|
if not cluster:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Cluster {cluster_id} not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cluster": cluster,
|
||||||
|
"extended_analysis": {
|
||||||
|
"profitability_score": cluster["avg_win_rate"] * cluster["confidence"],
|
||||||
|
"expected_value": (
|
||||||
|
cluster["avg_profit"] * cluster["avg_win_rate"] / 100
|
||||||
|
- cluster["avg_profit"] * (1 - cluster["avg_win_rate"] / 100) * 0.7
|
||||||
|
),
|
||||||
|
"consistency": f"{cluster['avg_win_rate']:.1f}% of trades profitable",
|
||||||
|
"risk_level": "Low" if cluster["avg_win_rate"] > 70 else "Medium" if cluster["avg_win_rate"] > 55 else "High",
|
||||||
|
"recommended_for": "Aggressive traders" if cluster["avg_profit"] > 200 else "Conservative traders",
|
||||||
|
},
|
||||||
|
"similar_clusters": [c for c in SAMPLE_CLUSTERS if c["cluster_id"] != cluster_id][:3],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/cluster/{cluster_id}/simulate")
|
||||||
|
async def simulate_cluster_trades(
|
||||||
|
cluster_id: int, num_trades: int = Query(100, ge=10, le=1000)
|
||||||
|
):
|
||||||
|
"""Simulate future trades based on cluster characteristics"""
|
||||||
|
cluster = next((c for c in SAMPLE_CLUSTERS if c["cluster_id"] == cluster_id), None)
|
||||||
|
if not cluster:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Cluster {cluster_id} not found")
|
||||||
|
|
||||||
|
# Simulate trades
|
||||||
|
win_rate = cluster["avg_win_rate"] / 100
|
||||||
|
simulated_trades = []
|
||||||
|
cumulative_pnl = 0
|
||||||
|
|
||||||
|
for i in range(num_trades):
|
||||||
|
is_win = random.random() < win_rate
|
||||||
|
profit = (
|
||||||
|
cluster["avg_profit"] * random.uniform(0.7, 1.3)
|
||||||
|
if is_win
|
||||||
|
else -cluster["avg_profit"] * 0.7 * random.uniform(0.7, 1.3)
|
||||||
|
)
|
||||||
|
cumulative_pnl += profit
|
||||||
|
|
||||||
|
simulated_trades.append(
|
||||||
|
{
|
||||||
|
"trade_num": i + 1,
|
||||||
|
"result": "Win" if is_win else "Loss",
|
||||||
|
"profit": round(profit, 2),
|
||||||
|
"cumulative_pnl": round(cumulative_pnl, 2),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
wins = sum(1 for t in simulated_trades if t["result"] == "Win")
|
||||||
|
total_profit = sum(t["profit"] for t in simulated_trades)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cluster_id": cluster_id,
|
||||||
|
"simulation_size": num_trades,
|
||||||
|
"simulated_win_rate": f"{wins/num_trades*100:.1f}%",
|
||||||
|
"simulated_total_profit": round(total_profit, 2),
|
||||||
|
"simulated_avg_trade": round(total_profit / num_trades, 2),
|
||||||
|
"best_streak": max((len(list(g)) for k, g in __import__("itertools").groupby(simulated_trades, lambda x: x["result"] == "Win") if k), default=0),
|
||||||
|
"recent_trades": simulated_trades[-10:],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/market-condition")
|
||||||
|
async def analyze_market_condition():
|
||||||
|
"""Analyze current market condition and recommend best clusters"""
|
||||||
|
# In production, this would analyze real market data
|
||||||
|
current_condition = "trending_up"
|
||||||
|
condition_data = MARKET_CONDITIONS[current_condition]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"current_condition": current_condition,
|
||||||
|
"condition_analysis": condition_data,
|
||||||
|
"recommended_clusters": [
|
||||||
|
SAMPLE_CLUSTERS[SAMPLE_CLUSTERS[0]["cluster_id"] - 1 + i]
|
||||||
|
for i in range(min(len(condition_data["best_clusters"]), 3))
|
||||||
|
],
|
||||||
|
"expected_profitability": condition_data["confidence"],
|
||||||
|
"next_update": (datetime.now() + timedelta(minutes=15)).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/recommendations")
|
||||||
|
async def get_trading_recommendations(
|
||||||
|
current_price: float = Query(2000.0),
|
||||||
|
timeframe: str = Query("15m", regex="^(1m|5m|15m|1h|4h|1d)$"),
|
||||||
|
):
|
||||||
|
"""Get ML-based trading recommendations"""
|
||||||
|
# Analyze current conditions
|
||||||
|
market_analysis = await analyze_market_condition()
|
||||||
|
|
||||||
|
recommendations = []
|
||||||
|
for cluster in SAMPLE_CLUSTERS[:3]: # Top 3 clusters
|
||||||
|
if cluster["best_timeframe"].replace("m", "").replace("h", "") in timeframe:
|
||||||
|
recommendations.append(
|
||||||
|
{
|
||||||
|
"cluster_id": cluster["cluster_id"],
|
||||||
|
"strategy": cluster["name"],
|
||||||
|
"confidence": cluster["confidence"],
|
||||||
|
"win_rate": cluster["avg_win_rate"],
|
||||||
|
"action": "BUY" if market_analysis["current_condition"] == "trending_up" else "SELL",
|
||||||
|
"entry_price": current_price * (1 - 0.002) if "BUY" else current_price * (1 + 0.002),
|
||||||
|
"take_profit": current_price * (1 + cluster["characteristics"]["risk_reward_ratio"] * 0.005),
|
||||||
|
"stop_loss": current_price * (1 - 0.005),
|
||||||
|
"risk_reward": cluster["characteristics"]["risk_reward_ratio"],
|
||||||
|
"probability": round(cluster["avg_win_rate"] * cluster["confidence"], 2),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timeframe": timeframe,
|
||||||
|
"current_price": current_price,
|
||||||
|
"market_condition": market_analysis["current_condition"],
|
||||||
|
"recommendations": sorted(recommendations, key=lambda x: x["probability"], reverse=True),
|
||||||
|
"best_recommendation": recommendations[0] if recommendations else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/similarity/{cluster_id}")
|
||||||
|
async def find_similar_patterns(cluster_id: int):
|
||||||
|
"""Find similar trade patterns based on cluster characteristics"""
|
||||||
|
cluster = next((c for c in SAMPLE_CLUSTERS if c["cluster_id"] == cluster_id), None)
|
||||||
|
if not cluster:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Cluster {cluster_id} not found")
|
||||||
|
|
||||||
|
# Calculate similarity score (simplified)
|
||||||
|
similar = []
|
||||||
|
for c in SAMPLE_CLUSTERS:
|
||||||
|
if c["cluster_id"] != cluster_id:
|
||||||
|
similarity = (
|
||||||
|
(1 - abs(c["avg_win_rate"] - cluster["avg_win_rate"]) / 100)
|
||||||
|
+ (1 - abs(c["avg_profit"] - cluster["avg_profit"]) / 500)
|
||||||
|
) / 2
|
||||||
|
similar.append({"cluster": c, "similarity_score": similarity})
|
||||||
|
|
||||||
|
similar.sort(key=lambda x: x["similarity_score"], reverse=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"reference_cluster": cluster,
|
||||||
|
"similar_patterns": [s for s in similar[:5]],
|
||||||
|
"use_case": "Use similar patterns to confirm trade setup validity",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/performance-projection")
|
||||||
|
async def project_future_performance(
|
||||||
|
days_ahead: int = Query(30, ge=1, le=90),
|
||||||
|
assumed_trades_per_day: int = Query(5, ge=1, le=50),
|
||||||
|
):
|
||||||
|
"""Project future performance based on ML clusters"""
|
||||||
|
best_cluster = max(SAMPLE_CLUSTERS, key=lambda x: x["avg_win_rate"] * x["confidence"])
|
||||||
|
|
||||||
|
total_trades = days_ahead * assumed_trades_per_day
|
||||||
|
win_rate = best_cluster["avg_win_rate"] / 100
|
||||||
|
wins = int(total_trades * win_rate)
|
||||||
|
losses = total_trades - wins
|
||||||
|
|
||||||
|
total_profit = wins * best_cluster["avg_profit"] - losses * best_cluster["avg_profit"] * 0.7
|
||||||
|
|
||||||
|
return {
|
||||||
|
"projection_period": f"{days_ahead} days",
|
||||||
|
"assumed_trades_per_day": assumed_trades_per_day,
|
||||||
|
"total_projected_trades": total_trades,
|
||||||
|
"projected_wins": wins,
|
||||||
|
"projected_losses": losses,
|
||||||
|
"projected_win_rate": f"{win_rate*100:.1f}%",
|
||||||
|
"projected_total_profit": round(total_profit, 2),
|
||||||
|
"projected_avg_trade_profit": round(total_profit / total_trades, 2),
|
||||||
|
"daily_avg_profit": round(total_profit / days_ahead, 2),
|
||||||
|
"monthly_projection": round(total_profit / days_ahead * 30, 2),
|
||||||
|
"assumptions": [
|
||||||
|
"Based on best performing cluster",
|
||||||
|
f"Consistent {assumed_trades_per_day} trades per day",
|
||||||
|
"Market conditions remain stable",
|
||||||
|
"No slippage or commissions",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/feedback/{cluster_id}")
|
||||||
|
async def submit_cluster_feedback(
|
||||||
|
cluster_id: int,
|
||||||
|
actual_win_rate: float = Query(..., ge=0, le=100),
|
||||||
|
feedback: str = Query(...),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Submit feedback on cluster performance for model improvement
|
||||||
|
|
||||||
|
- **cluster_id**: ID of cluster being evaluated
|
||||||
|
- **actual_win_rate**: Observed win rate in real trading
|
||||||
|
- **feedback**: Qualitative feedback on pattern performance
|
||||||
|
"""
|
||||||
|
cluster = next((c for c in SAMPLE_CLUSTERS if c["cluster_id"] == cluster_id), None)
|
||||||
|
if not cluster:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Cluster {cluster_id} not found")
|
||||||
|
|
||||||
|
accuracy = abs(cluster["avg_win_rate"] - actual_win_rate)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "feedback_recorded",
|
||||||
|
"cluster_id": cluster_id,
|
||||||
|
"expected_win_rate": cluster["avg_win_rate"],
|
||||||
|
"actual_win_rate": actual_win_rate,
|
||||||
|
"prediction_accuracy": 100 - accuracy,
|
||||||
|
"feedback": feedback,
|
||||||
|
"message": "Thank you! This feedback helps improve our ML model.",
|
||||||
|
"next_model_update": (datetime.now() + timedelta(days=7)).date().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/model-stats")
|
||||||
|
async def get_ml_model_statistics():
|
||||||
|
"""Get statistics about the ML model and its performance"""
|
||||||
|
total_trades_analyzed = sum(c["size"] for c in SAMPLE_CLUSTERS)
|
||||||
|
avg_accuracy = sum(c["confidence"] for c in SAMPLE_CLUSTERS) / len(SAMPLE_CLUSTERS)
|
||||||
|
best_cluster = max(SAMPLE_CLUSTERS, key=lambda x: x["avg_win_rate"] * x["confidence"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"model_info": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"last_updated": "2024-11-10",
|
||||||
|
"training_data_size": 500,
|
||||||
|
},
|
||||||
|
"performance": {
|
||||||
|
"clusters_discovered": len(SAMPLE_CLUSTERS),
|
||||||
|
"total_trades_analyzed": total_trades_analyzed,
|
||||||
|
"average_cluster_accuracy": round(avg_accuracy, 3),
|
||||||
|
"best_cluster": best_cluster["name"],
|
||||||
|
"best_cluster_win_rate": f"{best_cluster['avg_win_rate']:.1f}%",
|
||||||
|
},
|
||||||
|
"ml_algorithms_used": [
|
||||||
|
"K-Means Clustering",
|
||||||
|
"Feature Extraction (Technical Indicators)",
|
||||||
|
"Win Rate Prediction Model",
|
||||||
|
"Pattern Recognition Neural Network",
|
||||||
|
],
|
||||||
|
"next_model_retraining": "2024-11-20",
|
||||||
|
}
|
||||||
+6
-1
@@ -7,7 +7,7 @@ from app.streaming.live_store import periodic_flush, periodic_maintenance
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
# Newly added routers
|
# Newly added routers
|
||||||
from app.api import account, performance, status, settings_api, prompts, daily_helper
|
from app.api import account, performance, status, settings_api, prompts, daily_helper, analytics, economic_calendar, indicators, ml_patterns, ai_coach
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=settings.APP_NAME,
|
title=settings.APP_NAME,
|
||||||
@@ -42,6 +42,11 @@ app.include_router(status.router, prefix="/api")
|
|||||||
app.include_router(settings_api.router, prefix="/api")
|
app.include_router(settings_api.router, prefix="/api")
|
||||||
app.include_router(prompts.router, prefix="/api")
|
app.include_router(prompts.router, prefix="/api")
|
||||||
app.include_router(daily_helper.router)
|
app.include_router(daily_helper.router)
|
||||||
|
app.include_router(analytics.router)
|
||||||
|
app.include_router(economic_calendar.router)
|
||||||
|
app.include_router(indicators.router)
|
||||||
|
app.include_router(ml_patterns.router)
|
||||||
|
app.include_router(ai_coach.router)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|||||||
@@ -169,3 +169,100 @@ class HabitTracker(Base):
|
|||||||
total_completions = Column(Integer, default=0)
|
total_completions = Column(Integer, default=0)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
# Phase 3: Advanced Analytics
|
||||||
|
|
||||||
|
class PerformanceSnapshot(Base):
|
||||||
|
"""Daily performance snapshot for historical tracking"""
|
||||||
|
__tablename__ = "performance_snapshots"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
user_id = Column(String, nullable=True)
|
||||||
|
snapshot_date = Column(Date, default=func.current_date())
|
||||||
|
daily_pnl = Column(Float, default=0.0)
|
||||||
|
daily_pnl_percent = Column(Float, default=0.0)
|
||||||
|
total_trades = Column(Integer, default=0)
|
||||||
|
winning_trades = Column(Integer, default=0)
|
||||||
|
losing_trades = Column(Integer, default=0)
|
||||||
|
win_rate = Column(Float, default=0.0)
|
||||||
|
best_trade = Column(Float, nullable=True)
|
||||||
|
worst_trade = Column(Float, nullable=True)
|
||||||
|
avg_win = Column(Float, nullable=True)
|
||||||
|
avg_loss = Column(Float, nullable=True)
|
||||||
|
sharpe_ratio = Column(Float, nullable=True)
|
||||||
|
profit_factor = Column(Float, nullable=True)
|
||||||
|
max_drawdown = Column(Float, nullable=True)
|
||||||
|
cumulative_pnl = Column(Float, default=0.0)
|
||||||
|
portfolio_value = Column(Float, nullable=True)
|
||||||
|
equity_curve = Column(JSON, default=[]) # Time series
|
||||||
|
streak_type = Column(String, nullable=True) # win_streak, loss_streak
|
||||||
|
streak_count = Column(Integer, default=0)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class TradePattern(Base):
|
||||||
|
"""Identified profitable trade patterns"""
|
||||||
|
__tablename__ = "trade_patterns"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
user_id = Column(String, nullable=True)
|
||||||
|
pattern_name = Column(String) # e.g., "Morning breakout", "Reversal near support"
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
win_rate = Column(Float) # Percentage
|
||||||
|
avg_win = Column(Float)
|
||||||
|
avg_loss = Column(Float)
|
||||||
|
sample_count = Column(Integer) # Number of matching trades
|
||||||
|
best_timeframe = Column(String, nullable=True) # 1m, 5m, 15m, 1h, 1d
|
||||||
|
best_time_of_day = Column(String, nullable=True) # e.g., "09:30-10:30"
|
||||||
|
confidence_score = Column(Float) # 0-100
|
||||||
|
indicators_used = Column(JSON, default=[]) # List of indicators
|
||||||
|
market_conditions = Column(String, nullable=True) # bullish, bearish, neutral
|
||||||
|
total_profit = Column(Float, default=0.0)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class LessonLearned(Base):
|
||||||
|
"""Track lessons and insights from trading"""
|
||||||
|
__tablename__ = "lessons_learned"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
user_id = Column(String, nullable=True)
|
||||||
|
date_learned = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
category = Column(String) # entry, exit, risk, psychology, market
|
||||||
|
lesson_text = Column(Text)
|
||||||
|
related_trades = Column(JSON, default=[]) # Trade IDs
|
||||||
|
impact = Column(String) # positive, negative, neutral
|
||||||
|
tags = Column(JSON, default=[]) # Searchable tags
|
||||||
|
importance = Column(String) # critical, important, helpful
|
||||||
|
status = Column(String, default="active") # active, archived
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class MonthlyReview(Base):
|
||||||
|
"""Monthly trading performance review"""
|
||||||
|
__tablename__ = "monthly_reviews"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
user_id = Column(String, nullable=True)
|
||||||
|
year = Column(Integer)
|
||||||
|
month = Column(Integer)
|
||||||
|
total_trades = Column(Integer, default=0)
|
||||||
|
total_pnl = Column(Float, default=0.0)
|
||||||
|
total_pnl_percent = Column(Float, default=0.0)
|
||||||
|
best_day = Column(Date, nullable=True)
|
||||||
|
worst_day = Column(Date, nullable=True)
|
||||||
|
best_trade = Column(Float, nullable=True)
|
||||||
|
worst_trade = Column(Float, nullable=True)
|
||||||
|
win_rate = Column(Float, default=0.0)
|
||||||
|
avg_daily_pnl = Column(Float, nullable=True)
|
||||||
|
sharpe_ratio = Column(Float, nullable=True)
|
||||||
|
max_drawdown = Column(Float, nullable=True)
|
||||||
|
trading_days = Column(Integer, default=0)
|
||||||
|
best_pattern = Column(String, nullable=True)
|
||||||
|
summary = Column(Text, nullable=True)
|
||||||
|
improvements = Column(JSON, default=[])
|
||||||
|
goals_met = Column(JSON, default=[])
|
||||||
|
goals_missed = Column(JSON, default=[])
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|||||||
@@ -384,3 +384,157 @@ class HabitTrackerResponse(BaseModel):
|
|||||||
class HabitCompletionRequest(BaseModel):
|
class HabitCompletionRequest(BaseModel):
|
||||||
habit_id: int
|
habit_id: int
|
||||||
completion_date: Optional[str] = None # ISO date string, defaults to today
|
completion_date: Optional[str] = None # ISO date string, defaults to today
|
||||||
|
|
||||||
|
|
||||||
|
# Phase 3: Advanced Analytics Schemas
|
||||||
|
|
||||||
|
class PerformanceSnapshotCreate(BaseModel):
|
||||||
|
snapshot_date: Optional[str] = None # ISO date, defaults to today
|
||||||
|
daily_pnl: float
|
||||||
|
daily_pnl_percent: float
|
||||||
|
total_trades: int
|
||||||
|
winning_trades: int
|
||||||
|
losing_trades: int
|
||||||
|
win_rate: float
|
||||||
|
best_trade: Optional[float] = None
|
||||||
|
worst_trade: Optional[float] = None
|
||||||
|
avg_win: Optional[float] = None
|
||||||
|
avg_loss: Optional[float] = None
|
||||||
|
sharpe_ratio: Optional[float] = None
|
||||||
|
profit_factor: Optional[float] = None
|
||||||
|
max_drawdown: Optional[float] = None
|
||||||
|
cumulative_pnl: float
|
||||||
|
portfolio_value: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PerformanceSnapshotResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
snapshot_date: str
|
||||||
|
daily_pnl: float
|
||||||
|
daily_pnl_percent: float
|
||||||
|
total_trades: int
|
||||||
|
winning_trades: int
|
||||||
|
losing_trades: int
|
||||||
|
win_rate: float
|
||||||
|
best_trade: Optional[float]
|
||||||
|
worst_trade: Optional[float]
|
||||||
|
avg_win: Optional[float]
|
||||||
|
avg_loss: Optional[float]
|
||||||
|
sharpe_ratio: Optional[float]
|
||||||
|
profit_factor: Optional[float]
|
||||||
|
max_drawdown: Optional[float]
|
||||||
|
cumulative_pnl: float
|
||||||
|
portfolio_value: Optional[float]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class TradePatternCreate(BaseModel):
|
||||||
|
pattern_name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
win_rate: float
|
||||||
|
avg_win: float
|
||||||
|
avg_loss: float
|
||||||
|
sample_count: int
|
||||||
|
best_timeframe: Optional[str] = None
|
||||||
|
best_time_of_day: Optional[str] = None
|
||||||
|
confidence_score: float
|
||||||
|
indicators_used: List[str] = []
|
||||||
|
market_conditions: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class TradePatternResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
pattern_name: str
|
||||||
|
description: Optional[str]
|
||||||
|
win_rate: float
|
||||||
|
avg_win: float
|
||||||
|
avg_loss: float
|
||||||
|
sample_count: int
|
||||||
|
best_timeframe: Optional[str]
|
||||||
|
best_time_of_day: Optional[str]
|
||||||
|
confidence_score: float
|
||||||
|
indicators_used: List[str]
|
||||||
|
market_conditions: Optional[str]
|
||||||
|
total_profit: float
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class LessonLearnedCreate(BaseModel):
|
||||||
|
category: str # entry, exit, risk, psychology, market
|
||||||
|
lesson_text: str
|
||||||
|
related_trades: List[int] = []
|
||||||
|
impact: str = "neutral" # positive, negative, neutral
|
||||||
|
tags: List[str] = []
|
||||||
|
importance: str = "helpful" # critical, important, helpful
|
||||||
|
|
||||||
|
|
||||||
|
class LessonLearnedResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
date_learned: datetime
|
||||||
|
category: str
|
||||||
|
lesson_text: str
|
||||||
|
related_trades: List[int]
|
||||||
|
impact: str
|
||||||
|
tags: List[str]
|
||||||
|
importance: str
|
||||||
|
status: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class MonthlyReviewCreate(BaseModel):
|
||||||
|
year: int
|
||||||
|
month: int
|
||||||
|
total_trades: int
|
||||||
|
total_pnl: float
|
||||||
|
total_pnl_percent: float
|
||||||
|
best_day: Optional[str] = None # ISO date
|
||||||
|
worst_day: Optional[str] = None
|
||||||
|
best_trade: Optional[float] = None
|
||||||
|
worst_trade: Optional[float] = None
|
||||||
|
win_rate: float
|
||||||
|
avg_daily_pnl: Optional[float] = None
|
||||||
|
sharpe_ratio: Optional[float] = None
|
||||||
|
max_drawdown: Optional[float] = None
|
||||||
|
trading_days: int
|
||||||
|
best_pattern: Optional[str] = None
|
||||||
|
summary: Optional[str] = None
|
||||||
|
improvements: List[str] = []
|
||||||
|
goals_met: List[str] = []
|
||||||
|
goals_missed: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
class MonthlyReviewResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
year: int
|
||||||
|
month: int
|
||||||
|
total_trades: int
|
||||||
|
total_pnl: float
|
||||||
|
total_pnl_percent: float
|
||||||
|
best_day: Optional[str]
|
||||||
|
worst_day: Optional[str]
|
||||||
|
best_trade: Optional[float]
|
||||||
|
worst_trade: Optional[float]
|
||||||
|
win_rate: float
|
||||||
|
avg_daily_pnl: Optional[float]
|
||||||
|
sharpe_ratio: Optional[float]
|
||||||
|
max_drawdown: Optional[float]
|
||||||
|
trading_days: int
|
||||||
|
best_pattern: Optional[str]
|
||||||
|
summary: Optional[str]
|
||||||
|
improvements: List[str]
|
||||||
|
goals_met: List[str]
|
||||||
|
goals_missed: List[str]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|||||||
+18
-2
@@ -14,6 +14,17 @@ import UserProfileSetup from './components/UserProfileSetup'
|
|||||||
import HabitTracker from './components/HabitTracker'
|
import HabitTracker from './components/HabitTracker'
|
||||||
import DailyChecklistPanel from './components/DailyChecklistPanel'
|
import DailyChecklistPanel from './components/DailyChecklistPanel'
|
||||||
|
|
||||||
|
// Phase 3: Advanced Analytics Components
|
||||||
|
import AnalyticsDashboard from './components/AnalyticsDashboard'
|
||||||
|
|
||||||
|
// Phase 4: Economic Calendar & Advanced Features
|
||||||
|
import EconomicCalendar from './components/EconomicCalendar'
|
||||||
|
import AdvancedIndicatorsPanel from './components/AdvancedIndicatorsPanel'
|
||||||
|
|
||||||
|
// Phase 5: ML Pattern Recognition & AI Trading Coach
|
||||||
|
import MLPatternRecognition from './components/MLPatternRecognition'
|
||||||
|
import AITradingCoach from './components/AITradingCoach'
|
||||||
|
|
||||||
function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onChange: (t: string) => void }) {
|
function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onChange: (t: string) => void }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||||
@@ -27,7 +38,7 @@ function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onCh
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [activeTab, setActiveTab] = useState<'Live' | 'Account' | 'Equity' | 'Decisions' | 'Settings' | 'Prompts' | 'Daily Helper'>('Live')
|
const [activeTab, setActiveTab] = useState<'Live' | 'Account' | 'Equity' | 'Decisions' | 'Analytics' | 'Economic Calendar' | 'Indicators' | 'ML Patterns' | 'AI Coach' | 'Settings' | 'Prompts' | 'Daily Helper'>('Live')
|
||||||
const [backendStatus, setBackendStatus] = useState<any>(null)
|
const [backendStatus, setBackendStatus] = useState<any>(null)
|
||||||
const [showProfileSetup, setShowProfileSetup] = useState(false)
|
const [showProfileSetup, setShowProfileSetup] = useState(false)
|
||||||
|
|
||||||
@@ -44,7 +55,7 @@ export default function App() {
|
|||||||
return () => { mounted = false }
|
return () => { mounted = false }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const tabs = ['Live', 'Account', 'Equity', 'Decisions', 'Daily Helper', 'Settings', 'Prompts']
|
const tabs = ['Live', 'Account', 'Equity', 'Decisions', 'Analytics', 'Economic Calendar', 'Indicators', 'ML Patterns', 'AI Coach', 'Daily Helper', 'Settings', 'Prompts']
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-dark-bg p-6">
|
<div className="min-h-screen bg-dark-bg p-6">
|
||||||
@@ -80,6 +91,11 @@ export default function App() {
|
|||||||
{activeTab === 'Account' && <AccountPositionsPanel />}
|
{activeTab === 'Account' && <AccountPositionsPanel />}
|
||||||
{activeTab === 'Equity' && <EquityPerformancePanel />}
|
{activeTab === 'Equity' && <EquityPerformancePanel />}
|
||||||
{activeTab === 'Decisions' && <DecisionLogPanel />}
|
{activeTab === 'Decisions' && <DecisionLogPanel />}
|
||||||
|
{activeTab === 'Analytics' && <AnalyticsDashboard />}
|
||||||
|
{activeTab === 'Economic Calendar' && <EconomicCalendar />}
|
||||||
|
{activeTab === 'Indicators' && <AdvancedIndicatorsPanel />}
|
||||||
|
{activeTab === 'ML Patterns' && <MLPatternRecognition />}
|
||||||
|
{activeTab === 'AI Coach' && <AITradingCoach />}
|
||||||
|
|
||||||
{activeTab === 'Daily Helper' && (
|
{activeTab === 'Daily Helper' && (
|
||||||
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))' }}>
|
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))' }}>
|
||||||
|
|||||||
@@ -0,0 +1,385 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { MessageCircle, Heart, Lightbulb, TrendingUp, AlertCircle } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface CoachingAdvice {
|
||||||
|
indicator: string;
|
||||||
|
signal: string;
|
||||||
|
advice: string;
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AITradingCoach() {
|
||||||
|
const [activeTab, setActiveTab] = useState<'session' | 'realtime' | 'review' | 'performance'>('session');
|
||||||
|
const [sessionData, setSessionData] = useState<any>(null);
|
||||||
|
const [realtimeAdvice, setRealtimeAdvice] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [tradingStyle, setTradingStyle] = useState('swing');
|
||||||
|
const [experience, setExperience] = useState('intermediate');
|
||||||
|
|
||||||
|
// Start coaching session
|
||||||
|
const startSession = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/ai-coach/coaching-session', {
|
||||||
|
params: { trading_style: tradingStyle, experience_level: experience },
|
||||||
|
});
|
||||||
|
setSessionData(response.data);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting coaching session:', error);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get real-time advice
|
||||||
|
const getRealTimeAdvice = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/ai-coach/real-time-advice', {
|
||||||
|
params: {
|
||||||
|
current_price: 2000,
|
||||||
|
high_24h: 2050,
|
||||||
|
low_24h: 1950,
|
||||||
|
rsi: 65,
|
||||||
|
macd_signal: 'bullish',
|
||||||
|
market_condition: 'trending_up',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setRealtimeAdvice(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting real-time advice:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
startSession();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<MessageCircle className="w-7 h-7 text-blue-500" />
|
||||||
|
AI Trading Coach
|
||||||
|
</h2>
|
||||||
|
<div className="text-sm text-gray-400">Your personal AI trading mentor</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex gap-2 flex-wrap mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('session')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||||
|
activeTab === 'session'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Coaching Session
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab('realtime');
|
||||||
|
getRealTimeAdvice();
|
||||||
|
}}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||||
|
activeTab === 'realtime'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Real-Time Advice
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('performance')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||||
|
activeTab === 'performance'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Performance Analysis
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Session Tab */}
|
||||||
|
{activeTab === 'session' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Personalized Coaching Setup</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-400 mb-2">Trading Style</label>
|
||||||
|
<select
|
||||||
|
value={tradingStyle}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTradingStyle(e.target.value);
|
||||||
|
}}
|
||||||
|
className="w-full bg-dark-bg text-gray-200 border border-dark-border rounded px-3 py-2"
|
||||||
|
>
|
||||||
|
<option value="scalping">Scalping (1-5 min)</option>
|
||||||
|
<option value="swing">Swing Trading (4h-1D)</option>
|
||||||
|
<option value="position">Position Trading (1D+)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-400 mb-2">Experience Level</label>
|
||||||
|
<select
|
||||||
|
value={experience}
|
||||||
|
onChange={(e) => setExperience(e.target.value)}
|
||||||
|
className="w-full bg-dark-bg text-gray-200 border border-dark-border rounded px-3 py-2"
|
||||||
|
>
|
||||||
|
<option value="beginner">Beginner</option>
|
||||||
|
<option value="intermediate">Intermediate</option>
|
||||||
|
<option value="advanced">Advanced</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={startSession}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 rounded-lg transition mb-4"
|
||||||
|
>
|
||||||
|
Start New Session
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{sessionData && (
|
||||||
|
<>
|
||||||
|
{/* Strategy Focus */}
|
||||||
|
<div className="bg-dark-bg rounded-lg p-4 border border-dark-border mb-4">
|
||||||
|
<h4 className="font-semibold text-gray-200 mb-3">Your Strategy Focus</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Holding Period:</span>
|
||||||
|
<span className="font-medium text-gray-200">{sessionData.strategy_focus?.holding_period}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Best Indicators:</span>
|
||||||
|
<span className="font-medium text-gray-200">{sessionData.strategy_focus?.best_indicators}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Position Sizing:</span>
|
||||||
|
<span className="font-medium text-gray-200">{sessionData.strategy_focus?.position_sizing}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Daily Goal:</span>
|
||||||
|
<span className="font-medium text-gray-200">{sessionData.strategy_focus?.daily_goal}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Focus Points */}
|
||||||
|
<div className="bg-blue-900 bg-opacity-20 border border-blue-700 rounded-lg p-4">
|
||||||
|
<h4 className="font-semibold text-blue-400 mb-3 flex items-center gap-2">
|
||||||
|
<Lightbulb className="w-5 h-5" />
|
||||||
|
Your Focus Points
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-300">
|
||||||
|
{sessionData.guidance?.focus_points.map((point: string, idx: number) => (
|
||||||
|
<li key={idx} className="flex gap-2">
|
||||||
|
<span className="text-blue-400">→</span>
|
||||||
|
<span>{point}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Common Mistakes to Avoid */}
|
||||||
|
<div className="bg-red-900 bg-opacity-20 border border-red-700 rounded-lg p-4 mt-4">
|
||||||
|
<h4 className="font-semibold text-red-400 mb-3 flex items-center gap-2">
|
||||||
|
<AlertCircle className="w-5 h-5" />
|
||||||
|
Common Mistakes to Avoid
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-300">
|
||||||
|
{sessionData.guidance?.common_mistakes.slice(0, 3).map((mistake: string, idx: number) => (
|
||||||
|
<li key={idx} className="flex gap-2">
|
||||||
|
<span className="text-red-400">✗</span>
|
||||||
|
<span>{mistake}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Daily Routine */}
|
||||||
|
<div className="bg-green-900 bg-opacity-20 border border-green-700 rounded-lg p-4 mt-4">
|
||||||
|
<h4 className="font-semibold text-green-400 mb-3">Your Daily Routine</h4>
|
||||||
|
<ol className="space-y-2 text-sm text-gray-300">
|
||||||
|
{sessionData.guidance?.daily_routine.map((routine: string, idx: number) => (
|
||||||
|
<li key={idx} className="flex gap-2">
|
||||||
|
<span className="text-green-400 font-bold">{idx + 1}.</span>
|
||||||
|
<span>{routine}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Real-Time Advice Tab */}
|
||||||
|
{activeTab === 'realtime' && realtimeAdvice && (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-green-500" />
|
||||||
|
Real-Time Trading Advice
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Current Status */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400">Current Price</p>
|
||||||
|
<p className="text-xl font-bold text-blue-500">${realtimeAdvice.current_price}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400">Market Condition</p>
|
||||||
|
<p className="text-lg font-bold text-gray-200 capitalize">{realtimeAdvice.market_condition.replace(/_/g, ' ')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400">RSI Level</p>
|
||||||
|
<p className="text-xl font-bold text-purple-500">{realtimeAdvice.rsi_level}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400">Confidence</p>
|
||||||
|
<p className="text-xl font-bold text-green-500">{(realtimeAdvice.confidence_level * 100).toFixed(0)}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recommendation */}
|
||||||
|
<div
|
||||||
|
className={`rounded-lg p-4 mb-6 border ${
|
||||||
|
realtimeAdvice.overall_recommendation.includes('STRONG')
|
||||||
|
? 'bg-green-900 bg-opacity-30 border-green-600'
|
||||||
|
: realtimeAdvice.overall_recommendation.includes('BUY')
|
||||||
|
? 'bg-blue-900 bg-opacity-30 border-blue-600'
|
||||||
|
: 'bg-yellow-900 bg-opacity-30 border-yellow-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400 mb-1">AI Coach Recommendation</p>
|
||||||
|
<p className="text-2xl font-bold">{realtimeAdvice.overall_recommendation}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm text-gray-400 mb-1">Risk Level</p>
|
||||||
|
<p className={`text-xl font-bold ${realtimeAdvice.risk_assessment === 'HIGH' ? 'text-red-400' : realtimeAdvice.risk_assessment === 'MEDIUM' ? 'text-yellow-400' : 'text-green-400'}`}>
|
||||||
|
{realtimeAdvice.risk_assessment}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Plan */}
|
||||||
|
{realtimeAdvice.suggested_action && (
|
||||||
|
<div className="bg-dark-bg rounded-lg p-4 border border-dark-border mb-6">
|
||||||
|
<h4 className="font-semibold text-gray-200 mb-3">Suggested Action</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 mb-1">Entry Price</p>
|
||||||
|
<p className="font-bold text-gray-200">${realtimeAdvice.suggested_action.entry.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 mb-1">Stop Loss</p>
|
||||||
|
<p className="font-bold text-red-400">${realtimeAdvice.suggested_action.stop_loss.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 mb-1">Take Profit</p>
|
||||||
|
<p className="font-bold text-green-400">${realtimeAdvice.suggested_action.take_profit.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 mb-1">Risk/Reward</p>
|
||||||
|
<p className="font-bold text-blue-400">1:1.875</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Advice Details */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="font-semibold text-gray-200">Detailed Analysis</h4>
|
||||||
|
{realtimeAdvice.advice_pieces?.map((advice: CoachingAdvice, idx: number) => (
|
||||||
|
<div key={idx} className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-gray-200">{advice.indicator}</p>
|
||||||
|
<p className="text-xs text-gray-500">{advice.signal}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-xs text-gray-400">Weight</p>
|
||||||
|
<p className="font-bold text-gray-200">{(advice.weight * 100).toFixed(0)}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-300">{advice.advice}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Performance Analysis Tab */}
|
||||||
|
{activeTab === 'performance' && (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Heart className="w-5 h-5 text-red-500" />
|
||||||
|
Performance Coaching
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="bg-blue-900 bg-opacity-20 border border-blue-700 rounded-lg p-4">
|
||||||
|
<p className="text-sm text-gray-300 mb-3">Enter your recent trading performance to get AI coaching feedback:</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Total trades"
|
||||||
|
className="bg-dark-bg text-gray-200 border border-dark-border rounded px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Winning trades"
|
||||||
|
className="bg-dark-bg text-gray-200 border border-dark-border rounded px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Total P&L"
|
||||||
|
className="bg-dark-bg text-gray-200 border border-dark-border rounded px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Avg win"
|
||||||
|
className="bg-dark-bg text-gray-200 border border-dark-border rounded px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 rounded-lg transition">
|
||||||
|
Get Performance Coaching
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 p-4 bg-green-900 bg-opacity-20 border border-green-700 rounded-lg">
|
||||||
|
<p className="text-green-400 font-semibold mb-2">💡 Coach Tip:</p>
|
||||||
|
<p className="text-sm text-gray-300">
|
||||||
|
Track your trades consistently and review them regularly. The best traders learn from every single trade, whether it's a win or a loss.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Quick Tips */}
|
||||||
|
<div className="card bg-yellow-900 bg-opacity-20 border border-yellow-700">
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-yellow-400">Quick AI Coach Tips</h3>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-300">
|
||||||
|
<li>✓ Always use stop losses on every trade</li>
|
||||||
|
<li>✓ Risk only 1-2% per trade to protect your account</li>
|
||||||
|
<li>✓ Let winners run and cut losers quickly</li>
|
||||||
|
<li>✓ Keep a detailed trading journal for learning</li>
|
||||||
|
<li>✓ Review your trades daily for improvement</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Settings, Zap, BookOpen, Grid3X3, Check } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface Indicator {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
periods?: number[];
|
||||||
|
default_period?: number;
|
||||||
|
description?: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndicatorCategory {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
indicators: Indicator[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Preset {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
indicators: string[];
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdvancedIndicatorsPanel() {
|
||||||
|
const [categories, setCategories] = useState<Record<string, IndicatorCategory>>({});
|
||||||
|
const [presets, setPresets] = useState<Preset[]>([]);
|
||||||
|
const [selectedIndicators, setSelectedIndicators] = useState<Set<string>>(new Set());
|
||||||
|
const [activeTab, setActiveTab] = useState<'presets' | 'custom' | 'guide'>('presets');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
|
||||||
|
const [cheatSheet, setCheatSheet] = useState<any>(null);
|
||||||
|
|
||||||
|
// Fetch indicators and presets
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Fetch available indicators
|
||||||
|
const indicatorsResponse = await axios.get('/api/indicators/available');
|
||||||
|
setCategories(indicatorsResponse.data.categories || {});
|
||||||
|
|
||||||
|
// Fetch presets
|
||||||
|
const presetsResponse = await axios.get('/api/indicators/presets');
|
||||||
|
setPresets(presetsResponse.data.presets || []);
|
||||||
|
|
||||||
|
// Fetch cheat sheet
|
||||||
|
const cheatSheetResponse = await axios.get('/api/indicators/cheat-sheet');
|
||||||
|
setCheatSheet(cheatSheetResponse.data || {});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching indicators data:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleIndicatorToggle = (indicatorId: string) => {
|
||||||
|
const newSet = new Set(selectedIndicators);
|
||||||
|
if (newSet.has(indicatorId)) {
|
||||||
|
newSet.delete(indicatorId);
|
||||||
|
} else {
|
||||||
|
newSet.add(indicatorId);
|
||||||
|
}
|
||||||
|
setSelectedIndicators(newSet);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePresetSelect = (presetId: string) => {
|
||||||
|
const preset = presets.find((p) => p.id === presetId);
|
||||||
|
if (preset) {
|
||||||
|
setSelectedIndicators(new Set(preset.indicators));
|
||||||
|
setSelectedPreset(presetId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeColor = (type: string): string => {
|
||||||
|
switch (type) {
|
||||||
|
case 'trend':
|
||||||
|
return 'bg-blue-900 text-blue-300';
|
||||||
|
case 'momentum':
|
||||||
|
return 'bg-purple-900 text-purple-300';
|
||||||
|
case 'volatility':
|
||||||
|
return 'bg-orange-900 text-orange-300';
|
||||||
|
case 'level':
|
||||||
|
return 'bg-green-900 text-green-300';
|
||||||
|
case 'volume':
|
||||||
|
return 'bg-pink-900 text-pink-300';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-700 text-gray-300';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Settings className="w-5 h-5 text-blue-500" />
|
||||||
|
Advanced Indicators
|
||||||
|
</h3>
|
||||||
|
<div className="text-center text-gray-400 py-8">Loading indicators...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="card">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Settings className="w-5 h-5 text-blue-500" />
|
||||||
|
Advanced Technical Indicators
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('presets')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||||
|
activeTab === 'presets'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Zap className="w-4 h-4 inline mr-2" />
|
||||||
|
Presets
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('custom')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||||
|
activeTab === 'custom'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Grid3X3 className="w-4 h-4 inline mr-2" />
|
||||||
|
Custom Setup ({selectedIndicators.size})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('guide')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||||
|
activeTab === 'guide'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<BookOpen className="w-4 h-4 inline mr-2" />
|
||||||
|
Quick Guide
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Presets Tab */}
|
||||||
|
{activeTab === 'presets' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{presets.map((preset) => (
|
||||||
|
<div
|
||||||
|
key={preset.id}
|
||||||
|
onClick={() => handlePresetSelect(preset.id)}
|
||||||
|
className={`p-4 rounded-lg border cursor-pointer transition ${
|
||||||
|
selectedPreset === preset.id
|
||||||
|
? 'bg-blue-900 border-blue-500'
|
||||||
|
: 'bg-dark-bg border-dark-border hover:border-blue-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-semibold text-gray-200 mb-1 flex items-center gap-2">
|
||||||
|
{preset.name}
|
||||||
|
{selectedPreset === preset.id && <Check className="w-4 h-4 text-green-500" />}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-400">{preset.description}</p>
|
||||||
|
</div>
|
||||||
|
<span className="bg-blue-900 text-blue-300 text-xs px-2 py-1 rounded">
|
||||||
|
{preset.indicators.length} indicators
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
|
{preset.indicators.map((ind) => (
|
||||||
|
<span key={ind} className="bg-gray-700 text-gray-300 text-xs px-2 py-1 rounded">
|
||||||
|
{ind}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Custom Setup Tab */}
|
||||||
|
{activeTab === 'custom' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{Object.entries(categories).map(([categoryKey, category]) => (
|
||||||
|
<div key={categoryKey} className="bg-dark-bg rounded-lg p-4 border border-dark-border">
|
||||||
|
<h4 className="font-semibold text-gray-200 mb-3 text-sm">
|
||||||
|
{category.name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-gray-400 mb-3">{category.description}</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{category.indicators.map((indicator) => (
|
||||||
|
<label key={indicator.id} className="flex items-start gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedIndicators.has(indicator.id)}
|
||||||
|
onChange={() => handleIndicatorToggle(indicator.id)}
|
||||||
|
className="mt-1 w-4 h-4"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium text-gray-200 text-sm">{indicator.name}</p>
|
||||||
|
<div className="flex gap-2 mt-1 flex-wrap">
|
||||||
|
<span className={`text-xs px-2 py-1 rounded ${getTypeColor(indicator.type)}`}>
|
||||||
|
{indicator.type}
|
||||||
|
</span>
|
||||||
|
{indicator.periods && indicator.default_period && (
|
||||||
|
<span className="text-xs bg-gray-700 text-gray-300 px-2 py-1 rounded">
|
||||||
|
Period: {indicator.default_period}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{selectedIndicators.size > 0 && (
|
||||||
|
<div className="bg-green-900 bg-opacity-20 border border-green-700 rounded-lg p-4">
|
||||||
|
<h4 className="font-semibold text-green-400 mb-2">Configuration Ready</h4>
|
||||||
|
<p className="text-sm text-gray-300 mb-3">
|
||||||
|
You've selected {selectedIndicators.size} indicator(s). Click below to apply.
|
||||||
|
</p>
|
||||||
|
<button className="w-full bg-green-600 hover:bg-green-700 text-white font-medium py-2 rounded-lg transition">
|
||||||
|
<Check className="w-4 h-4 inline mr-2" />
|
||||||
|
Apply Configuration
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Quick Guide Tab */}
|
||||||
|
{activeTab === 'guide' && cheatSheet && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{Object.entries(cheatSheet).map(([key, value]) => (
|
||||||
|
<div key={key} className="bg-dark-bg rounded-lg p-4 border border-dark-border">
|
||||||
|
<h4 className="font-semibold text-gray-200 mb-3 capitalize">
|
||||||
|
{key.replace(/_/g, ' ')}
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{typeof value === 'object' &&
|
||||||
|
!Array.isArray(value) &&
|
||||||
|
Object.entries(value).map(([subKey, subValue]) => (
|
||||||
|
<div key={subKey} className="text-sm">
|
||||||
|
<p className="font-medium text-blue-400">{subKey}</p>
|
||||||
|
<p className="text-gray-400 text-xs mt-1">{String(subValue)}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selected Indicators Summary */}
|
||||||
|
{selectedIndicators.size > 0 && activeTab !== 'guide' && (
|
||||||
|
<div className="card bg-blue-900 bg-opacity-20 border border-blue-700">
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-blue-400">Currently Selected</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Array.from(selectedIndicators).map((ind) => (
|
||||||
|
<div
|
||||||
|
key={ind}
|
||||||
|
className="bg-blue-900 border border-blue-700 text-blue-300 text-sm px-3 py-2 rounded-lg flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{ind}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const newSet = new Set(selectedIndicators);
|
||||||
|
newSet.delete(ind);
|
||||||
|
setSelectedIndicators(newSet);
|
||||||
|
}}
|
||||||
|
className="text-blue-400 hover:text-blue-200 font-bold"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Indicator Types Legend */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="font-semibold text-gray-300 mb-3 text-sm">Indicator Types</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-blue-600 rounded-full"></div>
|
||||||
|
<span className="text-xs text-gray-400">Trend</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-purple-600 rounded-full"></div>
|
||||||
|
<span className="text-xs text-gray-400">Momentum</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-orange-600 rounded-full"></div>
|
||||||
|
<span className="text-xs text-gray-400">Volatility</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-green-600 rounded-full"></div>
|
||||||
|
<span className="text-xs text-gray-400">Support/Resistance</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-pink-600 rounded-full"></div>
|
||||||
|
<span className="text-xs text-gray-400">Volume</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Best Practices */}
|
||||||
|
<div className="card bg-yellow-900 bg-opacity-20 border border-yellow-700">
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-yellow-400">Pro Tips</h3>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-300">
|
||||||
|
<li>✓ Use 2-3 indicators maximum to avoid signal conflicts</li>
|
||||||
|
<li>✓ Combine different indicator types (trend + momentum + volatility)</li>
|
||||||
|
<li>✓ Scalping: Use fast periods (5, 10, 14)</li>
|
||||||
|
<li>✓ Swing/Position: Use standard periods (20, 50, 200)</li>
|
||||||
|
<li>✓ Always confirm signals with price action and volume</li>
|
||||||
|
<li>✓ Use presets as a starting point, customize based on your style</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { BarChart3, TrendingUp, Calendar, RefreshCw } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import PerformanceHistoryChart from './PerformanceHistoryChart';
|
||||||
|
import EquityCurveChart from './EquityCurveChart';
|
||||||
|
import TradePatternAnalyzer from './TradePatternAnalyzer';
|
||||||
|
import LessonsPanel from './LessonsPanel';
|
||||||
|
|
||||||
|
interface DashboardStats {
|
||||||
|
period: string;
|
||||||
|
snapshot_count: number;
|
||||||
|
performance: {
|
||||||
|
total_pnl: number;
|
||||||
|
avg_daily_pnl: number;
|
||||||
|
total_trades: number;
|
||||||
|
winning_days: number;
|
||||||
|
losing_days: number;
|
||||||
|
avg_win_rate: number;
|
||||||
|
best_day: number;
|
||||||
|
worst_day: number;
|
||||||
|
};
|
||||||
|
top_patterns: Array<{
|
||||||
|
name: string;
|
||||||
|
confidence: number;
|
||||||
|
win_rate: number;
|
||||||
|
samples: number;
|
||||||
|
}>;
|
||||||
|
recent_lessons: Array<{
|
||||||
|
category: string;
|
||||||
|
lesson: string;
|
||||||
|
importance: string;
|
||||||
|
date: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnalyticsDashboard() {
|
||||||
|
const [period, setPeriod] = useState<'week' | 'month' | 'quarter' | 'year'>('month');
|
||||||
|
const [dashboardData, setDashboardData] = useState<DashboardStats | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [lastUpdated, setLastUpdated] = useState<string>('');
|
||||||
|
|
||||||
|
// Fetch dashboard data
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDashboard = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.get('/api/analytics/dashboard', {
|
||||||
|
params: { period },
|
||||||
|
});
|
||||||
|
setDashboardData(response.data || null);
|
||||||
|
setLastUpdated(new Date().toLocaleTimeString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching analytics dashboard:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDashboard();
|
||||||
|
}, [period]);
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading && !dashboardData) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-7 h-7 text-blue-500" />
|
||||||
|
Analytics Dashboard
|
||||||
|
</h2>
|
||||||
|
<div className="text-center text-gray-400 py-12">Loading dashboard...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-7 h-7 text-blue-500" />
|
||||||
|
Advanced Analytics Dashboard
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
{lastUpdated && (
|
||||||
|
<span className="text-xs text-gray-400">Updated: {lastUpdated}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Period Selector */}
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
{(['week', 'month', 'quarter', 'year'] as const).map((p) => (
|
||||||
|
<button
|
||||||
|
key={p}
|
||||||
|
onClick={() => setPeriod(p)}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||||
|
period === p
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Calendar className="w-4 h-4 inline mr-2" />
|
||||||
|
{p.charAt(0).toUpperCase() + p.slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Overview Stats */}
|
||||||
|
{dashboardData && (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Total P&L</p>
|
||||||
|
<p
|
||||||
|
className={`text-xl font-bold ${
|
||||||
|
dashboardData.performance.total_pnl >= 0
|
||||||
|
? 'text-green-500'
|
||||||
|
: 'text-red-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
${dashboardData.performance.total_pnl.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Avg Daily P&L</p>
|
||||||
|
<p
|
||||||
|
className={`text-xl font-bold ${
|
||||||
|
dashboardData.performance.avg_daily_pnl >= 0
|
||||||
|
? 'text-green-500'
|
||||||
|
: 'text-red-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
${dashboardData.performance.avg_daily_pnl.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Win Rate</p>
|
||||||
|
<p className="text-xl font-bold text-blue-500">
|
||||||
|
{dashboardData.performance.avg_win_rate.toFixed(1)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Trading Days</p>
|
||||||
|
<p className="text-xl font-bold text-purple-500">
|
||||||
|
{dashboardData.snapshot_count}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Secondary Stats */}
|
||||||
|
{dashboardData && (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-2 text-sm">
|
||||||
|
<div className="bg-dark-bg rounded p-2">
|
||||||
|
<p className="text-xs text-gray-400">Total Trades</p>
|
||||||
|
<p className="font-bold text-gray-200">{dashboardData.performance.total_trades}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-dark-bg rounded p-2">
|
||||||
|
<p className="text-xs text-gray-400">Win Days</p>
|
||||||
|
<p className="font-bold text-green-500">{dashboardData.performance.winning_days}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-dark-bg rounded p-2">
|
||||||
|
<p className="text-xs text-gray-400">Loss Days</p>
|
||||||
|
<p className="font-bold text-red-500">{dashboardData.performance.losing_days}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-dark-bg rounded p-2">
|
||||||
|
<p className="text-xs text-gray-400">Best Day</p>
|
||||||
|
<p className="font-bold text-green-500">${dashboardData.performance.best_day.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-dark-bg rounded p-2">
|
||||||
|
<p className="text-xs text-gray-400">Worst Day</p>
|
||||||
|
<p className="font-bold text-red-500">${dashboardData.performance.worst_day.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Charts Section */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<PerformanceHistoryChart period={period} />
|
||||||
|
<EquityCurveChart period={period} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pattern Analyzer */}
|
||||||
|
<TradePatternAnalyzer />
|
||||||
|
|
||||||
|
{/* Lessons Panel */}
|
||||||
|
<LessonsPanel />
|
||||||
|
|
||||||
|
{/* Recent Summary */}
|
||||||
|
{dashboardData && (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-blue-500" />
|
||||||
|
Recent Insights Summary
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Top Patterns */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-gray-300 mb-3 text-sm">Top Trade Patterns</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{dashboardData.top_patterns.length > 0 ? (
|
||||||
|
dashboardData.top_patterns.map((pattern, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-dark-bg rounded-lg p-2 flex items-center justify-between text-sm"
|
||||||
|
>
|
||||||
|
<span className="text-gray-300 truncate">{pattern.name}</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<span className="bg-blue-900 text-blue-300 px-2 py-1 rounded text-xs">
|
||||||
|
{pattern.confidence.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
<span className="bg-green-900 text-green-300 px-2 py-1 rounded text-xs">
|
||||||
|
{pattern.win_rate.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-400 text-sm">No patterns identified yet</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Lessons */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-gray-300 mb-3 text-sm">Recent Lessons</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{dashboardData.recent_lessons.length > 0 ? (
|
||||||
|
dashboardData.recent_lessons.map((lesson, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-dark-bg rounded-lg p-2 text-sm"
|
||||||
|
>
|
||||||
|
<p className="text-gray-300 text-xs mb-1">{lesson.lesson.substring(0, 60)}...</p>
|
||||||
|
<div className="flex items-center gap-1 flex-wrap">
|
||||||
|
<span className="bg-purple-900 text-purple-300 text-xs px-1.5 py-0.5 rounded">
|
||||||
|
{lesson.category}
|
||||||
|
</span>
|
||||||
|
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
||||||
|
lesson.importance === 'high'
|
||||||
|
? 'bg-red-900 text-red-300'
|
||||||
|
: 'bg-yellow-900 text-yellow-300'
|
||||||
|
}`}>
|
||||||
|
{lesson.importance}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-500 text-xs ml-auto">
|
||||||
|
{new Date(lesson.date).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-400 text-sm">No lessons logged yet</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Calendar, AlertTriangle, TrendingUp, Clock, Globe } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface EconomicEvent {
|
||||||
|
id: number;
|
||||||
|
country: string;
|
||||||
|
indicator: string;
|
||||||
|
event_date: string;
|
||||||
|
time: string;
|
||||||
|
impact: 'high' | 'medium' | 'low';
|
||||||
|
forecast: string;
|
||||||
|
previous: string;
|
||||||
|
actual: string | null;
|
||||||
|
description: string;
|
||||||
|
importance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventStats {
|
||||||
|
total_events_30_days: number;
|
||||||
|
total_events_7_days: number;
|
||||||
|
high_impact_events: number;
|
||||||
|
total_countries: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EconomicCalendar() {
|
||||||
|
const [events, setEvents] = useState<EconomicEvent[]>([]);
|
||||||
|
const [highImpactEvents, setHighImpactEvents] = useState<EconomicEvent[]>([]);
|
||||||
|
const [stats, setStats] = useState<EventStats | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeTab, setActiveTab] = useState<'upcoming' | 'today' | 'high-impact'>('upcoming');
|
||||||
|
const [selectedCountry, setSelectedCountry] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Fetch events and stats
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Fetch upcoming events
|
||||||
|
const upcomingResponse = await axios.get('/api/economic-calendar/events', {
|
||||||
|
params: { days_ahead: 30, sort_by: 'date' },
|
||||||
|
});
|
||||||
|
setEvents(upcomingResponse.data.events || []);
|
||||||
|
|
||||||
|
// Fetch high impact events
|
||||||
|
const highImpactResponse = await axios.get('/api/economic-calendar/high-impact');
|
||||||
|
setHighImpactEvents(highImpactResponse.data.events || []);
|
||||||
|
|
||||||
|
// Fetch stats
|
||||||
|
const statsResponse = await axios.get('/api/economic-calendar/stats');
|
||||||
|
setStats(statsResponse.data.summary || null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching economic calendar data:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getImpactColor = (impact: string): string => {
|
||||||
|
switch (impact) {
|
||||||
|
case 'high':
|
||||||
|
return 'bg-red-900 text-red-300 border-red-700';
|
||||||
|
case 'medium':
|
||||||
|
return 'bg-yellow-900 text-yellow-300 border-yellow-700';
|
||||||
|
default:
|
||||||
|
return 'bg-blue-900 text-blue-300 border-blue-700';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImpactBorder = (impact: string): string => {
|
||||||
|
switch (impact) {
|
||||||
|
case 'high':
|
||||||
|
return 'border-l-4 border-red-500';
|
||||||
|
case 'medium':
|
||||||
|
return 'border-l-4 border-yellow-500';
|
||||||
|
default:
|
||||||
|
return 'border-l-4 border-blue-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (isoDate: string, time: string): string => {
|
||||||
|
const date = new Date(isoDate);
|
||||||
|
return `${date.toLocaleDateString()} at ${time}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayedEvents = activeTab === 'high-impact' ? highImpactEvents : events;
|
||||||
|
const filteredEvents = selectedCountry
|
||||||
|
? displayedEvents.filter((e) => e.country === selectedCountry)
|
||||||
|
: displayedEvents;
|
||||||
|
|
||||||
|
const countries = Array.from(new Set(events.map((e) => e.country))).sort();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Calendar className="w-5 h-5 text-blue-500" />
|
||||||
|
Economic Calendar
|
||||||
|
</h3>
|
||||||
|
<div className="text-center text-gray-400 py-8">Loading calendar...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Stats Overview */}
|
||||||
|
{stats && (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-blue-500" />
|
||||||
|
Calendar Overview (Next 30 Days)
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Total Events</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-500">{stats.total_events_30_days}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">High Impact</p>
|
||||||
|
<p className="text-2xl font-bold text-red-500">{stats.high_impact_events}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Next 7 Days</p>
|
||||||
|
<p className="text-2xl font-bold text-yellow-500">{stats.total_events_7_days}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Countries</p>
|
||||||
|
<p className="text-2xl font-bold text-green-500">{stats.total_countries}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Calendar */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
<Calendar className="w-5 h-5 text-blue-500" />
|
||||||
|
Economic Events
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('upcoming')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||||
|
activeTab === 'upcoming'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Upcoming ({events.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('high-impact')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||||
|
activeTab === 'high-impact'
|
||||||
|
? 'bg-red-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<AlertTriangle className="w-4 h-4 inline mr-2" />
|
||||||
|
High Impact ({highImpactEvents.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Country Filter */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="text-sm text-gray-400 mb-2 block">Filter by Country:</label>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedCountry(null)}
|
||||||
|
className={`px-3 py-1 rounded text-sm transition ${
|
||||||
|
selectedCountry === null
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
All Countries
|
||||||
|
</button>
|
||||||
|
{countries.map((country) => (
|
||||||
|
<button
|
||||||
|
key={country}
|
||||||
|
onClick={() => setSelectedCountry(selectedCountry === country ? null : country)}
|
||||||
|
className={`px-3 py-1 rounded text-sm transition ${
|
||||||
|
selectedCountry === country
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{country}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Events List */}
|
||||||
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||||
|
{filteredEvents.length > 0 ? (
|
||||||
|
filteredEvents.map((event) => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className={`bg-dark-bg rounded-lg p-4 border ${getImpactBorder(event.impact)}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h4 className="font-semibold text-gray-200">{event.indicator}</h4>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-1 rounded border ${getImpactColor(event.impact)}`}
|
||||||
|
>
|
||||||
|
{event.impact.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs bg-gray-700 text-gray-300 px-2 py-1 rounded">
|
||||||
|
<Globe className="w-3 h-3 inline mr-1" />
|
||||||
|
{event.country}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-400 mb-2">{event.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3 mb-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 mb-1">Forecast</p>
|
||||||
|
<p className="font-semibold text-blue-400">{event.forecast}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 mb-1">Previous</p>
|
||||||
|
<p className="font-semibold text-gray-300">{event.previous}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 mb-1">Actual</p>
|
||||||
|
<p className={`font-semibold ${event.actual ? 'text-green-400' : 'text-gray-500'}`}>
|
||||||
|
{event.actual || 'Pending'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 text-xs text-gray-400 pt-2 border-t border-dark-border">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
<span>{formatDateTime(event.event_date, event.time)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-gray-400 py-4">No events found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trading Tips */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-yellow-500" />
|
||||||
|
Gold Trading Tips During Economic Events
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-red-900 bg-opacity-20 border border-red-800 rounded-lg p-3">
|
||||||
|
<p className="text-sm text-red-400 font-semibold mb-1">🔴 High-Impact Events</p>
|
||||||
|
<p className="text-xs text-gray-300">
|
||||||
|
Gold typically moves 100-200 pips. Set wider stops and consider doubling
|
||||||
|
position size around event release time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-yellow-900 bg-opacity-20 border border-yellow-800 rounded-lg p-3">
|
||||||
|
<p className="text-sm text-yellow-400 font-semibold mb-1">🟡 Medium-Impact Events</p>
|
||||||
|
<p className="text-xs text-gray-300">
|
||||||
|
Gold typically moves 50-100 pips. Wait 5-30 mins after release before
|
||||||
|
trading to let volatility settle.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-900 bg-opacity-20 border border-blue-800 rounded-lg p-3">
|
||||||
|
<p className="text-sm text-blue-400 font-semibold mb-1">🔵 Key Correlations</p>
|
||||||
|
<p className="text-xs text-gray-300">
|
||||||
|
Strong USD weakens gold (inverse). High rates reduce gold appeal. Watch
|
||||||
|
DXY and bond yields for context.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-green-900 bg-opacity-20 border border-green-800 rounded-lg p-3">
|
||||||
|
<p className="text-sm text-green-400 font-semibold mb-1">✅ Best Practice</p>
|
||||||
|
<p className="text-xs text-gray-300">
|
||||||
|
Always check: 1) Event importance 2) USD impact 3) Historical volatility
|
||||||
|
4) Current gold sentiment before trading.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { createChart, IChartApi, ISeriesApi } from 'lightweight-charts';
|
||||||
|
import { TrendingUp, TrendingDown } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface PerformanceSnapshot {
|
||||||
|
snapshot_date: string;
|
||||||
|
cumulative_pnl: number;
|
||||||
|
portfolio_value: number;
|
||||||
|
equity_curve: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EquityCurveChartProps {
|
||||||
|
period?: 'week' | 'month' | 'quarter' | 'year';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EquityCurveChart({
|
||||||
|
period = 'month',
|
||||||
|
}: EquityCurveChartProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const chartRef = useRef<IChartApi | null>(null);
|
||||||
|
const seriesRef = useRef<ISeriesApi<'Line'> | null>(null);
|
||||||
|
const [snapshots, setSnapshots] = useState<PerformanceSnapshot[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
currentEquity: 0,
|
||||||
|
maxEquity: 0,
|
||||||
|
minEquity: 0,
|
||||||
|
totalGain: 0,
|
||||||
|
gainPercent: 0,
|
||||||
|
drawdown: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch performance snapshots for equity curve
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSnapshots = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.get('/api/analytics/snapshots', {
|
||||||
|
params: { limit: period === 'week' ? 7 : period === 'month' ? 30 : period === 'quarter' ? 90 : 365 },
|
||||||
|
});
|
||||||
|
const data = response.data || [];
|
||||||
|
setSnapshots(data);
|
||||||
|
|
||||||
|
// Calculate stats
|
||||||
|
if (data.length > 0) {
|
||||||
|
// Assume initial portfolio value was 100000
|
||||||
|
const initialValue = 100000;
|
||||||
|
const currentEquity = initialValue + (data[data.length - 1]?.cumulative_pnl || 0);
|
||||||
|
const equityValues = data.map(
|
||||||
|
(s: PerformanceSnapshot) => initialValue + s.cumulative_pnl
|
||||||
|
);
|
||||||
|
const maxEquity = Math.max(...equityValues);
|
||||||
|
const minEquity = Math.min(...equityValues);
|
||||||
|
const totalGain = currentEquity - initialValue;
|
||||||
|
const gainPercent = (totalGain / initialValue) * 100;
|
||||||
|
const drawdown = ((maxEquity - currentEquity) / maxEquity) * 100;
|
||||||
|
|
||||||
|
setStats({
|
||||||
|
currentEquity,
|
||||||
|
maxEquity,
|
||||||
|
minEquity,
|
||||||
|
totalGain,
|
||||||
|
gainPercent,
|
||||||
|
drawdown,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching equity curve data:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSnapshots();
|
||||||
|
}, [period]);
|
||||||
|
|
||||||
|
// Initialize chart and update data
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || snapshots.length === 0) return;
|
||||||
|
|
||||||
|
// Initialize chart if not already done
|
||||||
|
if (!chartRef.current) {
|
||||||
|
const chart = createChart(containerRef.current, {
|
||||||
|
layout: { background: { color: '#0f172a' }, textColor: '#e2e8f0' },
|
||||||
|
grid: { vertLines: { color: '#1f2937' }, horzLines: { color: '#1f2937' } },
|
||||||
|
rightPriceScale: { borderColor: '#1f2937' },
|
||||||
|
timeScale: { borderColor: '#1f2937', timeVisible: true, secondsVisible: false },
|
||||||
|
height: 350,
|
||||||
|
width: containerRef.current.clientWidth,
|
||||||
|
});
|
||||||
|
chartRef.current = chart;
|
||||||
|
|
||||||
|
const series = chart.addLineSeries({
|
||||||
|
color: '#22c55e',
|
||||||
|
lineWidth: 2,
|
||||||
|
});
|
||||||
|
seriesRef.current = series;
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
if (!containerRef.current || !chartRef.current) return;
|
||||||
|
chartRef.current.applyOptions({ width: containerRef.current.clientWidth });
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
chart.remove();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update chart data
|
||||||
|
if (seriesRef.current) {
|
||||||
|
const initialValue = 100000;
|
||||||
|
const chartData = snapshots.map((snapshot) => {
|
||||||
|
const [year, month, day] = snapshot.snapshot_date.split('-');
|
||||||
|
const equity = initialValue + snapshot.cumulative_pnl;
|
||||||
|
return {
|
||||||
|
time: `${year}-${month}-${day}` as any,
|
||||||
|
value: equity,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
seriesRef.current.setData(chartData);
|
||||||
|
chartRef.current?.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
}, [snapshots]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-green-500" />
|
||||||
|
Equity Curve
|
||||||
|
</h3>
|
||||||
|
<div className="text-center text-gray-400 py-8">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||||
|
<TrendingUp className="w-5 h-5 text-green-500" />
|
||||||
|
Equity Curve ({period})
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mb-4">
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Current Equity</span>
|
||||||
|
<p className="text-lg font-bold text-blue-500">
|
||||||
|
${stats.currentEquity.toFixed(0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Total Gain</span>
|
||||||
|
<p className={`text-lg font-bold ${stats.totalGain >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||||
|
${stats.totalGain.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Return %</span>
|
||||||
|
<p className={`text-lg font-bold ${stats.gainPercent >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||||
|
{stats.gainPercent.toFixed(2)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Peak Equity</span>
|
||||||
|
<p className="text-lg font-bold text-green-500">
|
||||||
|
${stats.maxEquity.toFixed(0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Low Equity</span>
|
||||||
|
<p className="text-lg font-bold text-red-500">
|
||||||
|
${stats.minEquity.toFixed(0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Max Drawdown</span>
|
||||||
|
<p className="text-lg font-bold text-orange-500">
|
||||||
|
{stats.drawdown.toFixed(2)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{snapshots.length > 0 ? (
|
||||||
|
<div ref={containerRef} style={{ width: '100%', height: '350px' }} />
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-gray-400 py-8">
|
||||||
|
<TrendingDown className="w-12 h-12 mx-auto mb-2 text-gray-600" />
|
||||||
|
<p>No equity data available for this period</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { BookOpen, AlertCircle, Lightbulb, Trash2, Edit2 } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface Lesson {
|
||||||
|
id: number;
|
||||||
|
lesson_text: string;
|
||||||
|
category: string;
|
||||||
|
importance: string;
|
||||||
|
impact: string;
|
||||||
|
tags: string[];
|
||||||
|
date_learned: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecurringMistake {
|
||||||
|
tag: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LessonsPanel() {
|
||||||
|
const [lessons, setLessons] = useState<Lesson[]>([]);
|
||||||
|
const [recurringMistakes, setRecurringMistakes] = useState<RecurringMistake[]>([]);
|
||||||
|
const [categories, setCategories] = useState<string[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||||
|
const [selectedImportance, setSelectedImportance] = useState<string | null>(null);
|
||||||
|
const [newLesson, setNewLesson] = useState('');
|
||||||
|
const [newCategory, setNewCategory] = useState('entry');
|
||||||
|
const [newImportance, setNewImportance] = useState('medium');
|
||||||
|
|
||||||
|
// Fetch lessons and categories
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchLessons = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Fetch lessons
|
||||||
|
let lessonsUrl = '/api/analytics/lessons?limit=20';
|
||||||
|
if (selectedCategory) lessonsUrl += `&category=${selectedCategory}`;
|
||||||
|
if (selectedImportance) lessonsUrl += `&importance=${selectedImportance}`;
|
||||||
|
|
||||||
|
const lessonsResponse = await axios.get(lessonsUrl);
|
||||||
|
setLessons(lessonsResponse.data || []);
|
||||||
|
|
||||||
|
// Fetch categories if not loaded
|
||||||
|
if (categories.length === 0) {
|
||||||
|
const categoriesResponse = await axios.get('/api/analytics/lessons/categories');
|
||||||
|
setCategories(categoriesResponse.data?.available || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch recurring mistakes
|
||||||
|
const mistakesResponse = await axios.get('/api/analytics/lessons/recurring-mistakes?limit=5');
|
||||||
|
setRecurringMistakes(
|
||||||
|
mistakesResponse.data?.recurring_mistakes?.map(([tag, count]: [string, number]) => ({ tag, count })) || []
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching lessons:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLessons();
|
||||||
|
}, [selectedCategory, selectedImportance]);
|
||||||
|
|
||||||
|
const handleAddLesson = async () => {
|
||||||
|
if (!newLesson.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post('/api/analytics/lessons', {
|
||||||
|
lesson_text: newLesson,
|
||||||
|
category: newCategory,
|
||||||
|
importance: newImportance,
|
||||||
|
date_learned: new Date().toISOString().split('T')[0],
|
||||||
|
});
|
||||||
|
setNewLesson('');
|
||||||
|
// Refresh lessons
|
||||||
|
const lessonsResponse = await axios.get('/api/analytics/lessons?limit=20');
|
||||||
|
setLessons(lessonsResponse.data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding lesson:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImportanceColor = (importance: string): string => {
|
||||||
|
switch (importance) {
|
||||||
|
case 'critical':
|
||||||
|
return 'bg-red-900 text-red-300';
|
||||||
|
case 'high':
|
||||||
|
return 'bg-orange-900 text-orange-300';
|
||||||
|
case 'medium':
|
||||||
|
return 'bg-yellow-900 text-yellow-300';
|
||||||
|
default:
|
||||||
|
return 'bg-blue-900 text-blue-300';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImpactColor = (impact: string): string => {
|
||||||
|
if (impact === 'positive') return 'text-green-500';
|
||||||
|
if (impact === 'negative') return 'text-red-500';
|
||||||
|
return 'text-gray-400';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<BookOpen className="w-5 h-5 text-blue-500" />
|
||||||
|
Lessons Learned
|
||||||
|
</h3>
|
||||||
|
<div className="text-center text-gray-400 py-8">Loading lessons...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Add New Lesson */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Lightbulb className="w-5 h-5 text-yellow-500" />
|
||||||
|
Log New Lesson
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<textarea
|
||||||
|
value={newLesson}
|
||||||
|
onChange={(e) => setNewLesson(e.target.value)}
|
||||||
|
placeholder="Describe the lesson you learned..."
|
||||||
|
className="w-full bg-dark-bg text-gray-200 border border-dark-border rounded-lg p-3 text-sm focus:border-blue-500 outline-none"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">Category</label>
|
||||||
|
<select
|
||||||
|
value={newCategory}
|
||||||
|
onChange={(e) => setNewCategory(e.target.value)}
|
||||||
|
className="w-full bg-dark-bg text-gray-200 border border-dark-border rounded p-2 text-sm"
|
||||||
|
>
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<option key={cat} value={cat}>
|
||||||
|
{cat}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">Importance</label>
|
||||||
|
<select
|
||||||
|
value={newImportance}
|
||||||
|
onChange={(e) => setNewImportance(e.target.value)}
|
||||||
|
className="w-full bg-dark-bg text-gray-200 border border-dark-border rounded p-2 text-sm"
|
||||||
|
>
|
||||||
|
<option value="low">Low</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
<option value="critical">Critical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-end">
|
||||||
|
<button
|
||||||
|
onClick={handleAddLesson}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 rounded transition text-sm"
|
||||||
|
>
|
||||||
|
Save Lesson
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recurring Mistakes */}
|
||||||
|
{recurringMistakes.length > 0 && (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-500" />
|
||||||
|
Recurring Mistakes
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{recurringMistakes.map((mistake, idx) => (
|
||||||
|
<div key={idx} className="flex items-center justify-between bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="font-medium text-gray-200">{mistake.tag}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="bg-red-900 text-red-300 text-xs px-3 py-1 rounded font-bold">
|
||||||
|
{mistake.count}x
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400">occurrences</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-400 mt-3 p-2 bg-dark-bg rounded">
|
||||||
|
💡 Focus on preventing these recurring mistakes to improve your trading performance
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filter Controls */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<BookOpen className="w-5 h-5 text-blue-500" />
|
||||||
|
Lessons Learned ({lessons.length})
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex gap-2 mb-4 flex-wrap">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedCategory(null)}
|
||||||
|
className={`px-3 py-1 rounded text-sm transition ${
|
||||||
|
selectedCategory === null
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
All Categories
|
||||||
|
</button>
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
onClick={() => setSelectedCategory(selectedCategory === cat ? null : cat)}
|
||||||
|
className={`px-3 py-1 rounded text-sm transition ${
|
||||||
|
selectedCategory === cat
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-dark-bg text-gray-400 hover:text-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lessons List */}
|
||||||
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||||
|
{lessons.length > 0 ? (
|
||||||
|
lessons.map((lesson) => (
|
||||||
|
<div key={lesson.id} className="bg-dark-bg rounded-lg p-4 border border-dark-border">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<p className="flex-1 text-gray-200 text-sm">{lesson.lesson_text}</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button className="text-gray-400 hover:text-blue-400 transition">
|
||||||
|
<Edit2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button className="text-gray-400 hover:text-red-400 transition">
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 flex-wrap mt-2">
|
||||||
|
<span className={`text-xs px-2 py-1 rounded ${getImportanceColor(lesson.importance)}`}>
|
||||||
|
{lesson.importance}
|
||||||
|
</span>
|
||||||
|
<span className={`text-xs px-2 py-1 rounded ${lesson.category === 'entry' ? 'bg-blue-900 text-blue-300' : 'bg-purple-900 text-purple-300'}`}>
|
||||||
|
{lesson.category}
|
||||||
|
</span>
|
||||||
|
<span className={`text-xs px-2 py-1 rounded ${getImpactColor(lesson.impact).replace('text-', 'bg-').replace('500', '900')} text-${getImpactColor(lesson.impact).split('-')[1]}-300`}>
|
||||||
|
{lesson.impact}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">{new Date(lesson.date_learned).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{lesson.tags.length > 0 && (
|
||||||
|
<div className="flex gap-1 mt-2 flex-wrap">
|
||||||
|
{lesson.tags.map((tag, idx) => (
|
||||||
|
<span key={idx} className="bg-gray-700 text-gray-300 text-xs px-2 py-1 rounded">
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-gray-400 py-4">No lessons found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Brain, TrendingUp, Zap, BarChart3, Target } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface TradeCluster {
|
||||||
|
cluster_id: number;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
avg_win_rate: number;
|
||||||
|
avg_profit: number;
|
||||||
|
confidence: number;
|
||||||
|
characteristics: Record<string, string | number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MLPatternRecognition() {
|
||||||
|
const [clusters, setClusters] = useState<TradeCluster[]>([]);
|
||||||
|
const [selectedCluster, setSelectedCluster] = useState<TradeCluster | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [sortBy, setSortBy] = useState<'win_rate' | 'profit' | 'confidence' | 'size'>('win_rate');
|
||||||
|
const [clusterStats, setClusterStats] = useState<any>(null);
|
||||||
|
|
||||||
|
// Fetch clusters
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchClusters = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.get('/api/ml-patterns/clusters', {
|
||||||
|
params: { sort_by: sortBy },
|
||||||
|
});
|
||||||
|
setClusters(response.data.clusters || []);
|
||||||
|
|
||||||
|
// Fetch model stats
|
||||||
|
const statsResponse = await axios.get('/api/ml-patterns/model-stats');
|
||||||
|
setClusterStats(statsResponse.data || {});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching ML patterns:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchClusters();
|
||||||
|
}, [sortBy]);
|
||||||
|
|
||||||
|
const getQualityBadge = (winRate: number, confidence: number): { text: string; color: string } => {
|
||||||
|
const score = winRate * confidence / 100;
|
||||||
|
if (score >= 70) return { text: 'EXCELLENT', color: 'bg-green-900 text-green-300' };
|
||||||
|
if (score >= 55) return { text: 'GOOD', color: 'bg-blue-900 text-blue-300' };
|
||||||
|
if (score >= 40) return { text: 'FAIR', color: 'bg-yellow-900 text-yellow-300' };
|
||||||
|
return { text: 'POOR', color: 'bg-red-900 text-red-300' };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Brain className="w-5 h-5 text-blue-500" />
|
||||||
|
ML Pattern Recognition
|
||||||
|
</h3>
|
||||||
|
<div className="text-center text-gray-400 py-8">Training ML model...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Model Stats */}
|
||||||
|
{clusterStats.performance && (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Brain className="w-5 h-5 text-purple-500" />
|
||||||
|
ML Model Performance
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Clusters Found</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-500">{clusterStats.performance.clusters_discovered}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Trades Analyzed</p>
|
||||||
|
<p className="text-2xl font-bold text-green-500">{clusterStats.performance.total_trades_analyzed}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Model Accuracy</p>
|
||||||
|
<p className="text-2xl font-bold text-purple-500">
|
||||||
|
{(clusterStats.performance.average_cluster_accuracy * 100).toFixed(0)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Version</p>
|
||||||
|
<p className="text-lg font-bold text-gray-300">{clusterStats.model_info?.version}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{clusterStats.model_info && (
|
||||||
|
<div className="mt-3 text-xs text-gray-400 pt-3 border-t border-dark-border">
|
||||||
|
<p>Last Updated: {clusterStats.model_info.last_updated}</p>
|
||||||
|
<p>Next Retraining: {clusterStats.next_model_retraining}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cluster List */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
<Target className="w-5 h-5 text-green-500" />
|
||||||
|
Discovered Trade Clusters ({clusters.length})
|
||||||
|
</h3>
|
||||||
|
<select
|
||||||
|
value={sortBy}
|
||||||
|
onChange={(e) => setSortBy(e.target.value as any)}
|
||||||
|
className="bg-dark-bg text-gray-200 border border-dark-border rounded px-3 py-1 text-sm"
|
||||||
|
>
|
||||||
|
<option value="win_rate">Sort by Win Rate</option>
|
||||||
|
<option value="profit">Sort by Profit</option>
|
||||||
|
<option value="confidence">Sort by Confidence</option>
|
||||||
|
<option value="size">Sort by Size</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{clusters.map((cluster) => {
|
||||||
|
const quality = getQualityBadge(cluster.avg_win_rate, cluster.confidence);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={cluster.cluster_id}
|
||||||
|
onClick={() => setSelectedCluster(selectedCluster?.cluster_id === cluster.cluster_id ? null : cluster)}
|
||||||
|
className="bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-blue-500 cursor-pointer transition"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-semibold text-gray-200 mb-1">{cluster.name}</h4>
|
||||||
|
<p className="text-xs text-gray-400 mb-2">Sample Size: {cluster.size} trades</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className={`text-xs font-bold px-2 py-1 rounded ${quality.color}`}>
|
||||||
|
{quality.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-sm mb-3">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400 text-xs">Win Rate</span>
|
||||||
|
<p className="font-bold text-green-400">{cluster.avg_win_rate.toFixed(1)}%</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400 text-xs">Avg Profit</span>
|
||||||
|
<p className="font-bold text-blue-400">${cluster.avg_profit.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400 text-xs">Confidence</span>
|
||||||
|
<p className="font-bold text-purple-400">{(cluster.confidence * 100).toFixed(0)}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedCluster?.cluster_id === cluster.cluster_id && (
|
||||||
|
<div className="mt-3 pt-3 border-t border-dark-border text-sm">
|
||||||
|
<h5 className="font-semibold text-gray-200 mb-2">Pattern Characteristics:</h5>
|
||||||
|
<div className="space-y-1 text-xs">
|
||||||
|
{Object.entries(cluster.characteristics).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex justify-between text-gray-300">
|
||||||
|
<span className="text-gray-400 capitalize">{key.replace(/_/g, ' ')}:</span>
|
||||||
|
<span className="font-medium">{String(value)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ML Insights */}
|
||||||
|
<div className="card bg-blue-900 bg-opacity-20 border border-blue-700">
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-blue-400 flex items-center gap-2">
|
||||||
|
<Zap className="w-5 h-5" />
|
||||||
|
ML Insights
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-300">
|
||||||
|
<li>✓ Machine learning identified {clusters.length} distinct trading patterns</li>
|
||||||
|
<li>✓ Best pattern: {clusters[0]?.name} with {clusters[0]?.avg_win_rate.toFixed(1)}% win rate</li>
|
||||||
|
<li>✓ Model trained on {clusterStats.performance?.total_trades_analyzed} trades</li>
|
||||||
|
<li>✓ Average model accuracy: {(clusterStats.performance?.average_cluster_accuracy * 100).toFixed(0)}%</li>
|
||||||
|
<li>✓ Use these patterns to improve trading discipline and consistency</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { createChart, IChartApi, ISeriesApi } from 'lightweight-charts';
|
||||||
|
import { TrendingUp, TrendingDown, Calendar } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface PerformanceSnapshot {
|
||||||
|
snapshot_date: string;
|
||||||
|
daily_pnl: number;
|
||||||
|
daily_pnl_percent: number;
|
||||||
|
win_rate: number;
|
||||||
|
total_trades: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PerformanceHistoryChartProps {
|
||||||
|
period?: 'week' | 'month' | 'quarter' | 'year';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PerformanceHistoryChart({
|
||||||
|
period = 'month',
|
||||||
|
}: PerformanceHistoryChartProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const chartRef = useRef<IChartApi | null>(null);
|
||||||
|
const seriesRef = useRef<ISeriesApi<'Bar'> | null>(null);
|
||||||
|
const [snapshots, setSnapshots] = useState<PerformanceSnapshot[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
totalPnl: 0,
|
||||||
|
avgDailyPnl: 0,
|
||||||
|
bestDay: 0,
|
||||||
|
worstDay: 0,
|
||||||
|
winningDays: 0,
|
||||||
|
losingDays: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch performance snapshots
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSnapshots = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.get('/api/analytics/snapshots', {
|
||||||
|
params: { limit: period === 'week' ? 7 : period === 'month' ? 30 : period === 'quarter' ? 90 : 365 },
|
||||||
|
});
|
||||||
|
const data = response.data || [];
|
||||||
|
setSnapshots(data);
|
||||||
|
|
||||||
|
// Calculate stats
|
||||||
|
if (data.length > 0) {
|
||||||
|
const totalPnl = data.reduce((sum: number, s: PerformanceSnapshot) => sum + s.daily_pnl, 0);
|
||||||
|
const winningDays = data.filter((s: PerformanceSnapshot) => s.daily_pnl > 0).length;
|
||||||
|
const losingDays = data.length - winningDays;
|
||||||
|
const bestDay = Math.max(...data.map((s: PerformanceSnapshot) => s.daily_pnl));
|
||||||
|
const worstDay = Math.min(...data.map((s: PerformanceSnapshot) => s.daily_pnl));
|
||||||
|
|
||||||
|
setStats({
|
||||||
|
totalPnl,
|
||||||
|
avgDailyPnl: data.length > 0 ? totalPnl / data.length : 0,
|
||||||
|
bestDay,
|
||||||
|
worstDay,
|
||||||
|
winningDays,
|
||||||
|
losingDays,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching performance snapshots:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSnapshots();
|
||||||
|
}, [period]);
|
||||||
|
|
||||||
|
// Initialize chart and update data
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || snapshots.length === 0) return;
|
||||||
|
|
||||||
|
// Initialize chart if not already done
|
||||||
|
if (!chartRef.current) {
|
||||||
|
const chart = createChart(containerRef.current, {
|
||||||
|
layout: { background: { color: '#0f172a' }, textColor: '#e2e8f0' },
|
||||||
|
grid: { vertLines: { color: '#1f2937' }, horzLines: { color: '#1f2937' } },
|
||||||
|
rightPriceScale: { borderColor: '#1f2937' },
|
||||||
|
timeScale: { borderColor: '#1f2937', timeVisible: true, secondsVisible: false },
|
||||||
|
height: 350,
|
||||||
|
width: containerRef.current.clientWidth,
|
||||||
|
});
|
||||||
|
chartRef.current = chart;
|
||||||
|
|
||||||
|
const series = chart.addBarSeries({
|
||||||
|
color: '#3b82f6',
|
||||||
|
openColor: '#ef4444',
|
||||||
|
downColor: '#ef4444',
|
||||||
|
upColor: '#22c55e',
|
||||||
|
});
|
||||||
|
seriesRef.current = series;
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
if (!containerRef.current || !chartRef.current) return;
|
||||||
|
chartRef.current.applyOptions({ width: containerRef.current.clientWidth });
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
chart.remove();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update chart data
|
||||||
|
if (seriesRef.current) {
|
||||||
|
const chartData = snapshots.map((snapshot) => {
|
||||||
|
const [year, month, day] = snapshot.snapshot_date.split('-');
|
||||||
|
return {
|
||||||
|
time: `${year}-${month}-${day}` as any,
|
||||||
|
open: snapshot.daily_pnl >= 0 ? 0 : snapshot.daily_pnl,
|
||||||
|
close: snapshot.daily_pnl,
|
||||||
|
high: Math.max(0, snapshot.daily_pnl),
|
||||||
|
low: Math.min(0, snapshot.daily_pnl),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
seriesRef.current.setData(chartData);
|
||||||
|
chartRef.current?.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
}, [snapshots]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-blue-500" />
|
||||||
|
Daily Performance History
|
||||||
|
</h3>
|
||||||
|
<div className="text-center text-gray-400 py-8">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||||
|
<TrendingUp className="w-5 h-5 text-blue-500" />
|
||||||
|
Daily Performance History ({period})
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mb-4">
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Total P&L</span>
|
||||||
|
<p className={`text-lg font-bold ${stats.totalPnl >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||||
|
${stats.totalPnl.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Avg Daily</span>
|
||||||
|
<p className={`text-lg font-bold ${stats.avgDailyPnl >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||||
|
${stats.avgDailyPnl.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Win Days</span>
|
||||||
|
<p className="text-lg font-bold text-green-500">{stats.winningDays}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Best Day</span>
|
||||||
|
<p className="text-lg font-bold text-green-500">${stats.bestDay.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Worst Day</span>
|
||||||
|
<p className="text-lg font-bold text-red-500">${stats.worstDay.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-dark-bg rounded-lg p-3">
|
||||||
|
<span className="text-xs text-gray-400">Loss Days</span>
|
||||||
|
<p className="text-lg font-bold text-red-500">{stats.losingDays}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{snapshots.length > 0 ? (
|
||||||
|
<div ref={containerRef} style={{ width: '100%', height: '350px' }} />
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-gray-400 py-8">
|
||||||
|
<Calendar className="w-12 h-12 mx-auto mb-2 text-gray-600" />
|
||||||
|
<p>No performance data available for this period</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { TrendingUp, TrendingDown, Zap, Target } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface TradePattern {
|
||||||
|
id: number;
|
||||||
|
pattern_name: string;
|
||||||
|
description: string;
|
||||||
|
win_rate: number;
|
||||||
|
confidence_score: number;
|
||||||
|
sample_count: number;
|
||||||
|
total_profit: number;
|
||||||
|
indicators_used: string[];
|
||||||
|
best_timeframe: string;
|
||||||
|
best_time_of_day: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PatternStats {
|
||||||
|
pattern: string;
|
||||||
|
win_rate: number;
|
||||||
|
confidence: number;
|
||||||
|
sample_size: number;
|
||||||
|
total_profit: number;
|
||||||
|
best_timeframe: string;
|
||||||
|
best_time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TradePatternAnalyzer() {
|
||||||
|
const [patterns, setPatterns] = useState<PatternStats[]>([]);
|
||||||
|
const [allPatterns, setAllPatterns] = useState<TradePattern[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedPattern, setSelectedPattern] = useState<TradePattern | null>(null);
|
||||||
|
const [minConfidence, setMinConfidence] = useState(70);
|
||||||
|
|
||||||
|
// Fetch all patterns
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPatterns = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.get('/api/analytics/patterns/stats/best', {
|
||||||
|
params: { limit: 10 },
|
||||||
|
});
|
||||||
|
const data = response.data || [];
|
||||||
|
setPatterns(data);
|
||||||
|
|
||||||
|
// Fetch detailed patterns
|
||||||
|
const allResponse = await axios.get('/api/analytics/patterns', {
|
||||||
|
params: { min_confidence: minConfidence },
|
||||||
|
});
|
||||||
|
setAllPatterns(allResponse.data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching trade patterns:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPatterns();
|
||||||
|
}, [minConfidence]);
|
||||||
|
|
||||||
|
const getRating = (confidence: number): { text: string; color: string } => {
|
||||||
|
if (confidence >= 90) return { text: 'Excellent', color: 'text-green-500' };
|
||||||
|
if (confidence >= 80) return { text: 'Good', color: 'text-blue-500' };
|
||||||
|
if (confidence >= 70) return { text: 'Fair', color: 'text-yellow-500' };
|
||||||
|
return { text: 'Poor', color: 'text-red-500' };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Target className="w-5 h-5 text-purple-500" />
|
||||||
|
Trade Pattern Analyzer
|
||||||
|
</h3>
|
||||||
|
<div className="text-center text-gray-400 py-8">Loading patterns...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
<Target className="w-5 h-5 text-purple-500" />
|
||||||
|
Trade Pattern Analyzer
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-sm text-gray-400">Min Confidence:</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={minConfidence}
|
||||||
|
onChange={(e) => setMinConfidence(parseInt(e.target.value))}
|
||||||
|
className="w-24"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">{minConfidence}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Patterns Summary */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="text-md font-semibold mb-3 text-gray-300">Top Performing Patterns</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{patterns.length > 0 ? (
|
||||||
|
patterns.map((pattern, idx) => {
|
||||||
|
const rating = getRating(pattern.confidence);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-blue-500 cursor-pointer transition"
|
||||||
|
onClick={() => {
|
||||||
|
const full = allPatterns.find((p) => p.pattern_name === pattern.pattern);
|
||||||
|
setSelectedPattern(full || null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h5 className="font-semibold text-gray-200 mb-1">{pattern.pattern}</h5>
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Win Rate: <span className="text-green-400 font-medium">{pattern.win_rate.toFixed(1)}%</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Samples: <span className="text-blue-400 font-medium">{pattern.sample_size}</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Profit: <span className="text-yellow-400 font-medium">${pattern.total_profit.toFixed(2)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className={`text-2xl font-bold ${rating.color}`}>{pattern.confidence.toFixed(0)}%</p>
|
||||||
|
<p className={`text-xs ${rating.color}`}>{rating.text}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 flex-wrap mt-2">
|
||||||
|
{pattern.best_timeframe && (
|
||||||
|
<span className="bg-blue-900 bg-opacity-50 text-blue-300 text-xs px-2 py-1 rounded">
|
||||||
|
{pattern.best_timeframe}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{pattern.best_time && (
|
||||||
|
<span className="bg-purple-900 bg-opacity-50 text-purple-300 text-xs px-2 py-1 rounded">
|
||||||
|
{pattern.best_time}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-gray-400 py-4">No patterns found with confidence >= {minConfidence}%</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pattern Details */}
|
||||||
|
{selectedPattern && (
|
||||||
|
<div className="bg-dark-bg rounded-lg p-4 border border-blue-500 border-opacity-50">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h4 className="text-md font-semibold text-gray-200">Pattern Details: {selectedPattern.pattern_name}</h4>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedPattern(null)}
|
||||||
|
className="text-gray-400 hover:text-gray-200"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-300 text-sm mb-3">{selectedPattern.description}</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Win Rate</p>
|
||||||
|
<p className="text-lg font-bold text-green-500">{selectedPattern.win_rate.toFixed(1)}%</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Confidence Score</p>
|
||||||
|
<p className="text-lg font-bold text-blue-500">{selectedPattern.confidence_score.toFixed(0)}%</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Sample Size</p>
|
||||||
|
<p className="text-lg font-bold text-purple-500">{selectedPattern.sample_count}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Total Profit</p>
|
||||||
|
<p className="text-lg font-bold text-yellow-500">${selectedPattern.total_profit.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-xs text-gray-400 mb-2">Indicators Used</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedPattern.indicators_used.map((indicator, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="bg-blue-900 bg-opacity-50 text-blue-300 text-xs px-3 py-1 rounded"
|
||||||
|
>
|
||||||
|
{indicator}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 pt-3 border-t border-dark-border">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Best Timeframe</p>
|
||||||
|
<p className="font-medium text-gray-200">{selectedPattern.best_timeframe || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400 mb-1">Best Time of Day</p>
|
||||||
|
<p className="font-medium text-gray-200">{selectedPattern.best_time_of_day || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
<div className="mt-6 pt-4 border-t border-dark-border">
|
||||||
|
<button className="w-full bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 rounded-lg transition">
|
||||||
|
<Zap className="w-4 h-4 inline mr-2" />
|
||||||
|
Analyze New Pattern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user