전략편집기 평가탭 추가

This commit is contained in:
Macbook
2026-06-25 01:28:31 +09:00
parent 4ab8053b65
commit 3cc75ff770
6 changed files with 292 additions and 34 deletions
+56 -22
View File
@@ -121,6 +121,7 @@ import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
import LogicExpressionNarrative from './strategyEditor/LogicExpressionNarrative'; import LogicExpressionNarrative from './strategyEditor/LogicExpressionNarrative';
import StrategyEditorCanvas, { type FlowLayoutSeed, type StrategyEditorCanvasHandle } from './strategyEditor/StrategyEditorCanvas'; import StrategyEditorCanvas, { type FlowLayoutSeed, type StrategyEditorCanvasHandle } from './strategyEditor/StrategyEditorCanvas';
import StrategyListEditor from './strategyEditor/StrategyListEditor'; import StrategyListEditor from './strategyEditor/StrategyListEditor';
import StrategyEditorEvaluationPanel from './strategyEditor/StrategyEditorEvaluationPanel';
import StartCombineOpControl from './strategyEditor/StartCombineOpControl'; import StartCombineOpControl from './strategyEditor/StartCombineOpControl';
import { layoutFlushRef } from './strategyEditor/strategyEditorCallbacks'; import { layoutFlushRef } from './strategyEditor/strategyEditorCallbacks';
import { import {
@@ -178,6 +179,13 @@ import {
import '../styles/strategyEditor.css'; import '../styles/strategyEditor.css';
import '../styles/strategyEditorTheme.css'; import '../styles/strategyEditorTheme.css';
type SignalTab = 'buy' | 'sell' | 'eval';
type BuySellTab = 'buy' | 'sell';
function buySellTab(tab: SignalTab): BuySellTab {
return tab === 'sell' ? 'sell' : 'buy';
}
const BACKTEST_FOCUS_KEY = 'backtest_focus_id'; const BACKTEST_FOCUS_KEY = 'backtest_focus_id';
const LEFT_PANEL_MIN = 220; const LEFT_PANEL_MIN = 220;
@@ -301,7 +309,7 @@ export default function StrategyEditorPage({
const [selectedId, setSelectedId] = useState<number | null>(null); const [selectedId, setSelectedId] = useState<number | null>(null);
const [buyCondition, setBuyCondition] = useState<LogicNode | null>(null); const [buyCondition, setBuyCondition] = useState<LogicNode | null>(null);
const [sellCondition, setSellCondition] = useState<LogicNode | null>(null); const [sellCondition, setSellCondition] = useState<LogicNode | null>(null);
const [signalTab, setSignalTab] = useState<'buy' | 'sell'>('buy'); const [signalTab, setSignalTab] = useState<SignalTab>('buy');
const [rightTab, setRightTab] = useState<'indicators' | 'templates'>('indicators'); const [rightTab, setRightTab] = useState<'indicators' | 'templates'>('indicators');
const [indicatorSubTab, setIndicatorSubTab] = useState<'auxiliary' | 'composite' | 'range'>('auxiliary'); const [indicatorSubTab, setIndicatorSubTab] = useState<'auxiliary' | 'composite' | 'range'>('auxiliary');
const [auxiliaryPalette, setAuxiliaryPalette] = useState<PaletteItem[]>(() => loadPaletteItems('auxiliary')); const [auxiliaryPalette, setAuxiliaryPalette] = useState<PaletteItem[]>(() => loadPaletteItems('auxiliary'));
@@ -610,7 +618,7 @@ export default function StrategyEditorPage({
...prev, ...prev,
extraRoots: syncExtraRoots(prev.extraRoots ?? {}, 'sell'), extraRoots: syncExtraRoots(prev.extraRoots ?? {}, 'sell'),
})); }));
bumpLayoutSeed(layoutStrategyKey, signalTab); if (signalTab !== 'eval') bumpLayoutSeed(layoutStrategyKey, buySellTab(signalTab));
if (selectedId != null && stratName.trim()) scheduleStrategyPersist(); if (selectedId != null && stratName.trim()) scheduleStrategyPersist();
}, [settingsLoaded, settingsRevision, DEF, layoutStrategyKey, signalTab, bumpLayoutSeed, selectedId, stratName, scheduleStrategyPersist]); }, [settingsLoaded, settingsRevision, DEF, layoutStrategyKey, signalTab, bumpLayoutSeed, selectedId, stratName, scheduleStrategyPersist]);
@@ -647,7 +655,7 @@ export default function StrategyEditorPage({
const handleExtraStartIdsChange = useCallback((ids: string[]) => { const handleExtraStartIdsChange = useCallback((ids: string[]) => {
setCurrentLayout(prev => ({ ...prev, extraStartIds: ids })); setCurrentLayout(prev => ({ ...prev, extraStartIds: ids }));
scheduleStrategyPersist(); scheduleStrategyPersist();
bumpLayoutSeed(layoutStrategyKey, signalTab); if (signalTab !== 'eval') bumpLayoutSeed(layoutStrategyKey, buySellTab(signalTab));
}, [setCurrentLayout, layoutStrategyKey, scheduleStrategyPersist, bumpLayoutSeed, signalTab]); }, [setCurrentLayout, layoutStrategyKey, scheduleStrategyPersist, bumpLayoutSeed, signalTab]);
const handleExtraRootsChange = useCallback((roots: Record<string, LogicNode | null>) => { const handleExtraRootsChange = useCallback((roots: Record<string, LogicNode | null>) => {
@@ -814,14 +822,17 @@ export default function StrategyEditorPage({
handleEditorStateChange(prev => updateStartCombineOp(prev, op)); handleEditorStateChange(prev => updateStartCombineOp(prev, op));
}, [handleEditorStateChange]); }, [handleEditorStateChange]);
const switchSignalTab = useCallback((tab: 'buy' | 'sell') => { const switchSignalTab = useCallback((tab: SignalTab) => {
if (editorMode === 'graph') layoutFlushRef.current?.(); if (tab === signalTab) return;
flushStrategyPersist(); if (editorMode === 'graph' && signalTab !== 'eval') layoutFlushRef.current?.();
if (signalTab !== 'eval') flushStrategyPersist();
if (tab !== 'eval') {
layoutRevisionRef.current += 1; layoutRevisionRef.current += 1;
setLayoutSeedKey(`${layoutStrategyKey}:${tab}:${layoutRevisionRef.current}`); setLayoutSeedKey(`${layoutStrategyKey}:${tab}:${layoutRevisionRef.current}`);
}
setSignalTab(tab); setSignalTab(tab);
setSelectedNodeId(null); if (tab !== 'eval') setSelectedNodeId(null);
}, [layoutStrategyKey, flushStrategyPersist, editorMode]); }, [layoutStrategyKey, flushStrategyPersist, editorMode, signalTab]);
const handleEditorModeChange = useCallback((mode: StrategyEditorMode) => { const handleEditorModeChange = useCallback((mode: StrategyEditorMode) => {
if (mode === editorMode) return; if (mode === editorMode) return;
@@ -991,7 +1002,7 @@ export default function StrategyEditorPage({
startCombineOp: normalizeStartCombineOp(stored?.sell.startCombineOp ?? sellDecoded.startCombineOp), startCombineOp: normalizeStartCombineOp(stored?.sell.startCombineOp ?? sellDecoded.startCombineOp),
}); });
setSelectedNodeId(null); setSelectedNodeId(null);
bumpLayoutSeed(String(s.id), signalTab); bumpLayoutSeed(String(s.id), buySellTab(signalTab));
if (!s.flowLayout && stored) { if (!s.flowLayout && stored) {
void (async () => { void (async () => {
@@ -1144,7 +1155,7 @@ export default function StrategyEditorPage({
} }
setStratName(name); setStratName(name);
setStratDesc(saveDraftDesc); setStratDesc(saveDraftDesc);
bumpLayoutSeed(String(dbId), signalTab); bumpLayoutSeed(String(dbId), buySellTab(signalTab));
setSaveOpen(false); setSaveOpen(false);
setSaveToast(true); setSaveToast(true);
setTimeout(() => setSaveToast(false), 2500); setTimeout(() => setSaveToast(false), 2500);
@@ -1175,7 +1186,7 @@ export default function StrategyEditorPage({
notifyStrategiesChanged(); notifyStrategiesChanged();
}; };
const applyImportedFlowLayout = useCallback((layout: StrategyFlowLayoutStore | undefined, tab: 'buy' | 'sell' = signalTab) => { const applyImportedFlowLayout = useCallback((layout: StrategyFlowLayoutStore | undefined, tab: BuySellTab = buySellTab(signalTab)) => {
if (layout) { if (layout) {
setBuyLayout({ setBuyLayout({
positions: layout.buy.positions ?? {}, positions: layout.buy.positions ?? {},
@@ -1346,7 +1357,7 @@ export default function StrategyEditorPage({
const handleAddStartSection = useCallback(() => { const handleAddStartSection = useCallback(() => {
handleEditorStateChange(prev => addExtraStartSection(prev)); handleEditorStateChange(prev => addExtraStartSection(prev));
bumpLayoutSeed(layoutStrategyKey, signalTab); if (signalTab !== 'eval') bumpLayoutSeed(layoutStrategyKey, buySellTab(signalTab));
}, [handleEditorStateChange, bumpLayoutSeed, layoutStrategyKey, signalTab]); }, [handleEditorStateChange, bumpLayoutSeed, layoutStrategyKey, signalTab]);
const applyPalette = useCallback((type: string, value: string, _label: string, composite = false) => { const applyPalette = useCallback((type: string, value: string, _label: string, composite = false) => {
@@ -1354,7 +1365,7 @@ export default function StrategyEditorPage({
handleAddStartSection(); handleAddStartSection();
return; return;
} }
const newNode = makeNode(type, value, signalTab, DEF, composite ? { composite: true } : undefined); const newNode = makeNode(type, value, buySellTab(signalTab), DEF, composite ? { composite: true } : undefined);
handleEditorStateChange(prev => { handleEditorStateChange(prev => {
const root = prev.root; const root = prev.root;
if (!root) return updateBranchRoot(prev, START_NODE_ID, newNode); if (!root) return updateBranchRoot(prev, START_NODE_ID, newNode);
@@ -1385,7 +1396,7 @@ export default function StrategyEditorPage({
? buildStableStrategyTree(item.value, DEF) ? buildStableStrategyTree(item.value, DEF)
: ichimokuSituation : ichimokuSituation
? buildIchimokuSituationTree(item.value, DEF) ? buildIchimokuSituationTree(item.value, DEF)
: makeNode('indicator', item.value, signalTab, DEF, composite ? { composite: true } : undefined); : makeNode('indicator', item.value, buySellTab(signalTab), DEF, composite ? { composite: true } : undefined);
if (!newNode) return; if (!newNode) return;
handleEditorStateChange(prev => { handleEditorStateChange(prev => {
const root = prev.root; const root = prev.root;
@@ -1583,7 +1594,7 @@ export default function StrategyEditorPage({
showSnack('갱신할 조건이 없습니다', false); showSnack('갱신할 조건이 없습니다', false);
return; return;
} }
patch.signal = signalTab; patch.signal = buySellTab(signalTab);
patch.editorState = toEditorState(currentRoot, currentLayout); patch.editorState = toEditorState(currentRoot, currentLayout);
patch.orphans = currentOrphans; patch.orphans = currentOrphans;
} }
@@ -1595,7 +1606,7 @@ export default function StrategyEditorPage({
const saved = addUserStrategyTemplate({ const saved = addUserStrategyTemplate({
label, label,
description: templateSaveDesc.trim() || undefined, description: templateSaveDesc.trim() || undefined,
signal: signalTab, signal: buySellTab(signalTab),
editorState: toEditorState(currentRoot, currentLayout), editorState: toEditorState(currentRoot, currentLayout),
orphans: currentOrphans, orphans: currentOrphans,
}); });
@@ -1946,7 +1957,7 @@ export default function StrategyEditorPage({
<div className="se-main-row"> <div className="se-main-row">
<main className="se-center"> <main className="se-center">
<div className="se-center-panel"> <div className="se-center-panel">
<div className={`se-center-work${editorMode === 'list' ? ' se-center-work--list' : ''}`}> <div className={`se-center-work${editorMode === 'list' && signalTab !== 'eval' ? ' se-center-work--list' : ''}${signalTab === 'eval' ? ' se-center-work--eval' : ''}`}>
<div className="se-center-head"> <div className="se-center-head">
<div className="se-signal-tabs"> <div className="se-signal-tabs">
<button <button
@@ -1963,9 +1974,16 @@ export default function StrategyEditorPage({
> >
(Exit) (Exit)
</button> </button>
<button
type="button"
className={`se-signal-tab${signalTab === 'eval' ? ' se-signal-tab--eval-on' : ''}`}
onClick={() => switchSignalTab('eval')}
>
</button>
</div> </div>
{stratName && <span className="se-editing-name">{stratName}</span>} {stratName && <span className="se-editing-name">{stratName}</span>}
{hasMultipleStartSections(currentEditorState) && ( {signalTab !== 'eval' && hasMultipleStartSections(currentEditorState) && (
<StartCombineOpControl <StartCombineOpControl
value={normalizeStartCombineOp(currentEditorState.startCombineOp)} value={normalizeStartCombineOp(currentEditorState.startCombineOp)}
onChange={handleStartCombineOpChange} onChange={handleStartCombineOpChange}
@@ -1973,6 +1991,7 @@ export default function StrategyEditorPage({
)} )}
<div className="se-center-head-actions"> <div className="se-center-head-actions">
<button type="button" className="se-btn se-btn--ghost se-btn--sm" onClick={handleNew}> </button> <button type="button" className="se-btn se-btn--ghost se-btn--sm" onClick={handleNew}> </button>
{signalTab !== 'eval' && (
<div className="se-editor-mode se-editor-mode--compact" role="group" aria-label="편집 방식"> <div className="se-editor-mode se-editor-mode--compact" role="group" aria-label="편집 방식">
<button <button
type="button" type="button"
@@ -1989,6 +2008,8 @@ export default function StrategyEditorPage({
</button> </button>
</div> </div>
)}
{signalTab !== 'eval' && (
<button <button
type="button" type="button"
className="se-btn se-btn--ghost se-btn--sm" className="se-btn se-btn--ghost se-btn--sm"
@@ -1997,6 +2018,7 @@ export default function StrategyEditorPage({
> >
릿 릿
</button> </button>
)}
<button <button
type="button" type="button"
className="se-btn se-btn--gold se-btn--sm" className="se-btn se-btn--gold se-btn--sm"
@@ -2015,7 +2037,15 @@ export default function StrategyEditorPage({
</div> </div>
</div> </div>
{editorMode === 'graph' ? ( {signalTab === 'eval' ? (
<StrategyEditorEvaluationPanel
theme={theme}
stratName={stratName}
stratDesc={stratDesc}
buyEditorState={buyEditorState}
sellEditorState={sellEditorState}
/>
) : editorMode === 'graph' ? (
<> <>
<ReactFlowProvider> <ReactFlowProvider>
<StrategyEditorCanvas <StrategyEditorCanvas
@@ -2025,7 +2055,7 @@ export default function StrategyEditorPage({
orphans={currentOrphans} orphans={currentOrphans}
onOrphansChange={handleOrphansChange} onOrphansChange={handleOrphansChange}
def={DEF} def={DEF}
signalTab={signalTab} signalTab={buySellTab(signalTab)}
onChange={handleRootChange} onChange={handleRootChange}
selectedNodeId={selectedNodeId} selectedNodeId={selectedNodeId}
onSelectNode={setSelectedNodeId} onSelectNode={setSelectedNodeId}
@@ -2061,7 +2091,7 @@ export default function StrategyEditorPage({
</div> </div>
<CondEditor <CondEditor
cond={selectedLogicNode.condition} cond={selectedLogicNode.condition}
signalType={signalTab} signalType={buySellTab(signalTab)}
def={DEF} def={DEF}
stochPairEdit={stochPairEditForSelected} stochPairEdit={stochPairEditForSelected}
onChange={c => { onChange={c => {
@@ -2195,7 +2225,7 @@ export default function StrategyEditorPage({
) : ( ) : (
<StrategyListEditor <StrategyListEditor
editorState={currentEditorState} editorState={currentEditorState}
signalTab={signalTab} signalTab={buySellTab(signalTab)}
def={DEF} def={DEF}
onEditorStateChange={handleEditorStateChange} onEditorStateChange={handleEditorStateChange}
onAddStart={handleAddStartSection} onAddStart={handleAddStartSection}
@@ -2206,6 +2236,8 @@ export default function StrategyEditorPage({
</div> </div>
{signalTab !== 'eval' && (
<>
<div <div
className="se-splitter se-splitter--h" className="se-splitter se-splitter--h"
role="separator" role="separator"
@@ -2243,6 +2275,8 @@ export default function StrategyEditorPage({
</section> </section>
</div> </div>
</footer> </footer>
</>
)}
</div> </div>
</main> </main>
@@ -0,0 +1,125 @@
/**
* 전략편집기 — 작성 중 전략 평가 차트 (StrategyEvaluationChart 재사용)
*/
import '../../styles/backtestDashboard.css';
import '../../styles/strategyEvaluation.css';
import React, { useEffect, useMemo, useState } from 'react';
import type { Theme, Timeframe } from '../../types';
import StrategyEvaluationChart from '../strategyEvaluation/StrategyEvaluationChart';
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
import { useMarketTicker } from '../../hooks/useMarketTicker';
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
import {
BACKTEST_STRATEGY_TIMEFRAME,
buildQuickRunTimeframeSelectOptions,
resolveBacktestExecTimeframe,
type BacktestRunTimeframeChoice,
} from '../../utils/backtestRunTimeframe';
import {
buildDraftStrategyForEvaluation,
DRAFT_EVAL_STRATEGY_ID,
} from '../../utils/strategyEditorEvaluation';
import type { EditorConditionState } from '../../utils/strategyConditionSerde';
interface Props {
theme?: Theme;
stratName: string;
stratDesc: string;
buyEditorState: EditorConditionState;
sellEditorState: EditorConditionState;
}
const StrategyEditorEvaluationPanel: React.FC<Props> = ({
theme = 'dark',
stratName,
stratDesc,
buyEditorState,
sellEditorState,
}) => {
const { getParams, getVisualConfig, settingsRevision } = useIndicatorSettings();
const [market, setMarket] = useState('KRW-BTC');
const [timeframeChoice, setTimeframeChoice] = useState<BacktestRunTimeframeChoice>(BACKTEST_STRATEGY_TIMEFRAME);
const [selectedBarIndex, setSelectedBarIndex] = useState(0);
const [paramsRevision, setParamsRevision] = useState(0);
const draftStrategy = useMemo(
() => buildDraftStrategyForEvaluation({
name: stratName,
description: stratDesc,
buyEditorState,
sellEditorState,
}),
[stratName, stratDesc, buyEditorState, sellEditorState],
);
const strategyPrimaryTimeframe = useMemo(
() => (draftStrategy ? resolveStrategyPrimaryTimeframe(draftStrategy) : undefined),
[draftStrategy],
);
const chartTimeframe = useMemo((): Timeframe => {
return resolveBacktestExecTimeframe(timeframeChoice, strategyPrimaryTimeframe ?? '3m');
}, [timeframeChoice, strategyPrimaryTimeframe]);
const tfOptions = useMemo(
() => buildQuickRunTimeframeSelectOptions(strategyPrimaryTimeframe),
[strategyPrimaryTimeframe],
);
const draftRevisionKey = useMemo(
() => `${draftStrategy?.buyCondition ?? ''}|${draftStrategy?.sellCondition ?? ''}`,
[draftStrategy?.buyCondition, draftStrategy?.sellCondition],
);
useEffect(() => {
setParamsRevision(v => v + 1);
}, [draftRevisionKey, settingsRevision]);
const {
tickers: marketTickers,
marketInfos,
loading: marketLoading,
refreshAllTickers: refreshMarketTickers,
} = useMarketTicker();
if (!draftStrategy) {
return (
<div className="se-eval-panel se-eval-panel--empty">
<p className="se-eval-empty-msg">· .</p>
</div>
);
}
const chartStrategies = [draftStrategy];
return (
<div className="se-eval-panel">
<StrategyEvaluationChart
market={market}
timeframe={chartTimeframe}
timeframeChoice={timeframeChoice}
tfOptions={tfOptions}
strategies={chartStrategies}
selectedStrategyId={DRAFT_EVAL_STRATEGY_ID}
onStrategySelect={() => {}}
onMarketChange={setMarket}
onTimeframeChoiceChange={setTimeframeChoice}
theme={theme}
strategy={draftStrategy}
selectedBarIndex={selectedBarIndex}
onSelectedBarIndexChange={setSelectedBarIndex}
paramsRevision={paramsRevision}
getParamsOverride={getParams}
getVisualConfigOverride={getVisualConfig}
marketTickers={marketTickers}
marketInfos={marketInfos}
marketLoading={marketLoading}
refreshMarketTickers={refreshMarketTickers}
hideStrategySelect
useDraftDsl
/>
</div>
);
};
export default StrategyEditorEvaluationPanel;
@@ -126,6 +126,10 @@ interface Props {
/** 캔들 오버레이(SMA·볼린저·일목·캔들) 표시 — 미전달 시 내부 state */ /** 캔들 오버레이(SMA·볼린저·일목·캔들) 표시 — 미전달 시 내부 state */
overlayVisibility?: ChartOverlayVisibility; overlayVisibility?: ChartOverlayVisibility;
onSetOverlayVisibility?: (key: ChartOverlayToggleKey, visible: boolean) => void; onSetOverlayVisibility?: (key: ChartOverlayToggleKey, visible: boolean) => void;
/** 전략편집기 평가 탭 — 전략 선택 드롭다운 숨김 */
hideStrategySelect?: boolean;
/** 미저장 draft DSL로 시그널 계산 */
useDraftDsl?: boolean;
} }
const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true; const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true;
@@ -174,6 +178,8 @@ const StrategyEvaluationChart: React.FC<Props> = ({
onMarketSearchOpenChange, onMarketSearchOpenChange,
overlayVisibility: overlayVisibilityProp, overlayVisibility: overlayVisibilityProp,
onSetOverlayVisibility, onSetOverlayVisibility,
hideStrategySelect = false,
useDraftDsl = false,
}) => { }) => {
const { getParams: baseGetParams, getVisualConfig: baseGetVisual, settingsRevision } = useIndicatorSettings(); const { getParams: baseGetParams, getVisualConfig: baseGetVisual, settingsRevision } = useIndicatorSettings();
const getParams = getParamsOverride ?? baseGetParams; const getParams = getParamsOverride ?? baseGetParams;
@@ -417,6 +423,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
bars: barData, bars: barData,
evaluationBarCount: evaluationBarCountRef.current, evaluationBarCount: evaluationBarCountRef.current,
getParams, getParams,
useDraftDsl,
}); });
if (gen !== backtestGenRef.current) return; if (gen !== backtestGenRef.current) return;
setSignals(nextSignals); setSignals(nextSignals);
@@ -429,7 +436,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
} finally { } finally {
if (gen === backtestGenRef.current) setBacktestRunning(false); if (gen === backtestGenRef.current) setBacktestRunning(false);
} }
}, [strategy, market, timeframe, getParams, applyMarkers]); }, [strategy, market, timeframe, getParams, applyMarkers, useDraftDsl]);
useEffect(() => { useEffect(() => {
if (!strategy?.id || loading || bars.length < 10) { if (!strategy?.id || loading || bars.length < 10) {
@@ -444,7 +451,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
} }
void runSignalScanForBars(bars); void runSignalScanForBars(bars);
return () => { ++backtestGenRef.current; }; return () => { ++backtestGenRef.current; };
}, [strategy?.id, strategy?.buyCondition, market, timeframe, paramsRevision, indicatorParamsKey, bars, loading, runSignalScanForBars]); }, [strategy?.id, strategy?.buyCondition, strategy?.sellCondition, market, timeframe, paramsRevision, indicatorParamsKey, bars, loading, runSignalScanForBars]);
useEffect(() => { useEffect(() => {
applyMarkers(); applyMarkers();
@@ -523,6 +530,8 @@ const StrategyEvaluationChart: React.FC<Props> = ({
market, market,
timeframe, timeframe,
strategy?.id, strategy?.id,
strategy?.buyCondition,
strategy?.sellCondition,
paramsRevision, paramsRevision,
indicatorParamsKey, indicatorParamsKey,
windowAnchor, windowAnchor,
@@ -793,6 +802,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
<option key={o.value} value={o.value}>{o.label}</option> <option key={o.value} value={o.value}>{o.label}</option>
))} ))}
</select> </select>
{!hideStrategySelect && (
<select <select
className="btd-run-select btd-run-select--strategy seval-chart-strategy-select" className="btd-run-select btd-run-select--strategy seval-chart-strategy-select"
value={selectedStrategyId ?? ''} value={selectedStrategyId ?? ''}
@@ -810,6 +820,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
</option> </option>
))} ))}
</select> </select>
)}
{chartFullscreen && selectedBar && ( {chartFullscreen && selectedBar && (
<time <time
className="seval-selected-bar-time" className="seval-selected-bar-time"
+45
View File
@@ -856,6 +856,51 @@
background: color-mix(in srgb, var(--se-sell) 10%, transparent); background: color-mix(in srgb, var(--se-sell) 10%, transparent);
box-shadow: 0 0 16px color-mix(in srgb, var(--se-sell) 15%, transparent); box-shadow: 0 0 16px color-mix(in srgb, var(--se-sell) 15%, transparent);
} }
.se-signal-tab--eval-on {
color: var(--se-gold);
border-color: color-mix(in srgb, var(--se-gold) 50%, transparent);
background: color-mix(in srgb, var(--se-gold) 10%, transparent);
box-shadow: 0 0 16px color-mix(in srgb, var(--se-gold) 15%, transparent);
}
.se-center-work--eval {
display: flex;
flex-direction: column;
min-height: 0;
}
.se-eval-panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
margin: 0 10px 10px;
border: 1px solid var(--se-canvas-border);
border-radius: 12px;
overflow: hidden;
box-shadow: var(--se-canvas-inset);
}
.se-eval-panel .seval-chart {
flex: 1;
min-height: 0;
border: none;
border-radius: 0;
}
.se-eval-panel--empty {
align-items: center;
justify-content: center;
padding: 2rem;
}
.se-eval-empty-msg {
margin: 0;
font-size: 0.85rem;
color: var(--se-text-muted);
text-align: center;
line-height: 1.5;
}
.se-editing-name { .se-editing-name {
font-size: 0.72rem; font-size: 0.72rem;
@@ -0,0 +1,36 @@
import type { StrategyDto } from './backendApi';
import {
alignBuySellStartCandleTypesForSave,
encodeConditionForSave,
type EditorConditionState,
} from './strategyConditionSerde';
/** 편집 중(미저장) 전략 — StrategyEvaluationChart·시그널 API용 합성 ID */
export const DRAFT_EVAL_STRATEGY_ID = -1;
export function isDraftEvaluationStrategy(strategy: StrategyDto | null | undefined): boolean {
return strategy?.id === DRAFT_EVAL_STRATEGY_ID;
}
export function buildDraftStrategyForEvaluation(opts: {
name: string;
description: string;
buyEditorState: EditorConditionState;
sellEditorState: EditorConditionState;
}): StrategyDto | null {
const aligned = alignBuySellStartCandleTypesForSave(opts.buyEditorState, opts.sellEditorState);
const buyCondition = encodeConditionForSave(aligned.buy);
const sellCondition = encodeConditionForSave(aligned.sell);
if (!buyCondition && !sellCondition) return null;
return {
id: DRAFT_EVAL_STRATEGY_ID,
name: opts.name.trim() || '작성 중 전략',
description: opts.description,
buyCondition,
sellCondition,
enabled: true,
createdAt: '',
updatedAt: '',
};
}
@@ -11,6 +11,7 @@ import {
type StrategyDto, type StrategyDto,
} from './backendApi'; } from './backendApi';
import { buildEvalParamsFromStrategy } from './strategyEvaluationParams'; import { buildEvalParamsFromStrategy } from './strategyEvaluationParams';
import { isDraftEvaluationStrategy } from './strategyEditorEvaluation';
import { isScanSignalsExecutionMode, normalizeTradeExecutionMode } from './tradeExecutionMode'; import { isScanSignalsExecutionMode, normalizeTradeExecutionMode } from './tradeExecutionMode';
export interface StrategyEvaluationSignalsResult { export interface StrategyEvaluationSignalsResult {
@@ -46,6 +47,8 @@ export async function fetchStrategyEvaluationSignals(opts: {
evaluationBarCount: number; evaluationBarCount: number;
getParams: (type: string) => Record<string, number | string | boolean>; getParams: (type: string) => Record<string, number | string | boolean>;
backtestSettings?: BacktestSettingsDto | null; backtestSettings?: BacktestSettingsDto | null;
/** 미저장 draft — buyCondition/sellCondition 직접 전달 (scan-signals 대신 runBacktest) */
useDraftDsl?: boolean;
}): Promise<StrategyEvaluationSignalsResult> { }): Promise<StrategyEvaluationSignalsResult> {
const btSettings = opts.backtestSettings ?? await loadBacktestSettings(); const btSettings = opts.backtestSettings ?? await loadBacktestSettings();
const tradeExecutionMode = normalizeTradeExecutionMode(btSettings.tradeExecutionMode); const tradeExecutionMode = normalizeTradeExecutionMode(btSettings.tradeExecutionMode);
@@ -54,8 +57,9 @@ export async function fetchStrategyEvaluationSignals(opts: {
const commissionRate = resolveEvaluationCommissionRate(btSettings); const commissionRate = resolveEvaluationCommissionRate(btSettings);
const positionMode = btSettings.positionMode ?? 'LONG_ONLY'; const positionMode = btSettings.positionMode ?? 'LONG_ONLY';
const useDraftDsl = opts.useDraftDsl ?? isDraftEvaluationStrategy(opts.strategy);
if (isScanSignalsExecutionMode(btSettings)) { if (isScanSignalsExecutionMode(btSettings) && !useDraftDsl) {
const signals = await fetchLiveConditionScanSignals( const signals = await fetchLiveConditionScanSignals(
opts.market, opts.market,
opts.strategyId, opts.strategyId,
@@ -74,14 +78,17 @@ export async function fetchStrategyEvaluationSignals(opts: {
}; };
} }
const settings: BacktestSettingsDto = { const settings: BacktestSettingsDto = useDraftDsl
...btSettings, ? { ...btSettings, positionMode, tradeExecutionMode: btSettings.tradeExecutionMode ?? 'SCAN_SIGNALS' }
positionMode, : { ...btSettings, positionMode, tradeExecutionMode: 'BACKTEST_ENGINE' };
tradeExecutionMode: 'BACKTEST_ENGINE',
};
const res = await runBacktest({ const res = await runBacktest({
strategyId: opts.strategyId, ...(useDraftDsl
? {
buyCondition: opts.strategy.buyCondition,
sellCondition: opts.strategy.sellCondition,
}
: { strategyId: opts.strategyId }),
bars: mapBarsForRequest(opts.bars), bars: mapBarsForRequest(opts.bars),
timeframe: opts.timeframe, timeframe: opts.timeframe,
symbol: opts.market, symbol: opts.market,
@@ -100,6 +107,6 @@ export async function fetchStrategyEvaluationSignals(opts: {
initialCapital: btSettings.initialCapital ?? res.analysis?.initialCapital ?? initialCapital, initialCapital: btSettings.initialCapital ?? res.analysis?.initialCapital ?? initialCapital,
settings, settings,
commissionRate, commissionRate,
tradeExecutionMode: 'BACKTEST_ENGINE', tradeExecutionMode: normalizeTradeExecutionMode(settings.tradeExecutionMode),
}; };
} }