Spaces:
Running
Running
| import { useState, useEffect } from 'react'; | |
| import { patternsAPI } from '../api/client'; | |
| import { formatCurrency, detectCurrencyFromTicker } from '../utils/currencyUtils'; | |
| import TickerSearch from '../components/TickerSearch'; | |
| /* ββ SVG Icons (no emojis) βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const ScanIcon = () => <svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd" /></svg>; | |
| const ChartIcon = () => <svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path fillRule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 0l-2 2a1 1 0 101.414 1.414L8 10.414l1.293 1.293a1 1 0 001.414 0l4-4z" clipRule="evenodd" /></svg>; | |
| const CatalogIcon = () => <svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z" /></svg>; | |
| const AccuracyIcon = () => <svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path fillRule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /></svg>; | |
| const ArrowUp = () => <svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path fillRule="evenodd" d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z" clipRule="evenodd" /></svg>; | |
| const ArrowDown = () => <svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path fillRule="evenodd" d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z" clipRule="evenodd" /></svg>; | |
| const TABS = [ | |
| { id: 'scanner', label: 'Pattern Scanner', icon: <ScanIcon /> }, | |
| { id: 'catalog', label: 'Pattern Catalog', icon: <CatalogIcon /> }, | |
| { id: 'accuracy', label: 'Accuracy Report', icon: <AccuracyIcon /> }, | |
| ]; | |
| export default function PatternIntelligence() { | |
| const [activeTab, setActiveTab] = useState('scanner'); | |
| const [ticker, setTicker] = useState(''); | |
| const [period, setPeriod] = useState('2y'); | |
| const [horizon, setHorizon] = useState(5); | |
| const [loading, setLoading] = useState(false); | |
| const [analysis, setAnalysis] = useState<any>(null); | |
| const [catalog, setCatalog] = useState<any>(null); | |
| const [accuracy, setAccuracy] = useState<any>(null); | |
| const [error, setError] = useState(''); | |
| const handleAnalyze = async () => { | |
| if (!ticker.trim()) return; | |
| setLoading(true); | |
| setError(''); | |
| try { | |
| const { data } = await patternsAPI.analyze({ ticker: ticker.toUpperCase(), period, horizon }); | |
| setAnalysis(data); | |
| } catch (err: any) { | |
| setError(err.response?.data?.detail || 'Analysis failed'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const loadCatalog = async () => { | |
| try { | |
| const { data } = await patternsAPI.catalog(); | |
| setCatalog(data); | |
| } catch (err) { | |
| setError('Failed to load catalog'); | |
| } | |
| }; | |
| const handleBacktestAccuracy = async () => { | |
| if (!ticker.trim()) return; | |
| setLoading(true); | |
| setError(''); | |
| try { | |
| const { data } = await patternsAPI.backtestAccuracy({ ticker: ticker.toUpperCase(), period: '5y', horizon }); | |
| setAccuracy(data); | |
| } catch (err: any) { | |
| setError(err.response?.data?.detail || 'Backtest failed'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| if (activeTab === 'catalog' && !catalog) loadCatalog(); | |
| }, [activeTab]); | |
| const directionColor = (d: string) => { | |
| if (d === 'bullish' || d === 'strong_up') return 'var(--accent-green)'; | |
| if (d === 'bearish' || d === 'strong_down') return 'var(--accent-red, #ef4444)'; | |
| return 'var(--text-muted)'; | |
| }; | |
| const confidenceColor = (c: number) => { | |
| if (c >= 0.65) return 'var(--accent-green)'; | |
| if (c >= 0.45) return 'var(--accent-yellow, #f59e0b)'; | |
| return 'var(--accent-red, #ef4444)'; | |
| }; | |
| return ( | |
| <div className="page-container" style={{ padding: '1.5rem 2rem' }}> | |
| {/* Header */} | |
| <div style={{ marginBottom: '1.5rem' }}> | |
| <h1 style={{ fontSize: '1.5rem', fontWeight: 700, margin: 0, display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <ChartIcon /> Pattern Intelligence | |
| </h1> | |
| <p style={{ color: 'var(--text-muted)', margin: '0.25rem 0 0', fontSize: '0.85rem' }}> | |
| AI-powered candlestick pattern recognition with LightGBM ensemble prediction | |
| </p> | |
| </div> | |
| {/* Tabs */} | |
| <div className="qh-tabs" style={{ display: 'flex', gap: '0.25rem', marginBottom: '1.25rem', borderBottom: '1px solid var(--border-subtle)', paddingBottom: '0' }}> | |
| {TABS.map(tab => ( | |
| <button | |
| key={tab.id} | |
| onClick={() => setActiveTab(tab.id)} | |
| style={{ | |
| display: 'flex', alignItems: 'center', gap: '0.4rem', padding: '0.6rem 1rem', | |
| border: 'none', background: 'none', cursor: 'pointer', fontSize: '0.8rem', fontWeight: 600, | |
| color: activeTab === tab.id ? 'var(--accent-green)' : 'var(--text-muted)', | |
| borderBottom: activeTab === tab.id ? '2px solid var(--accent-green)' : '2px solid transparent', | |
| transition: 'all 0.2s ease', | |
| }} | |
| > | |
| {tab.icon} {tab.label} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Search Bar */} | |
| {(activeTab === 'scanner' || activeTab === 'accuracy') && ( | |
| <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.25rem', flexWrap: 'wrap' }}> | |
| <TickerSearch | |
| value={ticker} | |
| onChange={setTicker} | |
| onSelect={() => activeTab === 'scanner' ? handleAnalyze() : handleBacktestAccuracy()} | |
| placeholder="Search stocks, ETFs, crypto..." | |
| style={{ flex: '1 1 200px', minWidth: 180 }} | |
| /> | |
| <select value={period} onChange={e => setPeriod(e.target.value)} style={{ | |
| padding: '0.55rem 0.75rem', border: '1px solid var(--border-subtle)', borderRadius: '0.5rem', | |
| background: 'var(--bg-secondary)', color: 'var(--text-primary)', fontSize: '0.85rem', | |
| }}> | |
| <option value="1y">1 Year</option> | |
| <option value="2y">2 Years</option> | |
| <option value="3y">3 Years</option> | |
| <option value="5y">5 Years</option> | |
| </select> | |
| <select value={horizon} onChange={e => setHorizon(Number(e.target.value))} style={{ | |
| padding: '0.55rem 0.75rem', border: '1px solid var(--border-subtle)', borderRadius: '0.5rem', | |
| background: 'var(--bg-secondary)', color: 'var(--text-primary)', fontSize: '0.85rem', | |
| }}> | |
| <option value={1}>1-Day Horizon</option> | |
| <option value={3}>3-Day Horizon</option> | |
| <option value={5}>5-Day Horizon</option> | |
| <option value={10}>10-Day Horizon</option> | |
| <option value={20}>20-Day Horizon</option> | |
| </select> | |
| <button | |
| onClick={activeTab === 'scanner' ? handleAnalyze : handleBacktestAccuracy} | |
| disabled={loading || !ticker.trim()} | |
| style={{ | |
| padding: '0.55rem 1.2rem', borderRadius: '0.5rem', border: 'none', | |
| background: 'var(--accent-green)', color: '#fff', fontWeight: 600, | |
| fontSize: '0.85rem', cursor: loading ? 'wait' : 'pointer', | |
| opacity: loading || !ticker.trim() ? 0.6 : 1, | |
| }} | |
| > | |
| {loading ? 'Analyzing...' : activeTab === 'scanner' ? 'Analyze' : 'Run Accuracy Test'} | |
| </button> | |
| </div> | |
| )} | |
| {error && ( | |
| <div style={{ padding: '0.75rem 1rem', borderRadius: '0.5rem', background: 'rgba(239,68,68,0.1)', color: '#ef4444', fontSize: '0.8rem', marginBottom: '1rem', border: '1px solid rgba(239,68,68,0.2)' }}> | |
| {error} | |
| </div> | |
| )} | |
| {/* ββ Scanner Tab ββββββββββββββββββββββββββββββββββββββββββββββββ */} | |
| {activeTab === 'scanner' && analysis && ( | |
| <div style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))' }}> | |
| {/* Prediction Card */} | |
| <div className="card" style={{ padding: '1.25rem', gridColumn: 'span 2' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}> | |
| <div> | |
| <div style={{ fontSize: '0.7rem', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)', fontWeight: 600 }}>Prediction</div> | |
| <div style={{ fontSize: '1.5rem', fontWeight: 700, color: directionColor(analysis.prediction), display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| {analysis.prediction === 'strong_up' && <ArrowUp />} | |
| {analysis.prediction === 'strong_down' && <ArrowDown />} | |
| {analysis.prediction_label} | |
| </div> | |
| </div> | |
| <div style={{ textAlign: 'right' }}> | |
| <div style={{ fontSize: '0.7rem', textTransform: 'uppercase', color: 'var(--text-muted)', fontWeight: 600 }}>Confidence</div> | |
| <div style={{ fontSize: '1.5rem', fontWeight: 700, color: confidenceColor(analysis.confidence) }}> | |
| {(analysis.confidence * 100).toFixed(1)}% | |
| </div> | |
| <div style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>{analysis.confidence_level} confidence</div> | |
| </div> | |
| </div> | |
| {/* Probability Bar */} | |
| <div style={{ marginBottom: '0.75rem' }}> | |
| <div style={{ display: 'flex', fontSize: '0.7rem', color: 'var(--text-muted)', marginBottom: '0.25rem' }}> | |
| <span>Bearish</span><span style={{ marginLeft: 'auto' }}>Bullish</span> | |
| </div> | |
| <div style={{ display: 'flex', height: 8, borderRadius: 4, overflow: 'hidden', gap: 1 }}> | |
| <div style={{ width: `${analysis.probabilities.strong_down * 100}%`, background: '#ef4444' }} /> | |
| <div style={{ width: `${analysis.probabilities.neutral * 100}%`, background: '#6b7280' }} /> | |
| <div style={{ width: `${analysis.probabilities.strong_up * 100}%`, background: 'var(--accent-green)' }} /> | |
| </div> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.65rem', color: 'var(--text-muted)', marginTop: '0.2rem' }}> | |
| <span>{(analysis.probabilities.strong_down * 100).toFixed(1)}%</span> | |
| <span>{(analysis.probabilities.neutral * 100).toFixed(1)}%</span> | |
| <span>{(analysis.probabilities.strong_up * 100).toFixed(1)}%</span> | |
| </div> | |
| </div> | |
| {/* Stats Row */} | |
| <div style={{ display: 'flex', gap: '1.5rem', fontSize: '0.8rem', flexWrap: 'wrap' }}> | |
| <div><span style={{ color: 'var(--text-muted)' }}>Price: </span><strong>{formatCurrency(analysis.current_price, detectCurrencyFromTicker(ticker))}</strong></div> | |
| <div><span style={{ color: 'var(--text-muted)' }}>Expected Return: </span><strong style={{ color: directionColor(analysis.prediction) }}>{analysis.expected_return_pct > 0 ? '+' : ''}{analysis.expected_return_pct}%</strong></div> | |
| <div><span style={{ color: 'var(--text-muted)' }}>Horizon: </span><strong>{analysis.horizon_days}D</strong></div> | |
| <div><span style={{ color: 'var(--text-muted)' }}>Accuracy: </span><strong>{(analysis.model_metrics?.accuracy * 100).toFixed(1)}%</strong></div> | |
| <div><span style={{ color: 'var(--text-muted)' }}>F1: </span><strong>{analysis.model_metrics?.f1?.toFixed(4)}</strong></div> | |
| </div> | |
| </div> | |
| {/* Hedge Recommendation Card */} | |
| {analysis.hedge_recommendation && ( | |
| <div className="card" style={{ padding: '1.25rem', gridColumn: 'span 2', borderLeft: `4px solid ${analysis.hedge_recommendation.urgency === 'high' ? '#ef4444' : analysis.hedge_recommendation.urgency === 'medium' ? '#f59e0b' : 'var(--accent-green)'}` }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}> | |
| <div style={{ fontSize: '0.75rem', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)' }}>Hedge Recommendation</div> | |
| <span style={{ | |
| fontSize: '0.65rem', padding: '0.15rem 0.5rem', borderRadius: 20, fontWeight: 700, | |
| background: analysis.hedge_recommendation.urgency === 'high' ? 'rgba(239,68,68,0.15)' : analysis.hedge_recommendation.urgency === 'medium' ? 'rgba(245,158,11,0.15)' : 'rgba(16,185,129,0.15)', | |
| color: analysis.hedge_recommendation.urgency === 'high' ? '#ef4444' : analysis.hedge_recommendation.urgency === 'medium' ? '#f59e0b' : 'var(--accent-green)', | |
| }}>{analysis.hedge_recommendation.action?.replace(/_/g, ' ').toUpperCase()}</span> | |
| </div> | |
| <div style={{ display: 'grid', gap: '0.75rem', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', marginBottom: '0.75rem' }}> | |
| <div style={{ padding: '0.5rem 0.75rem', borderRadius: '0.4rem', background: 'var(--bg-tertiary, var(--bg-secondary))' }}> | |
| <div style={{ fontSize: '0.6rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Suggested Hedge</div> | |
| <div style={{ fontSize: '1.1rem', fontWeight: 700 }}>{analysis.hedge_recommendation.suggested_hedge_pct}%</div> | |
| </div> | |
| <div style={{ padding: '0.5rem 0.75rem', borderRadius: '0.4rem', background: 'var(--bg-tertiary, var(--bg-secondary))' }}> | |
| <div style={{ fontSize: '0.6rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Pattern Consensus</div> | |
| <div style={{ fontSize: '1.1rem', fontWeight: 700, color: directionColor(analysis.hedge_recommendation.pattern_consensus === 'bearish' ? 'bearish' : analysis.hedge_recommendation.pattern_consensus === 'bullish' ? 'bullish' : 'neutral') }}> | |
| {analysis.hedge_recommendation.pattern_consensus?.charAt(0).toUpperCase() + analysis.hedge_recommendation.pattern_consensus?.slice(1)} | |
| </div> | |
| </div> | |
| <div style={{ padding: '0.5rem 0.75rem', borderRadius: '0.4rem', background: 'var(--bg-tertiary, var(--bg-secondary))' }}> | |
| <div style={{ fontSize: '0.6rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Bearish Signals</div> | |
| <div style={{ fontSize: '1.1rem', fontWeight: 700, color: '#ef4444' }}>{analysis.hedge_recommendation.bearish_pattern_count}</div> | |
| </div> | |
| <div style={{ padding: '0.5rem 0.75rem', borderRadius: '0.4rem', background: 'var(--bg-tertiary, var(--bg-secondary))' }}> | |
| <div style={{ fontSize: '0.6rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Bullish Signals</div> | |
| <div style={{ fontSize: '1.1rem', fontWeight: 700, color: 'var(--accent-green)' }}>{analysis.hedge_recommendation.bullish_pattern_count}</div> | |
| </div> | |
| </div> | |
| <div style={{ padding: '0.6rem 0.85rem', borderRadius: '0.4rem', background: 'var(--bg-secondary)', fontSize: '0.78rem', color: 'var(--text-secondary)', lineHeight: 1.5, marginBottom: analysis.hedge_recommendation.instruments?.length ? '0.75rem' : 0 }}> | |
| {analysis.hedge_recommendation.rationale} | |
| </div> | |
| {analysis.hedge_recommendation.instruments?.length > 0 && ( | |
| <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}> | |
| {analysis.hedge_recommendation.instruments.map((inst: string, i: number) => ( | |
| <span key={i} style={{ fontSize: '0.7rem', padding: '0.2rem 0.5rem', borderRadius: 4, background: 'rgba(99,102,241,0.1)', color: '#6366f1', fontWeight: 600 }}>{inst}</span> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Detected Patterns */} | |
| <div className="card" style={{ padding: '1.25rem' }}> | |
| <div style={{ fontSize: '0.75rem', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)', marginBottom: '0.75rem' }}> | |
| Detected Patterns ({analysis.detected_patterns?.length || 0}) | |
| </div> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', maxHeight: 350, overflow: 'auto' }}> | |
| {(analysis.detected_patterns || []).map((p: any, i: number) => ( | |
| <div key={i} style={{ | |
| display: 'flex', alignItems: 'center', justifyContent: 'space-between', | |
| padding: '0.5rem 0.75rem', borderRadius: '0.4rem', | |
| background: 'var(--bg-tertiary, var(--bg-secondary))', | |
| border: '1px solid var(--border-subtle)', | |
| }}> | |
| <div> | |
| <div style={{ fontSize: '0.8rem', fontWeight: 600 }}>{p.name}</div> | |
| <div style={{ fontSize: '0.65rem', color: 'var(--text-muted)' }}>{p.category}</div> | |
| </div> | |
| <div style={{ textAlign: 'right' }}> | |
| <div style={{ fontSize: '0.75rem', fontWeight: 600, color: directionColor(p.direction) }}> | |
| {p.direction === 'bullish' ? 'Bullish' : p.direction === 'bearish' ? 'Bearish' : 'Neutral'} | |
| </div> | |
| <div style={{ fontSize: '0.65rem', color: 'var(--text-muted)' }}> | |
| {(p.reliability * 100).toFixed(0)}% reliability | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| {(!analysis.detected_patterns || analysis.detected_patterns.length === 0) && ( | |
| <div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', textAlign: 'center', padding: '1rem' }}> | |
| No patterns detected in recent candles | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Top Features */} | |
| <div className="card" style={{ padding: '1.25rem' }}> | |
| <div style={{ fontSize: '0.75rem', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)', marginBottom: '0.75rem' }}> | |
| Top Feature Importances | |
| </div> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.3rem' }}> | |
| {(analysis.top_features || []).slice(0, 10).map((f: any, i: number) => { | |
| const maxImp = analysis.top_features[0]?.importance || 1; | |
| return ( | |
| <div key={i} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem' }}> | |
| <span style={{ width: 130, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: 'var(--text-secondary)' }}> | |
| {f.name} | |
| </span> | |
| <div style={{ flex: 1, height: 6, borderRadius: 3, background: 'var(--bg-tertiary, var(--bg-secondary))' }}> | |
| <div style={{ width: `${(f.importance / maxImp) * 100}%`, height: '100%', borderRadius: 3, background: 'var(--accent-green)' }} /> | |
| </div> | |
| <span style={{ width: 45, textAlign: 'right', fontSize: '0.65rem', color: 'var(--text-muted)' }}> | |
| {f.importance.toFixed(1)} | |
| </span> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* Advanced Features */} | |
| <div className="card" style={{ padding: '1.25rem', gridColumn: 'span 2' }}> | |
| <div style={{ fontSize: '0.75rem', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)', marginBottom: '0.75rem' }}> | |
| Advanced Mathematical Features | |
| </div> | |
| <div style={{ display: 'grid', gap: '0.75rem', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))' }}> | |
| {analysis.advanced_features && Object.entries(analysis.advanced_features).filter(([_, v]) => typeof v === 'number').map(([key, val]: [string, any]) => ( | |
| <div key={key} style={{ padding: '0.5rem 0.75rem', borderRadius: '0.4rem', background: 'var(--bg-tertiary, var(--bg-secondary))', border: '1px solid var(--border-subtle)' }}> | |
| <div style={{ fontSize: '0.6rem', textTransform: 'uppercase', color: 'var(--text-muted)', letterSpacing: '0.03em' }}> | |
| {key.replace(/_/g, ' ')} | |
| </div> | |
| <div style={{ fontSize: '1rem', fontWeight: 700 }}>{typeof val === 'number' ? val.toFixed(4) : val}</div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* ββ Catalog Tab ββββββββββββββββββββββββββββββββββββββββββββββββ */} | |
| {activeTab === 'catalog' && catalog && ( | |
| <div style={{ display: 'grid', gap: '0.5rem', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))' }}> | |
| {(catalog.patterns || []).map((p: any, i: number) => ( | |
| <div key={i} className="card" style={{ padding: '0.75rem 1rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |
| <div style={{ flex: 1 }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.2rem' }}> | |
| <span style={{ fontSize: '0.85rem', fontWeight: 600 }}>{p.name}</span> | |
| <span style={{ | |
| fontSize: '0.6rem', padding: '0.1rem 0.4rem', borderRadius: 20, | |
| background: p.category === 'single' ? 'rgba(59,130,246,0.1)' : p.category === 'multi' ? 'rgba(168,85,247,0.1)' : 'rgba(251,146,60,0.1)', | |
| color: p.category === 'single' ? '#3b82f6' : p.category === 'multi' ? '#a855f7' : '#fb923c', | |
| fontWeight: 600, | |
| }}>{p.category}</span> | |
| </div> | |
| <div style={{ fontSize: '0.7rem', color: 'var(--text-muted)', lineHeight: 1.4 }}>{p.description}</div> | |
| </div> | |
| <div style={{ textAlign: 'right', minWidth: 80, marginLeft: '0.75rem' }}> | |
| <div style={{ fontSize: '0.75rem', fontWeight: 600, color: directionColor(p.direction) }}> | |
| {p.direction === 'bullish' ? 'Bullish' : p.direction === 'bearish' ? 'Bearish' : 'Neutral'} | |
| </div> | |
| <div style={{ fontSize: '0.65rem', color: 'var(--text-muted)' }}>{(p.reliability * 100).toFixed(0)}% reliable</div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {/* ββ Accuracy Tab βββββββββββββββββββββββββββββββββββββββββββββββ */} | |
| {activeTab === 'accuracy' && accuracy && ( | |
| <div> | |
| <div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem', flexWrap: 'wrap' }}> | |
| <div className="card" style={{ padding: '0.75rem 1rem', flex: 1, minWidth: 120 }}> | |
| <div style={{ fontSize: '0.6rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Bars Analyzed</div> | |
| <div style={{ fontSize: '1.25rem', fontWeight: 700 }}>{accuracy.total_bars_analyzed}</div> | |
| </div> | |
| <div className="card" style={{ padding: '0.75rem 1rem', flex: 1, minWidth: 120 }}> | |
| <div style={{ fontSize: '0.6rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Patterns Found</div> | |
| <div style={{ fontSize: '1.25rem', fontWeight: 700 }}>{accuracy.patterns_found}</div> | |
| </div> | |
| <div className="card" style={{ padding: '0.75rem 1rem', flex: 1, minWidth: 120 }}> | |
| <div style={{ fontSize: '0.6rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Horizon</div> | |
| <div style={{ fontSize: '1.25rem', fontWeight: 700 }}>{accuracy.horizon_days}D</div> | |
| </div> | |
| </div> | |
| <div className="card" style={{ padding: 0, overflow: 'hidden' }}> | |
| <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}> | |
| <thead> | |
| <tr style={{ background: 'var(--bg-secondary)', borderBottom: '1px solid var(--border-subtle)' }}> | |
| <th style={{ padding: '0.6rem 1rem', textAlign: 'left', fontWeight: 600, fontSize: '0.7rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Pattern</th> | |
| <th style={{ padding: '0.6rem 0.75rem', textAlign: 'center', fontWeight: 600, fontSize: '0.7rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Direction</th> | |
| <th style={{ padding: '0.6rem 0.75rem', textAlign: 'center', fontWeight: 600, fontSize: '0.7rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Occurrences</th> | |
| <th style={{ padding: '0.6rem 0.75rem', textAlign: 'center', fontWeight: 600, fontSize: '0.7rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Win Rate</th> | |
| <th style={{ padding: '0.6rem 0.75rem', textAlign: 'center', fontWeight: 600, fontSize: '0.7rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Avg Return</th> | |
| <th style={{ padding: '0.6rem 0.75rem', textAlign: 'center', fontWeight: 600, fontSize: '0.7rem', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Actual vs Theoretical</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {(accuracy.accuracy_report || []).map((r: any, i: number) => ( | |
| <tr key={i} style={{ borderBottom: '1px solid var(--border-subtle)' }}> | |
| <td style={{ padding: '0.5rem 1rem', fontWeight: 600 }}>{r.pattern}</td> | |
| <td style={{ padding: '0.5rem 0.75rem', textAlign: 'center', color: directionColor(r.direction) }}>{r.direction}</td> | |
| <td style={{ padding: '0.5rem 0.75rem', textAlign: 'center' }}>{r.occurrences}</td> | |
| <td style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, color: r.win_rate >= 0.5 ? 'var(--accent-green)' : '#ef4444' }}> | |
| {(r.win_rate * 100).toFixed(1)}% | |
| </td> | |
| <td style={{ padding: '0.5rem 0.75rem', textAlign: 'center', color: r.avg_return_pct >= 0 ? 'var(--accent-green)' : '#ef4444' }}> | |
| {r.avg_return_pct >= 0 ? '+' : ''}{r.avg_return_pct.toFixed(2)}% | |
| </td> | |
| <td style={{ padding: '0.5rem 0.75rem', textAlign: 'center', color: r.actual_vs_theoretical >= 0 ? 'var(--accent-green)' : '#ef4444' }}> | |
| {r.actual_vs_theoretical >= 0 ? '+' : ''}{(r.actual_vs_theoretical * 100).toFixed(1)}% | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {/* Empty state */} | |
| {activeTab === 'scanner' && !analysis && !loading && ( | |
| <div style={{ textAlign: 'center', padding: '4rem 2rem', color: 'var(--text-muted)' }}> | |
| <ChartIcon /> | |
| <p style={{ fontSize: '0.9rem', marginTop: '0.75rem' }}>Enter a ticker symbol and click Analyze to detect candlestick patterns and get AI predictions</p> | |
| <p style={{ fontSize: '0.75rem' }}>Supports all markets: US equities, Indian stocks (.NS), crypto, and more</p> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |