150 lines
5.6 KiB
Python
150 lines
5.6 KiB
Python
"""
|
|
KIS WebSocket approval 세션 시간 분할 (국내 ↔ 해외)
|
|
====================================================
|
|
KIS 정책: approval_key : WebSocket 세션 = 1 : 1
|
|
국내 H0STCNT0 / 해외 HDFSCNT0 는 동일 실전 URL(ops:21000)을 쓰므로
|
|
**동시에 두 소켓을 열면** 뒤쪽(보통 해외)이 즉시 끊긴다.
|
|
|
|
운용:
|
|
- 국내 hold 창: 국내 WS만 연결 (기본 07:00~20:00 — 장 전후·틱 적재 여유)
|
|
- 해외 hold 창: 해외 WS만 연결 (기본 21:00~06:00 — 미국장 전후 틱 적재 여유)
|
|
- 그 사이 갭(20:00~21:00, 06:00~07:00): 양쪽 모두 close (세션 비움·핸드오버)
|
|
- approval REST 재발급: 기존 6h 금지 · 24h 유효 · 응급 하드캡 유지
|
|
(이 모듈은 재발급하지 않음 — 소켓 close/open 만)
|
|
|
|
env (DB 등록):
|
|
KIS_WS_KR_HOLD_START_HM / KIS_WS_KR_HOLD_END_HM (기본 700 / 2000)
|
|
KIS_WS_US_HOLD_START_HM / KIS_WS_US_HOLD_END_HM (기본 2100 / 600)
|
|
KIS_WS_SESSION_GUARD_SEC — 연결 중 창 이탈 시 close 주기
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import datetime as _dt
|
|
from typing import Optional, Tuple
|
|
|
|
from .env import get_env_float, get_env_int
|
|
from .session_hm import hm_in_trading_window
|
|
|
|
|
|
def kr_ws_hold_bounds() -> Tuple[int, int]:
|
|
"""국내 WS 세션 점유 HHMM (당일 구간). 기본 0700~2000."""
|
|
start = int(get_env_int("KIS_WS_KR_HOLD_START_HM", 700) or 700)
|
|
end = int(get_env_int("KIS_WS_KR_HOLD_END_HM", 2000) or 2000)
|
|
if start <= 0:
|
|
start = 700
|
|
if end <= 0:
|
|
end = 2000
|
|
return start, end
|
|
|
|
|
|
def us_ws_hold_bounds() -> Tuple[int, int]:
|
|
"""해외 WS 세션 점유 HHMM (자정 넘김). 기본 2100~0600."""
|
|
start = int(get_env_int("KIS_WS_US_HOLD_START_HM", 2100) or 2100)
|
|
end = int(get_env_int("KIS_WS_US_HOLD_END_HM", 600) or 600)
|
|
if start <= 0:
|
|
start = 2100
|
|
if end <= 0:
|
|
end = 600
|
|
return start, end
|
|
|
|
|
|
def session_guard_interval_sec() -> float:
|
|
"""연결 유지 중 hold 창 이탈 감시 주기(초)."""
|
|
return max(5.0, float(get_env_float("KIS_WS_SESSION_GUARD_SEC", 15.0) or 15.0))
|
|
|
|
|
|
def _hm_now(now: Optional[_dt.datetime] = None) -> int:
|
|
n = now or _dt.datetime.now()
|
|
return int(n.hour * 100 + n.minute)
|
|
|
|
|
|
def in_kr_ws_hold_window(now: Optional[_dt.datetime] = None) -> bool:
|
|
"""True = 국내 WS 가 approval 세션을 점유해도 되는 시간 (월~금)."""
|
|
n = now or _dt.datetime.now()
|
|
if n.weekday() >= 5:
|
|
return False
|
|
start, end = kr_ws_hold_bounds()
|
|
return hm_in_trading_window(_hm_now(n), start, end, wrap_midnight=False)
|
|
|
|
|
|
def in_us_ws_hold_window(now: Optional[_dt.datetime] = None) -> bool:
|
|
"""
|
|
True = 해외 WS 가 approval 세션을 점유해도 되는 시간.
|
|
- start~23:59: 월~금
|
|
- 00:00~end: 화~토 (미국장 새벽 마감)
|
|
"""
|
|
n = now or _dt.datetime.now()
|
|
wd = n.weekday()
|
|
hm = _hm_now(n)
|
|
start, end = us_ws_hold_bounds()
|
|
# 자정 넘김: start > end
|
|
if start > end:
|
|
if hm >= start:
|
|
return 0 <= wd <= 4
|
|
if hm < end:
|
|
return 1 <= wd <= 5
|
|
return False
|
|
# 비정상(같은날 구간) — 월~금만
|
|
if n.weekday() >= 5:
|
|
return False
|
|
return hm_in_trading_window(hm, start, end, wrap_midnight=False)
|
|
|
|
|
|
def seconds_until_kr_ws_open(now: Optional[_dt.datetime] = None) -> float:
|
|
"""다음 국내 hold 시작까지 초 (최소 60). hold 중이면 짧은 쿨다운."""
|
|
n = now or _dt.datetime.now()
|
|
if in_kr_ws_hold_window(n):
|
|
return max(30.0, float(get_env_float("KIS_WS_INSTANT_DROP_COOLDOWN_SEC", 90.0) or 90.0))
|
|
start, _end = kr_ws_hold_bounds()
|
|
sh, sm = divmod(int(start), 100)
|
|
hm = _hm_now(n)
|
|
target = n.replace(hour=sh, minute=sm, second=0, microsecond=0)
|
|
if n.weekday() >= 5:
|
|
# 토→월(+2), 일→월(+1)
|
|
days = (0 - n.weekday()) % 7
|
|
if days == 0:
|
|
days = 7
|
|
target += _dt.timedelta(days=days)
|
|
elif hm >= start:
|
|
target += _dt.timedelta(days=1)
|
|
while target.weekday() >= 5:
|
|
target += _dt.timedelta(days=1)
|
|
return max(60.0, (target - n).total_seconds())
|
|
|
|
|
|
def seconds_until_us_ws_open(now: Optional[_dt.datetime] = None) -> float:
|
|
"""다음 해외 hold 시작까지 초 (최소 60). hold 중이면 짧은 쿨다운."""
|
|
n = now or _dt.datetime.now()
|
|
if in_us_ws_hold_window(n):
|
|
return max(30.0, float(get_env_float("KIS_WS_INSTANT_DROP_COOLDOWN_SEC", 90.0) or 90.0))
|
|
start, end = us_ws_hold_bounds()
|
|
sh, sm = divmod(int(start), 100)
|
|
hm = _hm_now(n)
|
|
target = n.replace(hour=sh, minute=sm, second=0, microsecond=0)
|
|
if start > end:
|
|
# 주간 갭(end~start): 오늘 밤 start (단, 토요일이면 월)
|
|
if end <= hm < start:
|
|
if n.weekday() >= 5:
|
|
days = (0 - n.weekday()) % 7
|
|
if days == 0:
|
|
days = 7
|
|
target += _dt.timedelta(days=days)
|
|
elif hm >= start:
|
|
# 금요 밤 hold 가능 구간인데 hold False = 이상 → 다음 평일
|
|
# 토요 밤: 다음 월
|
|
target += _dt.timedelta(days=1)
|
|
while target.weekday() >= 5:
|
|
target += _dt.timedelta(days=1)
|
|
else:
|
|
# hm < end, hold 아님 (예: 일요일 새벽) → 오늘(일) 밤 스킵 → 월 start
|
|
if n.weekday() == 6:
|
|
target += _dt.timedelta(days=1)
|
|
while target.weekday() >= 5:
|
|
target += _dt.timedelta(days=1)
|
|
else:
|
|
if hm >= start or n.weekday() >= 5:
|
|
target += _dt.timedelta(days=1)
|
|
while target.weekday() >= 5:
|
|
target += _dt.timedelta(days=1)
|
|
return max(60.0, (target - n).total_seconds())
|