Files
goldenChart/frontend/src/utils/backtestUiUtils.ts
T
2026-06-12 01:41:34 +09:00

129 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ChartType, Timeframe } from '../types';
import type { BacktestResultRecord } from './backendApi';
import { formatUpbitKrwPrice } from './safeFormat';
import { INDICATOR_WARMUP } from './upbitApi';
export function toUpbitMarket(symbol: string): string {
return symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
}
const TIMEFRAME_KO: Record<Timeframe, string> = {
'1m': '1분',
'3m': '3분',
'5m': '5분',
'10m': '10분',
'15m': '15분',
'30m': '30분',
'1h': '1시간',
'4h': '4시간',
'1D': '1일',
'1W': '1주',
'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';
}
/** 백테스트 결과 차트 로드 봉 수 — 평가 구간 + 지표 워밍업 (차트 SMA와 백테스트 엔진 정렬) */
export function resolveBacktestChartBarCount(record: BacktestResultRecord): number {
const evalCount = record.barCount > 0 ? record.barCount : 200;
let warmup = INDICATOR_WARMUP;
if (record.executionSnapshotJson) {
try {
const snap = JSON.parse(record.executionSnapshotJson) as { warmupBarCount?: number };
if (typeof snap.warmupBarCount === 'number' && snap.warmupBarCount > 0) {
warmup = snap.warmupBarCount;
}
} catch { /* ignore */ }
}
return evalCount + warmup;
}
const CHART_TYPE_KO: Record<ChartType, string> = {
candlestick: '캔들스틱',
bar: '바',
line: '라인',
area: '영역',
baseline: '기준선',
};
export function formatChartTypeKo(type: ChartType | string | undefined): string {
if (!type) return '캔들스틱';
return CHART_TYPE_KO[type as ChartType] ?? String(type);
}
export function normalizeChartTimeframe(raw: string | undefined): Timeframe {
const t = (raw ?? '1h').trim();
const lower = t.toLowerCase();
if (lower === '1d') return '1D';
if (lower === '1w') return '1W';
if (lower === '1m' && t === '1M') return '1M';
if (['1m', '3m', '5m', '10m', '15m', '30m', '1h', '4h'].includes(lower)) return lower as Timeframe;
if (t === '1D' || t === '1W' || t === '1M') return t as Timeframe;
return '1h';
}
/** Unix epoch → 초 단위 정규화 (유효하지 않으면 0) */
export function normalizeEpochSecOptional(t: number | undefined | null): number {
if (t == null || !Number.isFinite(t) || t <= 0) return 0;
return t > 1e12 ? Math.floor(t / 1000) : Math.floor(t);
}
/** Unix epoch → 초 단위 정규화 (유효하지 않으면 현재 시각) */
export function normalizeEpochSec(t: number | undefined | null): number {
const v = normalizeEpochSecOptional(t);
return v > 0 ? v : Math.floor(Date.now() / 1000);
}
export function fmtListTimestamp(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
export function fmtShortTimestamp(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
const yy = String(d.getFullYear()).slice(2);
return `${yy}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
export const pct = (v: number, dec = 1) =>
isFinite(v) && v !== 0 ? `${v >= 0 ? '+' : ''}${(v * 100).toFixed(dec)}%` : '0.0%';
export const pctAbs = (v: number, dec = 0) =>
isFinite(v) ? `${(v * 100).toFixed(dec)}%` : '0%';
export const krw = (v: number) => {
if (!isFinite(v)) return '';
const sign = v >= 0 ? '+' : '-';
return `${sign}${formatUpbitKrwPrice(Math.abs(v), '0')}`;
};
const TF_SEC: Record<string, number> = {
'1m': 60, '3m': 180, '5m': 300, '10m': 600, '15m': 900, '30m': 1800,
'1h': 3600, '4h': 14400, '1D': 86400, '1W': 604800, '1M': 2592000,
};
export function timeframeBarSeconds(tf: string): number {
const n = normalizeChartTimeframe(tf);
return TF_SEC[n] ?? 3600;
}