[OQI-003] feat: Add chart analysis and trading journal components

- IndicatorConfigPanel: Advanced technical indicator configuration
- ChartDrawingToolsPanel: Drawing tools for technical analysis
- SymbolInfoPanel: Comprehensive symbol information sidebar
- TradeJournalPanel: Trade review and journaling for paper trading

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 11:59:12 -06:00
parent 51c0a846c0
commit 7ac32466be
5 changed files with 1975 additions and 0 deletions

View File

@ -0,0 +1,419 @@
/**
* ChartDrawingToolsPanel Component
* Drawing tools for technical analysis on charts
* OQI-003: Trading y Charts
*/
import React, { useState, useMemo } from 'react';
import {
Pencil,
TrendingUp,
Minus,
Square,
Circle,
Type,
Trash2,
RotateCcw,
Layers,
Eye,
EyeOff,
Lock,
Unlock,
Palette,
ChevronDown,
ChevronUp,
Copy,
ArrowUpRight,
Triangle,
Hash,
} from 'lucide-react';
export type DrawingTool =
| 'trendline'
| 'horizontal'
| 'vertical'
| 'rectangle'
| 'circle'
| 'fibonacci'
| 'text'
| 'arrow'
| 'channel'
| 'pitchfork';
export interface Drawing {
id: string;
tool: DrawingTool;
color: string;
lineWidth: number;
visible: boolean;
locked: boolean;
label?: string;
points: { x: number; y: number; price?: number; time?: string }[];
createdAt: string;
}
interface ChartDrawingToolsPanelProps {
drawings?: Drawing[];
activeTool?: DrawingTool | null;
onToolSelect?: (tool: DrawingTool | null) => void;
onDrawingAdd?: (drawing: Drawing) => void;
onDrawingUpdate?: (drawing: Drawing) => void;
onDrawingRemove?: (drawingId: string) => void;
onDrawingsVisibilityToggle?: (visible: boolean) => void;
onClearAll?: () => void;
onUndo?: () => void;
compact?: boolean;
}
const DRAWING_TOOLS: { tool: DrawingTool; label: string; icon: React.ReactNode; description: string }[] = [
{ tool: 'trendline', label: 'Trendline', icon: <TrendingUp className="w-4 h-4" />, description: 'Draw trend lines' },
{ tool: 'horizontal', label: 'Horizontal', icon: <Minus className="w-4 h-4" />, description: 'Support/resistance levels' },
{ tool: 'vertical', label: 'Vertical', icon: <Minus className="w-4 h-4 rotate-90" />, description: 'Mark time points' },
{ tool: 'rectangle', label: 'Rectangle', icon: <Square className="w-4 h-4" />, description: 'Price zones' },
{ tool: 'circle', label: 'Circle', icon: <Circle className="w-4 h-4" />, description: 'Highlight areas' },
{ tool: 'fibonacci', label: 'Fibonacci', icon: <Hash className="w-4 h-4" />, description: 'Fib retracement' },
{ tool: 'text', label: 'Text', icon: <Type className="w-4 h-4" />, description: 'Add annotations' },
{ tool: 'arrow', label: 'Arrow', icon: <ArrowUpRight className="w-4 h-4" />, description: 'Direction markers' },
{ tool: 'channel', label: 'Channel', icon: <Layers className="w-4 h-4" />, description: 'Price channels' },
{ tool: 'pitchfork', label: 'Pitchfork', icon: <Triangle className="w-4 h-4" />, description: 'Andrews pitchfork' },
];
const PRESET_COLORS = [
'#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',
'#EC4899', '#06B6D4', '#84CC16', '#F97316', '#FFFFFF',
];
const ChartDrawingToolsPanel: React.FC<ChartDrawingToolsPanelProps> = ({
drawings = [],
activeTool = null,
onToolSelect,
onDrawingAdd,
onDrawingUpdate,
onDrawingRemove,
onDrawingsVisibilityToggle,
onClearAll,
onUndo,
compact = false,
}) => {
const [selectedColor, setSelectedColor] = useState('#3B82F6');
const [lineWidth, setLineWidth] = useState(2);
const [showDrawingsList, setShowDrawingsList] = useState(false);
const [expandedDrawingId, setExpandedDrawingId] = useState<string | null>(null);
const [allVisible, setAllVisible] = useState(true);
const visibleCount = useMemo(() => drawings.filter(d => d.visible).length, [drawings]);
const handleToolClick = (tool: DrawingTool) => {
if (activeTool === tool) {
onToolSelect?.(null);
} else {
onToolSelect?.(tool);
}
};
const handleToggleVisibility = (drawing: Drawing) => {
onDrawingUpdate?.({ ...drawing, visible: !drawing.visible });
};
const handleToggleLock = (drawing: Drawing) => {
onDrawingUpdate?.({ ...drawing, locked: !drawing.locked });
};
const handleUpdateColor = (drawing: Drawing, color: string) => {
onDrawingUpdate?.({ ...drawing, color });
};
const handleDuplicateDrawing = (drawing: Drawing) => {
const newDrawing: Drawing = {
...drawing,
id: `draw_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
createdAt: new Date().toISOString(),
locked: false,
points: drawing.points.map(p => ({ ...p, y: p.y + 10 })),
};
onDrawingAdd?.(newDrawing);
};
const handleToggleAllVisibility = () => {
const newVisible = !allVisible;
setAllVisible(newVisible);
onDrawingsVisibilityToggle?.(newVisible);
};
const getToolLabel = (tool: DrawingTool) => {
return DRAWING_TOOLS.find(t => t.tool === tool)?.label || tool;
};
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
};
return (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<Pencil className="w-5 h-5 text-blue-400" />
<div>
<h3 className="font-semibold text-white">Drawing Tools</h3>
<p className="text-xs text-gray-500">{visibleCount} of {drawings.length} visible</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={onUndo}
disabled={drawings.length === 0}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors disabled:opacity-50"
title="Undo"
>
<RotateCcw className="w-4 h-4" />
</button>
<button
onClick={handleToggleAllVisibility}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors"
title={allVisible ? 'Hide All' : 'Show All'}
>
{allVisible ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</button>
<button
onClick={onClearAll}
disabled={drawings.length === 0}
className="p-2 text-gray-400 hover:text-red-400 hover:bg-gray-700 rounded-lg transition-colors disabled:opacity-50"
title="Clear All"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
{/* Tool Grid */}
<div className="grid grid-cols-5 gap-2 mb-4">
{DRAWING_TOOLS.map(({ tool, label, icon, description }) => (
<button
key={tool}
onClick={() => handleToolClick(tool)}
className={`flex flex-col items-center gap-1 p-2 rounded-lg transition-all ${
activeTool === tool
? 'bg-blue-600 text-white ring-2 ring-blue-400'
: 'bg-gray-900/50 text-gray-400 hover:bg-gray-700 hover:text-white'
}`}
title={description}
>
{icon}
<span className="text-[10px]">{label}</span>
</button>
))}
</div>
{/* Active Tool Options */}
{activeTool && (
<div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg mb-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm text-blue-400 font-medium">
{getToolLabel(activeTool)} Selected
</span>
<button
onClick={() => onToolSelect?.(null)}
className="text-xs text-blue-400 hover:text-blue-300"
>
Cancel
</button>
</div>
<div className="flex items-center gap-4">
{/* Color Picker */}
<div>
<label className="block text-xs text-gray-500 mb-1">Color</label>
<div className="flex gap-1">
{PRESET_COLORS.slice(0, 5).map((color) => (
<button
key={color}
onClick={() => setSelectedColor(color)}
className={`w-6 h-6 rounded-full border-2 transition-all ${
selectedColor === color ? 'border-white scale-110' : 'border-transparent'
}`}
style={{ backgroundColor: color }}
/>
))}
</div>
</div>
{/* Line Width */}
<div>
<label className="block text-xs text-gray-500 mb-1">Width</label>
<div className="flex gap-1">
{[1, 2, 3, 4].map((width) => (
<button
key={width}
onClick={() => setLineWidth(width)}
className={`w-7 h-6 rounded text-xs font-medium ${
lineWidth === width
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{width}
</button>
))}
</div>
</div>
</div>
<p className="text-xs text-gray-500 mt-2">
Click on the chart to start drawing
</p>
</div>
)}
{/* Drawings List */}
<div>
<button
onClick={() => setShowDrawingsList(!showDrawingsList)}
className="flex items-center justify-between w-full p-2 bg-gray-900/50 rounded-lg hover:bg-gray-800 transition-colors"
>
<div className="flex items-center gap-2">
<Layers className="w-4 h-4 text-gray-400" />
<span className="text-sm text-white">Drawings ({drawings.length})</span>
</div>
{showDrawingsList ? (
<ChevronUp className="w-4 h-4 text-gray-500" />
) : (
<ChevronDown className="w-4 h-4 text-gray-500" />
)}
</button>
{showDrawingsList && (
<div className="mt-2 space-y-1 max-h-[200px] overflow-y-auto">
{drawings.length === 0 ? (
<div className="text-center py-4">
<p className="text-gray-500 text-xs">No drawings yet</p>
</div>
) : (
drawings.map((drawing) => {
const toolDef = DRAWING_TOOLS.find(t => t.tool === drawing.tool);
const isExpanded = expandedDrawingId === drawing.id;
return (
<div
key={drawing.id}
className={`bg-gray-900/50 rounded-lg border transition-all ${
drawing.visible ? 'border-gray-700' : 'border-gray-800 opacity-60'
}`}
>
<div
onClick={() => setExpandedDrawingId(isExpanded ? null : drawing.id)}
className="flex items-center justify-between p-2 cursor-pointer hover:bg-gray-800/50"
>
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: drawing.color }}
/>
<div className="p-1 bg-gray-800 rounded" style={{ color: drawing.color }}>
{toolDef?.icon}
</div>
<div>
<span className="text-sm text-white">{toolDef?.label}</span>
{drawing.label && (
<span className="text-xs text-gray-500 ml-2">{drawing.label}</span>
)}
</div>
</div>
<div className="flex items-center gap-1">
<button
onClick={(e) => {
e.stopPropagation();
handleToggleVisibility(drawing);
}}
className="p-1 text-gray-500 hover:text-white"
>
{drawing.visible ? <Eye className="w-3 h-3" /> : <EyeOff className="w-3 h-3" />}
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleToggleLock(drawing);
}}
className="p-1 text-gray-500 hover:text-white"
>
{drawing.locked ? <Lock className="w-3 h-3" /> : <Unlock className="w-3 h-3" />}
</button>
{isExpanded ? (
<ChevronUp className="w-3 h-3 text-gray-500" />
) : (
<ChevronDown className="w-3 h-3 text-gray-500" />
)}
</div>
</div>
{isExpanded && (
<div className="px-2 pb-2 pt-1 border-t border-gray-800">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs text-gray-500">Color:</span>
<div className="flex gap-1">
{PRESET_COLORS.slice(0, 5).map((color) => (
<button
key={color}
onClick={() => handleUpdateColor(drawing, color)}
className={`w-4 h-4 rounded-full border transition-all ${
drawing.color === color ? 'border-white' : 'border-transparent'
}`}
style={{ backgroundColor: color }}
/>
))}
</div>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-gray-500">
Created: {formatTime(drawing.createdAt)}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => handleDuplicateDrawing(drawing)}
className="p-1 text-gray-400 hover:text-white"
title="Duplicate"
>
<Copy className="w-3 h-3" />
</button>
<button
onClick={() => onDrawingRemove?.(drawing.id)}
disabled={drawing.locked}
className="p-1 text-gray-400 hover:text-red-400 disabled:opacity-50"
title="Delete"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
</div>
)}
</div>
);
})
)}
</div>
)}
</div>
{/* Quick Tips */}
{!compact && (
<div className="mt-4 pt-4 border-t border-gray-700">
<div className="text-xs text-gray-500">
<p className="mb-1"><strong>Tips:</strong></p>
<ul className="space-y-0.5 text-gray-600">
<li> Click and drag to draw</li>
<li> Double-click to finish</li>
<li> Hold Shift for straight lines</li>
</ul>
</div>
</div>
)}
</div>
);
};
export default ChartDrawingToolsPanel;

