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