/** * 매매 시그널 알림 표시용 포맷·라벨 */ import { formatUpbitKrwPrice } from './safeFormat'; import { formatUnixWithChartPattern, splitChartTimePattern } from './chartTimeFormat'; import { getKoreanName } from './marketNameCache'; import { getTradeAlertTimeFormat } from './tradeAlertTimeFormat'; import { getDisplayTimezone } from './timezone'; export interface TradeSignalDisplaySource { market: string; signalType: 'BUY' | 'SELL'; price: number; candleTime: number; strategyName?: string | null; strategyId?: number | null; executionType?: string; candleType?: string; receivedAt?: number; } export function parseMarket(market: string) { const parts = market.split('-'); const fiat = parts[0] ?? 'KRW'; const coin = parts.length > 1 ? parts.slice(1).join('-') : market; return { fiat, coin, code: market }; } export function formatSignalPrice(price: number): string { if (!Number.isFinite(price) || price <= 0) return '—'; return `₩${formatUpbitKrwPrice(price, '—')}`; } export function formatSignalTime(unixSec: number, withDate = true): string { if (!unixSec) return '—'; const tz = getDisplayTimezone(); const fmt = getTradeAlertTimeFormat(); if (!withDate) { const { time } = splitChartTimePattern(fmt); return formatUnixWithChartPattern(unixSec, time || 'HH:mm', tz); } return formatUnixWithChartPattern(unixSec, fmt, tz); } export function getSignalTypeKo(type: 'BUY' | 'SELL'): string { return type === 'BUY' ? '매수' : '매도'; } const CANDLE_TYPE_KO: Record = { '1m': '1분봉', '3m': '3분봉', '5m': '5분봉', '10m': '10분봉', '15m': '15분봉', '30m': '30분봉', '1h': '1시간봉', '4h': '4시간봉', '1w': '주간봉', '1d': '일봉', '1D': '일봉', }; /** 전략 체크·시그널 평가에 사용된 분봉 (한글) */ export function formatCandleTypeKo(candleType?: string | null): string { const raw = (candleType ?? '1m').trim(); if (!raw) return '1분봉'; const lower = raw.toLowerCase(); return CANDLE_TYPE_KO[raw] ?? CANDLE_TYPE_KO[lower] ?? raw; } export function getSignalHeadline(item: TradeSignalDisplaySource): string { const korean = getKoreanName(item.market); const { coin } = parseMarket(item.market); const name = korean || coin; return `${name} ${getSignalTypeKo(item.signalType)} 알림`; } export function getExecutionLabel(executionType?: string, candleType?: string): string { const candleKo = formatCandleTypeKo(candleType); if (executionType === 'REALTIME_TICK') return `실시간 틱 · ${candleKo}`; if (executionType === 'CANDLE_CLOSE') return `봉 마감 · ${candleKo}`; return candleKo; } export function getMarketDisplayLine(market: string): { primary: string; secondary: string } { const korean = getKoreanName(market); const { code } = parseMarket(market); return { primary: korean || code, secondary: code, }; } /** 상세 행 목록 (스낵바·목록 공통) */ export function buildSignalDetailRows(item: TradeSignalDisplaySource): Array<{ label: string; value: string; highlight?: boolean }> { const { primary, secondary } = getMarketDisplayLine(item.market); const rows: Array<{ label: string; value: string; highlight?: boolean }> = [ { label: '종목', value: primary !== secondary ? `${secondary} · ${primary}` : secondary }, { label: '매매 구분', value: getSignalTypeKo(item.signalType), highlight: true }, { label: '기준 가격', value: formatSignalPrice(item.price), highlight: true }, { label: '캔들 시각', value: `${formatSignalTime(item.candleTime)} · ${formatCandleTypeKo(item.candleType)}` }, ]; if (item.receivedAt) { rows.push({ label: '수신 시각', value: formatSignalTime(Math.floor(item.receivedAt / 1000)) }); } rows.push( { label: '전략', value: item.strategyName?.trim() || (item.strategyId != null ? `전략 #${item.strategyId}` : '실시간 전략') }, { label: '실행 방식', value: getExecutionLabel(item.executionType, item.candleType) }, ); return rows; }