View File

@ -0,0 +1,506 @@
/**
* IndicatorConfigPanel Component
* Advanced technical indicator configuration and management
* OQI-003: Trading y Charts
*/
import React, { useState, useMemo } from 'react';
import {
Settings,
Plus,
Trash2,
Save,
RotateCcw,
ChevronDown,
ChevronUp,
TrendingUp,
Activity,
BarChart3,
Waves,
Target,
Eye,
EyeOff,
Palette,
Copy,
} from 'lucide-react';
export type IndicatorType = 'sma' | 'ema' | 'rsi' | 'macd' | 'bollinger' | 'atr' | 'stochastic' | 'vwap';
export interface IndicatorConfig {
id: string;
type: IndicatorType;
enabled: boolean;
params: Record<string, number>;
color: string;
lineWidth: number;
}
interface IndicatorPreset {
name: string;
indicators: Omit<IndicatorConfig, 'id'>[];
}
interface IndicatorConfigPanelProps {
indicators?: IndicatorConfig[];
onIndicatorChange?: (indicators: IndicatorConfig[]) => void;
onIndicatorAdd?: (indicator: IndicatorConfig) => void;
onIndicatorRemove?: (indicatorId: string) => void;
onPresetSave?: (preset: IndicatorPreset) => void;
onPresetLoad?: (preset: IndicatorPreset) => void;
savedPresets?: IndicatorPreset[];
compact?: boolean;
}
const INDICATOR_DEFINITIONS: Record<IndicatorType, {
label: string;
icon: React.ReactNode;
defaultParams: Record<string, number>;
paramLabels: Record<string, string>;
color: string;
}> = {
sma: {
label: 'Simple Moving Average',
icon: <TrendingUp className="w-4 h-4" />,
defaultParams: { period: 20 },
paramLabels: { period: 'Period' },
color: '#3B82F6',
},
ema: {
label: 'Exponential Moving Average',
icon: <TrendingUp className="w-4 h-4" />,
defaultParams: { period: 12 },
paramLabels: { period: 'Period' },
color: '#10B981',
},
rsi: {
label: 'Relative Strength Index',
icon: <Activity className="w-4 h-4" />,
defaultParams: { period: 14, overbought: 70, oversold: 30 },
paramLabels: { period: 'Period', overbought: 'Overbought', oversold: 'Oversold' },
color: '#8B5CF6',
},
macd: {
label: 'MACD',
icon: <BarChart3 className="w-4 h-4" />,
defaultParams: { fastPeriod: 12, slowPeriod: 26, signalPeriod: 9 },
paramLabels: { fastPeriod: 'Fast', slowPeriod: 'Slow', signalPeriod: 'Signal' },
color: '#F59E0B',
},
bollinger: {
label: 'Bollinger Bands',
icon: <Waves className="w-4 h-4" />,
defaultParams: { period: 20, stdDev: 2 },
paramLabels: { period: 'Period', stdDev: 'Std Dev' },
color: '#EC4899',
},
atr: {
label: 'Average True Range',
icon: <Target className="w-4 h-4" />,
defaultParams: { period: 14 },
paramLabels: { period: 'Period' },
color: '#EF4444',
},
stochastic: {
label: 'Stochastic Oscillator',
icon: <Activity className="w-4 h-4" />,
defaultParams: { kPeriod: 14, dPeriod: 3, smooth: 3 },
paramLabels: { kPeriod: '%K Period', dPeriod: '%D Period', smooth: 'Smooth' },
color: '#06B6D4',
},
vwap: {
label: 'Volume Weighted Avg Price',
icon: <BarChart3 className="w-4 h-4" />,
defaultParams: { period: 1 },
paramLabels: { period: 'Days' },
color: '#84CC16',
},
};
const PRESET_COLORS = [
'#3B82F6', '#10B981', '#8B5CF6', '#F59E0B', '#EC4899',
'#EF4444', '#06B6D4', '#84CC16', '#F97316', '#6366F1',
];
const DEFAULT_PRESETS: IndicatorPreset[] = [
{
name: 'Trend Following',
indicators: [
{ type: 'sma', enabled: true, params: { period: 50 }, color: '#3B82F6', lineWidth: 2 },
{ type: 'sma', enabled: true, params: { period: 200 }, color: '#EF4444', lineWidth: 2 },
{ type: 'macd', enabled: true, params: { fastPeriod: 12, slowPeriod: 26, signalPeriod: 9 }, color: '#F59E0B', lineWidth: 1 },
],
},
{
name: 'Momentum',
indicators: [
{ type: 'rsi', enabled: true, params: { period: 14, overbought: 70, oversold: 30 }, color: '#8B5CF6', lineWidth: 1 },
{ type: 'stochastic', enabled: true, params: { kPeriod: 14, dPeriod: 3, smooth: 3 }, color: '#06B6D4', lineWidth: 1 },
],
},
{
name: 'Volatility',
indicators: [
{ type: 'bollinger', enabled: true, params: { period: 20, stdDev: 2 }, color: '#EC4899', lineWidth: 1 },
{ type: 'atr', enabled: true, params: { period: 14 }, color: '#EF4444', lineWidth: 1 },
],
},
];
const IndicatorConfigPanel: React.FC<IndicatorConfigPanelProps> = ({
indicators = [],
onIndicatorChange,
onIndicatorAdd,
onIndicatorRemove,
onPresetSave,
onPresetLoad,
savedPresets = DEFAULT_PRESETS,
compact = false,
}) => {
const [expandedId, setExpandedId] = useState<string | null>(null);
const [showAddMenu, setShowAddMenu] = useState(false);
const [showPresets, setShowPresets] = useState(false);
const [presetName, setPresetName] = useState('');
const generateId = () => `ind_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const handleAddIndicator = (type: IndicatorType) => {
const def = INDICATOR_DEFINITIONS[type];
const newIndicator: IndicatorConfig = {
id: generateId(),
type,
enabled: true,
params: { ...def.defaultParams },
color: def.color,
lineWidth: 2,
};
onIndicatorAdd?.(newIndicator);
setShowAddMenu(false);
};
const handleToggleEnabled = (id: string) => {
const updated = indicators.map(ind =>
ind.id === id ? { ...ind, enabled: !ind.enabled } : ind
);
onIndicatorChange?.(updated);
};
const handleParamChange = (id: string, paramKey: string, value: number) => {
const updated = indicators.map(ind =>
ind.id === id ? { ...ind, params: { ...ind.params, [paramKey]: value } } : ind
);
onIndicatorChange?.(updated);
};
const handleColorChange = (id: string, color: string) => {
const updated = indicators.map(ind =>
ind.id === id ? { ...ind, color } : ind
);
onIndicatorChange?.(updated);
};
const handleLineWidthChange = (id: string, lineWidth: number) => {
const updated = indicators.map(ind =>
ind.id === id ? { ...ind, lineWidth } : ind
);
onIndicatorChange?.(updated);
};
const handleResetDefaults = (id: string) => {
const indicator = indicators.find(ind => ind.id === id);
if (!indicator) return;
const def = INDICATOR_DEFINITIONS[indicator.type];
const updated = indicators.map(ind =>
ind.id === id ? { ...ind, params: { ...def.defaultParams }, color: def.color, lineWidth: 2 } : ind
);
onIndicatorChange?.(updated);
};
const handleDuplicateIndicator = (indicator: IndicatorConfig) => {
const newIndicator: IndicatorConfig = {
...indicator,
id: generateId(),
color: PRESET_COLORS[Math.floor(Math.random() * PRESET_COLORS.length)],
};
onIndicatorAdd?.(newIndicator);
};
const handleSavePreset = () => {
if (!presetName.trim()) return;
const preset: IndicatorPreset = {
name: presetName,
indicators: indicators.map(({ id, ...rest }) => rest),
};
onPresetSave?.(preset);
setPresetName('');
setShowPresets(false);
};
const handleLoadPreset = (preset: IndicatorPreset) => {
onPresetLoad?.(preset);
setShowPresets(false);
};
const enabledCount = useMemo(() => indicators.filter(i => i.enabled).length, [indicators]);
return (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<Settings className="w-5 h-5 text-blue-400" />
<div>
<h3 className="font-semibold text-white">Indicators</h3>
<p className="text-xs text-gray-500">{enabledCount} active</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Presets Button */}
<div className="relative">
<button
onClick={() => setShowPresets(!showPresets)}
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded text-xs font-medium transition-colors"
>
Presets
</button>
{showPresets && (
<div className="absolute top-full right-0 mt-2 w-64 bg-gray-800 rounded-lg border border-gray-700 shadow-xl z-50">
<div className="p-3 border-b border-gray-700">
<div className="text-xs text-gray-400 mb-2">Load Preset</div>
<div className="space-y-1">
{savedPresets.map((preset, idx) => (
<button
key={idx}
onClick={() => handleLoadPreset(preset)}
className="w-full text-left px-3 py-2 text-sm text-white hover:bg-gray-700 rounded transition-colors"
>
{preset.name}
<span className="text-xs text-gray-500 ml-2">
({preset.indicators.length} indicators)
</span>
</button>
))}
</div>
</div>
<div className="p-3">
<div className="text-xs text-gray-400 mb-2">Save Current</div>
<div className="flex gap-2">
<input
type="text"
value={presetName}
onChange={(e) => setPresetName(e.target.value)}
placeholder="Preset name..."
className="flex-1 px-2 py-1.5 bg-gray-900 border border-gray-700 rounded text-xs text-white focus:outline-none focus:border-blue-500"
/>
<button
onClick={handleSavePreset}
disabled={!presetName.trim()}
className="p-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded disabled:opacity-50"
>
<Save className="w-4 h-4" />
</button>
</div>
</div>
</div>
)}
</div>
{/* Add Indicator Button */}
<div className="relative">
<button
onClick={() => setShowAddMenu(!showAddMenu)}
className="flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded text-xs font-medium transition-colors"
>
<Plus className="w-4 h-4" />
Add
</button>
{showAddMenu && (
<div className="absolute top-full right-0 mt-2 w-56 bg-gray-800 rounded-lg border border-gray-700 shadow-xl z-50 max-h-80 overflow-y-auto">
{Object.entries(INDICATOR_DEFINITIONS).map(([type, def]) => (
<button
key={type}
onClick={() => handleAddIndicator(type as IndicatorType)}
className="w-full flex items-center gap-3 px-4 py-2.5 text-white hover:bg-gray-700 transition-colors"
>
<div className="p-1.5 bg-gray-700 rounded" style={{ color: def.color }}>
{def.icon}
</div>
<span className="text-sm">{def.label}</span>
</button>
))}
</div>
)}
</div>
</div>
</div>
{/* Indicators List */}
{indicators.length === 0 ? (
<div className="text-center py-8">
<Activity className="w-10 h-10 text-gray-600 mx-auto mb-2" />
<p className="text-gray-400 text-sm">No indicators configured</p>
<p className="text-gray-500 text-xs mt-1">Click "Add" to add technical indicators</p>
</div>
) : (
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{indicators.map((indicator) => {
const def = INDICATOR_DEFINITIONS[indicator.type];
const isExpanded = expandedId === indicator.id;
return (
<div
key={indicator.id}
className={`bg-gray-900/50 rounded-lg border transition-all ${
indicator.enabled ? 'border-gray-700' : 'border-gray-800 opacity-60'
}`}
>
{/* Header Row */}
<div
onClick={() => setExpandedId(isExpanded ? null : indicator.id)}
className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-800/50"
>
<div className="flex items-center gap-3">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: indicator.color }}
/>
<div className="p-1.5 bg-gray-800 rounded" style={{ color: indicator.color }}>
{def.icon}
</div>
<div>
<span className="font-medium text-white text-sm">{def.label}</span>
<div className="text-xs text-gray-500">
{Object.entries(indicator.params)
.map(([k, v]) => `${def.paramLabels[k]}: ${v}`)
.join(' • ')}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={(e) => {
e.stopPropagation();
handleToggleEnabled(indicator.id);
}}
className="p-1.5 text-gray-400 hover:text-white transition-colors"
>
{indicator.enabled ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</button>
{isExpanded ? (
<ChevronUp className="w-4 h-4 text-gray-500" />
) : (
<ChevronDown className="w-4 h-4 text-gray-500" />
)}
</div>
</div>
{/* Expanded Config */}
{isExpanded && (
<div className="px-3 pb-3 pt-1 border-t border-gray-800">
{/* Parameters */}
<div className="grid grid-cols-2 gap-3 mb-3">
{Object.entries(indicator.params).map(([paramKey, paramValue]) => (
<div key={paramKey}>
<label className="block text-xs text-gray-500 mb-1">
{def.paramLabels[paramKey]}
</label>
<input
type="number"
value={paramValue}
onChange={(e) => handleParamChange(indicator.id, paramKey, Number(e.target.value))}
className="w-full px-2 py-1.5 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:border-blue-500"
/>
</div>
))}
</div>
{/* Style Options */}
<div className="flex items-center gap-4 mb-3">
<div>
<label className="block text-xs text-gray-500 mb-1">Color</label>
<div className="flex gap-1">
{PRESET_COLORS.slice(0, 5).map((color) => (
<button
key={color}
onClick={() => handleColorChange(indicator.id, color)}
className={`w-6 h-6 rounded-full border-2 transition-all ${
indicator.color === color ? 'border-white scale-110' : 'border-transparent'
}`}
style={{ backgroundColor: color }}
/>
))}
</div>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Line Width</label>
<div className="flex gap-1">
{[1, 2, 3].map((width) => (
<button
key={width}
onClick={() => handleLineWidthChange(indicator.id, width)}
className={`w-8 h-6 rounded text-xs ${
indicator.lineWidth === width
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300'
}`}
>
{width}px
</button>
))}
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-2 border-t border-gray-800">
<div className="flex items-center gap-2">
<button
onClick={() => handleResetDefaults(indicator.id)}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-400 hover:text-white transition-colors"
>
<RotateCcw className="w-3 h-3" />
Reset
</button>
<button
onClick={() => handleDuplicateIndicator(indicator)}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-400 hover:text-white transition-colors"
>
<Copy className="w-3 h-3" />
Duplicate
</button>
</div>
<button
onClick={() => onIndicatorRemove?.(indicator.id)}
className="flex items-center gap-1 px-2 py-1 text-xs text-red-400 hover:text-red-300 transition-colors"
>
<Trash2 className="w-3 h-3" />
Remove
</button>
</div>
</div>
)}
</div>
);
})}
</div>
)}
{/* Close menus on outside click */}
{(showAddMenu || showPresets) && (
<div
className="fixed inset-0 z-40"
onClick={() => {
setShowAddMenu(false);
setShowPresets(false);
}}
/>
)}
</div>
);
};
export default IndicatorConfigPanel;

View File

@ -0,0 +1,433 @@
/**
* SymbolInfoPanel Component
* Comprehensive symbol information and details sidebar
* OQI-003: Trading y Charts
*/
import React, { useState, useMemo } from 'react';
import {
Info,
TrendingUp,
TrendingDown,
BarChart3,
Activity,
Clock,
DollarSign,
Percent,
Globe,
Star,
StarOff,
ExternalLink,
RefreshCw,
ChevronDown,
ChevronUp,
Zap,
AlertCircle,
} from 'lucide-react';
export interface SymbolStats {
price: number;
change24h: number;
changePercent24h: number;
high24h: number;
low24h: number;
open24h: number;
volume24h: number;
volumeQuote24h: number;
marketCap?: number;
circulatingSupply?: number;
totalSupply?: number;
allTimeHigh?: number;
allTimeLow?: number;
athDate?: string;
atlDate?: string;
}
export interface SymbolInfo {
symbol: string;
name: string;
baseAsset: string;
quoteAsset: string;
exchange: string;
type: 'spot' | 'futures' | 'forex' | 'crypto';
status: 'trading' | 'halt' | 'break';
minNotional?: number;
tickSize?: number;
stepSize?: number;
maxLeverage?: number;
tradingHours?: string;
description?: string;
}
interface RelatedSymbol {
symbol: string;
correlation: number;
change24h: number;
}
interface SymbolInfoPanelProps {
symbol: string;
info?: SymbolInfo;
stats?: SymbolStats;
relatedSymbols?: RelatedSymbol[];
isFavorite?: boolean;
onToggleFavorite?: (symbol: string) => void;
onSymbolSelect?: (symbol: string) => void;
onRefresh?: () => void;
isLoading?: boolean;
compact?: boolean;
}
const SymbolInfoPanel: React.FC<SymbolInfoPanelProps> = ({
symbol,
info,
stats,
relatedSymbols = [],
isFavorite = false,
onToggleFavorite,
onSymbolSelect,
onRefresh,
isLoading = false,
compact = false,
}) => {
const [expandedSection, setExpandedSection] = useState<string | null>('stats');
const priceChangeColor = useMemo(() => {
if (!stats) return 'text-gray-400';
return stats.change24h >= 0 ? 'text-green-400' : 'text-red-400';
}, [stats]);
const formatNumber = (num: number, decimals = 2) => {
if (num >= 1e9) return `${(num / 1e9).toFixed(decimals)}B`;
if (num >= 1e6) return `${(num / 1e6).toFixed(decimals)}M`;
if (num >= 1e3) return `${(num / 1e3).toFixed(decimals)}K`;
return num.toFixed(decimals);
};
const formatPrice = (price: number) => {
if (price >= 1000) return price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
if (price >= 1) return price.toFixed(4);
return price.toFixed(8);
};
const formatPercent = (percent: number) => {
const sign = percent >= 0 ? '+' : '';
return `${sign}${percent.toFixed(2)}%`;
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'trading':
return <span className="px-2 py-0.5 bg-green-500/20 text-green-400 rounded text-xs">Trading</span>;
case 'halt':
return <span className="px-2 py-0.5 bg-red-500/20 text-red-400 rounded text-xs">Halted</span>;
case 'break':
return <span className="px-2 py-0.5 bg-yellow-500/20 text-yellow-400 rounded text-xs">Break</span>;
default:
return null;
}
};
const toggleSection = (section: string) => {
setExpandedSection(expandedSection === section ? null : section);
};
return (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Info className="w-5 h-5 text-blue-400" />
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-bold text-white text-lg">{symbol}</h3>
{info && getStatusBadge(info.status)}
</div>
{info && (
<p className="text-xs text-gray-500">{info.name}</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => onToggleFavorite?.(symbol)}
className={`p-2 rounded-lg transition-colors ${
isFavorite
? 'text-yellow-400 bg-yellow-500/10'
: 'text-gray-500 hover:text-yellow-400 hover:bg-gray-700'
}`}
>
{isFavorite ? <Star className="w-4 h-4 fill-current" /> : <StarOff className="w-4 h-4" />}
</button>
<button
onClick={onRefresh}
disabled={isLoading}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
{/* Price Display */}
{stats && (
<div className="p-4 bg-gray-900/50 rounded-lg mb-4">
<div className="flex items-end justify-between mb-2">
<div>
<span className="text-3xl font-bold text-white">${formatPrice(stats.price)}</span>
<div className="flex items-center gap-2 mt-1">
{stats.change24h >= 0 ? (
<TrendingUp className="w-4 h-4 text-green-400" />
) : (
<TrendingDown className="w-4 h-4 text-red-400" />
)}
<span className={`font-medium ${priceChangeColor}`}>
{stats.change24h >= 0 ? '+' : ''}{formatPrice(stats.change24h)}
</span>
<span className={`text-sm ${priceChangeColor}`}>
({formatPercent(stats.changePercent24h)})
</span>
</div>
</div>
<div className="text-right text-xs text-gray-500">
<div>24h High: <span className="text-green-400">${formatPrice(stats.high24h)}</span></div>
<div>24h Low: <span className="text-red-400">${formatPrice(stats.low24h)}</span></div>
</div>
</div>
{/* Mini Stats Row */}
<div className="grid grid-cols-3 gap-3 pt-3 border-t border-gray-700">
<div className="text-center">
<div className="text-xs text-gray-500">Volume</div>
<div className="text-white font-medium">${formatNumber(stats.volumeQuote24h)}</div>
</div>
{stats.marketCap && (
<div className="text-center">
<div className="text-xs text-gray-500">Market Cap</div>
<div className="text-white font-medium">${formatNumber(stats.marketCap)}</div>
</div>
)}
<div className="text-center">
<div className="text-xs text-gray-500">Open</div>
<div className="text-white font-medium">${formatPrice(stats.open24h)}</div>
</div>
</div>
</div>
)}
{/* Expandable Sections */}
<div className="space-y-2">
{/* Stats Section */}
{stats && (
<div className="bg-gray-900/50 rounded-lg">
<button
onClick={() => toggleSection('stats')}
className="w-full flex items-center justify-between p-3 hover:bg-gray-800/50 transition-colors"
>
<div className="flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-gray-400" />
<span className="text-sm font-medium text-white">Statistics</span>
</div>
{expandedSection === 'stats' ? (
<ChevronUp className="w-4 h-4 text-gray-500" />
) : (
<ChevronDown className="w-4 h-4 text-gray-500" />
)}
</button>
{expandedSection === 'stats' && (
<div className="px-3 pb-3 space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-500">24h Volume ({info?.baseAsset})</span>
<span className="text-white">{formatNumber(stats.volume24h)}</span>
</div>
{stats.allTimeHigh && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">All-Time High</span>
<div className="text-right">
<span className="text-green-400">${formatPrice(stats.allTimeHigh)}</span>
{stats.athDate && (
<div className="text-xs text-gray-600">{stats.athDate}</div>
)}
</div>
</div>
)}
{stats.allTimeLow && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">All-Time Low</span>
<div className="text-right">
<span className="text-red-400">${formatPrice(stats.allTimeLow)}</span>
{stats.atlDate && (
<div className="text-xs text-gray-600">{stats.atlDate}</div>
)}
</div>
</div>
)}
{stats.circulatingSupply && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Circulating Supply</span>
<span className="text-white">{formatNumber(stats.circulatingSupply, 0)}</span>
</div>
)}
</div>
)}
</div>
)}
{/* Info Section */}
{info && (
<div className="bg-gray-900/50 rounded-lg">
<button
onClick={() => toggleSection('info')}
className="w-full flex items-center justify-between p-3 hover:bg-gray-800/50 transition-colors"
>
<div className="flex items-center gap-2">
<Activity className="w-4 h-4 text-gray-400" />
<span className="text-sm font-medium text-white">Trading Info</span>
</div>
{expandedSection === 'info' ? (
<ChevronUp className="w-4 h-4 text-gray-500" />
) : (
<ChevronDown className="w-4 h-4 text-gray-500" />
)}
</button>
{expandedSection === 'info' && (
<div className="px-3 pb-3 space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-500">Exchange</span>
<span className="text-white">{info.exchange}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Type</span>
<span className="text-white capitalize">{info.type}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Base Asset</span>
<span className="text-white">{info.baseAsset}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Quote Asset</span>
<span className="text-white">{info.quoteAsset}</span>
</div>
{info.tickSize && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Tick Size</span>
<span className="text-white">{info.tickSize}</span>
</div>
)}
{info.minNotional && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Min Order</span>
<span className="text-white">${info.minNotional}</span>
</div>
)}
{info.maxLeverage && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Max Leverage</span>
<span className="text-yellow-400">{info.maxLeverage}x</span>
</div>
)}
{info.tradingHours && (
<div className="flex justify-between text-sm">
<span className="text-gray-500">Trading Hours</span>
<span className="text-white">{info.tradingHours}</span>
</div>
)}
</div>
)}
</div>
)}
{/* Related Symbols */}
{relatedSymbols.length > 0 && (
<div className="bg-gray-900/50 rounded-lg">
<button
onClick={() => toggleSection('related')}
className="w-full flex items-center justify-between p-3 hover:bg-gray-800/50 transition-colors"
>
<div className="flex items-center gap-2">
<Globe className="w-4 h-4 text-gray-400" />
<span className="text-sm font-medium text-white">Related Symbols</span>
</div>
{expandedSection === 'related' ? (
<ChevronUp className="w-4 h-4 text-gray-500" />
) : (
<ChevronDown className="w-4 h-4 text-gray-500" />
)}
</button>
{expandedSection === 'related' && (
<div className="px-3 pb-3 space-y-1">
{relatedSymbols.map((related) => (
<button
key={related.symbol}
onClick={() => onSymbolSelect?.(related.symbol)}
className="w-full flex items-center justify-between p-2 hover:bg-gray-800 rounded transition-colors"
>
<div className="flex items-center gap-2">
<span className="text-white text-sm">{related.symbol}</span>
<span className="text-xs text-gray-500">
{related.correlation > 0 ? '+' : ''}{(related.correlation * 100).toFixed(0)}% corr
</span>
</div>
<span className={`text-sm ${related.change24h >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{formatPercent(related.change24h)}
</span>
</button>
))}
</div>
)}
</div>
)}
{/* Description */}
{info?.description && (
<div className="bg-gray-900/50 rounded-lg">
<button
onClick={() => toggleSection('desc')}
className="w-full flex items-center justify-between p-3 hover:bg-gray-800/50 transition-colors"
>
<div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-gray-400" />
<span className="text-sm font-medium text-white">About</span>
</div>
{expandedSection === 'desc' ? (
<ChevronUp className="w-4 h-4 text-gray-500" />
) : (
<ChevronDown className="w-4 h-4 text-gray-500" />
)}
</button>
{expandedSection === 'desc' && (
<div className="px-3 pb-3">
<p className="text-sm text-gray-400 leading-relaxed">{info.description}</p>
</div>
)}
</div>
)}
</div>
{/* Quick Actions */}
{!compact && (
<div className="mt-4 pt-4 border-t border-gray-700">
<div className="grid grid-cols-2 gap-2">
<button className="flex items-center justify-center gap-2 py-2 bg-green-600 hover:bg-green-500 text-white rounded-lg text-sm font-medium transition-colors">
<TrendingUp className="w-4 h-4" />
Buy
</button>
<button className="flex items-center justify-center gap-2 py-2 bg-red-600 hover:bg-red-500 text-white rounded-lg text-sm font-medium transition-colors">
<TrendingDown className="w-4 h-4" />
Sell
</button>
</div>
</div>
)}
</div>
);
};
export default SymbolInfoPanel;

