React frontend with: - Authentication UI - Trading dashboard - ML signals display - Portfolio management Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
567 lines
19 KiB
TypeScript
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;
|