전략평가 화면 기능 수정
This commit is contained in:
@@ -20,7 +20,8 @@ import type { VirtualIndicatorSnapshot } from '../hooks/useVirtualIndicatorSnaps
|
||||
import VirtualTargetSignalPanel from './virtual/VirtualTargetSignalPanel';
|
||||
import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart';
|
||||
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
|
||||
import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
|
||||
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
||||
import { readStoredSize, readStoredBool, storeBool, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
|
||||
import { getKoreanName } from '../utils/marketNameCache';
|
||||
import { useIndicatorSettings } from '../hooks/useIndicatorSettings';
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
import type { OHLCVBar } from '../types';
|
||||
|
||||
const LEFT_KEY = 'seval-left-width';
|
||||
const LEFT_OPEN_KEY = 'seval-left-open';
|
||||
const RIGHT_KEY = 'seval-right-width';
|
||||
const LEFT_DEFAULT = 380;
|
||||
const RIGHT_DEFAULT = 440;
|
||||
@@ -68,6 +70,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
const [evalError, setEvalError] = useState<string | null>(null);
|
||||
const [appliedIndicatorParams, setAppliedIndicatorParams] = useState<EvalIndicatorParams | null>(null);
|
||||
const [paramsRevision, setParamsRevision] = useState(0);
|
||||
const [chartLoading, setChartLoading] = useState(false);
|
||||
const evalSessionRef = useRef(0);
|
||||
const paramsRevisionRef = useRef(paramsRevision);
|
||||
|
||||
const getEvalParams = useMemo(
|
||||
() => mergeEvalGetParams(baseGetParams, appliedIndicatorParams),
|
||||
@@ -75,6 +80,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
);
|
||||
|
||||
const [leftWidth, setLeftWidth] = useState(() => readMinWidth(LEFT_KEY, LEFT_DEFAULT));
|
||||
const [leftOpen, setLeftOpen] = useState(() => readStoredBool(LEFT_OPEN_KEY, true));
|
||||
const [rightWidth, setRightWidth] = useState(() => readMinWidth(RIGHT_KEY, RIGHT_DEFAULT));
|
||||
const leftRef = useRef(leftWidth);
|
||||
const rightRef = useRef(rightWidth);
|
||||
@@ -82,6 +88,19 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
rightRef.current = rightWidth;
|
||||
|
||||
const onLeftSplit = usePanelResize('vertical', setLeftWidth, () => leftRef.current, 300, 520, v => storeSize(LEFT_KEY, v));
|
||||
|
||||
const toggleLeftPanel = useCallback(() => {
|
||||
setLeftOpen(prev => {
|
||||
const next = !prev;
|
||||
storeBool(LEFT_OPEN_KEY, next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const leftWrapStyle = useMemo(
|
||||
() => ({ '--seval-left-width': `${leftWidth}px` }) as React.CSSProperties,
|
||||
[leftWidth],
|
||||
);
|
||||
const onRightSplit = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const startX = e.clientX;
|
||||
const start = rightRef.current;
|
||||
@@ -121,6 +140,27 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
[strategyPrimaryTimeframe],
|
||||
);
|
||||
|
||||
/** 종목·시간봉·전략 변경 — 차트·평가 컨텍스트 초기화 */
|
||||
useEffect(() => {
|
||||
evalSessionRef.current += 1;
|
||||
setSnapshot(undefined);
|
||||
setEvalError(null);
|
||||
setEvalLoading(true);
|
||||
setBars([]);
|
||||
setSelectedBarIndex(0);
|
||||
}, [market, chartTimeframe, selectedStrategyId]);
|
||||
|
||||
/** 전략설정 파라미터 저장·초기화 */
|
||||
useEffect(() => {
|
||||
if (paramsRevisionRef.current === paramsRevision) return;
|
||||
paramsRevisionRef.current = paramsRevision;
|
||||
if (paramsRevision === 0) return;
|
||||
evalSessionRef.current += 1;
|
||||
setSnapshot(undefined);
|
||||
setEvalError(null);
|
||||
setEvalLoading(true);
|
||||
}, [paramsRevision]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadStrategies().then(list => setStrategies(list ?? []));
|
||||
}, []);
|
||||
@@ -150,10 +190,17 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
const selectedBarTimeSec = bars[selectedBarIndex]?.time ?? null;
|
||||
|
||||
const refreshEvaluation = useCallback(async () => {
|
||||
if (!selectedStrategyId || !selectedStrategy || selectedBarTimeSec == null) {
|
||||
if (!selectedStrategyId || !selectedStrategy) {
|
||||
setSnapshot(undefined);
|
||||
setEvalLoading(false);
|
||||
return;
|
||||
}
|
||||
if (chartLoading || bars.length === 0 || selectedBarTimeSec == null) {
|
||||
setSnapshot(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const session = evalSessionRef.current;
|
||||
setEvalLoading(true);
|
||||
setEvalError(null);
|
||||
try {
|
||||
@@ -164,14 +211,26 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
selectedBarTimeSec,
|
||||
appliedIndicatorParams,
|
||||
);
|
||||
if (session !== evalSessionRef.current) return;
|
||||
setSnapshot(snap ?? undefined);
|
||||
} catch (e) {
|
||||
if (session !== evalSessionRef.current) return;
|
||||
setEvalError(e instanceof Error ? e.message : '조건 평가 실패');
|
||||
setSnapshot(undefined);
|
||||
} finally {
|
||||
setEvalLoading(false);
|
||||
if (session === evalSessionRef.current) {
|
||||
setEvalLoading(false);
|
||||
}
|
||||
}
|
||||
}, [market, selectedStrategyId, selectedStrategy, selectedBarTimeSec, appliedIndicatorParams]);
|
||||
}, [
|
||||
chartLoading,
|
||||
market,
|
||||
selectedStrategyId,
|
||||
selectedStrategy,
|
||||
selectedBarTimeSec,
|
||||
bars.length,
|
||||
appliedIndicatorParams,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshEvaluation();
|
||||
@@ -225,8 +284,12 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
</header>
|
||||
|
||||
<div className="btd-body">
|
||||
<aside className="btd-sidebar" style={{ width: leftWidth, flex: `0 0 ${leftWidth}px` }}>
|
||||
<div className="btd-exec-list seval-left-panel">
|
||||
<div
|
||||
className={`seval-left-wrap${leftOpen ? ' seval-left-wrap--open' : ''}`}
|
||||
style={leftWrapStyle}
|
||||
>
|
||||
<aside className={`btd-sidebar seval-left-sidebar${leftOpen ? ' seval-left-sidebar--open' : ''}`}>
|
||||
<div className="btd-exec-list seval-left-panel">
|
||||
<div className="btd-exec-tabs" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
@@ -306,8 +369,16 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</aside>
|
||||
<SePanelCollapseHandle
|
||||
side="left"
|
||||
open={leftOpen}
|
||||
onToggle={toggleLeftPanel}
|
||||
label="전략 패널"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{leftOpen && (
|
||||
<div
|
||||
className="btd-splitter btd-splitter--v"
|
||||
role="separator"
|
||||
@@ -315,8 +386,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
aria-label="좌측 패널 너비"
|
||||
onPointerDown={onLeftSplit}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main className="btd-main">
|
||||
<main className={`btd-main${leftOpen ? '' : ' seval-main--left-collapsed'}`}>
|
||||
<div className="btd-main-content seval-main-content">
|
||||
<StrategyEvaluationChart
|
||||
market={market}
|
||||
@@ -328,6 +400,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
onStrategySelect={handleSelectStrategyById}
|
||||
onMarketChange={setMarket}
|
||||
onTimeframeChoiceChange={setTimeframeChoice}
|
||||
onChartLoadingChange={setChartLoading}
|
||||
theme={theme}
|
||||
strategy={selectedStrategy}
|
||||
selectedBarIndex={selectedBarIndex}
|
||||
@@ -336,6 +409,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
paramsRevision={paramsRevision}
|
||||
getParamsOverride={getEvalParams}
|
||||
getVisualConfigOverride={getVisualConfig}
|
||||
evalLoading={evalLoading}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
@@ -351,19 +425,33 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
<aside className="btd-right seval-right-panel" style={{ width: rightWidth, flex: `0 0 ${rightWidth}px` }}>
|
||||
<div className="seval-signal-head">
|
||||
<span className="seval-signal-market">{getKoreanName(market)}</span>
|
||||
<span className="seval-live-dot" title="선택 봉 기준 평가" />
|
||||
<span className="seval-live-label">봉 평가</span>
|
||||
<span
|
||||
className={`seval-live-dot${evalLoading ? ' seval-live-dot--loading' : ''}`}
|
||||
title={evalLoading ? '전략 조건 확인 중' : '선택 봉 기준 평가'}
|
||||
/>
|
||||
<span className={`seval-live-label${evalLoading ? ' seval-live-label--loading' : ''}`}>
|
||||
{evalLoading ? '조건 확인 중…' : '봉 평가'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{evalError && <p className="seval-eval-error">{evalError}</p>}
|
||||
|
||||
<VirtualTargetSignalPanel
|
||||
snapshot={snapshot}
|
||||
viewMode="summary"
|
||||
loading={evalLoading}
|
||||
hasStrategy={selectedStrategyId != null}
|
||||
className="seval-signal-panel"
|
||||
/>
|
||||
<div className="seval-signal-body">
|
||||
{evalLoading && (
|
||||
<div className="seval-eval-loading" aria-live="polite" aria-busy="true">
|
||||
<span className="btd-run-spinner seval-eval-loading-spinner" aria-hidden />
|
||||
<span className="seval-eval-loading-text">전략 조건 확인 중…</span>
|
||||
</div>
|
||||
)}
|
||||
<VirtualTargetSignalPanel
|
||||
snapshot={snapshot}
|
||||
viewMode="summary"
|
||||
loading={evalLoading}
|
||||
loadingMessage="전략 조건 확인 중…"
|
||||
hasStrategy={selectedStrategyId != null}
|
||||
className={`seval-signal-panel${evalLoading ? ' seval-signal-panel--loading' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<footer className="seval-signal-footer">
|
||||
<span className="seval-updated">
|
||||
|
||||
Reference in New Issue
Block a user