백테스팅 수정
This commit is contained in:
@@ -26,6 +26,10 @@ public class BacktestResponse {
|
|||||||
private BacktestAnalysisDto analysis;
|
private BacktestAnalysisDto analysis;
|
||||||
/** DB 저장 후 부여된 결과 ID */
|
/** DB 저장 후 부여된 결과 ID */
|
||||||
private Long resultId;
|
private Long resultId;
|
||||||
|
/** 실행에 사용된 시간봉 (요청 bars 기준) */
|
||||||
|
private String timeframe;
|
||||||
|
/** 실행 종목 */
|
||||||
|
private String symbol;
|
||||||
|
|
||||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||||
public static class Signal {
|
public static class Signal {
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ public class BacktestingService {
|
|||||||
.stats(stats)
|
.stats(stats)
|
||||||
.analysis(analysis)
|
.analysis(analysis)
|
||||||
.resultId(resultId)
|
.resultId(resultId)
|
||||||
|
.timeframe(normalizeTf(req.getTimeframe()))
|
||||||
|
.symbol(req.getSymbol())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +618,8 @@ public class BacktestingService {
|
|||||||
snapshot.put("strategyId", req.getStrategyId());
|
snapshot.put("strategyId", req.getStrategyId());
|
||||||
snapshot.put("strategyName", strategyName);
|
snapshot.put("strategyName", strategyName);
|
||||||
snapshot.put("symbol", req.getSymbol() != null ? req.getSymbol() : "UNKNOWN");
|
snapshot.put("symbol", req.getSymbol() != null ? req.getSymbol() : "UNKNOWN");
|
||||||
snapshot.put("timeframe", req.getTimeframe());
|
String execTf = normalizeTf(req.getTimeframe());
|
||||||
|
snapshot.put("timeframe", execTf);
|
||||||
snapshot.put("barCount", bars.size());
|
snapshot.put("barCount", bars.size());
|
||||||
if (execParams != null && !execParams.isEmpty()) snapshot.put("indicatorParams", execParams);
|
if (execParams != null && !execParams.isEmpty()) snapshot.put("indicatorParams", execParams);
|
||||||
if (execBuyDsl != null && !execBuyDsl.isNull()) snapshot.put("buyCondition", execBuyDsl);
|
if (execBuyDsl != null && !execBuyDsl.isNull()) snapshot.put("buyCondition", execBuyDsl);
|
||||||
@@ -627,7 +630,7 @@ public class BacktestingService {
|
|||||||
.strategyId(req.getStrategyId())
|
.strategyId(req.getStrategyId())
|
||||||
.strategyName(strategyName)
|
.strategyName(strategyName)
|
||||||
.symbol(req.getSymbol() != null ? req.getSymbol() : "UNKNOWN")
|
.symbol(req.getSymbol() != null ? req.getSymbol() : "UNKNOWN")
|
||||||
.timeframe(req.getTimeframe())
|
.timeframe(execTf)
|
||||||
.barCount(bars.size())
|
.barCount(bars.size())
|
||||||
.fromTime(fromTime)
|
.fromTime(fromTime)
|
||||||
.toTime(toTime)
|
.toTime(toTime)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
formatTimeframeKo,
|
formatTimeframeKo,
|
||||||
normalizeEpochSec,
|
normalizeEpochSec,
|
||||||
normalizeEpochSecOptional,
|
normalizeEpochSecOptional,
|
||||||
|
resolveBacktestRecordExecTimeframe,
|
||||||
toUpbitMarket,
|
toUpbitMarket,
|
||||||
} from '../utils/backtestUiUtils';
|
} from '../utils/backtestUiUtils';
|
||||||
import BacktestExecutionList, { type ExecutionListTab } from './backtest/BacktestExecutionList';
|
import BacktestExecutionList, { type ExecutionListTab } from './backtest/BacktestExecutionList';
|
||||||
@@ -52,6 +53,13 @@ import { repairUtf8Mojibake } from '../utils/textEncoding';
|
|||||||
import { enrichStrategyNameMap, strategyNamesFromList } from '../utils/strategyNameResolver';
|
import { enrichStrategyNameMap, strategyNamesFromList } from '../utils/strategyNameResolver';
|
||||||
import { markStrategyIdsMissingUnlessValid } from '../utils/strategyMissingCache';
|
import { markStrategyIdsMissingUnlessValid } from '../utils/strategyMissingCache';
|
||||||
import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
|
import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
|
||||||
|
import {
|
||||||
|
BACKTEST_STRATEGY_TIMEFRAME,
|
||||||
|
buildQuickRunTimeframeSelectOptions,
|
||||||
|
parseBacktestRunTimeframeChoice,
|
||||||
|
resolveBacktestExecTimeframe,
|
||||||
|
type BacktestRunTimeframeChoice,
|
||||||
|
} from '../utils/backtestRunTimeframe';
|
||||||
|
|
||||||
const LEFT_KEY = 'btd-left-width';
|
const LEFT_KEY = 'btd-left-width';
|
||||||
const RIGHT_KEY = 'btd-right-width';
|
const RIGHT_KEY = 'btd-right-width';
|
||||||
@@ -77,19 +85,6 @@ interface Props {
|
|||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUICK_RUN_TIMEFRAMES: { value: Timeframe; label: string }[] = [
|
|
||||||
{ value: '1m', label: '1분' },
|
|
||||||
{ value: '3m', label: '3분' },
|
|
||||||
{ value: '5m', label: '5분' },
|
|
||||||
{ value: '10m', label: '10분' },
|
|
||||||
{ value: '15m', label: '15분' },
|
|
||||||
{ value: '30m', label: '30분' },
|
|
||||||
{ value: '1h', label: '1시간' },
|
|
||||||
{ value: '4h', label: '4시간' },
|
|
||||||
{ value: '1D', label: '일봉' },
|
|
||||||
{ value: '1W', label: '주봉' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
||||||
const [tab, setTab] = useState<ExecutionListTab>('backtest');
|
const [tab, setTab] = useState<ExecutionListTab>('backtest');
|
||||||
const [records, setRecords] = useState<BacktestResultRecord[]>([]);
|
const [records, setRecords] = useState<BacktestResultRecord[]>([]);
|
||||||
@@ -102,7 +97,7 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
|
|
||||||
// ── 빠른 백테스팅 실행 ──────────────────────────────────────────────────────
|
// ── 빠른 백테스팅 실행 ──────────────────────────────────────────────────────
|
||||||
const [runMarket, setRunMarket] = useState<string>('KRW-BTC');
|
const [runMarket, setRunMarket] = useState<string>('KRW-BTC');
|
||||||
const [runTimeframe, setRunTimeframe] = useState<Timeframe>('1D');
|
const [runTimeframe, setRunTimeframe] = useState<BacktestRunTimeframeChoice>(BACKTEST_STRATEGY_TIMEFRAME);
|
||||||
const [runStrategyId, setRunStrategyId] = useState<number | null>(null);
|
const [runStrategyId, setRunStrategyId] = useState<number | null>(null);
|
||||||
const [runLoading, setRunLoading] = useState(false);
|
const [runLoading, setRunLoading] = useState(false);
|
||||||
const [runError, setRunError] = useState<string | null>(null);
|
const [runError, setRunError] = useState<string | null>(null);
|
||||||
@@ -214,15 +209,6 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
|
|
||||||
useEffect(() => { void fetchAll(); }, [fetchAll]);
|
useEffect(() => { void fetchAll(); }, [fetchAll]);
|
||||||
|
|
||||||
// 전략 선택 시 DSL 기준 시간봉으로 자동 동기화 (예: 전략 4=5m 조건인데 3m 실행 방지)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!runStrategyId) return;
|
|
||||||
const strat = strategies.find(s => s.id === runStrategyId);
|
|
||||||
if (!strat) return;
|
|
||||||
const tf = resolveStrategyPrimaryTimeframe(strat);
|
|
||||||
setRunTimeframe(prev => (prev === tf ? prev : tf));
|
|
||||||
}, [runStrategyId, strategies]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onTradesChanged = () => { void refreshLiveTrades(); };
|
const onTradesChanged = () => { void refreshLiveTrades(); };
|
||||||
window.addEventListener(PAPER_TRADES_CHANGED_EVENT, onTradesChanged);
|
window.addEventListener(PAPER_TRADES_CHANGED_EVENT, onTradesChanged);
|
||||||
@@ -245,7 +231,10 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
setRunError(null);
|
setRunError(null);
|
||||||
try {
|
try {
|
||||||
const strategy = strategies.find(s => s.id === runStrategyId);
|
const strategy = strategies.find(s => s.id === runStrategyId);
|
||||||
const execTimeframe = strategy ? resolveStrategyPrimaryTimeframe(strategy) : runTimeframe;
|
const strategyTimeframe = strategy
|
||||||
|
? resolveStrategyPrimaryTimeframe(strategy)
|
||||||
|
: '1m';
|
||||||
|
const execTimeframe = resolveBacktestExecTimeframe(runTimeframe, strategyTimeframe);
|
||||||
const [settings, indicatorParams, barsRes] = await Promise.all([
|
const [settings, indicatorParams, barsRes] = await Promise.all([
|
||||||
loadBacktestSettings(),
|
loadBacktestSettings(),
|
||||||
loadIndicatorSettings(),
|
loadIndicatorSettings(),
|
||||||
@@ -272,15 +261,34 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
strategyName: strategy?.name,
|
strategyName: strategy?.name,
|
||||||
});
|
});
|
||||||
if (!result) throw new Error('백테스팅 실행에 실패했습니다.');
|
if (!result) throw new Error('백테스팅 실행에 실패했습니다.');
|
||||||
// 결과 목록 갱신 후 새 결과 자동 선택
|
const list = await loadBacktestResults();
|
||||||
await fetchAll();
|
setRecords(list);
|
||||||
|
if (result.resultId) {
|
||||||
|
const fresh = list.find(r => r.id === result.resultId) ?? list[0] ?? null;
|
||||||
|
setSelectedBacktest(fresh);
|
||||||
|
} else {
|
||||||
|
setSelectedBacktest(list[0] ?? null);
|
||||||
|
}
|
||||||
setTab('backtest');
|
setTab('backtest');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setRunError(e instanceof Error ? e.message : '실행 중 오류가 발생했습니다.');
|
setRunError(e instanceof Error ? e.message : '실행 중 오류가 발생했습니다.');
|
||||||
} finally {
|
} finally {
|
||||||
setRunLoading(false);
|
setRunLoading(false);
|
||||||
}
|
}
|
||||||
}, [runMarket, runTimeframe, runStrategyId, strategies, fetchAll]);
|
}, [runMarket, runTimeframe, runStrategyId, strategies]);
|
||||||
|
|
||||||
|
const runStrategy = useMemo(
|
||||||
|
() => (runStrategyId ? strategies.find(s => s.id === runStrategyId) : undefined),
|
||||||
|
[runStrategyId, strategies],
|
||||||
|
);
|
||||||
|
const runStrategyTimeframe = useMemo(
|
||||||
|
() => (runStrategy ? resolveStrategyPrimaryTimeframe(runStrategy) : undefined),
|
||||||
|
[runStrategy],
|
||||||
|
);
|
||||||
|
const runTimeframeOptions = useMemo(
|
||||||
|
() => buildQuickRunTimeframeSelectOptions(runStrategyTimeframe),
|
||||||
|
[runStrategyTimeframe],
|
||||||
|
);
|
||||||
|
|
||||||
const backtestDetail = selectedBacktest ? parseDetail(selectedBacktest) : null;
|
const backtestDetail = selectedBacktest ? parseDetail(selectedBacktest) : null;
|
||||||
|
|
||||||
@@ -312,7 +320,7 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
const fromSec = normalizeEpochSecOptional(selectedBacktest.fromTime);
|
const fromSec = normalizeEpochSecOptional(selectedBacktest.fromTime);
|
||||||
return {
|
return {
|
||||||
symbol: selectedBacktest.symbol,
|
symbol: selectedBacktest.symbol,
|
||||||
timeframe: selectedBacktest.timeframe,
|
timeframe: resolveBacktestRecordExecTimeframe(selectedBacktest),
|
||||||
toTimeSec: toSec,
|
toTimeSec: toSec,
|
||||||
fromTimeSec: fromSec > 0 ? fromSec : undefined,
|
fromTimeSec: fromSec > 0 ? fromSec : undefined,
|
||||||
barCount: selectedBacktest.barCount || 300,
|
barCount: selectedBacktest.barCount || 300,
|
||||||
@@ -341,7 +349,7 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
ko: getKoreanName(market),
|
ko: getKoreanName(market),
|
||||||
strategy: selectedBacktest.strategyName || '전략 없음',
|
strategy: selectedBacktest.strategyName || '전략 없음',
|
||||||
metaParts: [
|
metaParts: [
|
||||||
formatTimeframeKo(selectedBacktest.timeframe),
|
formatTimeframeKo(resolveBacktestRecordExecTimeframe(selectedBacktest)),
|
||||||
formatChartTypeKo('candlestick'),
|
formatChartTypeKo('candlestick'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -452,11 +460,11 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
<select
|
<select
|
||||||
className="btd-run-select"
|
className="btd-run-select"
|
||||||
value={runTimeframe}
|
value={runTimeframe}
|
||||||
onChange={e => setRunTimeframe(e.target.value as Timeframe)}
|
onChange={e => setRunTimeframe(parseBacktestRunTimeframeChoice(e.target.value))}
|
||||||
disabled={runLoading}
|
disabled={runLoading}
|
||||||
title="타임프레임"
|
title="백테스팅 실행 시간봉"
|
||||||
>
|
>
|
||||||
{QUICK_RUN_TIMEFRAMES.map(tf => (
|
{runTimeframeOptions.map(tf => (
|
||||||
<option key={tf.value} value={tf.value}>{tf.label}</option>
|
<option key={tf.value} value={tf.value}>{tf.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -105,22 +105,16 @@ import ReactDOM from 'react-dom';
|
|||||||
import { MarketSearchPanel } from './MarketSearchPanel';
|
import { MarketSearchPanel } from './MarketSearchPanel';
|
||||||
import { getKoreanName } from '../utils/marketNameCache';
|
import { getKoreanName } from '../utils/marketNameCache';
|
||||||
import type { Timeframe } from '../types';
|
import type { Timeframe } from '../types';
|
||||||
|
import {
|
||||||
|
BACKTEST_STRATEGY_TIMEFRAME,
|
||||||
|
buildQuickRunTimeframeSelectOptions,
|
||||||
|
parseBacktestRunTimeframeChoice,
|
||||||
|
resolveBacktestExecTimeframe,
|
||||||
|
type BacktestRunTimeframeChoice,
|
||||||
|
} from '../utils/backtestRunTimeframe';
|
||||||
import '../styles/strategyEditor.css';
|
import '../styles/strategyEditor.css';
|
||||||
import '../styles/strategyEditorTheme.css';
|
import '../styles/strategyEditorTheme.css';
|
||||||
|
|
||||||
const QUICK_RUN_TIMEFRAMES: { value: Timeframe; label: string }[] = [
|
|
||||||
{ value: '1m', label: '1분' },
|
|
||||||
{ value: '3m', label: '3분' },
|
|
||||||
{ value: '5m', label: '5분' },
|
|
||||||
{ value: '10m', label: '10분' },
|
|
||||||
{ value: '15m', label: '15분' },
|
|
||||||
{ value: '30m', label: '30분' },
|
|
||||||
{ value: '1h', label: '1시간' },
|
|
||||||
{ value: '4h', label: '4시간' },
|
|
||||||
{ value: '1D', label: '일봉' },
|
|
||||||
{ value: '1W', label: '주봉' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const BACKTEST_FOCUS_KEY = 'backtest_focus_id';
|
const BACKTEST_FOCUS_KEY = 'backtest_focus_id';
|
||||||
|
|
||||||
const LEFT_PANEL_MIN = 220;
|
const LEFT_PANEL_MIN = 220;
|
||||||
@@ -222,23 +216,13 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
|
|
||||||
// ── 빠른 백테스팅 실행 ────────────────────────────────────────────────────
|
// ── 빠른 백테스팅 실행 ────────────────────────────────────────────────────
|
||||||
const [qbMarket, setQbMarket] = useState('KRW-BTC');
|
const [qbMarket, setQbMarket] = useState('KRW-BTC');
|
||||||
const [qbTimeframe, setQbTimeframe] = useState<Timeframe>('1D');
|
const [qbTimeframe, setQbTimeframe] = useState<BacktestRunTimeframeChoice>(BACKTEST_STRATEGY_TIMEFRAME);
|
||||||
const [qbLoading, setQbLoading] = useState(false);
|
const [qbLoading, setQbLoading] = useState(false);
|
||||||
const [qbError, setQbError] = useState<string | null>(null);
|
const [qbError, setQbError] = useState<string | null>(null);
|
||||||
const [qbMarketOpen, setQbMarketOpen] = useState(false);
|
const [qbMarketOpen, setQbMarketOpen] = useState(false);
|
||||||
const [qbMarketQ, setQbMarketQ] = useState('');
|
const [qbMarketQ, setQbMarketQ] = useState('');
|
||||||
const qbMarketBtnRef = useRef<HTMLButtonElement>(null);
|
const qbMarketBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
// 전략 선택/조건 변경 시 DSL 기준 시간봉으로 빠른 실행 TF 동기화
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedId == null) return;
|
|
||||||
const tf = resolveStrategyPrimaryTimeframe({
|
|
||||||
buyCondition: buyCondition,
|
|
||||||
sellCondition: sellCondition,
|
|
||||||
} as ApiStrategyDto);
|
|
||||||
setQbTimeframe(prev => (prev === tf ? prev : tf));
|
|
||||||
}, [selectedId, buyCondition, sellCondition]);
|
|
||||||
|
|
||||||
const [leftWidth, setLeftWidth] = useState(() => readStoredSize('se-left-width', LEFT_PANEL_DEFAULT));
|
const [leftWidth, setLeftWidth] = useState(() => readStoredSize('se-left-width', LEFT_PANEL_DEFAULT));
|
||||||
const [terminalHeight, setTerminalHeight] = useState(() => readStoredSize('se-terminal-height', TERMINAL_DEFAULT));
|
const [terminalHeight, setTerminalHeight] = useState(() => readStoredSize('se-terminal-height', TERMINAL_DEFAULT));
|
||||||
const leftWidthRef = useRef(leftWidth);
|
const leftWidthRef = useRef(leftWidth);
|
||||||
@@ -1189,10 +1173,11 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
setQbError(null);
|
setQbError(null);
|
||||||
try {
|
try {
|
||||||
const selectedStrategy = strategies.find(s => s.id === selectedId);
|
const selectedStrategy = strategies.find(s => s.id === selectedId);
|
||||||
const execTimeframe = resolveStrategyPrimaryTimeframe({
|
const strategyTimeframe = resolveStrategyPrimaryTimeframe({
|
||||||
buyCondition: buyCondition,
|
buyCondition: buyCondition,
|
||||||
sellCondition: sellCondition,
|
sellCondition: sellCondition,
|
||||||
} as ApiStrategyDto);
|
} as ApiStrategyDto);
|
||||||
|
const execTimeframe = resolveBacktestExecTimeframe(qbTimeframe, strategyTimeframe);
|
||||||
const [settings, indicatorParams, barsRes] = await Promise.all([
|
const [settings, indicatorParams, barsRes] = await Promise.all([
|
||||||
loadBacktestSettings(),
|
loadBacktestSettings(),
|
||||||
loadIndicatorSettings(),
|
loadIndicatorSettings(),
|
||||||
@@ -1228,7 +1213,19 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
} finally {
|
} finally {
|
||||||
setQbLoading(false);
|
setQbLoading(false);
|
||||||
}
|
}
|
||||||
}, [selectedId, strategies, qbMarket, buyCondition, sellCondition, onNavigateToBacktest]);
|
}, [selectedId, strategies, qbMarket, qbTimeframe, buyCondition, sellCondition, onNavigateToBacktest]);
|
||||||
|
|
||||||
|
const qbStrategyTimeframe = useMemo(
|
||||||
|
() => resolveStrategyPrimaryTimeframe({
|
||||||
|
buyCondition: buyCondition,
|
||||||
|
sellCondition: sellCondition,
|
||||||
|
} as ApiStrategyDto),
|
||||||
|
[buyCondition, sellCondition],
|
||||||
|
);
|
||||||
|
const qbTimeframeOptions = useMemo(
|
||||||
|
() => buildQuickRunTimeframeSelectOptions(qbStrategyTimeframe),
|
||||||
|
[qbStrategyTimeframe],
|
||||||
|
);
|
||||||
|
|
||||||
const operators = [
|
const operators = [
|
||||||
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
|
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
|
||||||
@@ -1303,11 +1300,11 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
<select
|
<select
|
||||||
className="se-qr-select"
|
className="se-qr-select"
|
||||||
value={qbTimeframe}
|
value={qbTimeframe}
|
||||||
onChange={e => setQbTimeframe(e.target.value as Timeframe)}
|
onChange={e => setQbTimeframe(parseBacktestRunTimeframeChoice(e.target.value))}
|
||||||
disabled={qbLoading}
|
disabled={qbLoading}
|
||||||
title="타임프레임"
|
title="백테스팅 실행 시간봉"
|
||||||
>
|
>
|
||||||
{QUICK_RUN_TIMEFRAMES.map(tf => (
|
{qbTimeframeOptions.map(tf => (
|
||||||
<option key={tf.value} value={tf.value}>{tf.label}</option>
|
<option key={tf.value} value={tf.value}>{tf.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
buildVirtualTradingChartIndicators,
|
buildVirtualTradingChartIndicators,
|
||||||
ensurePaperChartOverlays,
|
ensurePaperChartOverlays,
|
||||||
resolveStrategyPrimaryTimeframe,
|
|
||||||
} from '../../utils/strategyToChartIndicators';
|
} from '../../utils/strategyToChartIndicators';
|
||||||
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
|
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
|
||||||
|
|
||||||
@@ -145,10 +144,9 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
const market = symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
|
const market = symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
|
||||||
|
|
||||||
const chartTimeframe = useMemo((): Timeframe => {
|
const chartTimeframe = useMemo((): Timeframe => {
|
||||||
// 백테스트 결과: 실행 타임프레임 우선 (전략 DSL TF와 실행 TF 불일치 시 차트·시그널 어긋남 방지)
|
const tf = timeframeRaw?.trim() ? timeframeRaw : '1m';
|
||||||
const tf = timeframeRaw || (strategy ? resolveStrategyPrimaryTimeframe(strategy) : '1m');
|
|
||||||
return normalizeChartTimeframe(tf) as Timeframe;
|
return normalizeChartTimeframe(tf) as Timeframe;
|
||||||
}, [strategy, timeframeRaw]);
|
}, [timeframeRaw]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!strategyId || isStrategyMissingOnServer(strategyId)) {
|
if (!strategyId || isStrategyMissingOnServer(strategyId)) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
formatTimeframeKo,
|
formatTimeframeKo,
|
||||||
pct,
|
pct,
|
||||||
pctAbs,
|
pctAbs,
|
||||||
|
resolveBacktestRecordExecTimeframe,
|
||||||
toUpbitMarket,
|
toUpbitMarket,
|
||||||
} from '../../utils/backtestUiUtils';
|
} from '../../utils/backtestUiUtils';
|
||||||
import { getKoreanName } from '../../utils/marketNameCache';
|
import { getKoreanName } from '../../utils/marketNameCache';
|
||||||
@@ -88,7 +89,7 @@ export default function BacktestExecutionList({
|
|||||||
const ko = getKoreanName(market).toLowerCase();
|
const ko = getKoreanName(market).toLowerCase();
|
||||||
const code = r.symbol.replace(/^KRW-/i, '').toLowerCase();
|
const code = r.symbol.replace(/^KRW-/i, '').toLowerCase();
|
||||||
const strategy = (r.strategyName ?? '').toLowerCase();
|
const strategy = (r.strategyName ?? '').toLowerCase();
|
||||||
const tf = r.timeframe?.toLowerCase() ?? '';
|
const tf = resolveBacktestRecordExecTimeframe(r).toLowerCase();
|
||||||
return ko.includes(normalizedQuery)
|
return ko.includes(normalizedQuery)
|
||||||
|| code.includes(normalizedQuery)
|
|| code.includes(normalizedQuery)
|
||||||
|| strategy.includes(normalizedQuery)
|
|| strategy.includes(normalizedQuery)
|
||||||
@@ -200,7 +201,7 @@ export default function BacktestExecutionList({
|
|||||||
<div className="btd-history-card-row">
|
<div className="btd-history-card-row">
|
||||||
<div className="btd-history-card-main">
|
<div className="btd-history-card-main">
|
||||||
<span className="btd-history-ko">{ko}</span>
|
<span className="btd-history-ko">{ko}</span>
|
||||||
<span className="btd-history-strategy">{strategy} · {formatTimeframeKo(r.timeframe)}</span>
|
<span className="btd-history-strategy">{strategy} · {formatTimeframeKo(resolveBacktestRecordExecTimeframe(r))}</span>
|
||||||
<span className="btd-history-date">{fmtListTimestamp(r.createdAt)}</span>
|
<span className="btd-history-date">{fmtListTimestamp(r.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="btd-history-card-side">
|
<div className="btd-history-card-side">
|
||||||
|
|||||||
@@ -1209,6 +1209,9 @@ export interface BacktestResponse {
|
|||||||
stats: BacktestStats;
|
stats: BacktestStats;
|
||||||
analysis?: BacktestAnalysis;
|
analysis?: BacktestAnalysis;
|
||||||
resultId?: number;
|
resultId?: number;
|
||||||
|
/** 실행에 사용된 시간봉 */
|
||||||
|
timeframe?: string;
|
||||||
|
symbol?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** DB 저장 이력 레코드 */
|
/** DB 저장 이력 레코드 */
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
import type { BacktestAnalysisReportModel } from '../components/backtest/BacktestAnalysisReportModal';
|
import type { BacktestAnalysisReportModel } from '../components/backtest/BacktestAnalysisReportModal';
|
||||||
import type { LiveExecutionItem } from './liveExecutionGroups';
|
import type { LiveExecutionItem } from './liveExecutionGroups';
|
||||||
import { buildAnalysisFromPaperTrades } from './paperMetrics';
|
import { buildAnalysisFromPaperTrades } from './paperMetrics';
|
||||||
import { fmtListTimestamp, toUpbitMarket } from './backtestUiUtils';
|
import { fmtListTimestamp, resolveBacktestRecordExecTimeframe, toUpbitMarket } from './backtestUiUtils';
|
||||||
import { getKoreanName } from './marketNameCache';
|
import { getKoreanName } from './marketNameCache';
|
||||||
import { repairUtf8Mojibake } from './textEncoding';
|
import { repairUtf8Mojibake } from './textEncoding';
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ export function buildBacktestReportModel(
|
|||||||
strategyName,
|
strategyName,
|
||||||
symbol: record.symbol,
|
symbol: record.symbol,
|
||||||
symbolKo: repairUtf8Mojibake(getKoreanName(market)),
|
symbolKo: repairUtf8Mojibake(getKoreanName(market)),
|
||||||
timeframe: record.timeframe,
|
timeframe: resolveBacktestRecordExecTimeframe(record),
|
||||||
barCount: record.barCount || 300,
|
barCount: record.barCount || 300,
|
||||||
createdAt: record.createdAt ? fmtListTimestamp(record.createdAt) : undefined,
|
createdAt: record.createdAt ? fmtListTimestamp(record.createdAt) : undefined,
|
||||||
reportKind: 'backtest',
|
reportKind: 'backtest',
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { Timeframe } from '../types';
|
||||||
|
import { formatTimeframeKo } from './backtestUiUtils';
|
||||||
|
|
||||||
|
/** 빠른 백테스팅 — 전략 DSL에 저장된 시간봉 사용 */
|
||||||
|
export const BACKTEST_STRATEGY_TIMEFRAME = '__strategy__' as const;
|
||||||
|
|
||||||
|
export type BacktestRunTimeframeChoice = Timeframe | typeof BACKTEST_STRATEGY_TIMEFRAME;
|
||||||
|
|
||||||
|
export const QUICK_RUN_TIMEFRAME_ITEMS: { value: Timeframe; label: string }[] = [
|
||||||
|
{ value: '1m', label: '1분' },
|
||||||
|
{ value: '3m', label: '3분' },
|
||||||
|
{ value: '5m', label: '5분' },
|
||||||
|
{ value: '10m', label: '10분' },
|
||||||
|
{ value: '15m', label: '15분' },
|
||||||
|
{ value: '30m', label: '30분' },
|
||||||
|
{ value: '1h', label: '1시간' },
|
||||||
|
{ value: '4h', label: '4시간' },
|
||||||
|
{ value: '1D', label: '일봉' },
|
||||||
|
{ value: '1W', label: '주봉' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isBacktestStrategyTimeframeChoice(
|
||||||
|
value: string,
|
||||||
|
): value is typeof BACKTEST_STRATEGY_TIMEFRAME {
|
||||||
|
return value === BACKTEST_STRATEGY_TIMEFRAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseBacktestRunTimeframeChoice(value: string): BacktestRunTimeframeChoice {
|
||||||
|
if (isBacktestStrategyTimeframeChoice(value)) return BACKTEST_STRATEGY_TIMEFRAME;
|
||||||
|
return value as Timeframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** UI 선택 → 실제 실행 시간봉 */
|
||||||
|
export function resolveBacktestExecTimeframe(
|
||||||
|
choice: BacktestRunTimeframeChoice,
|
||||||
|
strategyTimeframe: Timeframe,
|
||||||
|
): Timeframe {
|
||||||
|
if (choice === BACKTEST_STRATEGY_TIMEFRAME) return strategyTimeframe;
|
||||||
|
return choice;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildQuickRunTimeframeSelectOptions(strategyTimeframe?: Timeframe) {
|
||||||
|
const stratLabel = strategyTimeframe
|
||||||
|
? `전략 시간봉 (${formatTimeframeKo(strategyTimeframe)})`
|
||||||
|
: '전략 시간봉';
|
||||||
|
return [
|
||||||
|
{ value: BACKTEST_STRATEGY_TIMEFRAME, label: stratLabel },
|
||||||
|
...QUICK_RUN_TIMEFRAME_ITEMS,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ChartType, Timeframe } from '../types';
|
import type { ChartType, Timeframe } from '../types';
|
||||||
|
import type { BacktestResultRecord } from './backendApi';
|
||||||
import { formatUpbitKrwPrice } from './safeFormat';
|
import { formatUpbitKrwPrice } from './safeFormat';
|
||||||
|
|
||||||
export function toUpbitMarket(symbol: string): string {
|
export function toUpbitMarket(symbol: string): string {
|
||||||
@@ -19,12 +20,26 @@ const TIMEFRAME_KO: Record<Timeframe, string> = {
|
|||||||
'1M': '1월',
|
'1M': '1월',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 차트·목록용 시간봉 한글 라벨 */
|
/** 차트·목록용 한글 라벨 */
|
||||||
export function formatTimeframeKo(raw: string | undefined): string {
|
export function formatTimeframeKo(raw: string | undefined): string {
|
||||||
const tf = normalizeChartTimeframe(raw);
|
const tf = normalizeChartTimeframe(raw);
|
||||||
return TIMEFRAME_KO[tf] ?? tf;
|
return TIMEFRAME_KO[tf] ?? tf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 백테스트 결과 — 실제 실행 시간봉 (스냅샷 우선, 전략 DSL과 무관) */
|
||||||
|
export function resolveBacktestRecordExecTimeframe(record: BacktestResultRecord): string {
|
||||||
|
if (record.executionSnapshotJson) {
|
||||||
|
try {
|
||||||
|
const snap = JSON.parse(record.executionSnapshotJson) as { timeframe?: string };
|
||||||
|
const fromSnap = snap?.timeframe?.trim();
|
||||||
|
if (fromSnap) return fromSnap;
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
const fromCol = record.timeframe?.trim();
|
||||||
|
if (fromCol && fromCol !== 'unknown') return fromCol;
|
||||||
|
return '1m';
|
||||||
|
}
|
||||||
|
|
||||||
const CHART_TYPE_KO: Record<ChartType, string> = {
|
const CHART_TYPE_KO: Record<ChartType, string> = {
|
||||||
candlestick: '캔들스틱',
|
candlestick: '캔들스틱',
|
||||||
bar: '바',
|
bar: '바',
|
||||||
|
|||||||
Reference in New Issue
Block a user