first commit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.auth import token_manager
|
||||
from app.core.config import get_base_url, settings
|
||||
from app.core.logger import setup_logger
|
||||
from app.core.rate_limiter import RateLimiter
|
||||
|
||||
logger = setup_logger("account")
|
||||
|
||||
_BALANCE_URL = "/uapi/domestic-stock/v1/trading/inquire-balance"
|
||||
_ORDERABLE_URL = "/uapi/domestic-stock/v1/trading/inquire-psamount"
|
||||
|
||||
_BALANCE_TR_IDS = {"real": "TTTC0311R", "vps": "VTTC0311R"}
|
||||
|
||||
|
||||
class AccountService:
|
||||
def __init__(self) -> None:
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second)
|
||||
|
||||
async def get_balance(self) -> dict:
|
||||
await self._rate_limiter.acquire()
|
||||
token = await token_manager.get_access_token()
|
||||
tr_id = _BALANCE_TR_IDS.get(settings.kis.server_mode, "VTTC0311R")
|
||||
headers = token_manager.get_auth_headers(tr_id)
|
||||
|
||||
params = {
|
||||
"CANO": settings.kis.account_no,
|
||||
"ACNT_PRDT_CD": settings.kis.account_code,
|
||||
"AFHR_FLPR_YN": "N",
|
||||
"OFLN_YN": "N",
|
||||
"INQR_DVSN": "02",
|
||||
"UNPR_DVSN": "01",
|
||||
"FUND_STTL_ICLD_YN": "N",
|
||||
"CNCD_UNCLD_YN": "N",
|
||||
}
|
||||
url = f"{get_base_url()}{_BALANCE_URL}"
|
||||
|
||||
resp = await self._client.get(url, headers=headers, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if data.get("rt_cd") != "0":
|
||||
logger.warning("잔고 조회 실패: %s", data.get("msg1"))
|
||||
return {"deposits": [], "stocks": []}
|
||||
|
||||
stocks = []
|
||||
for item in data.get("output1", []):
|
||||
stocks.append({
|
||||
"stock_code": item.get("pdno", ""),
|
||||
"stock_name": item.get("hsts_km_name", ""),
|
||||
"qty": int(item.get("hldg_qty", 0)),
|
||||
"avg_price": float(item.get("pchs_avg_pric", 0)),
|
||||
"current_price": int(item.get("prpr", 0)),
|
||||
"profit": float(item.get("evlu_pfls_amt", 0)),
|
||||
"profit_rate": float(item.get("pfls_rt", 0)),
|
||||
"buy_amount": float(item.get("pchs_amt", 0)),
|
||||
"eval_amount": float(item.get("evlu_amt", 0)),
|
||||
})
|
||||
|
||||
deposits = []
|
||||
for item in data.get("output2", []):
|
||||
deposits.append({
|
||||
"currency": item.get("crcy_cd", "KRW"),
|
||||
"amount": float(item.get("nmbdy_now_amt", 0)),
|
||||
"orderable": float(item.get("ord_psbl_amt", 0)),
|
||||
})
|
||||
|
||||
return {"deposits": deposits, "stocks": stocks}
|
||||
|
||||
async def get_orderable_amount(self, stock_code: str, price: int) -> dict:
|
||||
await self._rate_limiter.acquire()
|
||||
token = await token_manager.get_access_token()
|
||||
headers = token_manager.get_auth_headers("VTTC0830R" if settings.kis.server_mode == "vps" else "TTTC0830R")
|
||||
|
||||
params = {
|
||||
"CANO": settings.kis.account_no,
|
||||
"ACNT_PRDT_CD": settings.kis.account_code,
|
||||
"PDNO": stock_code,
|
||||
"ORD_UNPR": str(price),
|
||||
"ORD_DVSN": "00",
|
||||
}
|
||||
url = f"{get_base_url()}{_ORDERABLE_URL}"
|
||||
|
||||
resp = await self._client.get(url, headers=headers, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if data.get("rt_cd") != "0":
|
||||
return {"orderable_amount": 0, "orderable_qty": 0}
|
||||
|
||||
output = data.get("output", {})
|
||||
return {
|
||||
"orderable_amount": float(output.get("psmps_numb", 0)),
|
||||
"orderable_qty": int(output.get("ord_psbl_qty", 0)),
|
||||
"max_buy_amount": float(output.get("max_buy_psbl_amt", 0)),
|
||||
}
|
||||
|
||||
async def close(self) -> None:
|
||||
await self._client.aclose()
|
||||
|
||||
|
||||
account_service = AccountService()
|
||||
@@ -0,0 +1,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.auth import token_manager
|
||||
from app.core.config import get_base_url, settings
|
||||
from app.core.logger import setup_logger
|
||||
from app.core.rate_limiter import RateLimiter
|
||||
|
||||
logger = setup_logger("market_data")
|
||||
|
||||
_PRICE_URL = "/uapi/domestic-stock/v1/quotations/inquire-price"
|
||||
_CHART_URL = "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"
|
||||
_ORDERBOOK_URL = "/uapi/domestic-stock/v1/quotations/inquire-asking-price"
|
||||
|
||||
_FID_INPUT_ISCD = "FID_INPUT_ISCD"
|
||||
_FID_COND_MRKT_DIV_CODE = "FID_COND_MRKT_DIV_CODE"
|
||||
|
||||
|
||||
class MarketDataService:
|
||||
def __init__(self) -> None:
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second)
|
||||
|
||||
async def get_current_price(self, stock_code: str) -> dict:
|
||||
await self._rate_limiter.acquire()
|
||||
token = await token_manager.get_access_token()
|
||||
headers = token_manager.get_auth_headers("FHKST01010100")
|
||||
params = {_FID_COND_MRKT_DIV_CODE: "J", _FID_INPUT_ISCD: stock_code}
|
||||
url = f"{get_base_url()}{_PRICE_URL}"
|
||||
|
||||
resp = await self._client.get(url, headers=headers, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if data.get("rt_cd") != "0":
|
||||
logger.warning("현재가 조회 실패: %s - %s", stock_code, data.get("msg1"))
|
||||
return {}
|
||||
|
||||
output = data.get("output", {})
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"current_price": int(output.get("stck_prpr", 0)),
|
||||
"change_price": int(output.get("prdy_vrss", 0)),
|
||||
"change_rate": float(output.get("prdy_ctrt", 0)),
|
||||
"open_price": int(output.get("stck_oprc", 0)),
|
||||
"high_price": int(output.get("stck_hgpr", 0)),
|
||||
"low_price": int(output.get("stck_lwpr", 0)),
|
||||
"volume": int(output.get("acml_vol", 0)),
|
||||
"trade_amount": int(output.get("acml_tr_pbmn", 0)),
|
||||
"stock_name": output.get("hts_kor_isnm", ""),
|
||||
}
|
||||
|
||||
async def get_daily_chart(
|
||||
self, stock_code: str, start_date: str, end_date: str, count: int = 30
|
||||
) -> list[dict]:
|
||||
await self._rate_limiter.acquire()
|
||||
token = await token_manager.get_access_token()
|
||||
headers = token_manager.get_auth_headers("FHKST03010200")
|
||||
params = {
|
||||
_FID_COND_MRKT_DIV_CODE: "J",
|
||||
_FID_INPUT_ISCD: stock_code,
|
||||
"FID_INPUT_DATE_1": start_date,
|
||||
"FID_INPUT_DATE_2": end_date,
|
||||
"FID_PERIOD_DIV_CODE": "D",
|
||||
"FID_ADJ_PRC": "1",
|
||||
}
|
||||
url = f"{get_base_url()}{_CHART_URL}"
|
||||
|
||||
resp = await self._client.get(url, headers=headers, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if data.get("rt_cd") != "0":
|
||||
logger.warning("차트 조회 실패: %s - %s", stock_code, data.get("msg1"))
|
||||
return []
|
||||
|
||||
result = []
|
||||
for item in data.get("output2", [])[:count]:
|
||||
result.append({
|
||||
"date": item.get("stck_bsop_date", ""),
|
||||
"open": int(item.get("stck_oprc", 0)),
|
||||
"high": int(item.get("stck_hgpr", 0)),
|
||||
"low": int(item.get("stck_lwpr", 0)),
|
||||
"close": int(item.get("stck_clpr", 0)),
|
||||
"volume": int(item.get("acml_vol", 0)),
|
||||
})
|
||||
return result
|
||||
|
||||
async def get_orderbook(self, stock_code: str) -> dict:
|
||||
await self._rate_limiter.acquire()
|
||||
token = await token_manager.get_access_token()
|
||||
headers = token_manager.get_auth_headers("FHKST01010200")
|
||||
params = {_FID_COND_MRKT_DIV_CODE: "J", _FID_INPUT_ISCD: stock_code}
|
||||
url = f"{get_base_url()}{_ORDERBOOK_URL}"
|
||||
|
||||
resp = await self._client.get(url, headers=headers, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if data.get("rt_cd") != "0":
|
||||
return {}
|
||||
|
||||
output = data.get("output", [{}])[0] if data.get("output") else {}
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"bid_prices": [
|
||||
int(output.get(f"phsc_kprc_{i}", 0)) for i in range(1, 6)
|
||||
],
|
||||
"ask_prices": [
|
||||
int(output.get(f"sats_kprc_{i}", 0)) for i in range(1, 6)
|
||||
],
|
||||
"bid_volumes": [
|
||||
int(output.get(f"phsc_vola_{i}", 0)) for i in range(1, 6)
|
||||
],
|
||||
"ask_volumes": [
|
||||
int(output.get(f"sats_ac_vola_{i}", 0)) for i in range(1, 6)
|
||||
],
|
||||
}
|
||||
|
||||
async def close(self) -> None:
|
||||
await self._client.aclose()
|
||||
|
||||
|
||||
market_data_service = MarketDataService()
|
||||
@@ -0,0 +1,198 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Callable
|
||||
|
||||
import websockets
|
||||
|
||||
from app.core.auth import token_manager
|
||||
from app.core.config import get_ws_url, settings
|
||||
from app.core.logger import setup_logger
|
||||
from app.core.rate_limiter import RateLimiter
|
||||
|
||||
logger = setup_logger("realtime")
|
||||
|
||||
_SUBSCRIBE_TR_IDS = {
|
||||
"price": "H0STCNT0",
|
||||
"orderbook": "H0STASP0",
|
||||
"trade": "H0STCNI0",
|
||||
}
|
||||
|
||||
|
||||
class RealtimeService:
|
||||
def __init__(self) -> None:
|
||||
self._ws: websockets.WebSocketClientProtocol | None = None
|
||||
self._running = False
|
||||
self._callbacks: dict[str, list[Callable]] = {}
|
||||
self._subscriptions: dict[str, list[str]] = {}
|
||||
self._rate_limiter = RateLimiter(requests_per_second=0.5)
|
||||
self._heartbeat_task: asyncio.Task | None = None
|
||||
self._receive_task: asyncio.Task | None = None
|
||||
|
||||
def on(self, event: str, callback: Callable) -> None:
|
||||
self._callbacks.setdefault(event, []).append(callback)
|
||||
|
||||
async def connect(self) -> None:
|
||||
approval_key = await token_manager.get_approval_key()
|
||||
url = get_ws_url()
|
||||
|
||||
try:
|
||||
self._ws = await websockets.connect(
|
||||
url,
|
||||
extra_headers={"approval_key": approval_key, "type": "Y"},
|
||||
)
|
||||
self._running = True
|
||||
self._receive_task = asyncio.create_task(self._receive_loop())
|
||||
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
||||
logger.info("WebSocket 연결 성공: %s", url)
|
||||
except Exception as e:
|
||||
logger.error("WebSocket 연결 실패: %s", e)
|
||||
raise
|
||||
|
||||
async def subscribe(self, stock_code: str, data_type: str = "price") -> None:
|
||||
if not self._ws:
|
||||
await self.connect()
|
||||
|
||||
tr_id = _SUBSCRIBE_TR_IDS.get(data_type, "H0STCNT0")
|
||||
msg = {
|
||||
"header": {
|
||||
"approval_key": await token_manager.get_approval_key(),
|
||||
"custtype": "P",
|
||||
"tr_type": "1",
|
||||
"content-type": "utf-8",
|
||||
},
|
||||
"body": {
|
||||
"input": {
|
||||
"tr_id": tr_id,
|
||||
"tr_key": stock_code,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
await self._ws.send(json.dumps(msg))
|
||||
self._subscriptions.setdefault(data_type, []).append(stock_code)
|
||||
logger.info("구독 요청: %s - %s", data_type, stock_code)
|
||||
|
||||
async def unsubscribe(self, stock_code: str, data_type: str = "price") -> None:
|
||||
if not self._ws:
|
||||
return
|
||||
|
||||
tr_id = _SUBSCRIBE_TR_IDS.get(data_type, "H0STCNT0")
|
||||
msg = {
|
||||
"header": {
|
||||
"approval_key": await token_manager.get_approval_key(),
|
||||
"custtype": "P",
|
||||
"tr_type": "2",
|
||||
"content-type": "utf-8",
|
||||
},
|
||||
"body": {
|
||||
"input": {
|
||||
"tr_id": tr_id,
|
||||
"tr_key": stock_code,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
await self._ws.send(json.dumps(msg))
|
||||
subs = self._subscriptions.get(data_type, [])
|
||||
if stock_code in subs:
|
||||
subs.remove(stock_code)
|
||||
logger.info("구독 해제: %s - %s", data_type, stock_code)
|
||||
|
||||
async def _receive_loop(self) -> None:
|
||||
try:
|
||||
async for raw_msg in self._ws:
|
||||
await self._handle_message(raw_msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logger.warning("WebSocket 연결 끊김. 5초 후 재연결...")
|
||||
if self._running:
|
||||
await asyncio.sleep(5)
|
||||
await self.connect()
|
||||
except Exception as e:
|
||||
logger.error("WebSocket 수신 오류: %s", e)
|
||||
|
||||
async def _handle_message(self, raw_msg: str) -> None:
|
||||
try:
|
||||
msg = json.loads(raw_msg)
|
||||
header = msg.get("header", {})
|
||||
body = msg.get("body", {})
|
||||
|
||||
tr_id = header.get("tr_id", "")
|
||||
|
||||
if tr_id == "PINGPONG":
|
||||
return
|
||||
|
||||
parsed = self._parse_data(tr_id, body.get("output", {}))
|
||||
if not parsed:
|
||||
return
|
||||
|
||||
event_type = "price"
|
||||
if "bid_prices" in parsed:
|
||||
event_type = "orderbook"
|
||||
elif "trade_price" in parsed:
|
||||
event_type = "trade"
|
||||
|
||||
for callback in self._callbacks.get(event_type, []):
|
||||
try:
|
||||
result = callback(parsed)
|
||||
if asyncio.iscoroutine(result):
|
||||
await result
|
||||
except Exception as e:
|
||||
logger.error("콜백 실행 오류: %s", e)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("JSON 파싱 실패")
|
||||
|
||||
def _parse_data(self, tr_id: str, output: dict) -> dict | None:
|
||||
if not output:
|
||||
return None
|
||||
|
||||
if tr_id == "H0STCNT0":
|
||||
return {
|
||||
"type": "price",
|
||||
"stock_code": output.get("mksc_shrn_iscd", ""),
|
||||
"trade_price": int(output.get("stck_prpr", 0)),
|
||||
"change_price": int(output.get("prdy_vrss", 0)),
|
||||
"change_rate": float(output.get("prdy_ctrt", 0)),
|
||||
"open_price": int(output.get("stck_oprc", 0)),
|
||||
"high_price": int(output.get("stck_hgpr", 0)),
|
||||
"low_price": int(output.get("stck_lwpr", 0)),
|
||||
"volume": int(output.get("acml_vol", 0)),
|
||||
"trade_time": output.get("stck_cntg_hour", ""),
|
||||
}
|
||||
|
||||
if tr_id == "H0STASP0":
|
||||
return {
|
||||
"type": "orderbook",
|
||||
"stock_code": output.get("mksc_shrn_iscd", ""),
|
||||
"bid_prices": [int(output.get(f"phsc_kprc_{i}", 0)) for i in range(1, 6)],
|
||||
"ask_prices": [int(output.get(f"sats_kprc_{i}", 0)) for i in range(1, 6)],
|
||||
"bid_volumes": [int(output.get(f"phsc_vola_{i}", 0)) for i in range(1, 6)],
|
||||
"ask_volumes": [int(output.get(f"sats_ac_vola_{i}", 0)) for i in range(1, 6)],
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
async def _heartbeat_loop(self) -> None:
|
||||
while self._running:
|
||||
try:
|
||||
await asyncio.sleep(30)
|
||||
if self._ws and self._ws.open:
|
||||
await self._ws.send(json.dumps({"header": {"tr_id": "PINGPONG"}}))
|
||||
except Exception:
|
||||
break
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
self._running = False
|
||||
if self._heartbeat_task:
|
||||
self._heartbeat_task.cancel()
|
||||
if self._receive_task:
|
||||
self._receive_task.cancel()
|
||||
if self._ws:
|
||||
await self._ws.close()
|
||||
self._ws = None
|
||||
logger.info("WebSocket 연결 종료")
|
||||
|
||||
|
||||
realtime_service = RealtimeService()
|
||||
@@ -0,0 +1,165 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.auth import token_manager
|
||||
from app.core.config import get_base_url, settings
|
||||
from app.core.logger import setup_logger
|
||||
from app.core.rate_limiter import RateLimiter
|
||||
|
||||
logger = setup_logger("trading")
|
||||
|
||||
_ORDER_URL = "/uapi/domestic-stock/v1/trading/order-cash"
|
||||
_MODIFY_CANCEL_URL = "/uapi/domestic-stock/v1/trading/order-rvsecncl"
|
||||
|
||||
_TR_IDS = {
|
||||
"real": {"buy": "TTTC0802U", "sell": "TTTC0801U", "modify": "TTTC0803U", "cancel": "TTTC0804U"},
|
||||
"vps": {"buy": "VTTC0802U", "sell": "VTTC0801U", "modify": "VTTC0803U", "cancel": "VTTC0804U"},
|
||||
}
|
||||
|
||||
|
||||
class TradingService:
|
||||
def __init__(self) -> None:
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second)
|
||||
|
||||
def _get_tr_id(self, side: str) -> str:
|
||||
mode = settings.kis.server_mode
|
||||
return _TR_IDS.get(mode, _TR_IDS["vps"]).get(side, "VTTC0802U")
|
||||
|
||||
async def place_order(
|
||||
self,
|
||||
stock_code: str,
|
||||
side: str,
|
||||
qty: int,
|
||||
price: int = 0,
|
||||
order_type: str = "00",
|
||||
) -> dict:
|
||||
await self._rate_limiter.acquire()
|
||||
|
||||
tr_id = self._get_tr_id(side)
|
||||
headers = token_manager.get_auth_headers(tr_id)
|
||||
|
||||
body = {
|
||||
"CANO": settings.kis.account_no,
|
||||
"ACNT_PRDT_CD": settings.kis.account_code,
|
||||
"PDNO": stock_code,
|
||||
"ORD_DVSN": order_type,
|
||||
"ORD_QTY": str(qty),
|
||||
"ORD_UNPR": str(price) if order_type == "00" else "0",
|
||||
}
|
||||
|
||||
hashkey = await token_manager.generate_hashkey(body)
|
||||
headers["hashkey"] = hashkey
|
||||
|
||||
url = f"{get_base_url()}{_ORDER_URL}"
|
||||
resp = await self._client.post(url, json=body, headers=headers)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
result = {
|
||||
"order_no": data.get("output", {}).get("odno", ""),
|
||||
"rt_cd": data.get("rt_cd"),
|
||||
"msg_cd": data.get("msg_cd"),
|
||||
"msg": data.get("msg1"),
|
||||
"stock_code": stock_code,
|
||||
"side": side,
|
||||
"qty": qty,
|
||||
"price": price,
|
||||
"order_type": order_type,
|
||||
}
|
||||
|
||||
if data.get("rt_cd") == "0":
|
||||
logger.info(
|
||||
"주문 성공: %s %s %s주 %s @ %s원",
|
||||
stock_code,
|
||||
"매수" if side == "buy" else "매도",
|
||||
qty,
|
||||
order_type,
|
||||
price,
|
||||
)
|
||||
else:
|
||||
logger.warning("주문 실패: %s - %s", stock_code, data.get("msg1"))
|
||||
|
||||
return result
|
||||
|
||||
async def modify_order(
|
||||
self, order_no: str, stock_code: str, qty: int, price: int, order_type: str = "00"
|
||||
) -> dict:
|
||||
await self._rate_limiter.acquire()
|
||||
|
||||
tr_id = self._get_tr_id("modify")
|
||||
headers = token_manager.get_auth_headers(tr_id)
|
||||
|
||||
body = {
|
||||
"CANO": settings.kis.account_no,
|
||||
"ACNT_PRDT_CD": settings.kis.account_code,
|
||||
"ODNO": order_no,
|
||||
"PDNO": stock_code,
|
||||
"ORD_DVSN": order_type,
|
||||
"ORD_QTY": str(qty),
|
||||
"ORD_UNPR": str(price),
|
||||
}
|
||||
|
||||
hashkey = await token_manager.generate_hashkey(body)
|
||||
headers["hashkey"] = hashkey
|
||||
|
||||
url = f"{get_base_url()}{_MODIFY_CANCEL_URL}"
|
||||
resp = await self._client.post(url, json=body, headers=headers)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
result = {
|
||||
"order_no": data.get("output", {}).get("odno", ""),
|
||||
"rt_cd": data.get("rt_cd"),
|
||||
"msg": data.get("msg1"),
|
||||
}
|
||||
|
||||
if data.get("rt_cd") == "0":
|
||||
logger.info("정정 성공: 주문번호 %s", order_no)
|
||||
else:
|
||||
logger.warning("정정 실패: %s - %s", order_no, data.get("msg1"))
|
||||
|
||||
return result
|
||||
|
||||
async def cancel_order(self, order_no: str, stock_code: str, qty: int) -> dict:
|
||||
await self._rate_limiter.acquire()
|
||||
|
||||
tr_id = self._get_tr_id("cancel")
|
||||
headers = token_manager.get_auth_headers(tr_id)
|
||||
|
||||
body = {
|
||||
"CANO": settings.kis.account_no,
|
||||
"ACNT_PRDT_CD": settings.kis.account_code,
|
||||
"ODNO": order_no,
|
||||
"PDNO": stock_code,
|
||||
"ORD_DVSN": "00",
|
||||
"ORD_QTY": str(qty),
|
||||
"ORD_UNPR": "0",
|
||||
}
|
||||
|
||||
hashkey = await token_manager.generate_hashkey(body)
|
||||
headers["hashkey"] = hashkey
|
||||
|
||||
url = f"{get_base_url()}{_MODIFY_CANCEL_URL}"
|
||||
resp = await self._client.post(url, json=body, headers=headers)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
result = {
|
||||
"rt_cd": data.get("rt_cd"),
|
||||
"msg": data.get("msg1"),
|
||||
}
|
||||
|
||||
if data.get("rt_cd") == "0":
|
||||
logger.info("취소 성공: 주문번호 %s", order_no)
|
||||
else:
|
||||
logger.warning("취소 실패: %s - %s", order_no, data.get("msg1"))
|
||||
|
||||
return result
|
||||
|
||||
async def close(self) -> None:
|
||||
await self._client.aclose()
|
||||
|
||||
|
||||
trading_service = TradingService()
|
||||
Reference in New Issue
Block a user