/** * @file lib/kis/dashboard-helpers.ts * @description 대시보드 계산/포맷 공통 헬퍼 모음 */ /** * @description 기준일 기준 N일 범위의 YYYYMMDD 기간을 반환합니다. * @see lib/kis/dashboard.ts getDomesticOrderHistory/getDomesticTradeJournal 조회 기간 계산 */ export function getLookbackRangeYmd(lookbackDays: number) { const end = new Date(); const start = new Date(end); start.setDate(end.getDate() - lookbackDays); return { startDate: formatYmd(start), endDate: formatYmd(end), }; } /** * @description Date를 YYYYMMDD 문자열로 변환합니다. * @see lib/kis/dashboard-helpers.ts getLookbackRangeYmd */ export function formatYmd(date: Date) { const year = String(date.getFullYear()); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}${month}${day}`; } /** * @description 문자열에서 숫자만 추출합니다. * @see lib/kis/dashboard.ts 주문일자/매매일자/시각 정규화 */ export function toDigits(value?: string) { return (value ?? "").replace(/\D/g, ""); } /** * @description 주문 시각을 HHMMSS로 정규화합니다. * @see lib/kis/dashboard.ts getDomesticOrderHistory 정렬/표시 시각 생성 */ export function normalizeTimeDigits(value?: string) { const digits = toDigits(value); if (!digits) return "000000"; return digits.padEnd(6, "0").slice(0, 6); } /** * @description YYYYMMDD를 YYYY-MM-DD로 변환합니다. * @see lib/kis/dashboard.ts 주문내역/매매일지 날짜 컬럼 표시 */ export function formatDateLabel(value: string) { if (value.length !== 8) return "-"; return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`; } /** * @description HHMMSS를 HH:MM:SS로 변환합니다. * @see lib/kis/dashboard.ts 주문내역 시각 컬럼 표시 */ export function formatTimeLabel(value: string) { if (value.length !== 6) return "-"; return `${value.slice(0, 2)}:${value.slice(2, 4)}:${value.slice(4, 6)}`; } /** * @description KIS 매수/매도 코드를 공통 side 값으로 변환합니다. * @see lib/kis/dashboard.ts getDomesticOrderHistory/getDomesticTradeJournal */ export function parseTradeSide( code?: string, name?: string, ): "buy" | "sell" | "unknown" { const normalizedCode = (code ?? "").trim(); const normalizedName = (name ?? "").trim(); if (normalizedCode === "01") return "sell"; if (normalizedCode === "02") return "buy"; if (normalizedName.includes("매도")) return "sell"; if (normalizedName.includes("매수")) return "buy"; return "unknown"; } /** * @description 매매일지 요약 기본값을 반환합니다. * @see lib/kis/dashboard.ts getDomesticDashboardActivity 매매일지 실패 폴백 */ export function createEmptyJournalSummary() { return { totalRealizedProfit: 0, totalRealizedRate: 0, totalBuyAmount: 0, totalSellAmount: 0, totalFee: 0, totalTax: 0, }; } /** * @description 문자열 숫자를 number로 변환합니다. * @see lib/kis/dashboard.ts 잔고/지수 필드 공통 파싱 */ export function toNumber(value?: string) { if (!value) return 0; const normalized = value.replaceAll(",", "").trim(); if (!normalized) return 0; const parsed = Number(normalized); return Number.isFinite(parsed) ? parsed : 0; } /** * @description 문자열 숫자를 number로 변환하되 0도 유효값으로 유지합니다. * @see lib/kis/dashboard.ts 요약값 폴백 순서 계산 */ export function toOptionalNumber(value?: string) { if (!value) return undefined; const normalized = value.replaceAll(",", "").trim(); if (!normalized) return undefined; const parsed = Number(normalized); return Number.isFinite(parsed) ? parsed : undefined; } /** * @description output 계열 데이터를 배열 형태로 변환합니다. * @see lib/kis/dashboard.ts 잔고 output1/output2 파싱 */ export function parseRows(value: unknown): T[] { if (Array.isArray(value)) return value as T[]; if (value && typeof value === "object") return [value as T]; return []; } /** * @description output 계열 데이터의 첫 행을 반환합니다. * @see lib/kis/dashboard.ts 잔고 요약(output2) 파싱 */ export function parseFirstRow(value: unknown) { const rows = parseRows(value); return rows[0]; } /** * @description 지수 output을 단일 레코드로 정규화합니다. * @see lib/kis/dashboard.ts getDomesticDashboardIndices */ export function parseIndexRow( output: unknown, ): T { if (Array.isArray(output) && output[0] && typeof output[0] === "object") { return output[0] as T; } if (output && typeof output === "object") { return output as T; } return {} as T; } /** * @description KIS 부호 코드(1/2 상승, 4/5 하락)를 실제 부호로 반영합니다. * @see lib/kis/dashboard.ts getDomesticDashboardIndices */ export function normalizeSignedValue(value: number, signCode?: string) { const abs = Math.abs(value); if (signCode === "4" || signCode === "5") return -abs; if (signCode === "1" || signCode === "2") return abs; return value; } /** * @description undefined가 아닌 첫 값을 반환합니다. * @see lib/kis/dashboard.ts 요약 수익률/손익 폴백 계산 */ export function firstDefinedNumber(...values: Array) { return values.find((value) => value !== undefined) ?? 0; } /** * @description 숫자 배열 합계를 계산합니다. * @see lib/kis/dashboard.ts 보유종목 합계 계산 */ export function sumNumbers(values: number[]) { return values.reduce((total, value) => total + value, 0); } /** * @description 총자산 대비 손익률을 계산합니다. * @see lib/kis/dashboard.ts 요약 수익률 폴백 계산 */ export function calcProfitRate(profit: number, totalAmount: number) { if (totalAmount <= 0) return 0; const baseAmount = totalAmount - profit; if (baseAmount <= 0) return 0; return (profit / baseAmount) * 100; } /** * @description 매입금액 대비 손익률을 계산합니다. * @see lib/kis/dashboard.ts getDomesticDashboardBalance 현재 손익률 산출 */ export function calcProfitRateByPurchase(profit: number, purchaseAmount: number) { if (purchaseAmount <= 0) return 0; return (profit / purchaseAmount) * 100; } /** * @description 총자산과 평가금액 기준으로 일관된 현금성 자산(예수금)을 계산합니다. * @remarks UI 흐름: 대시보드 API 응답 생성 -> 총자산/평가금/예수금 카드 반영 * @see lib/kis/dashboard.ts getDomesticDashboardBalance */ export function resolveCashBalance(params: { apiReportedTotalAmount: number; apiReportedNetAssetAmount: number; evaluationAmount: number; cashCandidates: Array; }) { const { apiReportedTotalAmount, apiReportedNetAssetAmount, evaluationAmount, cashCandidates, } = params; const referenceTotalAmount = pickPreferredAmount( apiReportedNetAssetAmount, apiReportedTotalAmount, ); const candidateCash = pickPreferredAmount(...cashCandidates); const derivedCash = referenceTotalAmount > 0 ? Math.max(referenceTotalAmount - evaluationAmount, 0) : undefined; if (derivedCash === undefined) return candidateCash; const recomposedWithCandidate = candidateCash + evaluationAmount; const mismatchWithApi = Math.abs( recomposedWithCandidate - referenceTotalAmount, ); if (mismatchWithApi >= 1) { return derivedCash; } return candidateCash; } /** * @description 금액 후보 중 양수 값을 우선 선택합니다. * @see lib/kis/dashboard.ts getDomesticDashboardBalance 금액 필드 폴백 계산 */ export function pickPreferredAmount(...values: Array) { const positive = values.find( (value): value is number => value !== undefined && value > 0, ); if (positive !== undefined) return positive; return firstDefinedNumber(...values); } /** * @description 숫자 후보 중 0이 아닌 값을 우선 선택합니다. * @see lib/kis/dashboard.ts getDomesticDashboardBalance 손익/수익률 폴백 계산 */ export function pickNonZeroNumber(...values: Array) { const nonZero = values.find( (value): value is number => value !== undefined && value !== 0, ); if (nonZero !== undefined) return nonZero; return firstDefinedNumber(...values); }