[SPRINT-2] feat: Add AMD zones overlay and enhance screener
ST-003.2: ML Overlay (13 SP) - Add AMDZonesOverlay component for accumulation/manipulation/distribution visualization - AMD zones rendered as colored shaded areas on chart - Prediction lines and signal markers already implemented ST-003.6: Screener Advanced (8 SP) - Add volumeRatio, trend, mlSignal, mlConfidence columns - Add 4 new filter presets: Bullish/Bearish Trend, Change >2%, ML Buy Signal - Vol Ratio column with color coding (>2x amber, >1x green) - ML Signal badge with confidence percentage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a3bd7af7b7
commit
950d0a7804
@ -19,8 +19,8 @@ import {
|
|||||||
SeriesMarker,
|
SeriesMarker,
|
||||||
} from 'lightweight-charts';
|
} from 'lightweight-charts';
|
||||||
import type { Candle, CandlestickChartProps } from '../../../types/trading.types';
|
import type { Candle, CandlestickChartProps } from '../../../types/trading.types';
|
||||||
import type { MLSignal, RangePrediction, AMDPhase } from '../../../services/mlService';
|
import type { MLSignal, RangePrediction, AMDPhase, AMDZone } from '../../../services/mlService';
|
||||||
import { getLatestSignal, getRangePrediction, getAMDPhase } from '../../../services/mlService';
|
import { getLatestSignal, getRangePrediction, getAMDPhase, getAMDZones } from '../../../services/mlService';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@ -51,6 +51,7 @@ interface MLOverlays {
|
|||||||
signal?: MLSignal | null;
|
signal?: MLSignal | null;
|
||||||
rangePrediction?: RangePrediction | null;
|
rangePrediction?: RangePrediction | null;
|
||||||
amdPhase?: AMDPhase | null;
|
amdPhase?: AMDPhase | null;
|
||||||
|
amdZones?: AMDZone[];
|
||||||
orderBlocks?: OrderBlock[];
|
orderBlocks?: OrderBlock[];
|
||||||
fairValueGaps?: FairValueGap[];
|
fairValueGaps?: FairValueGap[];
|
||||||
}
|
}
|
||||||
@ -62,6 +63,7 @@ interface CandlestickChartWithMLProps extends CandlestickChartProps {
|
|||||||
showOrderBlocks?: boolean;
|
showOrderBlocks?: boolean;
|
||||||
showFairValueGaps?: boolean;
|
showFairValueGaps?: boolean;
|
||||||
showAMDPhase?: boolean;
|
showAMDPhase?: boolean;
|
||||||
|
showAMDZones?: boolean;
|
||||||
autoRefreshML?: boolean;
|
autoRefreshML?: boolean;
|
||||||
refreshInterval?: number;
|
refreshInterval?: number;
|
||||||
}
|
}
|
||||||
@ -146,6 +148,7 @@ export const CandlestickChartWithML: React.FC<CandlestickChartWithMLProps> = ({
|
|||||||
showOrderBlocks = false,
|
showOrderBlocks = false,
|
||||||
showFairValueGaps = false,
|
showFairValueGaps = false,
|
||||||
showAMDPhase = true,
|
showAMDPhase = true,
|
||||||
|
showAMDZones = true,
|
||||||
autoRefreshML = true,
|
autoRefreshML = true,
|
||||||
refreshInterval = 30000,
|
refreshInterval = 30000,
|
||||||
}) => {
|
}) => {
|
||||||
@ -157,6 +160,7 @@ export const CandlestickChartWithML: React.FC<CandlestickChartWithMLProps> = ({
|
|||||||
const rangeLowSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
|
const rangeLowSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
|
||||||
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||||
const priceLinesRef = useRef<ReturnType<ISeriesApi<'Candlestick'>['createPriceLine']>[]>([]);
|
const priceLinesRef = useRef<ReturnType<ISeriesApi<'Candlestick'>['createPriceLine']>[]>([]);
|
||||||
|
const amdZoneSeriesRef = useRef<Map<string, ISeriesApi<'Area'>>>(new Map());
|
||||||
|
|
||||||
const [mlOverlays, setMlOverlays] = useState<MLOverlays>({});
|
const [mlOverlays, setMlOverlays] = useState<MLOverlays>({});
|
||||||
const [isLoadingML, setIsLoadingML] = useState(false);
|
const [isLoadingML, setIsLoadingML] = useState(false);
|
||||||
@ -169,23 +173,25 @@ export const CandlestickChartWithML: React.FC<CandlestickChartWithMLProps> = ({
|
|||||||
|
|
||||||
setIsLoadingML(true);
|
setIsLoadingML(true);
|
||||||
try {
|
try {
|
||||||
const [signal, rangePred, amd] = await Promise.all([
|
const [signal, rangePred, amd, zones] = await Promise.all([
|
||||||
showSignalLevels ? getLatestSignal(symbol) : Promise.resolve(null),
|
showSignalLevels ? getLatestSignal(symbol) : Promise.resolve(null),
|
||||||
showRangePrediction ? getRangePrediction(symbol, interval) : Promise.resolve(null),
|
showRangePrediction ? getRangePrediction(symbol, interval) : Promise.resolve(null),
|
||||||
showAMDPhase ? getAMDPhase(symbol) : Promise.resolve(null),
|
showAMDPhase ? getAMDPhase(symbol) : Promise.resolve(null),
|
||||||
|
showAMDZones ? getAMDZones(symbol, interval, 10) : Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setMlOverlays({
|
setMlOverlays({
|
||||||
signal,
|
signal,
|
||||||
rangePrediction: rangePred,
|
rangePrediction: rangePred,
|
||||||
amdPhase: amd,
|
amdPhase: amd,
|
||||||
|
amdZones: zones,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching ML data:', error);
|
console.error('Error fetching ML data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingML(false);
|
setIsLoadingML(false);
|
||||||
}
|
}
|
||||||
}, [symbol, interval, enableMLOverlays, showSignalLevels, showRangePrediction, showAMDPhase]);
|
}, [symbol, interval, enableMLOverlays, showSignalLevels, showRangePrediction, showAMDPhase, showAMDZones]);
|
||||||
|
|
||||||
// Auto-refresh ML data
|
// Auto-refresh ML data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -415,6 +421,84 @@ export const CandlestickChartWithML: React.FC<CandlestickChartWithMLProps> = ({
|
|||||||
}
|
}
|
||||||
}, [mlOverlays, showSignalLevels, showRangePrediction]);
|
}, [mlOverlays, showSignalLevels, showRangePrediction]);
|
||||||
|
|
||||||
|
// Update AMD zones visualization
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartRef.current || !showAMDZones) return;
|
||||||
|
|
||||||
|
const chart = chartRef.current;
|
||||||
|
|
||||||
|
// Clear existing AMD zone series
|
||||||
|
amdZoneSeriesRef.current.forEach((series) => {
|
||||||
|
try {
|
||||||
|
chart.removeSeries(series);
|
||||||
|
} catch {
|
||||||
|
// Series might already be removed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
amdZoneSeriesRef.current.clear();
|
||||||
|
|
||||||
|
// Render new AMD zones
|
||||||
|
const zones = mlOverlays.amdZones || [];
|
||||||
|
if (zones.length === 0) return;
|
||||||
|
|
||||||
|
zones.forEach((zone, index) => {
|
||||||
|
const zoneId = `amd-zone-${index}-${zone.phase}`;
|
||||||
|
|
||||||
|
// Get color based on phase
|
||||||
|
let zoneColor: string;
|
||||||
|
switch (zone.phase) {
|
||||||
|
case 'accumulation':
|
||||||
|
zoneColor = ML_COLORS.amdAccumulation;
|
||||||
|
break;
|
||||||
|
case 'manipulation':
|
||||||
|
zoneColor = ML_COLORS.amdManipulation;
|
||||||
|
break;
|
||||||
|
case 'distribution':
|
||||||
|
zoneColor = ML_COLORS.amdDistribution;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
zoneColor = 'rgba(107, 114, 128, 0.1)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust opacity based on confidence
|
||||||
|
const opacity = Math.max(0.05, 0.15 * zone.confidence);
|
||||||
|
const adjustedColor = zoneColor.replace(/[\d.]+\)$/, `${opacity})`);
|
||||||
|
|
||||||
|
// Create area series for zone
|
||||||
|
const areaSeries = chart.addAreaSeries({
|
||||||
|
topColor: adjustedColor,
|
||||||
|
bottomColor: 'transparent',
|
||||||
|
lineColor: 'transparent',
|
||||||
|
lineWidth: 1,
|
||||||
|
crosshairMarkerVisible: false,
|
||||||
|
lastValueVisible: false,
|
||||||
|
priceLineVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set zone data
|
||||||
|
const startTime = (zone.startTime / 1000) as Time;
|
||||||
|
const endTime = (zone.endTime / 1000) as Time;
|
||||||
|
|
||||||
|
areaSeries.setData([
|
||||||
|
{ time: startTime, value: zone.priceHigh },
|
||||||
|
{ time: endTime, value: zone.priceHigh },
|
||||||
|
]);
|
||||||
|
|
||||||
|
amdZoneSeriesRef.current.set(zoneId, areaSeries);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
amdZoneSeriesRef.current.forEach((series) => {
|
||||||
|
try {
|
||||||
|
chart.removeSeries(series);
|
||||||
|
} catch {
|
||||||
|
// Series might already be removed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
amdZoneSeriesRef.current.clear();
|
||||||
|
};
|
||||||
|
}, [mlOverlays.amdZones, showAMDZones]);
|
||||||
|
|
||||||
// Update data method
|
// Update data method
|
||||||
const updateData = useCallback(
|
const updateData = useCallback(
|
||||||
(candles: Candle[]) => {
|
(candles: Candle[]) => {
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export interface ScreenerResult {
|
|||||||
low52w: number;
|
low52w: number;
|
||||||
rsi?: number;
|
rsi?: number;
|
||||||
macdSignal?: 'bullish' | 'bearish' | 'neutral';
|
macdSignal?: 'bullish' | 'bearish' | 'neutral';
|
||||||
|
trend?: 'bullish' | 'bearish' | 'sideways';
|
||||||
sma20?: number;
|
sma20?: number;
|
||||||
sma50?: number;
|
sma50?: number;
|
||||||
sma200?: number;
|
sma200?: number;
|
||||||
@ -47,6 +48,9 @@ export interface ScreenerResult {
|
|||||||
beta?: number;
|
beta?: number;
|
||||||
dividendYield?: number;
|
dividendYield?: number;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
|
volumeRatio?: number;
|
||||||
|
mlSignal?: 'buy' | 'sell' | 'hold' | null;
|
||||||
|
mlConfidence?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScreenerFilter {
|
export interface ScreenerFilter {
|
||||||
@ -111,12 +115,29 @@ const FILTER_PRESETS: { name: string; filters: Omit<ScreenerFilter, 'id'>[] }[]
|
|||||||
name: 'Near 52W High',
|
name: 'Near 52W High',
|
||||||
filters: [{ field: 'changePercent', operator: 'gt', value: 5, enabled: true }],
|
filters: [{ field: 'changePercent', operator: 'gt', value: 5, enabled: true }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Bullish Trend',
|
||||||
|
filters: [{ field: 'trend', operator: 'eq', value: 'bullish', enabled: true }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Bearish Trend',
|
||||||
|
filters: [{ field: 'trend', operator: 'eq', value: 'bearish', enabled: true }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Change > 2%',
|
||||||
|
filters: [{ field: 'changePercent', operator: 'gt', value: 2, enabled: true }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ML Buy Signal',
|
||||||
|
filters: [{ field: 'mlSignal', operator: 'eq', value: 'buy', enabled: true }],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const FILTERABLE_FIELDS: { field: keyof ScreenerResult; label: string; type: 'number' | 'string' }[] = [
|
const FILTERABLE_FIELDS: { field: keyof ScreenerResult; label: string; type: 'number' | 'string' }[] = [
|
||||||
{ field: 'price', label: 'Price', type: 'number' },
|
{ field: 'price', label: 'Price', type: 'number' },
|
||||||
{ field: 'changePercent', label: 'Change %', type: 'number' },
|
{ field: 'changePercent', label: 'Change %', type: 'number' },
|
||||||
{ field: 'volume', label: 'Volume', type: 'number' },
|
{ field: 'volume', label: 'Volume', type: 'number' },
|
||||||
|
{ field: 'volumeRatio', label: 'Vol Ratio', type: 'number' },
|
||||||
{ field: 'marketCap', label: 'Market Cap', type: 'number' },
|
{ field: 'marketCap', label: 'Market Cap', type: 'number' },
|
||||||
{ field: 'pe', label: 'P/E Ratio', type: 'number' },
|
{ field: 'pe', label: 'P/E Ratio', type: 'number' },
|
||||||
{ field: 'rsi', label: 'RSI', type: 'number' },
|
{ field: 'rsi', label: 'RSI', type: 'number' },
|
||||||
@ -124,6 +145,9 @@ const FILTERABLE_FIELDS: { field: keyof ScreenerResult; label: string; type: 'nu
|
|||||||
{ field: 'dividendYield', label: 'Dividend Yield', type: 'number' },
|
{ field: 'dividendYield', label: 'Dividend Yield', type: 'number' },
|
||||||
{ field: 'sector', label: 'Sector', type: 'string' },
|
{ field: 'sector', label: 'Sector', type: 'string' },
|
||||||
{ field: 'exchange', label: 'Exchange', type: 'string' },
|
{ field: 'exchange', label: 'Exchange', type: 'string' },
|
||||||
|
{ field: 'trend', label: 'Trend', type: 'string' },
|
||||||
|
{ field: 'mlSignal', label: 'ML Signal', type: 'string' },
|
||||||
|
{ field: 'mlConfidence', label: 'ML Confidence', type: 'number' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const TradingScreener: React.FC<TradingScreenerProps> = ({
|
const TradingScreener: React.FC<TradingScreenerProps> = ({
|
||||||
@ -152,7 +176,11 @@ const TradingScreener: React.FC<TradingScreenerProps> = ({
|
|||||||
|
|
||||||
// Apply filters and sorting
|
// Apply filters and sorting
|
||||||
const filteredResults = useMemo(() => {
|
const filteredResults = useMemo(() => {
|
||||||
let filtered = [...results];
|
// Enrich results with computed fields if not present
|
||||||
|
let filtered = results.map((r) => ({
|
||||||
|
...r,
|
||||||
|
volumeRatio: r.volumeRatio ?? (r.avgVolume > 0 ? r.volume / r.avgVolume : undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
// Search filter
|
// Search filter
|
||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
@ -529,6 +557,16 @@ const TradingScreener: React.FC<TradingScreenerProps> = ({
|
|||||||
RSI{renderSortIndicator('rsi')}
|
RSI{renderSortIndicator('rsi')}
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
|
<th className="text-right py-2 px-2 text-gray-400 font-medium hidden xl:table-cell">
|
||||||
|
<button onClick={() => handleSort('volumeRatio')} className="hover:text-white">
|
||||||
|
Vol Ratio{renderSortIndicator('volumeRatio')}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th className="text-center py-2 px-2 text-gray-400 font-medium hidden xl:table-cell">
|
||||||
|
<button onClick={() => handleSort('mlSignal')} className="hover:text-white">
|
||||||
|
ML Signal{renderSortIndicator('mlSignal')}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
<th className="text-center py-2 px-2 text-gray-400 font-medium w-20">Actions</th>
|
<th className="text-center py-2 px-2 text-gray-400 font-medium w-20">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -581,6 +619,37 @@ const TradingScreener: React.FC<TradingScreenerProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="py-2 px-2 text-right hidden xl:table-cell">
|
||||||
|
{result.volumeRatio !== undefined && (
|
||||||
|
<span
|
||||||
|
className={`font-mono ${
|
||||||
|
result.volumeRatio > 2 ? 'text-amber-400' : result.volumeRatio > 1 ? 'text-green-400' : 'text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{result.volumeRatio.toFixed(2)}x
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-2 px-2 text-center hidden xl:table-cell">
|
||||||
|
{result.mlSignal && (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium ${
|
||||||
|
result.mlSignal === 'buy'
|
||||||
|
? 'bg-green-500/20 text-green-400'
|
||||||
|
: result.mlSignal === 'sell'
|
||||||
|
? 'bg-red-500/20 text-red-400'
|
||||||
|
: 'bg-gray-500/20 text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{result.mlSignal.toUpperCase()}
|
||||||
|
{result.mlConfidence !== undefined && (
|
||||||
|
<span className="text-[8px] opacity-75">
|
||||||
|
{(result.mlConfidence * 100).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="py-2 px-2">
|
<td className="py-2 px-2">
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* AMDZonesOverlay Component
|
||||||
|
* Renders AMD (Accumulation, Manipulation, Distribution) zones as shaded areas on the chart
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import type { IChartApi, ISeriesApi, Time } from 'lightweight-charts';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type AMDPhaseType = 'accumulation' | 'manipulation' | 'distribution' | 'unknown';
|
||||||
|
|
||||||
|
export interface AMDZone {
|
||||||
|
phase: AMDPhaseType;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
priceHigh: number;
|
||||||
|
priceLow: number;
|
||||||
|
confidence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AMDZonesOverlayProps {
|
||||||
|
chartRef: React.RefObject<IChartApi>;
|
||||||
|
zones: AMDZone[];
|
||||||
|
visible?: boolean;
|
||||||
|
colors?: {
|
||||||
|
accumulation: string;
|
||||||
|
manipulation: string;
|
||||||
|
distribution: string;
|
||||||
|
unknown: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Default Colors
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const DEFAULT_AMD_COLORS = {
|
||||||
|
accumulation: 'rgba(59, 130, 246, 0.15)', // Blue - calm, building
|
||||||
|
manipulation: 'rgba(249, 115, 22, 0.15)', // Orange - caution, volatility
|
||||||
|
distribution: 'rgba(239, 68, 68, 0.15)', // Red - selling pressure
|
||||||
|
unknown: 'rgba(107, 114, 128, 0.1)', // Gray - uncertain
|
||||||
|
};
|
||||||
|
|
||||||
|
// Border colors for zone edges
|
||||||
|
const BORDER_COLORS: Record<AMDPhaseType, string> = {
|
||||||
|
accumulation: 'rgba(59, 130, 246, 0.5)',
|
||||||
|
manipulation: 'rgba(249, 115, 22, 0.5)',
|
||||||
|
distribution: 'rgba(239, 68, 68, 0.5)',
|
||||||
|
unknown: 'rgba(107, 114, 128, 0.3)',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const AMDZonesOverlay: React.FC<AMDZonesOverlayProps> = ({
|
||||||
|
chartRef,
|
||||||
|
zones,
|
||||||
|
visible = true,
|
||||||
|
colors = DEFAULT_AMD_COLORS,
|
||||||
|
}) => {
|
||||||
|
const seriesMapRef = useRef<Map<string, ISeriesApi<'Area'>>>(new Map());
|
||||||
|
const lineSeriesMapRef = useRef<Map<string, ISeriesApi<'Line'>>>(new Map());
|
||||||
|
|
||||||
|
// Initialize and update zones
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartRef.current || !visible) return;
|
||||||
|
|
||||||
|
const chart = chartRef.current;
|
||||||
|
|
||||||
|
// Clear existing series
|
||||||
|
seriesMapRef.current.forEach((series) => {
|
||||||
|
try {
|
||||||
|
chart.removeSeries(series);
|
||||||
|
} catch {
|
||||||
|
// Series might already be removed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
seriesMapRef.current.clear();
|
||||||
|
|
||||||
|
lineSeriesMapRef.current.forEach((series) => {
|
||||||
|
try {
|
||||||
|
chart.removeSeries(series);
|
||||||
|
} catch {
|
||||||
|
// Series might already be removed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lineSeriesMapRef.current.clear();
|
||||||
|
|
||||||
|
if (zones.length === 0) return;
|
||||||
|
|
||||||
|
// Create area series for each zone
|
||||||
|
zones.forEach((zone, index) => {
|
||||||
|
const zoneId = `amd-zone-${index}-${zone.phase}`;
|
||||||
|
const zoneColor = colors[zone.phase] || DEFAULT_AMD_COLORS[zone.phase];
|
||||||
|
const borderColor = BORDER_COLORS[zone.phase];
|
||||||
|
|
||||||
|
// Create area series for the zone fill
|
||||||
|
const areaSeries = chart.addAreaSeries({
|
||||||
|
topColor: zoneColor,
|
||||||
|
bottomColor: 'transparent',
|
||||||
|
lineColor: 'transparent',
|
||||||
|
lineWidth: 1,
|
||||||
|
crosshairMarkerVisible: false,
|
||||||
|
lastValueVisible: false,
|
||||||
|
priceLineVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate opacity based on confidence
|
||||||
|
const adjustedColor = adjustColorOpacity(zoneColor, zone.confidence);
|
||||||
|
|
||||||
|
areaSeries.applyOptions({
|
||||||
|
topColor: adjustedColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create data points for the zone area
|
||||||
|
const startTime = (zone.startTime / 1000) as Time;
|
||||||
|
const endTime = (zone.endTime / 1000) as Time;
|
||||||
|
|
||||||
|
const areaData = [
|
||||||
|
{ time: startTime, value: zone.priceHigh },
|
||||||
|
{ time: endTime, value: zone.priceHigh },
|
||||||
|
];
|
||||||
|
|
||||||
|
areaSeries.setData(areaData);
|
||||||
|
seriesMapRef.current.set(zoneId, areaSeries);
|
||||||
|
|
||||||
|
// Create top line for visual boundary
|
||||||
|
const topLine = chart.addLineSeries({
|
||||||
|
color: borderColor,
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: 2, // Dashed
|
||||||
|
crosshairMarkerVisible: false,
|
||||||
|
lastValueVisible: false,
|
||||||
|
priceLineVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
topLine.setData([
|
||||||
|
{ time: startTime, value: zone.priceHigh },
|
||||||
|
{ time: endTime, value: zone.priceHigh },
|
||||||
|
]);
|
||||||
|
|
||||||
|
lineSeriesMapRef.current.set(`${zoneId}-top`, topLine);
|
||||||
|
|
||||||
|
// Create bottom line for visual boundary
|
||||||
|
const bottomLine = chart.addLineSeries({
|
||||||
|
color: borderColor,
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: 2, // Dashed
|
||||||
|
crosshairMarkerVisible: false,
|
||||||
|
lastValueVisible: false,
|
||||||
|
priceLineVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
bottomLine.setData([
|
||||||
|
{ time: startTime, value: zone.priceLow },
|
||||||
|
{ time: endTime, value: zone.priceLow },
|
||||||
|
]);
|
||||||
|
|
||||||
|
lineSeriesMapRef.current.set(`${zoneId}-bottom`, bottomLine);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Cleanup on unmount
|
||||||
|
seriesMapRef.current.forEach((series) => {
|
||||||
|
try {
|
||||||
|
chart.removeSeries(series);
|
||||||
|
} catch {
|
||||||
|
// Series might already be removed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
seriesMapRef.current.clear();
|
||||||
|
|
||||||
|
lineSeriesMapRef.current.forEach((series) => {
|
||||||
|
try {
|
||||||
|
chart.removeSeries(series);
|
||||||
|
} catch {
|
||||||
|
// Series might already be removed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lineSeriesMapRef.current.clear();
|
||||||
|
};
|
||||||
|
}, [chartRef, zones, visible, colors]);
|
||||||
|
|
||||||
|
// Update visibility
|
||||||
|
useEffect(() => {
|
||||||
|
seriesMapRef.current.forEach((series) => {
|
||||||
|
series.applyOptions({ visible });
|
||||||
|
});
|
||||||
|
|
||||||
|
lineSeriesMapRef.current.forEach((series) => {
|
||||||
|
series.applyOptions({ visible });
|
||||||
|
});
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helper Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts the opacity of an rgba color based on confidence
|
||||||
|
*/
|
||||||
|
function adjustColorOpacity(color: string, confidence: number): string {
|
||||||
|
// Extract rgba components
|
||||||
|
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
||||||
|
|
||||||
|
if (!match) return color;
|
||||||
|
|
||||||
|
const r = match[1];
|
||||||
|
const g = match[2];
|
||||||
|
const b = match[3];
|
||||||
|
const baseOpacity = parseFloat(match[4] || '1');
|
||||||
|
|
||||||
|
// Scale opacity by confidence (minimum 0.05 to keep zones visible)
|
||||||
|
const adjustedOpacity = Math.max(0.05, baseOpacity * confidence);
|
||||||
|
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${adjustedOpacity})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AMDZonesOverlay;
|
||||||
@ -11,3 +11,6 @@ export type { SignalMarkersProps } from './SignalMarkers';
|
|||||||
|
|
||||||
export { ICTConceptsOverlay } from './ICTConceptsOverlay';
|
export { ICTConceptsOverlay } from './ICTConceptsOverlay';
|
||||||
export type { ICTConceptsOverlayProps } from './ICTConceptsOverlay';
|
export type { ICTConceptsOverlayProps } from './ICTConceptsOverlay';
|
||||||
|
|
||||||
|
export { AMDZonesOverlay } from './AMDZonesOverlay';
|
||||||
|
export type { AMDZonesOverlayProps, AMDZone, AMDPhaseType } from './AMDZonesOverlay';
|
||||||
|
|||||||
@ -42,6 +42,15 @@ export interface AMDPhase {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AMDZone {
|
||||||
|
phase: 'accumulation' | 'manipulation' | 'distribution' | 'unknown';
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
priceHigh: number;
|
||||||
|
priceLow: number;
|
||||||
|
confidence: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RangePrediction {
|
export interface RangePrediction {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
timeframe: string;
|
timeframe: string;
|
||||||
@ -113,6 +122,27 @@ export async function getAMDPhase(symbol: string): Promise<AMDPhase | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get historical AMD zones for chart visualization
|
||||||
|
* Returns zones within the specified time range
|
||||||
|
*/
|
||||||
|
export async function getAMDZones(
|
||||||
|
symbol: string,
|
||||||
|
timeframe: string = '1h',
|
||||||
|
limit: number = 10
|
||||||
|
): Promise<AMDZone[]> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`/proxy/ml/amd/${symbol}/zones`, {
|
||||||
|
params: { timeframe, limit },
|
||||||
|
});
|
||||||
|
return response.data?.zones || response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as { response?: { status: number } }).response?.status === 404) return [];
|
||||||
|
console.error('Error fetching AMD zones:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get range prediction for a symbol
|
* Get range prediction for a symbol
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -45,6 +45,35 @@ export interface ICTConcept {
|
|||||||
priceBottom: number;
|
priceBottom: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// AMD Zone Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type AMDPhaseType = 'accumulation' | 'manipulation' | 'distribution' | 'unknown';
|
||||||
|
|
||||||
|
export interface AMDZone {
|
||||||
|
phase: AMDPhaseType;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
priceHigh: number;
|
||||||
|
priceLow: number;
|
||||||
|
confidence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AMDZoneColors {
|
||||||
|
accumulation: string;
|
||||||
|
manipulation: string;
|
||||||
|
distribution: string;
|
||||||
|
unknown: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_AMD_COLORS: AMDZoneColors = {
|
||||||
|
accumulation: 'rgba(59, 130, 246, 0.15)', // Blue - calm, building
|
||||||
|
manipulation: 'rgba(249, 115, 22, 0.15)', // Orange - caution, volatility
|
||||||
|
distribution: 'rgba(239, 68, 68, 0.15)', // Red - selling pressure
|
||||||
|
unknown: 'rgba(107, 114, 128, 0.1)', // Gray - uncertain
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Overlay Configuration Types
|
// Overlay Configuration Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user