스킬 정리 및 리팩토링
This commit is contained in:
@@ -8,6 +8,28 @@ import { kisGet } from "@/lib/kis/client";
|
||||
import type { KisCredentialInput } from "@/lib/kis/config";
|
||||
import { normalizeTradingEnv } from "@/lib/kis/config";
|
||||
import type { KisAccountParts } from "@/lib/kis/account";
|
||||
import {
|
||||
calcProfitRate,
|
||||
calcProfitRateByPurchase,
|
||||
createEmptyJournalSummary,
|
||||
firstDefinedNumber,
|
||||
formatDateLabel,
|
||||
formatTimeLabel,
|
||||
getLookbackRangeYmd,
|
||||
normalizeSignedValue,
|
||||
normalizeTimeDigits,
|
||||
parseFirstRow,
|
||||
parseIndexRow,
|
||||
parseRows,
|
||||
parseTradeSide,
|
||||
pickNonZeroNumber,
|
||||
pickPreferredAmount,
|
||||
resolveCashBalance,
|
||||
sumNumbers,
|
||||
toDigits,
|
||||
toNumber,
|
||||
toOptionalNumber,
|
||||
} from "@/lib/kis/dashboard-helpers";
|
||||
|
||||
interface KisBalanceOutput1Row {
|
||||
pdno?: string;
|
||||
@@ -478,7 +500,7 @@ export async function getDomesticDashboardIndices(
|
||||
credentials,
|
||||
);
|
||||
|
||||
const row = parseIndexRow(response.output);
|
||||
const row = parseIndexRow<KisIndexOutputRow>(response.output);
|
||||
const rawChange = toNumber(row.bstp_nmix_prdy_vrss);
|
||||
const rawChangeRate = toNumber(row.bstp_nmix_prdy_ctrt);
|
||||
|
||||
@@ -780,309 +802,3 @@ async function getDomesticTradeJournal(
|
||||
summary,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 기준일 기준 N일 범위의 YYYYMMDD 기간을 반환합니다.
|
||||
* @param lookbackDays 과거 조회 일수
|
||||
* @returns 시작/종료 일자
|
||||
* @see lib/kis/dashboard.ts getDomesticOrderHistory/getDomesticTradeJournal 조회 기간 계산
|
||||
*/
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Date를 YYYYMMDD 문자열로 변환합니다.
|
||||
* @param date 기준 일자
|
||||
* @returns YYYYMMDD
|
||||
* @see lib/kis/dashboard.ts getLookbackRangeYmd
|
||||
*/
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열에서 숫자만 추출합니다.
|
||||
* @param value 원본 문자열
|
||||
* @returns 숫자 문자열
|
||||
* @see lib/kis/dashboard.ts 주문일자/매매일자/시각 정규화
|
||||
*/
|
||||
function toDigits(value?: string) {
|
||||
return (value ?? "").replace(/\D/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 주문 시각을 HHMMSS로 정규화합니다.
|
||||
* @param value 시각 문자열
|
||||
* @returns 6자리 시각 문자열
|
||||
* @see lib/kis/dashboard.ts getDomesticOrderHistory 정렬/표시 시각 생성
|
||||
*/
|
||||
function normalizeTimeDigits(value?: string) {
|
||||
const digits = toDigits(value);
|
||||
if (!digits) return "000000";
|
||||
return digits.padEnd(6, "0").slice(0, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* YYYYMMDD를 YYYY-MM-DD로 변환합니다.
|
||||
* @param value 날짜 문자열
|
||||
* @returns YYYY-MM-DD 또는 "-"
|
||||
* @see lib/kis/dashboard.ts 주문내역/매매일지 날짜 컬럼 표시
|
||||
*/
|
||||
function formatDateLabel(value: string) {
|
||||
if (value.length !== 8) return "-";
|
||||
return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* HHMMSS를 HH:MM:SS로 변환합니다.
|
||||
* @param value 시각 문자열
|
||||
* @returns HH:MM:SS 또는 "-"
|
||||
* @see lib/kis/dashboard.ts 주문내역 시각 컬럼 표시
|
||||
*/
|
||||
function formatTimeLabel(value: string) {
|
||||
if (value.length !== 6) return "-";
|
||||
return `${value.slice(0, 2)}:${value.slice(2, 4)}:${value.slice(4, 6)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* KIS 매수/매도 코드를 공통 side 값으로 변환합니다.
|
||||
* @param code 매수매도구분코드
|
||||
* @param name 매수매도구분명 또는 매매구분명
|
||||
* @returns buy/sell/unknown
|
||||
* @see lib/kis/dashboard.ts getDomesticOrderHistory/getDomesticTradeJournal
|
||||
*/
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* 매매일지 요약 기본값을 반환합니다.
|
||||
* @returns 0으로 채운 요약 객체
|
||||
* @see lib/kis/dashboard.ts getDomesticDashboardActivity 매매일지 실패 폴백
|
||||
*/
|
||||
function createEmptyJournalSummary(): DomesticTradeJournalSummary {
|
||||
return {
|
||||
totalRealizedProfit: 0,
|
||||
totalRealizedRate: 0,
|
||||
totalBuyAmount: 0,
|
||||
totalSellAmount: 0,
|
||||
totalFee: 0,
|
||||
totalTax: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 숫자를 number로 변환합니다.
|
||||
* @param value KIS 숫자 문자열
|
||||
* @returns 파싱된 숫자(실패 시 0)
|
||||
* @see lib/kis/dashboard.ts 잔고/지수 필드 공통 파싱
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 숫자를 number로 변환하되 0도 유효값으로 유지합니다.
|
||||
* @param value KIS 숫자 문자열
|
||||
* @returns 파싱된 숫자 또는 undefined
|
||||
* @see lib/kis/dashboard.ts 요약값 폴백 순서 계산
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* output 계열 데이터를 배열 형태로 변환합니다.
|
||||
* @param value KIS output 값
|
||||
* @returns 레코드 배열
|
||||
* @see lib/kis/dashboard.ts 잔고 output1/output2 파싱
|
||||
*/
|
||||
function parseRows<T>(value: unknown): T[] {
|
||||
if (Array.isArray(value)) return value as T[];
|
||||
if (value && typeof value === "object") return [value as T];
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* output 계열 데이터의 첫 행을 반환합니다.
|
||||
* @param value KIS output 값
|
||||
* @returns 첫 번째 레코드
|
||||
* @see lib/kis/dashboard.ts 잔고 요약(output2) 파싱
|
||||
*/
|
||||
function parseFirstRow<T>(value: unknown) {
|
||||
const rows = parseRows<T>(value);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 지수 output을 단일 레코드로 정규화합니다.
|
||||
* @param output KIS output
|
||||
* @returns 지수 레코드
|
||||
* @see lib/kis/dashboard.ts getDomesticDashboardIndices
|
||||
*/
|
||||
function parseIndexRow(output: unknown): KisIndexOutputRow {
|
||||
if (Array.isArray(output) && output[0] && typeof output[0] === "object") {
|
||||
return output[0] as KisIndexOutputRow;
|
||||
}
|
||||
if (output && typeof output === "object") {
|
||||
return output as KisIndexOutputRow;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* KIS 부호 코드(1/2 상승, 4/5 하락)를 실제 부호로 반영합니다.
|
||||
* @param value 변동값
|
||||
* @param signCode 부호 코드
|
||||
* @returns 부호 적용 숫자
|
||||
* @see lib/kis/dashboard.ts getDomesticDashboardIndices
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* undefined가 아닌 첫 값을 반환합니다.
|
||||
* @param values 후보 숫자 목록
|
||||
* @returns 첫 번째 유효값, 없으면 0
|
||||
* @see lib/kis/dashboard.ts 요약 수익률/손익 폴백 계산
|
||||
*/
|
||||
function firstDefinedNumber(...values: Array<number | undefined>) {
|
||||
return values.find((value) => value !== undefined) ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자 배열 합계를 계산합니다.
|
||||
* @param values 숫자 배열
|
||||
* @returns 합계
|
||||
* @see lib/kis/dashboard.ts 보유종목 합계 계산
|
||||
*/
|
||||
function sumNumbers(values: number[]) {
|
||||
return values.reduce((total, value) => total + value, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 총자산 대비 손익률을 계산합니다.
|
||||
* @param profit 손익 금액
|
||||
* @param totalAmount 총자산 금액
|
||||
* @returns 손익률(%)
|
||||
* @see lib/kis/dashboard.ts 요약 수익률 폴백 계산
|
||||
*/
|
||||
function calcProfitRate(profit: number, totalAmount: number) {
|
||||
if (totalAmount <= 0) return 0;
|
||||
const baseAmount = totalAmount - profit;
|
||||
if (baseAmount <= 0) return 0;
|
||||
return (profit / baseAmount) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* 매입금액 대비 손익률을 계산합니다.
|
||||
* @param profit 손익 금액
|
||||
* @param purchaseAmount 매입금액
|
||||
* @returns 손익률(%)
|
||||
* @see lib/kis/dashboard.ts getDomesticDashboardBalance 현재 손익률 산출
|
||||
*/
|
||||
function calcProfitRateByPurchase(profit: number, purchaseAmount: number) {
|
||||
if (purchaseAmount <= 0) return 0;
|
||||
return (profit / purchaseAmount) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* 총자산과 평가금액 기준으로 일관된 현금성 자산(예수금)을 계산합니다.
|
||||
* @param params 계산 파라미터
|
||||
* @returns 현금성 자산 금액
|
||||
* @remarks UI 흐름: 대시보드 API 응답 생성 -> 총자산/평가금/예수금 카드 반영
|
||||
* @see lib/kis/dashboard.ts getDomesticDashboardBalance
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 금액 후보 중 양수 값을 우선 선택합니다.
|
||||
* @param values 금액 후보
|
||||
* @returns 양수 우선 금액
|
||||
* @see lib/kis/dashboard.ts getDomesticDashboardBalance 금액 필드 폴백 계산
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자 후보 중 0이 아닌 값을 우선 선택합니다.
|
||||
* @param values 숫자 후보
|
||||
* @returns 0이 아닌 값 우선 결과
|
||||
* @see lib/kis/dashboard.ts getDomesticDashboardBalance 손익/수익률 폴백 계산
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user