@@ -80,6 +83,7 @@ export default function App() {
{activeTab === 'Account' &&
}
{activeTab === 'Equity' &&
}
{activeTab === 'Decisions' &&
}
+ {activeTab === 'Analytics' &&
}
{activeTab === 'Daily Helper' && (
diff --git a/frontend/src/components/AnalyticsDashboard.tsx b/frontend/src/components/AnalyticsDashboard.tsx
new file mode 100644
index 0000000..d726fd0
--- /dev/null
+++ b/frontend/src/components/AnalyticsDashboard.tsx
@@ -0,0 +1,277 @@
+import { useState, useEffect } from 'react';
+import { BarChart3, TrendingUp, Calendar, RefreshCw } from 'lucide-react';
+import axios from 'axios';
+import PerformanceHistoryChart from './PerformanceHistoryChart';
+import EquityCurveChart from './EquityCurveChart';
+import TradePatternAnalyzer from './TradePatternAnalyzer';
+import LessonsPanel from './LessonsPanel';
+
+interface DashboardStats {
+ period: string;
+ snapshot_count: number;
+ performance: {
+ total_pnl: number;
+ avg_daily_pnl: number;
+ total_trades: number;
+ winning_days: number;
+ losing_days: number;
+ avg_win_rate: number;
+ best_day: number;
+ worst_day: number;
+ };
+ top_patterns: Array<{
+ name: string;
+ confidence: number;
+ win_rate: number;
+ samples: number;
+ }>;
+ recent_lessons: Array<{
+ category: string;
+ lesson: string;
+ importance: string;
+ date: string;
+ }>;
+}
+
+export default function AnalyticsDashboard() {
+ const [period, setPeriod] = useState<'week' | 'month' | 'quarter' | 'year'>('month');
+ const [dashboardData, setDashboardData] = useState
(null);
+ const [loading, setLoading] = useState(true);
+ const [lastUpdated, setLastUpdated] = useState('');
+
+ // Fetch dashboard data
+ useEffect(() => {
+ const fetchDashboard = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get('/api/analytics/dashboard', {
+ params: { period },
+ });
+ setDashboardData(response.data || null);
+ setLastUpdated(new Date().toLocaleTimeString());
+ } catch (error) {
+ console.error('Error fetching analytics dashboard:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchDashboard();
+ }, [period]);
+
+ const handleRefresh = () => {
+ window.location.reload();
+ };
+
+ if (loading && !dashboardData) {
+ return (
+
+
+
+ Analytics Dashboard
+
+
Loading dashboard...
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Advanced Analytics Dashboard
+
+
+
+ {lastUpdated && (
+ Updated: {lastUpdated}
+ )}
+
+
+
+ {/* Period Selector */}
+
+ {(['week', 'month', 'quarter', 'year'] as const).map((p) => (
+
+ ))}
+
+
+ {/* Overview Stats */}
+ {dashboardData && (
+
+
+
Total P&L
+
= 0
+ ? 'text-green-500'
+ : 'text-red-500'
+ }`}
+ >
+ ${dashboardData.performance.total_pnl.toFixed(2)}
+
+
+
+
+
Avg Daily P&L
+
= 0
+ ? 'text-green-500'
+ : 'text-red-500'
+ }`}
+ >
+ ${dashboardData.performance.avg_daily_pnl.toFixed(2)}
+
+
+
+
+
Win Rate
+
+ {dashboardData.performance.avg_win_rate.toFixed(1)}%
+
+
+
+
+
Trading Days
+
+ {dashboardData.snapshot_count}
+
+
+
+ )}
+
+ {/* Secondary Stats */}
+ {dashboardData && (
+
+
+
Total Trades
+
{dashboardData.performance.total_trades}
+
+
+
Win Days
+
{dashboardData.performance.winning_days}
+
+
+
Loss Days
+
{dashboardData.performance.losing_days}
+
+
+
Best Day
+
${dashboardData.performance.best_day.toFixed(2)}
+
+
+
Worst Day
+
${dashboardData.performance.worst_day.toFixed(2)}
+
+
+ )}
+
+
+ {/* Charts Section */}
+
+
+ {/* Pattern Analyzer */}
+
+
+ {/* Lessons Panel */}
+
+
+ {/* Recent Summary */}
+ {dashboardData && (
+
+
+
+ Recent Insights Summary
+
+
+
+ {/* Top Patterns */}
+
+
Top Trade Patterns
+
+ {dashboardData.top_patterns.length > 0 ? (
+ dashboardData.top_patterns.map((pattern, idx) => (
+
+
{pattern.name}
+
+
+ {pattern.confidence.toFixed(0)}%
+
+
+ {pattern.win_rate.toFixed(0)}%
+
+
+
+ ))
+ ) : (
+
No patterns identified yet
+ )}
+
+
+
+ {/* Recent Lessons */}
+
+
Recent Lessons
+
+ {dashboardData.recent_lessons.length > 0 ? (
+ dashboardData.recent_lessons.map((lesson, idx) => (
+
+
{lesson.lesson.substring(0, 60)}...
+
+
+ {lesson.category}
+
+
+ {lesson.importance}
+
+
+ {new Date(lesson.date).toLocaleDateString()}
+
+
+
+ ))
+ ) : (
+
No lessons logged yet
+ )}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/EquityCurveChart.tsx b/frontend/src/components/EquityCurveChart.tsx
new file mode 100644
index 0000000..5d31c15
--- /dev/null
+++ b/frontend/src/components/EquityCurveChart.tsx
@@ -0,0 +1,203 @@
+import { useEffect, useRef, useState } from 'react';
+import { createChart, IChartApi, ISeriesApi } from 'lightweight-charts';
+import { TrendingUp, TrendingDown } from 'lucide-react';
+import axios from 'axios';
+
+interface PerformanceSnapshot {
+ snapshot_date: string;
+ cumulative_pnl: number;
+ portfolio_value: number;
+ equity_curve: number[];
+}
+
+interface EquityCurveChartProps {
+ period?: 'week' | 'month' | 'quarter' | 'year';
+}
+
+export default function EquityCurveChart({
+ period = 'month',
+}: EquityCurveChartProps) {
+ const containerRef = useRef(null);
+ const chartRef = useRef(null);
+ const seriesRef = useRef | null>(null);
+ const [snapshots, setSnapshots] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [stats, setStats] = useState({
+ currentEquity: 0,
+ maxEquity: 0,
+ minEquity: 0,
+ totalGain: 0,
+ gainPercent: 0,
+ drawdown: 0,
+ });
+
+ // Fetch performance snapshots for equity curve
+ useEffect(() => {
+ const fetchSnapshots = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get('/api/analytics/snapshots', {
+ params: { limit: period === 'week' ? 7 : period === 'month' ? 30 : period === 'quarter' ? 90 : 365 },
+ });
+ const data = response.data || [];
+ setSnapshots(data);
+
+ // Calculate stats
+ if (data.length > 0) {
+ // Assume initial portfolio value was 100000
+ const initialValue = 100000;
+ const currentEquity = initialValue + (data[data.length - 1]?.cumulative_pnl || 0);
+ const equityValues = data.map(
+ (s: PerformanceSnapshot) => initialValue + s.cumulative_pnl
+ );
+ const maxEquity = Math.max(...equityValues);
+ const minEquity = Math.min(...equityValues);
+ const totalGain = currentEquity - initialValue;
+ const gainPercent = (totalGain / initialValue) * 100;
+ const drawdown = ((maxEquity - currentEquity) / maxEquity) * 100;
+
+ setStats({
+ currentEquity,
+ maxEquity,
+ minEquity,
+ totalGain,
+ gainPercent,
+ drawdown,
+ });
+ }
+ } catch (error) {
+ console.error('Error fetching equity curve data:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchSnapshots();
+ }, [period]);
+
+ // Initialize chart and update data
+ useEffect(() => {
+ if (!containerRef.current || snapshots.length === 0) return;
+
+ // Initialize chart if not already done
+ if (!chartRef.current) {
+ const chart = createChart(containerRef.current, {
+ layout: { background: { color: '#0f172a' }, textColor: '#e2e8f0' },
+ grid: { vertLines: { color: '#1f2937' }, horzLines: { color: '#1f2937' } },
+ rightPriceScale: { borderColor: '#1f2937' },
+ timeScale: { borderColor: '#1f2937', timeVisible: true, secondsVisible: false },
+ height: 350,
+ width: containerRef.current.clientWidth,
+ });
+ chartRef.current = chart;
+
+ const series = chart.addLineSeries({
+ color: '#22c55e',
+ lineWidth: 2,
+ });
+ seriesRef.current = series;
+
+ const onResize = () => {
+ if (!containerRef.current || !chartRef.current) return;
+ chartRef.current.applyOptions({ width: containerRef.current.clientWidth });
+ };
+ window.addEventListener('resize', onResize);
+ return () => {
+ window.removeEventListener('resize', onResize);
+ chart.remove();
+ };
+ }
+
+ // Update chart data
+ if (seriesRef.current) {
+ const initialValue = 100000;
+ const chartData = snapshots.map((snapshot) => {
+ const [year, month, day] = snapshot.snapshot_date.split('-');
+ const equity = initialValue + snapshot.cumulative_pnl;
+ return {
+ time: `${year}-${month}-${day}` as any,
+ value: equity,
+ };
+ });
+ seriesRef.current.setData(chartData);
+ chartRef.current?.timeScale().fitContent();
+ }
+ }, [snapshots]);
+
+ if (loading) {
+ return (
+
+
+
+ Equity Curve
+
+
Loading...
+
+ );
+ }
+
+ return (
+
+
+
+
+ Equity Curve ({period})
+
+
+ {/* Stats Grid */}
+
+
+
Current Equity
+
+ ${stats.currentEquity.toFixed(0)}
+
+
+
+
+
Total Gain
+
= 0 ? 'text-green-500' : 'text-red-500'}`}>
+ ${stats.totalGain.toFixed(2)}
+
+
+
+
+
Return %
+
= 0 ? 'text-green-500' : 'text-red-500'}`}>
+ {stats.gainPercent.toFixed(2)}%
+
+
+
+
+
Peak Equity
+
+ ${stats.maxEquity.toFixed(0)}
+
+
+
+
+
Low Equity
+
+ ${stats.minEquity.toFixed(0)}
+
+
+
+
+
Max Drawdown
+
+ {stats.drawdown.toFixed(2)}%
+
+
+
+
+
+ {snapshots.length > 0 ? (
+
+ ) : (
+
+
+
No equity data available for this period
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/LessonsPanel.tsx b/frontend/src/components/LessonsPanel.tsx
new file mode 100644
index 0000000..b2ac13c
--- /dev/null
+++ b/frontend/src/components/LessonsPanel.tsx
@@ -0,0 +1,286 @@
+import { useEffect, useState } from 'react';
+import { BookOpen, AlertCircle, Lightbulb, Trash2, Edit2 } from 'lucide-react';
+import axios from 'axios';
+
+interface Lesson {
+ id: number;
+ lesson_text: string;
+ category: string;
+ importance: string;
+ impact: string;
+ tags: string[];
+ date_learned: string;
+ status: string;
+}
+
+interface RecurringMistake {
+ tag: string;
+ count: number;
+}
+
+export default function LessonsPanel() {
+ const [lessons, setLessons] = useState([]);
+ const [recurringMistakes, setRecurringMistakes] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedCategory, setSelectedCategory] = useState(null);
+ const [selectedImportance, setSelectedImportance] = useState(null);
+ const [newLesson, setNewLesson] = useState('');
+ const [newCategory, setNewCategory] = useState('entry');
+ const [newImportance, setNewImportance] = useState('medium');
+
+ // Fetch lessons and categories
+ useEffect(() => {
+ const fetchLessons = async () => {
+ try {
+ setLoading(true);
+
+ // Fetch lessons
+ let lessonsUrl = '/api/analytics/lessons?limit=20';
+ if (selectedCategory) lessonsUrl += `&category=${selectedCategory}`;
+ if (selectedImportance) lessonsUrl += `&importance=${selectedImportance}`;
+
+ const lessonsResponse = await axios.get(lessonsUrl);
+ setLessons(lessonsResponse.data || []);
+
+ // Fetch categories if not loaded
+ if (categories.length === 0) {
+ const categoriesResponse = await axios.get('/api/analytics/lessons/categories');
+ setCategories(categoriesResponse.data?.available || []);
+ }
+
+ // Fetch recurring mistakes
+ const mistakesResponse = await axios.get('/api/analytics/lessons/recurring-mistakes?limit=5');
+ setRecurringMistakes(
+ mistakesResponse.data?.recurring_mistakes?.map(([tag, count]: [string, number]) => ({ tag, count })) || []
+ );
+ } catch (error) {
+ console.error('Error fetching lessons:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchLessons();
+ }, [selectedCategory, selectedImportance]);
+
+ const handleAddLesson = async () => {
+ if (!newLesson.trim()) return;
+
+ try {
+ await axios.post('/api/analytics/lessons', {
+ lesson_text: newLesson,
+ category: newCategory,
+ importance: newImportance,
+ date_learned: new Date().toISOString().split('T')[0],
+ });
+ setNewLesson('');
+ // Refresh lessons
+ const lessonsResponse = await axios.get('/api/analytics/lessons?limit=20');
+ setLessons(lessonsResponse.data || []);
+ } catch (error) {
+ console.error('Error adding lesson:', error);
+ }
+ };
+
+ const getImportanceColor = (importance: string): string => {
+ switch (importance) {
+ case 'critical':
+ return 'bg-red-900 text-red-300';
+ case 'high':
+ return 'bg-orange-900 text-orange-300';
+ case 'medium':
+ return 'bg-yellow-900 text-yellow-300';
+ default:
+ return 'bg-blue-900 text-blue-300';
+ }
+ };
+
+ const getImpactColor = (impact: string): string => {
+ if (impact === 'positive') return 'text-green-500';
+ if (impact === 'negative') return 'text-red-500';
+ return 'text-gray-400';
+ };
+
+ if (loading) {
+ return (
+
+
+
+ Lessons Learned
+
+
Loading lessons...
+
+ );
+ }
+
+ return (
+
+ {/* Add New Lesson */}
+
+
+
+ Log New Lesson
+
+
+
+
+
+ {/* Recurring Mistakes */}
+ {recurringMistakes.length > 0 && (
+
+
+
+ Recurring Mistakes
+
+
+
+ {recurringMistakes.map((mistake, idx) => (
+
+
{mistake.tag}
+
+
+ {mistake.count}x
+
+ occurrences
+
+
+ ))}
+
+
+
+ 💡 Focus on preventing these recurring mistakes to improve your trading performance
+
+
+ )}
+
+ {/* Filter Controls */}
+
+
+
+ Lessons Learned ({lessons.length})
+
+
+
+
+ {categories.map((cat) => (
+
+ ))}
+
+
+ {/* Lessons List */}
+
+ {lessons.length > 0 ? (
+ lessons.map((lesson) => (
+
+
+
{lesson.lesson_text}
+
+
+
+
+
+
+
+
+ {lesson.importance}
+
+
+ {lesson.category}
+
+
+ {lesson.impact}
+
+ {new Date(lesson.date_learned).toLocaleDateString()}
+
+
+ {lesson.tags.length > 0 && (
+
+ {lesson.tags.map((tag, idx) => (
+
+ #{tag}
+
+ ))}
+
+ )}
+
+ ))
+ ) : (
+
No lessons found
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/PerformanceHistoryChart.tsx b/frontend/src/components/PerformanceHistoryChart.tsx
new file mode 100644
index 0000000..152ae15
--- /dev/null
+++ b/frontend/src/components/PerformanceHistoryChart.tsx
@@ -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(null);
+ const chartRef = useRef(null);
+ const seriesRef = useRef | null>(null);
+ const [snapshots, setSnapshots] = useState([]);
+ 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 (
+
+
+
+ Daily Performance History
+
+
Loading...
+
+ );
+ }
+
+ return (
+
+
+
+
+ Daily Performance History ({period})
+
+
+ {/* Stats Grid */}
+
+
+
Total P&L
+
= 0 ? 'text-green-500' : 'text-red-500'}`}>
+ ${stats.totalPnl.toFixed(2)}
+
+
+
+
+
Avg Daily
+
= 0 ? 'text-green-500' : 'text-red-500'}`}>
+ ${stats.avgDailyPnl.toFixed(2)}
+
+
+
+
+
Win Days
+
{stats.winningDays}
+
+
+
+
Best Day
+
${stats.bestDay.toFixed(2)}
+
+
+
+
Worst Day
+
${stats.worstDay.toFixed(2)}
+
+
+
+
Loss Days
+
{stats.losingDays}
+
+
+
+
+ {snapshots.length > 0 ? (
+
+ ) : (
+
+
+
No performance data available for this period
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/TradePatternAnalyzer.tsx b/frontend/src/components/TradePatternAnalyzer.tsx
new file mode 100644
index 0000000..9fd1ac0
--- /dev/null
+++ b/frontend/src/components/TradePatternAnalyzer.tsx
@@ -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([]);
+ const [allPatterns, setAllPatterns] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedPattern, setSelectedPattern] = useState(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 (
+
+
+
+ Trade Pattern Analyzer
+
+
Loading patterns...
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Trade Pattern Analyzer
+
+
+
+ setMinConfidence(parseInt(e.target.value))}
+ className="w-24"
+ />
+ {minConfidence}%
+
+
+
+
+ {/* Top Patterns Summary */}
+
+
Top Performing Patterns
+
+ {patterns.length > 0 ? (
+ patterns.map((pattern, idx) => {
+ const rating = getRating(pattern.confidence);
+ return (
+
{
+ const full = allPatterns.find((p) => p.pattern_name === pattern.pattern);
+ setSelectedPattern(full || null);
+ }}
+ >
+
+
+
{pattern.pattern}
+
+
+ Win Rate: {pattern.win_rate.toFixed(1)}%
+
+
+ Samples: {pattern.sample_size}
+
+
+ Profit: ${pattern.total_profit.toFixed(2)}
+
+
+
+
+
{pattern.confidence.toFixed(0)}%
+
{rating.text}
+
+
+
+
+ {pattern.best_timeframe && (
+
+ {pattern.best_timeframe}
+
+ )}
+ {pattern.best_time && (
+
+ {pattern.best_time}
+
+ )}
+
+
+ );
+ })
+ ) : (
+
No patterns found with confidence >= {minConfidence}%
+ )}
+
+
+
+ {/* Pattern Details */}
+ {selectedPattern && (
+
+
+
Pattern Details: {selectedPattern.pattern_name}
+
+
+
+
{selectedPattern.description}
+
+
+
+
Win Rate
+
{selectedPattern.win_rate.toFixed(1)}%
+
+
+
Confidence Score
+
{selectedPattern.confidence_score.toFixed(0)}%
+
+
+
Sample Size
+
{selectedPattern.sample_count}
+
+
+
Total Profit
+
${selectedPattern.total_profit.toFixed(2)}
+
+
+
+
+
Indicators Used
+
+ {selectedPattern.indicators_used.map((indicator, idx) => (
+
+ {indicator}
+
+ ))}
+
+
+
+
+
+
Best Timeframe
+
{selectedPattern.best_timeframe || 'N/A'}
+
+
+
Best Time of Day
+
{selectedPattern.best_time_of_day || 'N/A'}
+
+
+
+ )}
+
+ {/* Action Button */}
+
+
+
+
+ );
+}