Phase 3: Advanced Analytics Frontend Components

Implemented comprehensive frontend visualizations for Phase 3 analytics:

1. PerformanceHistoryChart.tsx - Daily P&L bar chart with period filtering
   - Visualizes daily performance with open/close/high/low bars
   - Shows P&L trend, win/loss days, best/worst days
   - Supports week/month/quarter/year periods

2. EquityCurveChart.tsx - Equity growth line chart
   - Displays cumulative portfolio equity over time
   - Shows peak equity, drawdown %, total return
   - Real-time equity value tracking

3. TradePatternAnalyzer.tsx - Trade pattern visualization
   - Shows top performing patterns with confidence scores
   - Detailed pattern information (win rate, samples, profit)
   - Configurable confidence filter
   - Pattern indicators and best timeframes

4. LessonsPanel.tsx - Lessons learned management
   - Create and categorize trading lessons
   - Track recurring mistakes automatically
   - Filter by category and importance
   - Display related lessons with tags

5. AnalyticsDashboard.tsx - Comprehensive analytics hub
   - Integrates all analytics components
   - Period selector (week/month/quarter/year)
   - Overview statistics and KPIs
   - Recent insights summary
   - Refresh functionality

Integration with App.tsx:
- Added Analytics tab to main navigation
- Imported all Phase 3 components
- Integrated dashboard into tab system

All components use:
- lightweight-charts for charting
- axios for API calls
- Real-time data fetching
- TypeScript for type safety
This commit is contained in:
Claude
2025-11-16 05:56:56 +00:00
parent e3f1a8079c
commit 3f7b69b52c
6 changed files with 1196 additions and 2 deletions
+6 -2
View File
@@ -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 (
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
@@ -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<any>(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 (
<div className="min-h-screen bg-dark-bg p-6">
@@ -80,6 +83,7 @@ export default function App() {
{activeTab === 'Account' && <AccountPositionsPanel />}
{activeTab === 'Equity' && <EquityPerformancePanel />}
{activeTab === 'Decisions' && <DecisionLogPanel />}
{activeTab === 'Analytics' && <AnalyticsDashboard />}
{activeTab === 'Daily Helper' && (
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))' }}>
@@ -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,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>
);
}
+286
View File
@@ -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,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>
);
}