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:
@@ -14,6 +14,9 @@ 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'
|
||||||
|
|
||||||
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 +30,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' | '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 +47,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', '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 +83,7 @@ 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 === '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,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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user