import { kisGet, kisPost } from "@/lib/kis/client"; import { KisCredentialInput } from "@/lib/kis/config"; import { DashboardOrderSide, DashboardOrderType, } from "@/features/trade/types/trade.types"; /** * @file lib/kis/trade.ts * @description KIS 주식 주문/잔고 관련 API */ export interface KisOrderCashOutput { KRX_FWDG_ORD_ORGNO?: string; // 한국거래소전송주문조직번호 ODNO?: string; // 주문번호 ORD_TMD?: string; // 주문시각 } interface KisOrderCashBody { CANO: string; // 종합계좌번호(8자리) ACNT_PRDT_CD: string; // 계좌상품코드(2자리) PDNO: string; // 종목코드 ORD_DVSN: string; // 주문구분(00:지정가, 01:시장가...) ORD_QTY: string; // 주문수량 ORD_UNPR: string; // 주문단가 } interface KisInquirePsblOrderOutput { ord_psbl_cash?: string; // 주문가능현금 nrcvb_buy_amt?: string; // 미수없는매수금액 max_buy_amt?: string; // 최대매수금액 nrcvb_buy_qty?: string; // 미수없는매수수량 max_buy_qty?: string; // 최대매수수량 } /** * 현금 주문(매수/매도) 실행 */ export async function executeOrderCash( params: { symbol: string; side: DashboardOrderSide; orderType: DashboardOrderType; quantity: number; price: number; accountNo: string; accountProductCode: string; }, credentials?: KisCredentialInput, ) { const trId = resolveOrderTrId(params.side, credentials?.tradingEnv); const ordDvsn = resolveOrderDivision(params.orderType); const body: KisOrderCashBody = { CANO: params.accountNo, ACNT_PRDT_CD: params.accountProductCode, PDNO: params.symbol, ORD_DVSN: ordDvsn, ORD_QTY: String(params.quantity), ORD_UNPR: String(params.price), }; const response = await kisPost( "/uapi/domestic-stock/v1/trading/order-cash", trId, body, credentials, ); return response.output ?? {}; } /** * 매수가능금액(주문가능현금) 조회 */ export async function executeInquireOrderableCash( params: { symbol: string; price: number; orderType: DashboardOrderType; accountNo: string; accountProductCode: string; }, credentials?: KisCredentialInput, ) { const trId = resolveInquireOrderableTrId(credentials?.tradingEnv); const ordDvsn = resolveOrderDivision(params.orderType); const ordUnpr = Math.max(1, Math.floor(params.price || 0)); const response = await kisGet( "/uapi/domestic-stock/v1/trading/inquire-psbl-order", trId, { CANO: params.accountNo, ACNT_PRDT_CD: params.accountProductCode, PDNO: params.symbol, ORD_UNPR: String(ordUnpr), ORD_DVSN: ordDvsn, CMA_EVLU_AMT_ICLD_YN: "N", OVRS_ICLD_YN: "N", }, credentials, ); const rawRow = Array.isArray(response.output) ? (response.output[0] ?? {}) : (response.output ?? {}); const orderableCash = pickFirstPositive( toSafeNumber(rawRow.nrcvb_buy_amt), toSafeNumber(rawRow.ord_psbl_cash), toSafeNumber(rawRow.max_buy_amt), ); return { orderableCash, noReceivableBuyAmount: toSafeNumber(rawRow.nrcvb_buy_amt), maxBuyAmount: toSafeNumber(rawRow.max_buy_amt), noReceivableBuyQuantity: Math.floor(toSafeNumber(rawRow.nrcvb_buy_qty)), maxBuyQuantity: Math.floor(toSafeNumber(rawRow.max_buy_qty)), }; } function resolveOrderTrId(side: DashboardOrderSide, env?: "real" | "mock") { const isMock = env === "mock"; if (side === "buy") { // 매수 return isMock ? "VTTC0802U" : "TTTC0802U"; } else { // 매도 return isMock ? "VTTC0801U" : "TTTC0801U"; } } function resolveInquireOrderableTrId(env?: "real" | "mock") { return env === "mock" ? "VTTC8908R" : "TTTC8908R"; } function resolveOrderDivision(type: DashboardOrderType) { // 00: 지정가, 01: 시장가 if (type === "market") return "01"; return "00"; } function toSafeNumber(value?: string) { if (!value) return 0; const parsed = Number(value.replaceAll(",", "").trim()); if (!Number.isFinite(parsed)) return 0; return parsed; } function pickFirstPositive(...values: number[]) { for (const value of values) { if (value > 0) return value; } return 0; }