trading-platform-frontend-v2/src/modules/trading/components/CandlestickChartWithML.tsx
rckrdmrd 5b53c2539a feat: Initial commit - Trading Platform Frontend
React frontend with:
- Authentication UI
- Trading dashboard
- ML signals display
- Portfolio management

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 04:30:39 -06:00

567 lines
19 KiB
TypeScript

/**
* CandlestickChartWithML Component
* Lightweight Charts with ML prediction overlays
* Displays: Order Blocks, Fair Value Gaps, Range Predictions, Signal Markers
*/
import { useEffect, useRef, useCallback, useState } from 'react';
import {
createChart,
IChartApi,
ISeriesApi,
CandlestickData,
HistogramData,
Time,
ColorType,
CrosshairMode,
LineStyle,
PriceLineOptions,
SeriesMarker,
} from 'lightweight-charts';
import type { Candle, CandlestickChartProps } from '../../../types/trading.types';
import type { MLSignal, RangePrediction, AMDPhase } from '../../../services/mlService';
import { getLatestSignal, getRangePrediction, getAMDPhase } from '../../../services/mlService';
// ============================================================================
// Types
// ============================================================================
interface OrderBlock {
id: string;
type: 'bullish' | 'bearish';
priceHigh: number;
priceLow: number;
timeStart: number;
timeEnd?: number;
strength: number;
tested: boolean;
}
interface FairValueGap {
id: string;
type: 'bullish' | 'bearish';
priceHigh: number;
priceLow: number;
time: number;
filled: boolean;
fillPercentage: number;
}
interface MLOverlays {
signal?: MLSignal | null;
rangePrediction?: RangePrediction | null;
amdPhase?: AMDPhase | null;
orderBlocks?: OrderBlock[];
fairValueGaps?: FairValueGap[];
}
interface CandlestickChartWithMLProps extends CandlestickChartProps {
enableMLOverlays?: boolean;
showSignalLevels?: boolean;
showRangePrediction?: boolean;
showOrderBlocks?: boolean;
showFairValueGaps?: boolean;
showAMDPhase?: boolean;
autoRefreshML?: boolean;
refreshInterval?: number;
}
// ============================================================================
// Theme Configuration
// ============================================================================
interface ChartTheme {
backgroundColor: string;
textColor: string;
gridColor: string;
upColor: string;
downColor: string;
borderUpColor: string;
borderDownColor: string;
wickUpColor: string;
wickDownColor: string;
volumeUpColor: string;
volumeDownColor: string;
}
const THEMES: Record<'dark' | 'light', ChartTheme> = {
dark: {
backgroundColor: '#1a1a2e',
textColor: '#d1d4dc',
gridColor: '#2B2B43',
upColor: '#10b981',
downColor: '#ef4444',
borderUpColor: '#10b981',
borderDownColor: '#ef4444',
wickUpColor: '#10b981',
wickDownColor: '#ef4444',
volumeUpColor: 'rgba(16, 185, 129, 0.5)',
volumeDownColor: 'rgba(239, 68, 68, 0.5)',
},
light: {
backgroundColor: '#ffffff',
textColor: '#131722',
gridColor: '#e1e1e1',
upColor: '#10b981',
downColor: '#ef4444',
borderUpColor: '#10b981',
borderDownColor: '#ef4444',
wickUpColor: '#10b981',
wickDownColor: '#ef4444',
volumeUpColor: 'rgba(16, 185, 129, 0.5)',
volumeDownColor: 'rgba(239, 68, 68, 0.5)',
},
};
// ML Overlay Colors
const ML_COLORS = {
entryLine: '#3b82f6',
stopLoss: '#ef4444',
takeProfit: '#10b981',
rangePredictionHigh: 'rgba(16, 185, 129, 0.3)',
rangePredictionLow: 'rgba(239, 68, 68, 0.3)',
orderBlockBullish: 'rgba(16, 185, 129, 0.15)',
orderBlockBearish: 'rgba(239, 68, 68, 0.15)',
fvgBullish: 'rgba(59, 130, 246, 0.2)',
fvgBearish: 'rgba(249, 115, 22, 0.2)',
amdAccumulation: 'rgba(59, 130, 246, 0.1)',
amdManipulation: 'rgba(249, 115, 22, 0.1)',
amdDistribution: 'rgba(139, 92, 246, 0.1)',
};
// ============================================================================
// Component
// ============================================================================
export const CandlestickChartWithML: React.FC<CandlestickChartWithMLProps> = ({
symbol,
interval = '1h',
height = 500,
theme = 'dark',
showVolume = true,
onCrosshairMove,
enableMLOverlays = true,
showSignalLevels = true,
showRangePrediction = true,
showOrderBlocks = false,
showFairValueGaps = false,
showAMDPhase = true,
autoRefreshML = true,
refreshInterval = 30000,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
const candleSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
const volumeSeriesRef = useRef<ISeriesApi<'Histogram'> | null>(null);
const rangeHighSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
const rangeLowSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
const resizeObserverRef = useRef<ResizeObserver | null>(null);
const priceLinesRef = useRef<ReturnType<ISeriesApi<'Candlestick'>['createPriceLine']>[]>([]);
const [mlOverlays, setMlOverlays] = useState<MLOverlays>({});
const [isLoadingML, setIsLoadingML] = useState(false);
const chartTheme = THEMES[theme];
// Fetch ML data
const fetchMLData = useCallback(async () => {
if (!enableMLOverlays || !symbol) return;
setIsLoadingML(true);
try {
const [signal, rangePred, amd] = await Promise.all([
showSignalLevels ? getLatestSignal(symbol) : Promise.resolve(null),
showRangePrediction ? getRangePrediction(symbol, interval) : Promise.resolve(null),
showAMDPhase ? getAMDPhase(symbol) : Promise.resolve(null),
]);
setMlOverlays({
signal,
rangePrediction: rangePred,
amdPhase: amd,
});
} catch (error) {
console.error('Error fetching ML data:', error);
} finally {
setIsLoadingML(false);
}
}, [symbol, interval, enableMLOverlays, showSignalLevels, showRangePrediction, showAMDPhase]);
// Auto-refresh ML data
useEffect(() => {
if (enableMLOverlays && autoRefreshML) {
fetchMLData();
const intervalId = setInterval(fetchMLData, refreshInterval);
return () => clearInterval(intervalId);
}
}, [fetchMLData, autoRefreshML, refreshInterval, enableMLOverlays]);
// Initialize chart
useEffect(() => {
if (!containerRef.current) return;
const chart = createChart(containerRef.current, {
width: containerRef.current.clientWidth,
height,
layout: {
background: { type: ColorType.Solid, color: chartTheme.backgroundColor },
textColor: chartTheme.textColor,
},
grid: {
vertLines: { color: chartTheme.gridColor },
horzLines: { color: chartTheme.gridColor },
},
crosshair: {
mode: CrosshairMode.Normal,
vertLine: {
width: 1,
color: 'rgba(224, 227, 235, 0.4)',
style: LineStyle.Solid,
labelBackgroundColor: chartTheme.backgroundColor,
},
horzLine: {
width: 1,
color: 'rgba(224, 227, 235, 0.4)',
style: LineStyle.Solid,
labelBackgroundColor: chartTheme.backgroundColor,
},
},
rightPriceScale: {
borderColor: chartTheme.gridColor,
},
timeScale: {
borderColor: chartTheme.gridColor,
timeVisible: true,
secondsVisible: false,
},
});
// Add candlestick series
const candleSeries = chart.addCandlestickSeries({
upColor: chartTheme.upColor,
downColor: chartTheme.downColor,
borderUpColor: chartTheme.borderUpColor,
borderDownColor: chartTheme.borderDownColor,
wickUpColor: chartTheme.wickUpColor,
wickDownColor: chartTheme.wickDownColor,
});
// Add volume series
let volumeSeries: ISeriesApi<'Histogram'> | null = null;
if (showVolume) {
volumeSeries = chart.addHistogramSeries({
color: chartTheme.volumeUpColor,
priceFormat: { type: 'volume' },
priceScaleId: '',
});
volumeSeries.priceScale().applyOptions({
scaleMargins: { top: 0.8, bottom: 0 },
});
}
// Add range prediction lines (invisible initially)
const rangeHighSeries = chart.addLineSeries({
color: ML_COLORS.rangePredictionHigh,
lineWidth: 1,
lineStyle: LineStyle.Dashed,
priceLineVisible: false,
lastValueVisible: false,
crosshairMarkerVisible: false,
});
const rangeLowSeries = chart.addLineSeries({
color: ML_COLORS.rangePredictionLow,
lineWidth: 1,
lineStyle: LineStyle.Dashed,
priceLineVisible: false,
lastValueVisible: false,
crosshairMarkerVisible: false,
});
// Handle crosshair movement
chart.subscribeCrosshairMove((param) => {
if (!param || !param.time || !param.seriesData) {
onCrosshairMove?.(null);
return;
}
const candleData = param.seriesData.get(candleSeries) as CandlestickData;
if (candleData) {
onCrosshairMove?.({
time: param.time,
price: candleData.close as number,
});
}
});
chartRef.current = chart;
candleSeriesRef.current = candleSeries;
volumeSeriesRef.current = volumeSeries;
rangeHighSeriesRef.current = rangeHighSeries;
rangeLowSeriesRef.current = rangeLowSeries;
// Setup ResizeObserver
const resizeObserver = new ResizeObserver((entries) => {
if (entries[0] && chartRef.current) {
const { width } = entries[0].contentRect;
chartRef.current.applyOptions({ width });
}
});
resizeObserver.observe(containerRef.current);
resizeObserverRef.current = resizeObserver;
return () => {
resizeObserver.disconnect();
chart.remove();
};
}, [height, showVolume, chartTheme, onCrosshairMove]);
// Update price lines when ML signal changes
useEffect(() => {
if (!candleSeriesRef.current || !showSignalLevels) return;
// Remove old price lines
priceLinesRef.current.forEach((line) => {
candleSeriesRef.current?.removePriceLine(line);
});
priceLinesRef.current = [];
// Add new price lines for signal
if (mlOverlays.signal) {
const signal = mlOverlays.signal;
// Entry line
const entryLine = candleSeriesRef.current.createPriceLine({
price: signal.entry_price,
color: ML_COLORS.entryLine,
lineWidth: 2,
lineStyle: LineStyle.Solid,
axisLabelVisible: true,
title: `Entry ${signal.direction.toUpperCase()}`,
} as PriceLineOptions);
priceLinesRef.current.push(entryLine);
// Stop Loss line
const slLine = candleSeriesRef.current.createPriceLine({
price: signal.stop_loss,
color: ML_COLORS.stopLoss,
lineWidth: 1,
lineStyle: LineStyle.Dashed,
axisLabelVisible: true,
title: 'SL',
} as PriceLineOptions);
priceLinesRef.current.push(slLine);
// Take Profit line
const tpLine = candleSeriesRef.current.createPriceLine({
price: signal.take_profit,
color: ML_COLORS.takeProfit,
lineWidth: 1,
lineStyle: LineStyle.Dashed,
axisLabelVisible: true,
title: 'TP',
} as PriceLineOptions);
priceLinesRef.current.push(tpLine);
}
// Add range prediction lines
if (mlOverlays.rangePrediction && rangeHighSeriesRef.current && rangeLowSeriesRef.current) {
const pred = mlOverlays.rangePrediction;
// Support/Resistance from AMD
if (mlOverlays.amdPhase?.key_levels) {
const supportLine = candleSeriesRef.current.createPriceLine({
price: mlOverlays.amdPhase.key_levels.support,
color: 'rgba(59, 130, 246, 0.5)',
lineWidth: 1,
lineStyle: LineStyle.Dotted,
axisLabelVisible: true,
title: 'Support',
} as PriceLineOptions);
priceLinesRef.current.push(supportLine);
const resistanceLine = candleSeriesRef.current.createPriceLine({
price: mlOverlays.amdPhase.key_levels.resistance,
color: 'rgba(249, 115, 22, 0.5)',
lineWidth: 1,
lineStyle: LineStyle.Dotted,
axisLabelVisible: true,
title: 'Resistance',
} as PriceLineOptions);
priceLinesRef.current.push(resistanceLine);
}
// Predicted range lines
const predHighLine = candleSeriesRef.current.createPriceLine({
price: pred.predicted_high,
color: ML_COLORS.rangePredictionHigh.replace('0.3', '0.7'),
lineWidth: 1,
lineStyle: LineStyle.LargeDashed,
axisLabelVisible: true,
title: `Pred High (${(pred.prediction_confidence * 100).toFixed(0)}%)`,
} as PriceLineOptions);
priceLinesRef.current.push(predHighLine);
const predLowLine = candleSeriesRef.current.createPriceLine({
price: pred.predicted_low,
color: ML_COLORS.rangePredictionLow.replace('0.3', '0.7'),
lineWidth: 1,
lineStyle: LineStyle.LargeDashed,
axisLabelVisible: true,
title: `Pred Low`,
} as PriceLineOptions);
priceLinesRef.current.push(predLowLine);
}
}, [mlOverlays, showSignalLevels, showRangePrediction]);
// Update data method
const updateData = useCallback(
(candles: Candle[]) => {
if (!candleSeriesRef.current || candles.length === 0) return;
// Transform data for candlestick series
const candleData: CandlestickData[] = candles.map((c) => ({
time: (c.time / 1000) as Time,
open: c.open,
high: c.high,
low: c.low,
close: c.close,
}));
candleSeriesRef.current.setData(candleData);
// Add signal markers
if (mlOverlays.signal) {
const signal = mlOverlays.signal;
const signalTime = new Date(signal.created_at).getTime() / 1000;
// Find the candle closest to signal time
const closestCandle = candleData.reduce((prev, curr) => {
return Math.abs((curr.time as number) - signalTime) < Math.abs((prev.time as number) - signalTime) ? curr : prev;
});
const markers: SeriesMarker<Time>[] = [{
time: closestCandle.time,
position: signal.direction === 'long' ? 'belowBar' : 'aboveBar',
color: signal.direction === 'long' ? ML_COLORS.takeProfit : ML_COLORS.stopLoss,
shape: signal.direction === 'long' ? 'arrowUp' : 'arrowDown',
text: `${signal.direction.toUpperCase()} ${(signal.confidence_score * 100).toFixed(0)}%`,
}];
candleSeriesRef.current.setMarkers(markers);
}
// Transform data for volume series
if (volumeSeriesRef.current && showVolume) {
const volumeData: HistogramData[] = candles.map((c) => ({
time: (c.time / 1000) as Time,
value: c.volume,
color: c.close >= c.open ? chartTheme.volumeUpColor : chartTheme.volumeDownColor,
}));
volumeSeriesRef.current.setData(volumeData);
}
// Fit content
chartRef.current?.timeScale().fitContent();
},
[showVolume, chartTheme, mlOverlays.signal]
);
// Expose updateData method through ref
useEffect(() => {
if (containerRef.current) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(containerRef.current as any).updateData = updateData;
}
}, [updateData]);
return (
<div className="relative w-full" style={{ height }}>
{/* ML Status Indicator */}
{enableMLOverlays && (
<div className="absolute top-2 right-2 z-10 flex items-center gap-2">
{isLoadingML && (
<div className="flex items-center gap-1 px-2 py-1 bg-gray-800/80 rounded text-xs text-gray-400">
<svg className="animate-spin h-3 w-3" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span>ML</span>
</div>
)}
{mlOverlays.amdPhase && (
<div className={`px-2 py-1 rounded text-xs font-medium ${
mlOverlays.amdPhase.phase === 'accumulation' ? 'bg-blue-500/20 text-blue-400' :
mlOverlays.amdPhase.phase === 'manipulation' ? 'bg-orange-500/20 text-orange-400' :
mlOverlays.amdPhase.phase === 'distribution' ? 'bg-purple-500/20 text-purple-400' :
'bg-gray-500/20 text-gray-400'
}`}>
{mlOverlays.amdPhase.phase.toUpperCase()} {(mlOverlays.amdPhase.confidence * 100).toFixed(0)}%
</div>
)}
{mlOverlays.rangePrediction && (
<div className="px-2 py-1 bg-gray-800/80 rounded text-xs text-gray-300">
Range: {mlOverlays.rangePrediction.expected_range_percent.toFixed(2)}%
</div>
)}
{mlOverlays.signal && (
<div className={`px-2 py-1 rounded text-xs font-medium ${
mlOverlays.signal.direction === 'long' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
}`}>
{mlOverlays.signal.direction.toUpperCase()} RR {mlOverlays.signal.risk_reward_ratio.toFixed(1)}
</div>
)}
</div>
)}
{/* Signal Details Panel */}
{enableMLOverlays && mlOverlays.signal && (
<div className="absolute bottom-2 left-2 z-10 bg-gray-900/90 backdrop-blur-sm rounded-lg p-3 border border-gray-700 text-xs max-w-xs">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-0.5 rounded font-bold ${
mlOverlays.signal.direction === 'long' ? 'bg-green-500 text-white' : 'bg-red-500 text-white'
}`}>
{mlOverlays.signal.direction.toUpperCase()}
</span>
<span className="text-gray-400">
{(mlOverlays.signal.confidence_score * 100).toFixed(0)}% confidence
</span>
</div>
<div className="grid grid-cols-3 gap-2 text-gray-300">
<div>
<span className="text-gray-500 block">Entry</span>
<span className="text-blue-400 font-mono">{mlOverlays.signal.entry_price.toFixed(5)}</span>
</div>
<div>
<span className="text-gray-500 block">SL</span>
<span className="text-red-400 font-mono">{mlOverlays.signal.stop_loss.toFixed(5)}</span>
</div>
<div>
<span className="text-gray-500 block">TP</span>
<span className="text-green-400 font-mono">{mlOverlays.signal.take_profit.toFixed(5)}</span>
</div>
</div>
<div className="mt-2 pt-2 border-t border-gray-700 flex justify-between text-gray-400">
<span>P(TP First): {(mlOverlays.signal.prob_tp_first * 100).toFixed(0)}%</span>
<span>RR: {mlOverlays.signal.risk_reward_ratio.toFixed(2)}</span>
</div>
</div>
)}
{/* Chart Container */}
<div
ref={containerRef}
className="w-full h-full"
data-symbol={symbol}
data-interval={interval}
/>
</div>
);
};
export default CandlestickChartWithML;