전략편집기 조건 목록 수정
This commit is contained in:
@@ -26,6 +26,11 @@ export interface IndicatorSettingsListRowProps {
|
||||
hideChartToggle?: boolean;
|
||||
/** 출력·표시(가격축 라벨·타임프레임) 숨김 */
|
||||
hideOutputFooter?: boolean;
|
||||
/** 오버레이 지표(SMA·볼린저·일목) 전체 on/off — 오버레이 뱃지·기본값 버튼 대신 표시 */
|
||||
overlayMasterToggle?: {
|
||||
visible: boolean;
|
||||
onChange: (visible: boolean) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
@@ -47,6 +52,7 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
onDrop,
|
||||
hideChartToggle = false,
|
||||
hideOutputFooter = false,
|
||||
overlayMasterToggle,
|
||||
}) => {
|
||||
const labels = getIndicatorListLabels(type);
|
||||
const def = getIndicatorDef(type);
|
||||
@@ -92,9 +98,9 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
<span className={enClass}>{labels.en}</span>
|
||||
</div>
|
||||
<div className={badgesClass}>
|
||||
{def?.overlay && <span className="ind-panel-badge">오버레이</span>}
|
||||
{!overlayMasterToggle && def?.overlay && <span className="ind-panel-badge">오버레이</span>}
|
||||
{def?.returnsMarkers && <span className="ind-panel-badge">마커</span>}
|
||||
{!hideChartToggle && (
|
||||
{!overlayMasterToggle && !hideChartToggle && (
|
||||
enabled ? (
|
||||
<span className={onBadgeClass}>차트 적용</span>
|
||||
) : (
|
||||
@@ -102,21 +108,36 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{!hideChartToggle && (
|
||||
{overlayMasterToggle ? (
|
||||
<label
|
||||
className={`ibsm-toggle${variant === 'settings' ? ' stg-ind-toggle' : ''}`}
|
||||
title={enabled ? '차트에서 제거' : '차트에 추가'}
|
||||
title={overlayMasterToggle.visible ? '차트에서 숨기기' : '차트에 표시'}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={e => onToggleEnabled(e.target.checked)}
|
||||
checked={overlayMasterToggle.visible}
|
||||
onChange={e => overlayMasterToggle.onChange(e.target.checked)}
|
||||
/>
|
||||
<span className="ism-toggle-slider" />
|
||||
</label>
|
||||
) : (
|
||||
!hideChartToggle && (
|
||||
<label
|
||||
className={`ibsm-toggle${variant === 'settings' ? ' stg-ind-toggle' : ''}`}
|
||||
title={enabled ? '차트에서 제거' : '차트에 추가'}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={e => onToggleEnabled(e.target.checked)}
|
||||
/>
|
||||
<span className="ism-toggle-slider" />
|
||||
</label>
|
||||
)
|
||||
)}
|
||||
{variant === 'settings' && onRowDefaults && (
|
||||
{variant === 'settings' && onRowDefaults && !overlayMasterToggle && (
|
||||
<button type="button" className="stg-ind-card-defaults" onClick={onRowDefaults}>
|
||||
기본값
|
||||
</button>
|
||||
|
||||
@@ -55,6 +55,11 @@ import StrategyEvaluationChartSettingsTab from './strategyEvaluation/StrategyEva
|
||||
import PaletteDragOverlay from './strategyEditor/PaletteDragOverlay';
|
||||
import { needsPointerPaletteDrag } from '../utils/paletteDragSession';
|
||||
import { useStrategyEvaluationEditor } from '../hooks/useStrategyEvaluationEditor';
|
||||
import {
|
||||
DEFAULT_CHART_OVERLAY_VISIBILITY,
|
||||
type ChartOverlayToggleKey,
|
||||
type ChartOverlayVisibility,
|
||||
} from '../utils/chartOverlayVisibility';
|
||||
import {
|
||||
buildStrategyEvaluationAiVerifyContext,
|
||||
requestStrategyEvaluationAiVerify,
|
||||
@@ -112,6 +117,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
const [reportModel, setReportModel] = useState<BacktestAnalysisReportModel | null>(null);
|
||||
const [reportLoading, setReportLoading] = useState(false);
|
||||
const [rightTab, setRightTab] = useState<RightTab>('signal');
|
||||
const [overlayVisibility, setOverlayVisibility] = useState<ChartOverlayVisibility>(
|
||||
() => ({ ...DEFAULT_CHART_OVERLAY_VISIBILITY }),
|
||||
);
|
||||
const [aiVerifyUnread, setAiVerifyUnread] = useState(false);
|
||||
const [aiVerifyRunning, setAiVerifyRunning] = useState(false);
|
||||
const [aiVerifyError, setAiVerifyError] = useState<string | null>(null);
|
||||
@@ -143,6 +151,10 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
[baseGetParams, appliedIndicatorParams],
|
||||
);
|
||||
|
||||
const handleSetOverlayVisibility = useCallback((key: ChartOverlayToggleKey, visible: boolean) => {
|
||||
setOverlayVisibility(prev => ({ ...prev, [key]: visible }));
|
||||
}, []);
|
||||
|
||||
const handleStrategyEditorSaved = useCallback((saved: StrategyDto, meta?: { created?: boolean }) => {
|
||||
const hydrated = hydrateStrategyDto(saved);
|
||||
setSelectedStrategy(hydrated);
|
||||
@@ -344,6 +356,13 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
|
||||
const selectedBarTimeSec = bars[selectedBarIndex]?.time ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (bars.length === 0) return;
|
||||
if (selectedBarIndex >= bars.length) {
|
||||
setSelectedBarIndex(bars.length - 1);
|
||||
}
|
||||
}, [bars.length, selectedBarIndex]);
|
||||
|
||||
const barSignalHighlight = useMemo(
|
||||
() => resolveBarSignalHighlight(backtestSignals, selectedBarTimeSec, selectedBarIndex),
|
||||
[backtestSignals, selectedBarTimeSec, selectedBarIndex],
|
||||
@@ -815,6 +834,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
marketLoading={marketFeed.loading}
|
||||
refreshMarketTickers={marketFeed.refreshAllTickers}
|
||||
onMarketSearchOpenChange={setMarketSearchOpen}
|
||||
overlayVisibility={overlayVisibility}
|
||||
onSetOverlayVisibility={handleSetOverlayVisibility}
|
||||
/>
|
||||
<StrategyEvaluationStrategyPanel
|
||||
strategy={selectedStrategy}
|
||||
@@ -980,6 +1001,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
saveVisual={saveVisual}
|
||||
settingsRevision={settingsRevision}
|
||||
onPersisted={handleChartSettingsPersisted}
|
||||
overlayVisibility={overlayVisibility}
|
||||
onSetOverlayVisibility={handleSetOverlayVisibility}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
DEFAULT_CHART_OVERLAY_VISIBILITY,
|
||||
effectiveConfigForOverlay,
|
||||
listChartOverlayToggleItems,
|
||||
CHART_OVERLAY_TOGGLE_ORDER,
|
||||
type ChartOverlayToggleKey,
|
||||
type ChartOverlayVisibility,
|
||||
} from '../../utils/chartOverlayVisibility';
|
||||
@@ -49,6 +50,7 @@ import {
|
||||
parseBacktestRunTimeframeChoice,
|
||||
type BacktestRunTimeframeChoice,
|
||||
} from '../../utils/backtestRunTimeframe';
|
||||
import { timeframeBarSeconds } from '../../utils/backtestUiUtils';
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
@@ -100,6 +102,9 @@ interface Props {
|
||||
marketLoading?: boolean;
|
||||
refreshMarketTickers?: () => Promise<void>;
|
||||
onMarketSearchOpenChange?: (open: boolean) => void;
|
||||
/** 캔들 오버레이(SMA·볼린저·일목·캔들) 표시 — 미전달 시 내부 state */
|
||||
overlayVisibility?: ChartOverlayVisibility;
|
||||
onSetOverlayVisibility?: (key: ChartOverlayToggleKey, visible: boolean) => void;
|
||||
}
|
||||
|
||||
const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true;
|
||||
@@ -144,6 +149,8 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
marketLoading,
|
||||
refreshMarketTickers,
|
||||
onMarketSearchOpenChange,
|
||||
overlayVisibility: overlayVisibilityProp,
|
||||
onSetOverlayVisibility,
|
||||
}) => {
|
||||
const { getParams: baseGetParams, getVisualConfig: baseGetVisual, settingsRevision } = useIndicatorSettings();
|
||||
const getParams = getParamsOverride ?? baseGetParams;
|
||||
@@ -163,13 +170,15 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
const [magnifierActive, setMagnifierActive] = useState(false);
|
||||
const marketBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const managerRef = useRef<ChartManager | null>(null);
|
||||
const [overlayVisibility, setOverlayVisibility] = useState<ChartOverlayVisibility>(
|
||||
const [internalOverlayVisibility, setInternalOverlayVisibility] = useState<ChartOverlayVisibility>(
|
||||
() => ({ ...DEFAULT_CHART_OVERLAY_VISIBILITY }),
|
||||
);
|
||||
const overlayVisibility = overlayVisibilityProp ?? internalOverlayVisibility;
|
||||
const overlayVisibilityRef = useRef(overlayVisibility);
|
||||
useEffect(() => {
|
||||
overlayVisibilityRef.current = overlayVisibility;
|
||||
}, [overlayVisibility]);
|
||||
|
||||
const backtestGenRef = useRef(0);
|
||||
const evaluationBarCountRef = useRef(BACKTEST_DISPLAY_BAR_COUNT);
|
||||
const signalsRef = useRef(signals);
|
||||
@@ -196,23 +205,48 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
[overlayVisibility],
|
||||
);
|
||||
|
||||
const applyOverlayToManager = useCallback((
|
||||
key: ChartOverlayToggleKey,
|
||||
fullVisibility: ChartOverlayVisibility,
|
||||
) => {
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr) return;
|
||||
mgr.setChartOverlayVisibility(fullVisibility);
|
||||
for (const info of mgr.getIndicatorPaneInfo()) {
|
||||
if (chartOverlayKeyForType(info.type) !== key) continue;
|
||||
const reactInd = indicatorsRef.current.find(i => i.id === info.id);
|
||||
mgr.applyIndicatorStyle(reactInd
|
||||
? { ...info.config, ...reactInd, id: info.id, type: info.type }
|
||||
: info.config);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleCandleOverlay = useCallback((key: ChartOverlayToggleKey) => {
|
||||
setOverlayVisibility(prev => {
|
||||
const next = { ...prev, [key]: !prev[key] };
|
||||
const mgr = managerRef.current;
|
||||
mgr?.setChartOverlayGroupVisible(key, next[key]);
|
||||
if (mgr) {
|
||||
for (const info of mgr.getIndicatorPaneInfo()) {
|
||||
if (chartOverlayKeyForType(info.type) !== key) continue;
|
||||
const reactInd = indicatorsRef.current.find(i => i.id === info.id);
|
||||
mgr.applyIndicatorStyle(reactInd
|
||||
? { ...info.config, ...reactInd, id: info.id, type: info.type }
|
||||
: info.config);
|
||||
const nextVisible = !overlayVisibilityRef.current[key];
|
||||
const next = { ...overlayVisibilityRef.current, [key]: nextVisible };
|
||||
if (onSetOverlayVisibility) {
|
||||
onSetOverlayVisibility(key, nextVisible);
|
||||
} else {
|
||||
setInternalOverlayVisibility(next);
|
||||
applyOverlayToManager(key, next);
|
||||
}
|
||||
}, [onSetOverlayVisibility, applyOverlayToManager]);
|
||||
|
||||
const prevOverlayVisibilityRef = useRef(overlayVisibility);
|
||||
useEffect(() => {
|
||||
const prev = prevOverlayVisibilityRef.current;
|
||||
prevOverlayVisibilityRef.current = overlayVisibility;
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr) return;
|
||||
mgr.setChartOverlayVisibility(overlayVisibility);
|
||||
if (onSetOverlayVisibility) {
|
||||
for (const key of CHART_OVERLAY_TOGGLE_ORDER) {
|
||||
if (prev[key] !== overlayVisibility[key]) {
|
||||
applyOverlayToManager(key, overlayVisibility);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
}, [overlayVisibility, onSetOverlayVisibility, applyOverlayToManager]);
|
||||
|
||||
const [customOverlaySelection, setCustomOverlaySelection] = useState<ChartCustomOverlaySelection>(
|
||||
() => createDefaultChartCustomOverlaySelection(),
|
||||
@@ -435,12 +469,25 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
}, [market, timeframe, strategy?.id, paramsRevision, onBarsLoaded, onSelectedBarIndexChange]);
|
||||
|
||||
const scrollToBar = useCallback((barIdx: number) => {
|
||||
const mgr = chartMgr;
|
||||
if (!mgr || barIdx < 0 || barIdx >= bars.length) return;
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr?.hasMainSeries() || barIdx < 0 || barIdx >= bars.length) return;
|
||||
const bar = bars[barIdx];
|
||||
const barSec = Math.max(60, 180);
|
||||
mgr.applyVisibleTimeRange(bar.time - barSec * 24, bar.time + barSec * 8);
|
||||
}, [chartMgr, bars]);
|
||||
const barSec = timeframeBarSeconds(timeframe);
|
||||
const pad = Math.max(barSec * 48, 3600);
|
||||
mgr.applyVisibleTimeRange(bar.time - pad, bar.time + Math.max(barSec * 8, 600));
|
||||
}, [bars, timeframe]);
|
||||
|
||||
const prevSelectedBarIndexRef = useRef(selectedBarIndex);
|
||||
useEffect(() => {
|
||||
prevSelectedBarIndexRef.current = -1;
|
||||
}, [market, timeframe, strategy?.id, paramsRevision]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bars.length === 0) return;
|
||||
if (prevSelectedBarIndexRef.current === selectedBarIndex) return;
|
||||
prevSelectedBarIndexRef.current = selectedBarIndex;
|
||||
scrollToBar(selectedBarIndex);
|
||||
}, [selectedBarIndex, bars.length, scrollToBar]);
|
||||
|
||||
const onManagerReady = useCallback((mgr: ChartManager) => {
|
||||
managerRef.current = mgr;
|
||||
@@ -467,15 +514,14 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
const stepBar = useCallback((delta: number) => {
|
||||
if (bars.length === 0) return;
|
||||
const next = Math.min(bars.length - 1, Math.max(0, selectedBarIndex + delta));
|
||||
if (next === selectedBarIndex) return;
|
||||
onSelectedBarIndexChange(next);
|
||||
}, [bars.length, selectedBarIndex, onSelectedBarIndexChange]);
|
||||
|
||||
const goLatest = useCallback(() => {
|
||||
if (bars.length === 0) return;
|
||||
const lastIdx = bars.length - 1;
|
||||
onSelectedBarIndexChange(lastIdx);
|
||||
scrollToBar(lastIdx);
|
||||
}, [bars.length, onSelectedBarIndexChange, scrollToBar]);
|
||||
onSelectedBarIndexChange(bars.length - 1);
|
||||
}, [bars.length, onSelectedBarIndexChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const mgr = chartMgr;
|
||||
|
||||
@@ -21,9 +21,15 @@ import {
|
||||
import type { IchimokuCloudColors } from '../../utils/ichimokuConfig';
|
||||
import ChartPriceAxisLabelSettings from './ChartPriceAxisLabelSettings';
|
||||
import { useAppSettings, resolveAppDefaults } from '../../hooks/useAppSettings';
|
||||
import {
|
||||
chartOverlayKeyForType,
|
||||
type ChartOverlayToggleKey,
|
||||
type ChartOverlayVisibility,
|
||||
} from '../../utils/chartOverlayVisibility';
|
||||
|
||||
const EVAL_CHART_OVERLAY_TYPES = ['SMA', 'BollingerBands', 'IchimokuCloud'] as const;
|
||||
const EVAL_CHART_OVERLAY_SET = new Set<string>(EVAL_CHART_OVERLAY_TYPES);
|
||||
const EVAL_OVERLAY_MASTER_TYPES = new Set<string>(EVAL_CHART_OVERLAY_TYPES);
|
||||
|
||||
function buildEvalChartSettingsTypes(strategy: StrategyDto | null): string[] {
|
||||
const strategyTypes = strategy ? collectStrategyRegistryTypes(strategy) : [];
|
||||
@@ -63,6 +69,9 @@ interface Props {
|
||||
settingsRevision?: number;
|
||||
/** DB 저장 후 세션 오버라이드 제거·차트 갱신 */
|
||||
onPersisted?: (type: string) => void;
|
||||
/** 캔들 오버레이(SMA·볼린저·일목) 표시 on/off */
|
||||
overlayVisibility: ChartOverlayVisibility;
|
||||
onSetOverlayVisibility: (key: ChartOverlayToggleKey, visible: boolean) => void;
|
||||
}
|
||||
|
||||
const StrategyEvaluationChartSettingsTab: React.FC<Props> = ({
|
||||
@@ -74,6 +83,8 @@ const StrategyEvaluationChartSettingsTab: React.FC<Props> = ({
|
||||
saveVisual,
|
||||
settingsRevision = 0,
|
||||
onPersisted,
|
||||
overlayVisibility,
|
||||
onSetOverlayVisibility,
|
||||
}) => {
|
||||
const indicatorTypes = useMemo(
|
||||
() => buildEvalChartSettingsTypes(strategy),
|
||||
@@ -153,6 +164,8 @@ const StrategyEvaluationChartSettingsTab: React.FC<Props> = ({
|
||||
const renderRows = (types: string[]) => types.map(type => {
|
||||
const cfg = configs[type];
|
||||
if (!cfg) return null;
|
||||
const overlayKey = chartOverlayKeyForType(type);
|
||||
const isOverlayMaster = EVAL_OVERLAY_MASTER_TYPES.has(type) && overlayKey != null;
|
||||
return (
|
||||
<IndicatorSettingsListRow
|
||||
key={type}
|
||||
@@ -167,7 +180,11 @@ const StrategyEvaluationChartSettingsTab: React.FC<Props> = ({
|
||||
onToggleExpand={() => toggleExpand(type)}
|
||||
onToggleEnabled={() => {}}
|
||||
onChange={schedulePersist}
|
||||
onRowDefaults={() => handleRowDefaults(type)}
|
||||
onRowDefaults={isOverlayMaster ? undefined : () => handleRowDefaults(type)}
|
||||
overlayMasterToggle={isOverlayMaster ? {
|
||||
visible: overlayVisibility[overlayKey!],
|
||||
onChange: visible => onSetOverlayVisibility(overlayKey!, visible),
|
||||
} : undefined}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user