[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:
parent
51c0a846c0
commit
7ac32466be
419
src/modules/trading/components/ChartDrawingToolsPanel.tsx
Normal file
419
src/modules/trading/components/ChartDrawingToolsPanel.tsx
Normal 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;
|
||||
506
src/modules/trading/components/IndicatorConfigPanel.tsx
Normal file
506
src/modules/trading/components/IndicatorConfigPanel.tsx
Normal 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;
|
||||
433
src/modules/trading/components/SymbolInfoPanel.tsx
Normal file
433
src/modules/trading/components/SymbolInfoPanel.tsx
Normal 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;
|
||||
609
src/modules/trading/components/TradeJournalPanel.tsx
Normal file
609
src/modules/trading/components/TradeJournalPanel.tsx
Normal 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;
|
||||
@ -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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user