Files
auto-trade/lib/kis/dashboard-helpers.ts

270 lines
8.3 KiB
TypeScript

/**
* @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<T>(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<T>(value: unknown) {
const rows = parseRows<T>(value);
return rows[0];
}
/**
* @description 지수 output을 단일 레코드로 정규화합니다.
* @see lib/kis/dashboard.ts getDomesticDashboardIndices
*/
export function parseIndexRow<T extends object>(
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<number | undefined>) {
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<number | undefined>;
}) {
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<number | undefined>) {
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<number | undefined>) {
const nonZero = values.find(
(value): value is number => value !== undefined && value !== 0,
);
if (nonZero !== undefined) return nonZero;
return firstDefinedNumber(...values);
}