주식 증권 api 연동 및 window pc AI 연동 기능 구현 시작
This commit is contained in:
182
stock-lab/app/kis_api.py
Normal file
182
stock-lab/app/kis_api.py
Normal file
@@ -0,0 +1,182 @@
|
||||
import os
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
class KisApi:
|
||||
"""한국투자증권 REST API 래퍼 (모의투자/실전투자 지원)"""
|
||||
|
||||
def __init__(self):
|
||||
self.app_key = os.getenv("KIS_APP_KEY", "")
|
||||
self.app_secret = os.getenv("KIS_APP_SECRET", "")
|
||||
self.account_no = os.getenv("KIS_ACCOUNT_NO", "") # 계좌번호 앞 8자리
|
||||
self.account_code = os.getenv("KIS_ACCOUNT_CODE", "01") # 계좌번호 뒤 2자리 (보통 01)
|
||||
|
||||
# 모의투자 여부 (환경변수가 "PROD"가 아니면 기본 모의투자)
|
||||
self.is_prod = os.getenv("KIS_MODE", "DEV") == "PROD"
|
||||
|
||||
if self.is_prod:
|
||||
self.base_url = "https://openapi.koreainvestment.com:9443"
|
||||
else:
|
||||
self.base_url = "https://openapivts.koreainvestment.com:29443"
|
||||
|
||||
self.access_token = None
|
||||
self.token_expired_at = None
|
||||
|
||||
def _get_headers(self, tr_id=None):
|
||||
"""공통 헤더 생성"""
|
||||
self._ensure_token()
|
||||
headers = {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"authorization": f"Bearer {self.access_token}",
|
||||
"appkey": self.app_key,
|
||||
"appsecret": self.app_secret,
|
||||
}
|
||||
if tr_id:
|
||||
headers["tr_id"] = tr_id
|
||||
return headers
|
||||
|
||||
def _ensure_token(self):
|
||||
"""토큰 유효성 검사 및 재발급"""
|
||||
now = time.time()
|
||||
# 토큰이 없거나 만료 1분 전이면 재발급
|
||||
if not self.access_token or not self.token_expired_at or now >= (self.token_expired_at - 60):
|
||||
self._issue_token()
|
||||
|
||||
def _issue_token(self):
|
||||
"""접근 토큰 발급"""
|
||||
url = f"{self.base_url}/oauth2/tokenP"
|
||||
payload = {
|
||||
"grant_type": "client_credentials",
|
||||
"appkey": self.app_key,
|
||||
"appsecret": self.app_secret
|
||||
}
|
||||
try:
|
||||
res = requests.post(url, json=payload, timeout=10)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
|
||||
self.access_token = data["access_token"]
|
||||
# 만료 시간 (보통 24시간인데 여유 있게 계산)
|
||||
expires_in = int(data.get("expires_in", 86400))
|
||||
self.token_expired_at = time.time() + expires_in
|
||||
print(f"[KisApi] Token issued. Expires in {expires_in} sec.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[KisApi] Token issue failed: {e}")
|
||||
raise
|
||||
|
||||
def get_balance(self):
|
||||
"""주식 잔고 조회"""
|
||||
url = f"{self.base_url}/uapi/domestic-stock/v1/trading/inquire-balance"
|
||||
|
||||
# 실전: TTTC8434R, 모의: VTTC8434R
|
||||
tr_id = "TTTC8434R" if self.is_prod else "VTTC8434R"
|
||||
|
||||
headers = self._get_headers(tr_id=tr_id)
|
||||
|
||||
params = {
|
||||
"CANO": self.account_no,
|
||||
"ACNT_PRDT_CD": self.account_code, #보통 01
|
||||
"AFHR_FLPR_YN": "N",
|
||||
"OFL_YN": "",
|
||||
"INQR_DVSN": "02",
|
||||
"UNPR_DVSN": "01",
|
||||
"FUND_STTL_ICLD_YN": "N",
|
||||
"FNCG_AMT_AUTO_RDPT_YN": "N",
|
||||
"PRCS_DVSN": "00", # 전일매매포함
|
||||
"CTX_AREA_FK100": "",
|
||||
"CTX_AREA_NK100": ""
|
||||
}
|
||||
|
||||
try:
|
||||
res = requests.get(url, headers=headers, params=params, timeout=10)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
|
||||
# API 응답 코드가 0이 아니면 에러
|
||||
if data["rt_cd"] != "0":
|
||||
return {"error": data["msg1"]}
|
||||
|
||||
output1 = data.get("output1", []) # 종목별 잔고
|
||||
output2 = data.get("output2", []) # 계좌 총 평가
|
||||
|
||||
holdings = []
|
||||
for item in output1:
|
||||
# 보유수량이 0인 것은 제외
|
||||
qty = int(item.get("hldg_qty", 0))
|
||||
if qty > 0:
|
||||
holdings.append({
|
||||
"code": item.get("pdno"),
|
||||
"name": item.get("prdt_name"),
|
||||
"qty": qty,
|
||||
"buy_price": float(item.get("pchs_avg_pric", 0)), # 매입단가
|
||||
"current_price": float(item.get("prpr", 0)), # 현재가
|
||||
"profit_rate": float(item.get("evlu_pfls_rt", 0)),# 수익률
|
||||
})
|
||||
|
||||
total_eval = 0
|
||||
deposit = 0
|
||||
if output2:
|
||||
total_eval = float(output2[0].get("tot_evlu_amt", 0)) # 총 평가금액
|
||||
deposit = float(output2[0].get("dnca_tot_amt", 0)) # 예수금
|
||||
|
||||
return {
|
||||
"holdings": holdings,
|
||||
"summary": {
|
||||
"total_eval": total_eval,
|
||||
"deposit": deposit
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def order(self, stock_code: str, qty: int, price: int, buy_sell: str):
|
||||
"""
|
||||
주문 실행
|
||||
buy_sell: 'buy' or 'sell'
|
||||
price: 0이면 시장가(추후 구현), 양수면 지정가
|
||||
"""
|
||||
url = f"{self.base_url}/uapi/domestic-stock/v1/trading/order-cash"
|
||||
|
||||
# 매수/매도 tr_id 구분
|
||||
# 실전: 매수 TTTC0802U, 매도 TTTC0801U
|
||||
# 모의: 매수 VTTC0802U, 매도 VTTC0801U
|
||||
if self.is_prod:
|
||||
tr_id = "TTTC0802U" if buy_sell == "buy" else "TTTC0801U"
|
||||
else:
|
||||
tr_id = "VTTC0802U" if buy_sell == "buy" else "VTTC0801U"
|
||||
|
||||
headers = self._get_headers(tr_id=tr_id)
|
||||
|
||||
# 가격이 0이면 시장가 "01", 아니면 지정가 "00"
|
||||
ord_dvsn = "01" if price == 0 else "00"
|
||||
|
||||
payload = {
|
||||
"CANO": self.account_no,
|
||||
"ACNT_PRDT_CD": self.account_code,
|
||||
"PDNO": stock_code, # 종목코드 (6자리)
|
||||
"ORD_DVSN": ord_dvsn, # 주문구분
|
||||
"ORD_QTY": str(qty), # 주문수량 (문자열)
|
||||
"ORD_UNPR": str(price), # 주문단가 (문자열)
|
||||
}
|
||||
|
||||
try:
|
||||
res = requests.post(url, headers=headers, json=payload, timeout=10)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
|
||||
if data["rt_cd"] != "0":
|
||||
return {"success": False, "message": data["msg1"]}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": data["msg1"],
|
||||
"order_no": data["output"]["ODNO"] # 주문번호
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
Reference in New Issue
Block a user