diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4b4ed35..ab296c9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,9 @@ import UserProfileSetup from './components/UserProfileSetup' import HabitTracker from './components/HabitTracker' import DailyChecklistPanel from './components/DailyChecklistPanel' +// Phase 3: Advanced Analytics Components +import AnalyticsDashboard from './components/AnalyticsDashboard' + function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onChange: (t: string) => void }) { return (
@@ -27,7 +30,7 @@ function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onCh } 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' | 'Settings' | 'Prompts' | 'Daily Helper'>('Live') const [backendStatus, setBackendStatus] = useState(null) const [showProfileSetup, setShowProfileSetup] = useState(false) @@ -44,7 +47,7 @@ export default function App() { return () => { mounted = false } }, []) - const tabs = ['Live', 'Account', 'Equity', 'Decisions', 'Daily Helper', 'Settings', 'Prompts'] + const tabs = ['Live', 'Account', 'Equity', 'Decisions', 'Analytics', 'Daily Helper', 'Settings', 'Prompts'] return (
@@ -80,6 +83,7 @@ export default function App() { {activeTab === 'Account' && } {activeTab === 'Equity' && } {activeTab === 'Decisions' && } + {activeTab === 'Analytics' && } {activeTab === 'Daily Helper' && (
diff --git a/frontend/src/components/AnalyticsDashboard.tsx b/frontend/src/components/AnalyticsDashboard.tsx new file mode 100644 index 0000000..d726fd0 --- /dev/null +++ b/frontend/src/components/AnalyticsDashboard.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [lastUpdated, setLastUpdated] = useState(''); + + // 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 ( +
+

+ + Analytics Dashboard +

+
Loading dashboard...
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

+ + Advanced Analytics Dashboard +

+
+ + {lastUpdated && ( + Updated: {lastUpdated} + )} +
+
+ + {/* Period Selector */} +
+ {(['week', 'month', 'quarter', 'year'] as const).map((p) => ( + + ))} +
+ + {/* Overview Stats */} + {dashboardData && ( +
+
+

Total P&L

+

= 0 + ? 'text-green-500' + : 'text-red-500' + }`} + > + ${dashboardData.performance.total_pnl.toFixed(2)} +

+
+ +
+

Avg Daily P&L

+

= 0 + ? 'text-green-500' + : 'text-red-500' + }`} + > + ${dashboardData.performance.avg_daily_pnl.toFixed(2)} +

+
+ +
+

Win Rate

+

+ {dashboardData.performance.avg_win_rate.toFixed(1)}% +

+
+ +
+

Trading Days

+

+ {dashboardData.snapshot_count} +

+
+
+ )} + + {/* Secondary Stats */} + {dashboardData && ( +
+
+

Total Trades

+

{dashboardData.performance.total_trades}

+
+
+

Win Days

+

{dashboardData.performance.winning_days}

+
+
+

Loss Days

+

{dashboardData.performance.losing_days}

+
+
+

Best Day

+

${dashboardData.performance.best_day.toFixed(2)}

+
+
+

Worst Day

+

${dashboardData.performance.worst_day.toFixed(2)}

+
+
+ )} +
+ + {/* Charts Section */} +
+ + +
+ + {/* Pattern Analyzer */} + + + {/* Lessons Panel */} + + + {/* Recent Summary */} + {dashboardData && ( +
+

+ + Recent Insights Summary +

+ +
+ {/* Top Patterns */} +
+

Top Trade Patterns

+
+ {dashboardData.top_patterns.length > 0 ? ( + dashboardData.top_patterns.map((pattern, idx) => ( +
+ {pattern.name} +
+ + {pattern.confidence.toFixed(0)}% + + + {pattern.win_rate.toFixed(0)}% + +
+
+ )) + ) : ( +

No patterns identified yet

+ )} +
+
+ + {/* Recent Lessons */} +
+

Recent Lessons

+
+ {dashboardData.recent_lessons.length > 0 ? ( + dashboardData.recent_lessons.map((lesson, idx) => ( +
+

{lesson.lesson.substring(0, 60)}...

+
+ + {lesson.category} + + + {lesson.importance} + + + {new Date(lesson.date).toLocaleDateString()} + +
+
+ )) + ) : ( +

No lessons logged yet

+ )} +
+
+
+
+ )} +
+ ); +} diff --git a/frontend/src/components/EquityCurveChart.tsx b/frontend/src/components/EquityCurveChart.tsx new file mode 100644 index 0000000..5d31c15 --- /dev/null +++ b/frontend/src/components/EquityCurveChart.tsx @@ -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(null); + const chartRef = useRef(null); + const seriesRef = useRef | null>(null); + const [snapshots, setSnapshots] = useState([]); + 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 ( +
+

+ + Equity Curve +

+
Loading...
+
+ ); + } + + return ( +
+
+

+ + Equity Curve ({period}) +

+ + {/* Stats Grid */} +
+
+ Current Equity +

+ ${stats.currentEquity.toFixed(0)} +

+
+ +
+ Total Gain +

= 0 ? 'text-green-500' : 'text-red-500'}`}> + ${stats.totalGain.toFixed(2)} +

+
+ +
+ Return % +

= 0 ? 'text-green-500' : 'text-red-500'}`}> + {stats.gainPercent.toFixed(2)}% +

+
+ +
+ Peak Equity +

+ ${stats.maxEquity.toFixed(0)} +

+
+ +
+ Low Equity +

+ ${stats.minEquity.toFixed(0)} +

+
+ +
+ Max Drawdown +

+ {stats.drawdown.toFixed(2)}% +

+
+
+
+ + {snapshots.length > 0 ? ( +
+ ) : ( +
+ +

No equity data available for this period

+
+ )} +
+ ); +} diff --git a/frontend/src/components/LessonsPanel.tsx b/frontend/src/components/LessonsPanel.tsx new file mode 100644 index 0000000..b2ac13c --- /dev/null +++ b/frontend/src/components/LessonsPanel.tsx @@ -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([]); + const [recurringMistakes, setRecurringMistakes] = useState([]); + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedCategory, setSelectedCategory] = useState(null); + const [selectedImportance, setSelectedImportance] = useState(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 ( +
+

+ + Lessons Learned +

+
Loading lessons...
+
+ ); + } + + return ( +
+ {/* Add New Lesson */} +
+

+ + Log New Lesson +

+ +
+