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 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>
|
||||
);
|
||||
}
|
||||
@@ -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