전략편집기 평가탭 추가
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user