실시간 차트 신고가,신저가 표시
This commit is contained in:
@@ -45,6 +45,9 @@ export interface ChartRightToolbarProps {
|
||||
onOpenCustom1OverlaySettings?: () => void;
|
||||
/** true — 캔들 pane 상단 오버레이 토글만 (미니 차트 등) */
|
||||
overlayControlsOnly?: boolean;
|
||||
/** 9·20일 신고가·신저가 라벨 표시 (실시간 차트) */
|
||||
priceExtremeLabelsVisible?: boolean;
|
||||
onTogglePriceExtremeLabels?: () => void;
|
||||
}
|
||||
|
||||
const BTN_SIZE = 32;
|
||||
@@ -150,6 +153,14 @@ const IconIchimoku = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
/** 신고가(상향)·신저가(하향) */
|
||||
const IconPriceExtreme = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden>
|
||||
<path d="M7 1.5 L10.5 6 H3.5 Z" fill="#4dabf7"/>
|
||||
<path d="M7 12.5 L3.5 8 H10.5 Z" fill="#ef5350"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const OVERLAY_ICONS: Record<ChartOverlayToggleKey, React.ReactNode> = {
|
||||
ma: <IconMa />,
|
||||
bollinger: <IconBollinger />,
|
||||
@@ -360,6 +371,8 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
onExpand, onRestore, focusedId,
|
||||
onDragStart, onToggleHidden, onSettings,
|
||||
overlayControlsOnly = false,
|
||||
priceExtremeLabelsVisible = false,
|
||||
onTogglePriceExtremeLabels,
|
||||
}) => {
|
||||
const [paneLayouts, setPaneLayouts] = useState<Array<{ paneIndex: number; topY: number; height: number }>>([]);
|
||||
const [paneItems, setPaneItems] = useState<ReturnType<typeof buildPaneItems>>([]);
|
||||
@@ -418,9 +431,11 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
|
||||
const candlePane = paneLayouts.find(l => l.paneIndex === 0);
|
||||
const showOverlayIcons = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
|
||||
const showPriceExtremeBtn = !!(candlePane && onTogglePriceExtremeLabels);
|
||||
const showOverlayCluster = showOverlayIcons || showPriceExtremeBtn;
|
||||
|
||||
if (paused || chartHeight <= 0) return null;
|
||||
if (overlayControlsOnly && !showOverlayIcons) return null;
|
||||
if (overlayControlsOnly && !showOverlayCluster) return null;
|
||||
|
||||
const innerHeight = paneLayouts.reduce(
|
||||
(max, l) => Math.max(max, l.topY + l.height),
|
||||
@@ -475,12 +490,12 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showOverlayIcons && (
|
||||
{showOverlayCluster && (
|
||||
<div
|
||||
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay-icons"
|
||||
style={{ top: candlePane!.topY + 4 }}
|
||||
>
|
||||
{candleOverlayToggles!.map((item, idx) => (
|
||||
{candleOverlayToggles?.map((item, idx) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
@@ -495,6 +510,20 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
{OVERLAY_ICONS[item.key]}
|
||||
</button>
|
||||
))}
|
||||
{showPriceExtremeBtn && (
|
||||
<button
|
||||
type="button"
|
||||
className={`tv-side-btn chart-right-toolbar-btn chart-right-toolbar-btn--overlay chart-right-toolbar-btn--price-extreme${priceExtremeLabelsVisible ? '' : ' chart-right-toolbar-btn--overlay-off'}`}
|
||||
title={priceExtremeLabelsVisible ? '9·20일 신고가·신저가 숨기기' : '9·20일 신고가·신저가 표시'}
|
||||
style={{ marginTop: (candleOverlayToggles?.length ?? 0) > 0 ? BTN_GAP : 0 }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onTogglePriceExtremeLabels?.();
|
||||
}}
|
||||
>
|
||||
<IconPriceExtreme />
|
||||
</button>
|
||||
)}
|
||||
{showCustomBtn && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -139,6 +139,8 @@ export interface ChartSlotHandle {
|
||||
setIndicatorAreaPriceLabelsEnabled: (enabled: boolean) => void;
|
||||
/** 거래량 pane on/off */
|
||||
setVolumeVisible: (visible: boolean) => void;
|
||||
/** 9·20일 신고가·신저가 표시 */
|
||||
setPriceExtremeLabelsVisible: (visible: boolean) => void;
|
||||
setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => void;
|
||||
/** 차트 시간축·크로스헤어 날짜 형식 */
|
||||
setChartTimeFormat: (format: string, timeframe?: Timeframe) => void;
|
||||
@@ -210,6 +212,9 @@ export interface ChartSlotProps {
|
||||
chartCrosshairInfoVisible?: boolean;
|
||||
/** 차트 마우스오버 줌·이동 플로팅 툴바 */
|
||||
chartHoverToolbarVisible?: boolean;
|
||||
/** 9·20일 신고가·신저가 차트 표시 */
|
||||
chartPriceExtremeLabelsVisible?: boolean;
|
||||
onTogglePriceExtremeLabels?: () => void;
|
||||
/** 업비트 실시간 티커 맵 (슬롯 심볼별 현재가·등락) */
|
||||
marketTickers?: Map<string, TickerData>;
|
||||
}
|
||||
@@ -239,6 +244,8 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
chartPaneSeparator,
|
||||
chartCrosshairInfoVisible = true,
|
||||
chartHoverToolbarVisible = true,
|
||||
chartPriceExtremeLabelsVisible = false,
|
||||
onTogglePriceExtremeLabels,
|
||||
marketTickers,
|
||||
}, ref) {
|
||||
// ── 전역 지표 파라미터 + 시각 설정 (DB 동기화) ────────────────────────
|
||||
@@ -364,6 +371,9 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
setVolumeVisible: (visible: boolean) => {
|
||||
managerRef.current?.setVolumeVisible(visible);
|
||||
},
|
||||
setPriceExtremeLabelsVisible: (visible: boolean) => {
|
||||
managerRef.current?.setPriceExtremeLabelsVisible(visible);
|
||||
},
|
||||
setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => {
|
||||
managerRef.current?.setPaneSeparatorOptions(opts);
|
||||
managerRef.current?.refreshPaneSeparatorOverlay();
|
||||
@@ -391,6 +401,10 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
managerRef.current?.setVolumeVisible(chartVolumeVisible);
|
||||
}, [chartVolumeVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
managerRef.current?.setPriceExtremeLabelsVisible(chartPriceExtremeLabelsVisible);
|
||||
}, [chartPriceExtremeLabelsVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartPaneSeparator) return;
|
||||
managerRef.current?.setPaneSeparatorOptions(chartPaneSeparator);
|
||||
@@ -1010,6 +1024,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
mgr.setCandleAreaPriceLabelsEnabled(chartCandleAreaPriceLabels);
|
||||
mgr.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels);
|
||||
mgr.setVolumeVisible(chartVolumeVisible);
|
||||
mgr.setPriceExtremeLabelsVisible(chartPriceExtremeLabelsVisible ?? false);
|
||||
if (chartPaneSeparator) mgr.setPaneSeparatorOptions(chartPaneSeparator);
|
||||
mgr.setChartOverlayVisibility(overlayVisibilityRef.current);
|
||||
syncCustomOverlaysToManager(mgr);
|
||||
@@ -1099,6 +1114,8 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
custom1OverlayActive={compactMode ? undefined : custom1OverlayActive}
|
||||
onCustom1OverlayActiveChange={compactMode ? undefined : handleCustom1OverlayActiveChange}
|
||||
onOpenCustom1OverlaySettings={compactMode ? undefined : () => setCustomOverlaySettingsSlot('custom1')}
|
||||
priceExtremeLabelsVisible={chartPriceExtremeLabelsVisible}
|
||||
onTogglePriceExtremeLabels={onTogglePriceExtremeLabels}
|
||||
/>
|
||||
|
||||
{customOverlaySettingsSlot === 'custom' && !compactMode && (
|
||||
|
||||
@@ -52,6 +52,10 @@ import {
|
||||
loadPaletteItems,
|
||||
type PaletteItem,
|
||||
} from '../utils/strategyPaletteStorage';
|
||||
import {
|
||||
buildCompositePaletteTemplateRows,
|
||||
buildSidewaysPaletteTemplateRows,
|
||||
} from '../utils/strategyEditorTemplatePaletteRows';
|
||||
import {
|
||||
buildPriceExtremeBreakoutTree,
|
||||
isPriceExtremeBreakoutPairPaletteValue,
|
||||
@@ -1307,22 +1311,22 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
|
||||
|
||||
const templateRows = useMemo(() => {
|
||||
const userRows = userTemplates.map(t => ({
|
||||
const userRows: TemplatePaletteRow[] = userTemplates.map(t => ({
|
||||
key: t.id,
|
||||
source: 'user' as const,
|
||||
source: 'user',
|
||||
label: t.label,
|
||||
description: t.description,
|
||||
signal: t.signal,
|
||||
user: t,
|
||||
}));
|
||||
const hidden = new Set(hiddenBuiltinTemplateIds);
|
||||
const builtinRows = templates
|
||||
const builtinRows: TemplatePaletteRow[] = templates
|
||||
.filter(t => !hidden.has(getBuiltinTemplateId(t)))
|
||||
.map(t => {
|
||||
const builtinId = getBuiltinTemplateId(t);
|
||||
return {
|
||||
key: builtinId,
|
||||
source: 'builtin' as const,
|
||||
source: 'builtin',
|
||||
label: t.label,
|
||||
description: 'description' in t ? t.description : undefined,
|
||||
signal: t.signal,
|
||||
@@ -1330,19 +1334,26 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
builtin: t,
|
||||
};
|
||||
});
|
||||
return [...userRows, ...builtinRows];
|
||||
}, [userTemplates, templates, hiddenBuiltinTemplateIds]);
|
||||
const compositeRows: TemplatePaletteRow[] = buildCompositePaletteTemplateRows(compositePalette, hidden);
|
||||
const sidewaysRows: TemplatePaletteRow[] = buildSidewaysPaletteTemplateRows(sidewaysPalette, hidden);
|
||||
return [...userRows, ...builtinRows, ...compositeRows, ...sidewaysRows];
|
||||
}, [userTemplates, templates, hiddenBuiltinTemplateIds, compositePalette, sidewaysPalette]);
|
||||
|
||||
const filteredTemplateRows = useMemo(() => {
|
||||
const q = templateSearch.trim().toLowerCase();
|
||||
if (!q) return templateRows;
|
||||
return templateRows.filter(row => {
|
||||
const signalLabel = row.signal === 'buy' ? '매수' : '매도';
|
||||
const badge = row.source === 'user' ? '내 템플릿'
|
||||
: row.source === 'composite-palette' ? '복합지표'
|
||||
: row.source === 'sideways-palette' ? '횡보지표'
|
||||
: '';
|
||||
return (
|
||||
row.label.toLowerCase().includes(q)
|
||||
|| row.description?.toLowerCase().includes(q)
|
||||
|| signalLabel.includes(q)
|
||||
|| row.signal.includes(q)
|
||||
|| badge.includes(q)
|
||||
);
|
||||
});
|
||||
}, [templateRows, templateSearch]);
|
||||
@@ -1468,14 +1479,31 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
handleDeleteUserTemplate(row.user.id, row.label);
|
||||
} else if (row.source === 'builtin' && row.builtinId) {
|
||||
handleDeleteBuiltinTemplate(row.builtinId, row.label);
|
||||
} else if (row.source === 'composite-palette' || row.source === 'sideways-palette') {
|
||||
handleDeleteBuiltinTemplate(row.key, row.label);
|
||||
}
|
||||
setSelectedTemplateKey(null);
|
||||
}, [selectedTemplateKey, templateRows, handleDeleteUserTemplate, handleDeleteBuiltinTemplate]);
|
||||
|
||||
const handleTemplateApply = useCallback((row: TemplatePaletteRow) => {
|
||||
if (row.source === 'user' && row.user) applyUserTemplate(row.user);
|
||||
else if (row.builtin) handleTemplate(row.builtin);
|
||||
}, [applyUserTemplate, handleTemplate]);
|
||||
if (row.source === 'user' && row.user) {
|
||||
applyUserTemplate(row.user);
|
||||
return;
|
||||
}
|
||||
if (row.source === 'builtin' && row.builtin) {
|
||||
handleTemplate(row.builtin);
|
||||
return;
|
||||
}
|
||||
if (row.source === 'composite-palette' && row.paletteItem) {
|
||||
applyPaletteItem(row.paletteItem);
|
||||
showSnack(`"${row.label}" 조건이 추가됐습니다`);
|
||||
return;
|
||||
}
|
||||
if (row.source === 'sideways-palette' && row.sidewaysFilterId) {
|
||||
applySidewaysFilter(row.sidewaysFilterId);
|
||||
showSnack(`"${row.label}" 필터가 추가됐습니다`);
|
||||
}
|
||||
}, [applyUserTemplate, handleTemplate, applyPaletteItem, applySidewaysFilter]);
|
||||
|
||||
const handleTemplateSaveConfirm = useCallback(() => {
|
||||
const label = templateSaveLabel.trim();
|
||||
|
||||
@@ -143,6 +143,9 @@ export interface ToolbarProps {
|
||||
chartLiveQuote?: ChartLiveQuote | null;
|
||||
chartQuoteLoading?: boolean;
|
||||
chartQuoteError?: string | null;
|
||||
/** 9·20일 신고가·신저가 차트 표시 (실시간 차트 상단 툴바) */
|
||||
priceExtremeLabelsVisible?: boolean;
|
||||
onTogglePriceExtremeLabels?: () => void;
|
||||
}
|
||||
|
||||
// ─── SVG 아이콘 ────────────────────────────────────────────────────────────
|
||||
@@ -240,6 +243,12 @@ const IcTradeSignal = () => (
|
||||
<path d="M8.5 1.5 L4 8.5 H7.5 L6 13.5 L11 6 H7.5 Z" fill="currentColor" stroke="none" opacity="0.85"/>
|
||||
</svg>
|
||||
);
|
||||
const IcPriceExtreme = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden>
|
||||
<path d="M7 1.5 L10.5 6 H3.5 Z" fill="#4dabf7"/>
|
||||
<path d="M7 12.5 L3.5 8 H10.5 Z" fill="#ef5350"/>
|
||||
</svg>
|
||||
);
|
||||
const IcGridIcon = () => (
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" strokeWidth="1.3">
|
||||
<rect x="1" y="1" width="13" height="13" rx="1"/>
|
||||
@@ -826,6 +835,8 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
chartLiveQuote = null,
|
||||
chartQuoteLoading = false,
|
||||
chartQuoteError = null,
|
||||
priceExtremeLabelsVisible = false,
|
||||
onTogglePriceExtremeLabels,
|
||||
}) => {
|
||||
const [showMarket, setShowMarket] = React.useState(false);
|
||||
const [showTfMenu, setShowTfMenu] = React.useState(false);
|
||||
@@ -1097,6 +1108,17 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
<IcFibAuto />
|
||||
</button>
|
||||
|
||||
{/* 9·20일 신고가·신저가 표시 */}
|
||||
{onTogglePriceExtremeLabels && (
|
||||
<button
|
||||
className={`tv-icon-btn${priceExtremeLabelsVisible ? ' active' : ''}`}
|
||||
onClick={onTogglePriceExtremeLabels}
|
||||
title={priceExtremeLabelsVisible ? '9·20일 신고가·신저가 숨기기' : '9·20일 신고가·신저가 표시'}
|
||||
>
|
||||
<IcPriceExtreme />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 그리드 토글 */}
|
||||
<button
|
||||
className={`tv-icon-btn${showGrid ? ' active' : ''}`}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { ChartManager } from '../utils/ChartManager';
|
||||
import {
|
||||
layoutStackedSignalLabels,
|
||||
@@ -95,6 +96,18 @@ function renderTradeSignalLabels(
|
||||
|
||||
const TradeSignalLabelOverlay: React.FC<Props> = ({ manager }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
|
||||
const redrawRafRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const container = manager.getContainer();
|
||||
if (container) {
|
||||
setPortalTarget(container);
|
||||
return;
|
||||
}
|
||||
const tid = window.setTimeout(() => setPortalTarget(manager.getContainer()), 100);
|
||||
return () => clearTimeout(tid);
|
||||
}, [manager]);
|
||||
|
||||
const redraw = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
@@ -120,11 +133,18 @@ const TradeSignalLabelOverlay: React.FC<Props> = ({ manager }) => {
|
||||
}, [manager]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!portalTarget) return;
|
||||
|
||||
redraw();
|
||||
let layoutRaf = 0;
|
||||
|
||||
/** 패닝·스크롤 중 매 이벤트 즉시 + 다음 프레임 보정 (cancelAnimationFrame 미사용) */
|
||||
const scheduleRedraw = () => {
|
||||
cancelAnimationFrame(layoutRaf);
|
||||
layoutRaf = requestAnimationFrame(redraw);
|
||||
redraw();
|
||||
if (redrawRafRef.current) return;
|
||||
redrawRafRef.current = requestAnimationFrame(() => {
|
||||
redrawRafRef.current = 0;
|
||||
redraw();
|
||||
});
|
||||
};
|
||||
|
||||
const unsubs = [
|
||||
@@ -137,19 +157,23 @@ const TradeSignalLabelOverlay: React.FC<Props> = ({ manager }) => {
|
||||
const ro = new ResizeObserver(scheduleRedraw);
|
||||
if (container) ro.observe(container);
|
||||
|
||||
const raf1 = requestAnimationFrame(redraw);
|
||||
const raf2 = requestAnimationFrame(() => requestAnimationFrame(redraw));
|
||||
const scrollWrap = container?.closest('.tv-chart-wrap') as HTMLElement | null;
|
||||
scrollWrap?.addEventListener('scroll', scheduleRedraw, { passive: true });
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(u => u());
|
||||
ro.disconnect();
|
||||
cancelAnimationFrame(layoutRaf);
|
||||
cancelAnimationFrame(raf1);
|
||||
cancelAnimationFrame(raf2);
|
||||
scrollWrap?.removeEventListener('scroll', scheduleRedraw);
|
||||
if (redrawRafRef.current) {
|
||||
cancelAnimationFrame(redrawRafRef.current);
|
||||
redrawRafRef.current = 0;
|
||||
}
|
||||
};
|
||||
}, [manager, redraw]);
|
||||
}, [manager, portalTarget, redraw]);
|
||||
|
||||
return (
|
||||
if (!portalTarget) return null;
|
||||
|
||||
return createPortal(
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="trade-signal-label-overlay"
|
||||
@@ -160,7 +184,8 @@ const TradeSignalLabelOverlay: React.FC<Props> = ({ manager }) => {
|
||||
pointerEvents: 'none',
|
||||
zIndex: 8,
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
portalTarget,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -180,6 +180,9 @@ interface TradingChartProps {
|
||||
custom1OverlayActive?: boolean;
|
||||
onCustom1OverlayActiveChange?: (active: boolean) => void;
|
||||
onOpenCustom1OverlaySettings?: () => void;
|
||||
/** 9·20일 신고가·신저가 라벨 표시 (실시간 차트 우측 툴바) */
|
||||
priceExtremeLabelsVisible?: boolean;
|
||||
onTogglePriceExtremeLabels?: () => void;
|
||||
/** 미니 차트 — plot 너비에 맞는 초기 x축 캔들 수 (미지정 시 DISPLAY_COUNT) */
|
||||
resolveInitialDisplayCount?: (plotWidth: number) => number;
|
||||
/** true — reload·layout 복구 타이머의 반복 viewport 재조정 생략 (미니 카드 깜빡임 방지) */
|
||||
@@ -243,6 +246,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
custom1OverlayActive,
|
||||
onCustom1OverlayActiveChange,
|
||||
onOpenCustom1OverlaySettings,
|
||||
priceExtremeLabelsVisible,
|
||||
onTogglePriceExtremeLabels,
|
||||
resolveInitialDisplayCount,
|
||||
skipViewportRecovery = false,
|
||||
auxIndicatorOnly = false,
|
||||
@@ -1161,6 +1166,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
managerRef.current?.panByPixelDelta(dx, dy, {
|
||||
allowVerticalPan: allowVertical,
|
||||
});
|
||||
managerRef.current?.notifyViewportChanged();
|
||||
};
|
||||
|
||||
const onPanPointerUp = (e: PointerEvent) => {
|
||||
@@ -1827,6 +1833,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
custom1OverlayActive={custom1OverlayActive}
|
||||
onCustom1OverlayActiveChange={onCustom1OverlayActiveChange}
|
||||
onOpenCustom1OverlaySettings={onOpenCustom1OverlaySettings}
|
||||
priceExtremeLabelsVisible={priceExtremeLabelsVisible}
|
||||
onTogglePriceExtremeLabels={onTogglePriceExtremeLabels}
|
||||
onSplit={onSplitIndicatorPane}
|
||||
onRemove={onRemoveIndicator}
|
||||
onDuplicate={onDuplicateIndicator}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import React from 'react';
|
||||
import type { StrategyTemplateDef } from '../../utils/strategyPresets';
|
||||
import type { UserStrategyTemplate } from '../../utils/strategyTemplateStorage';
|
||||
import type { PaletteItem } from '../../utils/strategyPaletteStorage';
|
||||
import {
|
||||
templateBadgeForSource,
|
||||
type TemplatePaletteSource,
|
||||
} from '../../utils/strategyEditorTemplatePaletteRows';
|
||||
|
||||
export type { TemplatePaletteSource };
|
||||
|
||||
export type TemplatePaletteRow = {
|
||||
key: string;
|
||||
source: 'user' | 'builtin';
|
||||
source: TemplatePaletteSource;
|
||||
label: string;
|
||||
description?: string;
|
||||
signal: 'buy' | 'sell';
|
||||
user?: UserStrategyTemplate;
|
||||
builtinId?: string;
|
||||
builtin?: StrategyTemplateDef;
|
||||
paletteItem?: PaletteItem;
|
||||
sidewaysFilterId?: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
@@ -91,7 +100,9 @@ export default function TemplatePaletteTab({
|
||||
{rows.length === 0 ? (
|
||||
<div className="se-template-empty">검색 결과가 없습니다</div>
|
||||
) : (
|
||||
rows.map(row => (
|
||||
rows.map(row => {
|
||||
const sourceBadge = templateBadgeForSource(row.source);
|
||||
return (
|
||||
<div
|
||||
key={row.key}
|
||||
className={`se-template-row${selectedKey === row.key ? ' se-template-row--selected' : ''}`}
|
||||
@@ -108,7 +119,11 @@ export default function TemplatePaletteTab({
|
||||
>
|
||||
<div className="se-template-item">
|
||||
<span className="se-template-label">
|
||||
{row.source === 'user' && <span className="se-template-user-badge">내 템플릿</span>}
|
||||
{sourceBadge && (
|
||||
<span className={`se-template-source-badge se-template-source-badge--${row.source}`}>
|
||||
{sourceBadge}
|
||||
</span>
|
||||
)}
|
||||
{row.label}
|
||||
</span>
|
||||
{row.description && (
|
||||
@@ -119,7 +134,8 @@ export default function TemplatePaletteTab({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -481,6 +481,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
crosshairInfoVisible={false}
|
||||
showHoverToolbar={false}
|
||||
showChartRightToolbar={false}
|
||||
showCandlePaneControls={false}
|
||||
/>
|
||||
{chartMgr && (
|
||||
<StrategyEvaluationBarSelector
|
||||
|
||||
Reference in New Issue
Block a user