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

159 lines
4.2 KiB
TypeScript
Raw Permalink Normal View History

2026-03-12 09:26:27 +09:00
import { kisGet, kisPost } from "@/lib/kis/client";
2026-02-10 11:16:39 +09:00
import { KisCredentialInput } from "@/lib/kis/config";
import {
DashboardOrderSide,
DashboardOrderType,
2026-02-11 16:31:28 +09:00
} from "@/features/trade/types/trade.types";
2026-02-10 11:16:39 +09:00
/**
* @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; // 주문단가
}
2026-03-12 09:26:27 +09:00
interface KisInquirePsblOrderOutput {
ord_psbl_cash?: string; // 주문가능현금
nrcvb_buy_amt?: string; // 미수없는매수금액
max_buy_amt?: string; // 최대매수금액
nrcvb_buy_qty?: string; // 미수없는매수수량
max_buy_qty?: string; // 최대매수수량
}
2026-02-10 11:16:39 +09:00
/**
* (/)
*/
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<KisOrderCashOutput>(
"/uapi/domestic-stock/v1/trading/order-cash",
trId,
body,
credentials,
);
return response.output ?? {};
}
2026-03-12 09:26:27 +09:00
/**
* ()
*/
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<KisInquirePsblOrderOutput | KisInquirePsblOrderOutput[]>(
"/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)),
};
}
2026-02-10 11:16:39 +09:00
function resolveOrderTrId(side: DashboardOrderSide, env?: "real" | "mock") {
const isMock = env === "mock";
if (side === "buy") {
// 매수
return isMock ? "VTTC0802U" : "TTTC0802U";
} else {
// 매도
return isMock ? "VTTC0801U" : "TTTC0801U";
}
}
2026-03-12 09:26:27 +09:00
function resolveInquireOrderableTrId(env?: "real" | "mock") {
return env === "mock" ? "VTTC8908R" : "TTTC8908R";
}
2026-02-10 11:16:39 +09:00
function resolveOrderDivision(type: DashboardOrderType) {
// 00: 지정가, 01: 시장가
if (type === "market") return "01";
return "00";
}
2026-03-12 09:26:27 +09: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;
}