View File

@ -0,0 +1,609 @@
/**
* TradeJournalPanel Component
* Trade review, analysis, and journaling for paper trading
* OQI-003: Trading y Charts
*/
import React, { useState, useMemo } from 'react';
import {
BookOpen,
TrendingUp,
TrendingDown,
Calendar,
Tag,
MessageSquare,
Star,
Filter,
Search,
Download,
BarChart3,
Target,
Clock,
DollarSign,
Percent,
ChevronDown,
ChevronUp,
Edit3,
Save,
X,
Plus,
Award,
AlertTriangle,
Zap,
} from 'lucide-react';
export interface TradeEntry {
id: string;
ticket: number;
symbol: string;
type: 'BUY' | 'SELL';
entryPrice: number;
exitPrice: number;
lots: number;
profit: number;
profitPercent: number;
entryTime: string;
exitTime: string;
duration: number; // minutes
stopLoss?: number;
takeProfit?: number;
// Journal fields
notes?: string;
rating?: 1 | 2 | 3 | 4 | 5;
tags?: string[];
strategy?: string;
setup?: string;
emotions?: string;
lessonsLearned?: string;
screenshots?: string[];
}
interface TradeStats {
totalTrades: number;
winningTrades: number;
losingTrades: number;
winRate: number;
totalProfit: number;
averageWin: number;
averageLoss: number;
profitFactor: number;
largestWin: number;
largestLoss: number;
averageRR: number;
currentStreak: number;
longestWinStreak: number;
longestLoseStreak: number;
averageDuration: number;
}
interface TradeJournalPanelProps {
trades?: TradeEntry[];
onTradeUpdate?: (trade: TradeEntry) => void;
onExportJournal?: (format: 'csv' | 'pdf') => void;
compact?: boolean;
}
const STRATEGY_TAGS = ['Trend Following', 'Reversal', 'Breakout', 'Scalping', 'Swing', 'Range'];
const SETUP_TAGS = ['Support/Resistance', 'Moving Average', 'RSI Divergence', 'MACD Cross', 'Fibonacci', 'Pattern'];
const EMOTION_TAGS = ['Confident', 'Fearful', 'Greedy', 'Patient', 'Impulsive', 'Disciplined'];
const TradeJournalPanel: React.FC<TradeJournalPanelProps> = ({
trades = [],
onTradeUpdate,
onExportJournal,
compact = false,
}) => {
const [selectedTrade, setSelectedTrade] = useState<TradeEntry | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [editedNotes, setEditedNotes] = useState('');
const [editedRating, setEditedRating] = useState<number>(0);
const [editedTags, setEditedTags] = useState<string[]>([]);
const [editedStrategy, setEditedStrategy] = useState('');
const [editedLessons, setEditedLessons] = useState('');
const [filterSymbol, setFilterSymbol] = useState('');
const [filterResult, setFilterResult] = useState<'all' | 'wins' | 'losses'>('all');
const [showStats, setShowStats] = useState(true);
// Calculate statistics
const stats: TradeStats = useMemo(() => {
if (trades.length === 0) {
return {
totalTrades: 0,
winningTrades: 0,
losingTrades: 0,
winRate: 0,
totalProfit: 0,
averageWin: 0,
averageLoss: 0,
profitFactor: 0,
largestWin: 0,
largestLoss: 0,
averageRR: 0,
currentStreak: 0,
longestWinStreak: 0,
longestLoseStreak: 0,
averageDuration: 0,
};
}
const wins = trades.filter(t => t.profit > 0);
const losses = trades.filter(t => t.profit < 0);
const totalWins = wins.reduce((sum, t) => sum + t.profit, 0);
const totalLosses = Math.abs(losses.reduce((sum, t) => sum + t.profit, 0));
// Calculate streaks
let currentStreak = 0;
let longestWinStreak = 0;
let longestLoseStreak = 0;
let tempWinStreak = 0;
let tempLoseStreak = 0;
const sortedTrades = [...trades].sort((a, b) =>
new Date(b.exitTime).getTime() - new Date(a.exitTime).getTime()
);
sortedTrades.forEach((trade, idx) => {
if (trade.profit > 0) {
tempWinStreak++;
tempLoseStreak = 0;
if (tempWinStreak > longestWinStreak) longestWinStreak = tempWinStreak;
if (idx === 0) currentStreak = tempWinStreak;
} else {
tempLoseStreak++;
tempWinStreak = 0;
if (tempLoseStreak > longestLoseStreak) longestLoseStreak = tempLoseStreak;
if (idx === 0) currentStreak = -tempLoseStreak;
}
});
return {
totalTrades: trades.length,
winningTrades: wins.length,
losingTrades: losses.length,
winRate: (wins.length / trades.length) * 100,
totalProfit: trades.reduce((sum, t) => sum + t.profit, 0),
averageWin: wins.length > 0 ? totalWins / wins.length : 0,
averageLoss: losses.length > 0 ? totalLosses / losses.length : 0,
profitFactor: totalLosses > 0 ? totalWins / totalLosses : totalWins,
largestWin: wins.length > 0 ? Math.max(...wins.map(t => t.profit)) : 0,
largestLoss: losses.length > 0 ? Math.min(...losses.map(t => t.profit)) : 0,
averageRR: losses.length > 0 && wins.length > 0
? (totalWins / wins.length) / (totalLosses / losses.length) : 0,
currentStreak,
longestWinStreak,
longestLoseStreak,
averageDuration: trades.reduce((sum, t) => sum + t.duration, 0) / trades.length,
};
}, [trades]);
// Filter trades
const filteredTrades = useMemo(() => {
return trades.filter(trade => {
if (filterSymbol && !trade.symbol.toLowerCase().includes(filterSymbol.toLowerCase())) {
return false;
}
if (filterResult === 'wins' && trade.profit <= 0) return false;
if (filterResult === 'losses' && trade.profit >= 0) return false;
return true;
});
}, [trades, filterSymbol, filterResult]);
const handleEditTrade = (trade: TradeEntry) => {
setSelectedTrade(trade);
setEditedNotes(trade.notes || '');
setEditedRating(trade.rating || 0);
setEditedTags(trade.tags || []);
setEditedStrategy(trade.strategy || '');
setEditedLessons(trade.lessonsLearned || '');
setIsEditing(true);
};
const handleSaveTrade = () => {
if (!selectedTrade) return;
const updatedTrade: TradeEntry = {
...selectedTrade,
notes: editedNotes,
rating: editedRating as 1 | 2 | 3 | 4 | 5,
tags: editedTags,
strategy: editedStrategy,
lessonsLearned: editedLessons,
};
onTradeUpdate?.(updatedTrade);
setIsEditing(false);
setSelectedTrade(updatedTrade);
};
const handleToggleTag = (tag: string) => {
if (editedTags.includes(tag)) {
setEditedTags(editedTags.filter(t => t !== tag));
} else {
setEditedTags([...editedTags, tag]);
}
};
const formatDuration = (minutes: number) => {
if (minutes < 60) return `${minutes}m`;
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
return `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<BookOpen className="w-5 h-5 text-blue-400" />
<div>
<h3 className="font-semibold text-white">Trade Journal</h3>
<p className="text-xs text-gray-500">{trades.length} trades recorded</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => onExportJournal?.('csv')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors"
title="Export CSV"
>
<Download className="w-4 h-4" />
</button>
</div>
</div>
{/* Stats Summary */}
{showStats && (
<div className="grid grid-cols-4 gap-3 mb-4">
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className={`text-lg font-bold ${stats.totalProfit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
${stats.totalProfit.toFixed(2)}
</div>
<div className="text-xs text-gray-500">Total P&L</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="text-lg font-bold text-white">{stats.winRate.toFixed(1)}%</div>
<div className="text-xs text-gray-500">Win Rate</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="text-lg font-bold text-white">{stats.profitFactor.toFixed(2)}</div>
<div className="text-xs text-gray-500">Profit Factor</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className={`text-lg font-bold ${stats.currentStreak >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{stats.currentStreak >= 0 ? `+${stats.currentStreak}` : stats.currentStreak}
</div>
<div className="text-xs text-gray-500">Streak</div>
</div>
</div>
)}
{/* Extended Stats */}
{!compact && showStats && (
<div className="grid grid-cols-3 gap-3 mb-4 p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-2">
<Award className="w-4 h-4 text-green-400" />
<div>
<div className="text-xs text-gray-500">Best Trade</div>
<div className="text-green-400 font-medium">${stats.largestWin.toFixed(2)}</div>
</div>
</div>
<div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-red-400" />
<div>
<div className="text-xs text-gray-500">Worst Trade</div>
<div className="text-red-400 font-medium">${stats.largestLoss.toFixed(2)}</div>
</div>
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-blue-400" />
<div>
<div className="text-xs text-gray-500">Avg Duration</div>
<div className="text-white font-medium">{formatDuration(stats.averageDuration)}</div>
</div>
</div>
</div>
)}
{/* Filters */}
<div className="flex items-center gap-2 mb-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="text"
value={filterSymbol}
onChange={(e) => setFilterSymbol(e.target.value)}
placeholder="Filter by symbol..."
className="w-full pl-10 pr-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm focus:outline-none focus:border-blue-500"
/>
</div>
<select
value={filterResult}
onChange={(e) => setFilterResult(e.target.value as 'all' | 'wins' | 'losses')}
className="px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm"
>
<option value="all">All</option>
<option value="wins">Wins</option>
<option value="losses">Losses</option>
</select>
</div>
{/* Trades List */}
{filteredTrades.length === 0 ? (
<div className="text-center py-8">
<BookOpen className="w-10 h-10 text-gray-600 mx-auto mb-2" />
<p className="text-gray-400">No trades to journal</p>
<p className="text-gray-500 text-xs mt-1">Complete some trades to start journaling</p>
</div>
) : (
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{filteredTrades.map((trade) => (
<div
key={trade.id}
onClick={() => setSelectedTrade(selectedTrade?.id === trade.id ? null : trade)}
className={`p-3 bg-gray-900/50 rounded-lg border cursor-pointer transition-all ${
selectedTrade?.id === trade.id
? 'border-blue-500/50 bg-blue-500/5'
: 'border-gray-700 hover:border-gray-600'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded ${trade.type === 'BUY' ? 'bg-green-500/20' : 'bg-red-500/20'}`}>
{trade.type === 'BUY' ? (
<TrendingUp className="w-4 h-4 text-green-400" />
) : (
<TrendingDown className="w-4 h-4 text-red-400" />
)}
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-white">{trade.symbol}</span>
<span className="text-xs text-gray-500">{trade.lots} lots</span>
{trade.rating && (
<div className="flex items-center">
{Array.from({ length: trade.rating }).map((_, i) => (
<Star key={i} className="w-3 h-3 text-yellow-400 fill-current" />
))}
</div>
)}
</div>
<div className="text-xs text-gray-500">
{formatDate(trade.exitTime)} {formatDuration(trade.duration)}
</div>
</div>
</div>
<div className="text-right">
<div className={`font-mono font-medium ${trade.profit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{trade.profit >= 0 ? '+' : ''}{trade.profit.toFixed(2)}
</div>
<div className={`text-xs ${trade.profit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{trade.profitPercent >= 0 ? '+' : ''}{trade.profitPercent.toFixed(2)}%
</div>
</div>
</div>
{/* Tags */}
{trade.tags && trade.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{trade.tags.map((tag) => (
<span key={tag} className="px-2 py-0.5 bg-gray-800 text-gray-400 rounded text-xs">
{tag}
</span>
))}
</div>
)}
{/* Expanded Details */}
{selectedTrade?.id === trade.id && !isEditing && (
<div className="mt-3 pt-3 border-t border-gray-700">
<div className="grid grid-cols-2 gap-3 text-xs mb-3">
<div>
<span className="text-gray-500">Entry:</span>
<span className="text-white ml-2">{trade.entryPrice.toFixed(5)}</span>
</div>
<div>
<span className="text-gray-500">Exit:</span>
<span className="text-white ml-2">{trade.exitPrice.toFixed(5)}</span>
</div>
{trade.stopLoss && (
<div>
<span className="text-gray-500">SL:</span>
<span className="text-red-400 ml-2">{trade.stopLoss.toFixed(5)}</span>
</div>
)}
{trade.takeProfit && (
<div>
<span className="text-gray-500">TP:</span>
<span className="text-green-400 ml-2">{trade.takeProfit.toFixed(5)}</span>
</div>
)}
</div>
{trade.strategy && (
<div className="text-xs mb-2">
<span className="text-gray-500">Strategy:</span>
<span className="text-blue-400 ml-2">{trade.strategy}</span>
</div>
)}
{trade.notes && (
<div className="text-xs mb-2">
<span className="text-gray-500">Notes:</span>
<p className="text-gray-300 mt-1">{trade.notes}</p>
</div>
)}
{trade.lessonsLearned && (
<div className="text-xs mb-2">
<span className="text-gray-500">Lessons:</span>
<p className="text-yellow-400 mt-1">{trade.lessonsLearned}</p>
</div>
)}
<button
onClick={(e) => {
e.stopPropagation();
handleEditTrade(trade);
}}
className="flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded text-xs mt-2"
>
<Edit3 className="w-3 h-3" />
Edit Journal Entry
</button>
</div>
)}
</div>
))}
</div>
)}
{/* Edit Modal */}
{isEditing && selectedTrade && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70">
<div className="w-full max-w-lg bg-gray-800 rounded-xl shadow-2xl max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<h3 className="font-semibold text-white">Edit Journal Entry</h3>
<button
onClick={() => setIsEditing(false)}
className="p-2 text-gray-400 hover:text-white"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
{/* Trade Info */}
<div className="flex items-center gap-3 p-3 bg-gray-900/50 rounded-lg">
<div className={`p-2 rounded ${selectedTrade.type === 'BUY' ? 'bg-green-500/20' : 'bg-red-500/20'}`}>
{selectedTrade.type === 'BUY' ? (
<TrendingUp className="w-5 h-5 text-green-400" />
) : (
<TrendingDown className="w-5 h-5 text-red-400" />
)}
</div>
<div>
<div className="font-medium text-white">{selectedTrade.symbol}</div>
<div className={`text-sm ${selectedTrade.profit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{selectedTrade.profit >= 0 ? '+' : ''}{selectedTrade.profit.toFixed(2)}
</div>
</div>
</div>
{/* Rating */}
<div>
<label className="block text-sm text-gray-400 mb-2">Trade Rating</label>
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((rating) => (
<button
key={rating}
onClick={() => setEditedRating(rating)}
className="p-1"
>
<Star
className={`w-6 h-6 ${
rating <= editedRating ? 'text-yellow-400 fill-current' : 'text-gray-600'
}`}
/>
</button>
))}
</div>
</div>
{/* Strategy */}
<div>
<label className="block text-sm text-gray-400 mb-2">Strategy</label>
<div className="flex flex-wrap gap-2">
{STRATEGY_TAGS.map((tag) => (
<button
key={tag}
onClick={() => setEditedStrategy(tag)}
className={`px-3 py-1 rounded text-xs ${
editedStrategy === tag
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{tag}
</button>
))}
</div>
</div>
{/* Tags */}
<div>
<label className="block text-sm text-gray-400 mb-2">Tags</label>
<div className="flex flex-wrap gap-2">
{[...SETUP_TAGS, ...EMOTION_TAGS].map((tag) => (
<button
key={tag}
onClick={() => handleToggleTag(tag)}
className={`px-3 py-1 rounded text-xs ${
editedTags.includes(tag)
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{tag}
</button>
))}
</div>
</div>
{/* Notes */}
<div>
<label className="block text-sm text-gray-400 mb-2">Notes</label>
<textarea
value={editedNotes}
onChange={(e) => setEditedNotes(e.target.value)}
placeholder="What was your thought process?"
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm focus:outline-none focus:border-blue-500 resize-none"
rows={3}
/>
</div>
{/* Lessons Learned */}
<div>
<label className="block text-sm text-gray-400 mb-2">Lessons Learned</label>
<textarea
value={editedLessons}
onChange={(e) => setEditedLessons(e.target.value)}
placeholder="What would you do differently?"
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm focus:outline-none focus:border-blue-500 resize-none"
rows={2}
/>
</div>
</div>
<div className="flex items-center gap-3 p-4 border-t border-gray-700">
<button
onClick={() => setIsEditing(false)}
className="flex-1 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium"
>
Cancel
</button>
<button
onClick={handleSaveTrade}
className="flex-1 flex items-center justify-center gap-2 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium"
>
<Save className="w-4 h-4" />
Save Entry
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default TradeJournalPanel;

View File

@ -9,6 +9,14 @@ export { default as CandlestickChart } from './CandlestickChart';
export { default as CandlestickChartWithML } from './CandlestickChartWithML';
export { default as TradingChart } from './TradingChart';
export { default as ChartToolbar } from './ChartToolbar';
export { default as IndicatorConfigPanel } from './IndicatorConfigPanel';
export type { IndicatorType, IndicatorConfig } from './IndicatorConfigPanel';
export { default as ChartDrawingToolsPanel } from './ChartDrawingToolsPanel';
export type { DrawingTool, Drawing } from './ChartDrawingToolsPanel';
export { default as SymbolInfoPanel } from './SymbolInfoPanel';
export type { SymbolStats, SymbolInfo } from './SymbolInfoPanel';
export { default as TradeJournalPanel } from './TradeJournalPanel';
export type { TradeEntry } from './TradeJournalPanel';
// Order & Position Components
export { default as OrderForm } from './OrderForm';