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 = { '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 = { 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 = { '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; }