[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,
|
||||
} 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';
|
||||
import type { MLSignal, RangePrediction, AMDPhase, AMDZone } from '../../../services/mlService';
|
||||
import { getLatestSignal, getRangePrediction, getAMDPhase, getAMDZones } from '../../../services/mlService';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@ -51,6 +51,7 @@ interface MLOverlays {
|
||||
signal?: MLSignal | null;
|
||||
rangePrediction?: RangePrediction | null;
|
||||
amdPhase?: AMDPhase | null;
|
||||
amdZones?: AMDZone[];
|
||||
orderBlocks?: OrderBlock[];
|
||||
fairValueGaps?: FairValueGap[];
|
||||
}
|
||||
@ -62,6 +63,7 @@ interface CandlestickChartWithMLProps extends CandlestickChartProps {
|
||||
showOrderBlocks?: boolean;
|
||||
showFairValueGaps?: boolean;
|
||||
showAMDPhase?: boolean;
|
||||
showAMDZones?: boolean;
|
||||
autoRefreshML?: boolean;
|
||||
refreshInterval?: number;
|
||||
}
|
||||
@ -146,6 +148,7 @@ export const CandlestickChartWithML: React.FC<CandlestickChartWithMLProps> = ({
|
||||
showOrderBlocks = false,
|
||||
showFairValueGaps = false,
|
||||
showAMDPhase = true,
|
||||
showAMDZones = true,
|
||||
autoRefreshML = true,
|
||||
refreshInterval = 30000,
|
||||
}) => {
|
||||
@ -157,6 +160,7 @@ export const CandlestickChartWithML: React.FC<CandlestickChartWithMLProps> = ({
|
||||
const rangeLowSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
|
||||
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||
const priceLinesRef = useRef<ReturnType<ISeriesApi<'Candlestick'>['createPriceLine']>[]>([]);
|
||||
const amdZoneSeriesRef = useRef<Map<string, ISeriesApi<'Area'>>>(new Map());
|
||||
|
||||
const [mlOverlays, setMlOverlays] = useState<MLOverlays>({});
|
||||
const [isLoadingML, setIsLoadingML] = useState(false);
|
||||
@ -169,23 +173,25 @@ export const CandlestickChartWithML: React.FC<CandlestickChartWithMLProps> = ({
|
||||
|
||||
setIsLoadingML(true);
|
||||
try {
|
||||
const [signal, rangePred, amd] = await Promise.all([
|
||||
const [signal, rangePred, amd, zones] = await Promise.all([
|
||||
showSignalLevels ? getLatestSignal(symbol) : Promise.resolve(null),
|
||||
showRangePrediction ? getRangePrediction(symbol, interval) : Promise.resolve(null),
|
||||
showAMDPhase ? getAMDPhase(symbol) : Promise.resolve(null),
|
||||
showAMDZones ? getAMDZones(symbol, interval, 10) : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
setMlOverlays({
|
||||
signal,
|
||||
rangePrediction: rangePred,
|
||||
amdPhase: amd,
|
||||
amdZones: zones,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching ML data:', error);
|
||||
} finally {
|
||||
setIsLoadingML(false);
|
||||
}
|
||||
}, [symbol, interval, enableMLOverlays, showSignalLevels, showRangePrediction, showAMDPhase]);
|
||||
}, [symbol, interval, enableMLOverlays, showSignalLevels, showRangePrediction, showAMDPhase, showAMDZones]);
|
||||
|
||||
// Auto-refresh ML data
|
||||
useEffect(() => {
|
||||
@ -415,6 +421,84 @@ export const CandlestickChartWithML: React.FC<CandlestickChartWithMLProps> = ({
|
||||
}
|
||||
}, [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
|
||||
const updateData = useCallback(
|
||||
(candles: Candle[]) => {
|
||||
|
||||
@ -40,6 +40,7 @@ export interface ScreenerResult {
|
||||
low52w: number;
|
||||
rsi?: number;
|
||||
macdSignal?: 'bullish' | 'bearish' | 'neutral';
|
||||
trend?: 'bullish' | 'bearish' | 'sideways';
|
||||
sma20?: number;
|
||||
sma50?: number;
|
||||
sma200?: number;
|
||||
@ -47,6 +48,9 @@ export interface ScreenerResult {
|
||||
beta?: number;
|
||||
dividendYield?: number;
|
||||
isFavorite?: boolean;
|
||||
volumeRatio?: number;
|
||||
mlSignal?: 'buy' | 'sell' | 'hold' | null;
|
||||
mlConfidence?: number;
|
||||
}
|
||||
|
||||
export interface ScreenerFilter {
|
||||
@ -111,12 +115,29 @@ const FILTER_PRESETS: { name: string; filters: Omit<ScreenerFilter, 'id'>[] }[]
|
||||
name: 'Near 52W High',
|
||||
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' }[] = [
|
||||
{ field: 'price', label: 'Price', type: 'number' },
|
||||
{ field: 'changePercent', label: 'Change %', type: 'number' },
|
||||
{ field: 'volume', label: 'Volume', type: 'number' },
|
||||
{ field: 'volumeRatio', label: 'Vol Ratio', type: 'number' },
|
||||
{ field: 'marketCap', label: 'Market Cap', type: 'number' },
|
||||
{ field: 'pe', label: 'P/E Ratio', 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: 'sector', label: 'Sector', 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> = ({
|
||||
@ -152,7 +176,11 @@ const TradingScreener: React.FC<TradingScreenerProps> = ({
|
||||
|
||||
// Apply filters and sorting
|
||||
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
|
||||
if (searchQuery) {
|
||||
@ -529,6 +557,16 @@ const TradingScreener: React.FC<TradingScreenerProps> = ({
|
||||
RSI{renderSortIndicator('rsi')}
|
||||
</button>
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -581,6 +619,37 @@ const TradingScreener: React.FC<TradingScreenerProps> = ({
|
||||
</span>
|
||||
)}
|
||||
</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">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<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 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 {
|
||||
symbol: 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
|
||||
*/
|
||||
|
||||
@ -45,6 +45,35 @@ export interface ICTConcept {
|
||||
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
|
||||
// ============================================================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user