feat(tests): 신규 키움 웹소켓 조건검색 및 실시간 조건검색 테스트 추가
변경 사항 ---- - _test_kiwoom_condition_list.py: 키움 웹소켓 조건검색 '목록조회' 기능을 단독으로 테스트하는 스크립트 추가 - _test_kiwoom_condition_realtime.py: 'momentum' 조건식을 실시간으로 등록하고 초기 매칭 종목 리스트 및 실시간 편입/이탈을 수신하는 테스트 스크립트 추가 - _verify_columnar_bitid.py, _verify_shared_e2e_breakout.py, _verify_shared_e2e.py: 공유 메모리 및 dict 간의 데이터 일관성을 검증하는 테스트 추가 영향 ---- - 신규 테스트 스크립트 추가로 키움 웹소켓 API의 기능 검증 및 안정성을 높임 - 기존 기능에 대한 영향 없음 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
212
kis_trader/engine/dbband_env_keys.py
Normal file
212
kis_trader/engine/dbband_env_keys.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
kis_trader/engine/dbband_env_keys.py — 더블 볼린저(DBBAND) env 키 단일 정의
|
||||
=============================================================================
|
||||
캐시 리엔 더블 BB(20/2 + 20/3) + 추세 MA 필터 — config_dbband · dbband_engine · 웹 · 파라서치 공통.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
# config_dbband 전용 키
|
||||
DBBAND_CONFIG_KEYS = frozenset({
|
||||
"DBBAND_BB_PERIOD",
|
||||
"DBBAND_BB_INNER_STD",
|
||||
"DBBAND_BB_OUTER_STD",
|
||||
"DBBAND_TREND_MA_PERIOD",
|
||||
"DBBAND_USE_TREND_FILTER",
|
||||
"DBBAND_SIDE_MODE",
|
||||
"DBBAND_ENTRY_VALID_BARS",
|
||||
"DBBAND_ENTRY_MODE",
|
||||
"DBBAND_STOP_MODE",
|
||||
"DBBAND_STOP_BUFFER_PCT",
|
||||
"DBBAND_STOP_LOSS_PCT",
|
||||
"DBBAND_TP_MODE",
|
||||
"DBBAND_TAKE_PROFIT_PCT",
|
||||
"DBBAND_RR_RATIO",
|
||||
"DBBAND_EXIT_MODE",
|
||||
"DBBAND_SHOULDER_MIN_HIGH_PCT",
|
||||
"DBBAND_SHOULDER_CUT_PCT",
|
||||
"DBBAND_TRAIL_PCT",
|
||||
"DBBAND_TRAIL_ARM_PCT",
|
||||
"DBBAND_TIME_START",
|
||||
"DBBAND_TIME_END",
|
||||
"DBBAND_COOLDOWN_SEC",
|
||||
"DBBAND_MAX_DAILY",
|
||||
"DBBAND_MIN_PRICE",
|
||||
"DBBAND_SLOT_MONEY",
|
||||
"DBBAND_MAX_STOCKS",
|
||||
"DBBAND_TOTAL_BUDGET_KRW",
|
||||
"DBBAND_MAX_BUY_AMOUNT",
|
||||
"DBBAND_MAX_HOLD_BARS",
|
||||
"DBBAND_TIMEFRAME",
|
||||
"DBBAND_MIN_INVEST_RATIO_OF_SLOT",
|
||||
"DBBAND_FORCE_EOD_EXIT",
|
||||
"DBBAND_LIVE_MIN_CANDLES",
|
||||
"DBBAND_LIVE_SIGNAL_LOOKBACK_BARS",
|
||||
"DBBAND_UNIVERSE_SOURCE",
|
||||
"DBBAND_GRID_BB_PERIOD0",
|
||||
"DBBAND_GRID_BB_PERIOD1",
|
||||
"DBBAND_GRID_INNER_STD0",
|
||||
"DBBAND_GRID_INNER_STD1",
|
||||
"DBBAND_GRID_OUTER_STD0",
|
||||
"DBBAND_GRID_OUTER_STD1",
|
||||
"DBBAND_GRID_TREND_MA0",
|
||||
"DBBAND_GRID_TREND_MA1",
|
||||
"DBBAND_GRID_SL0",
|
||||
"DBBAND_GRID_SL1",
|
||||
"DBBAND_GRID_TP0",
|
||||
"DBBAND_GRID_TP1",
|
||||
"DBBAND_GRID_RR0",
|
||||
"DBBAND_GRID_RR1",
|
||||
})
|
||||
|
||||
|
||||
def _row_val(row: Dict[str, Any], key: str, default: Any = None) -> Any:
|
||||
v = row.get(key)
|
||||
if v not in (None, "", "None"):
|
||||
return v
|
||||
return default
|
||||
|
||||
|
||||
def dbband_env_float(row: Dict[str, Any], key: str, default: float) -> float:
|
||||
v = _row_val(row, key)
|
||||
if v is None:
|
||||
return float(default)
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return float(default)
|
||||
|
||||
|
||||
def dbband_env_int(row: Dict[str, Any], key: str, default: int) -> int:
|
||||
v = _row_val(row, key)
|
||||
if v is None:
|
||||
return int(default)
|
||||
try:
|
||||
return int(float(v))
|
||||
except (TypeError, ValueError):
|
||||
return int(default)
|
||||
|
||||
|
||||
def dbband_env_bool(row: Dict[str, Any], key: str, default: bool) -> bool:
|
||||
v = _row_val(row, key)
|
||||
if v is None:
|
||||
return default
|
||||
s = str(v).strip().lower()
|
||||
if s in ("1", "true", "t", "y", "yes", "on"):
|
||||
return True
|
||||
if s in ("0", "false", "f", "n", "no", "off", ""):
|
||||
return False
|
||||
return default
|
||||
|
||||
|
||||
def params_to_dbband_env_patch(p: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""파라서치·웹 JSON params → DBBAND env 패치 (문자열)."""
|
||||
patch: Dict[str, str] = {}
|
||||
if not p:
|
||||
return patch
|
||||
|
||||
def _set(k: str, v: Any) -> None:
|
||||
if v is not None and v != "":
|
||||
patch[k] = str(v)
|
||||
|
||||
mapping = (
|
||||
("bb_period", "DBBAND_BB_PERIOD"),
|
||||
("bb_inner_std", "DBBAND_BB_INNER_STD"),
|
||||
("bb_outer_std", "DBBAND_BB_OUTER_STD"),
|
||||
("trend_ma_period", "DBBAND_TREND_MA_PERIOD"),
|
||||
("use_trend_filter", "DBBAND_USE_TREND_FILTER"),
|
||||
("side_mode", "DBBAND_SIDE_MODE"),
|
||||
("entry_valid_bars", "DBBAND_ENTRY_VALID_BARS"),
|
||||
("entry_mode", "DBBAND_ENTRY_MODE"),
|
||||
("stop_mode", "DBBAND_STOP_MODE"),
|
||||
("stop_buffer_pct", "DBBAND_STOP_BUFFER_PCT"),
|
||||
("sl_pct", "DBBAND_STOP_LOSS_PCT"),
|
||||
("tp_mode", "DBBAND_TP_MODE"),
|
||||
("tp_pct", "DBBAND_TAKE_PROFIT_PCT"),
|
||||
("rr_ratio", "DBBAND_RR_RATIO"),
|
||||
("exit_mode", "DBBAND_EXIT_MODE"),
|
||||
("shoulder_min_high", "DBBAND_SHOULDER_MIN_HIGH_PCT"),
|
||||
("shoulder_cut_pct", "DBBAND_SHOULDER_CUT_PCT"),
|
||||
("trail_pct", "DBBAND_TRAIL_PCT"),
|
||||
("trail_arm_pct", "DBBAND_TRAIL_ARM_PCT"),
|
||||
("time_start_hm", "DBBAND_TIME_START"),
|
||||
("time_end_hm", "DBBAND_TIME_END"),
|
||||
("max_daily", "DBBAND_MAX_DAILY"),
|
||||
("min_price", "DBBAND_MIN_PRICE"),
|
||||
("slot_money", "DBBAND_SLOT_MONEY"),
|
||||
("max_stocks", "DBBAND_MAX_STOCKS"),
|
||||
("total_budget_krw", "DBBAND_TOTAL_BUDGET_KRW"),
|
||||
("max_hold_bars", "DBBAND_MAX_HOLD_BARS"),
|
||||
("timeframe", "DBBAND_TIMEFRAME"),
|
||||
("min_invest_ratio_of_slot", "DBBAND_MIN_INVEST_RATIO_OF_SLOT"),
|
||||
)
|
||||
for js_k, env_k in mapping:
|
||||
if js_k in p and p[js_k] is not None:
|
||||
_set(env_k, p[js_k])
|
||||
|
||||
if "use_trend_filter" in p:
|
||||
_set("DBBAND_USE_TREND_FILTER", str(p["use_trend_filter"]).lower())
|
||||
if "cooldown_min" in p and p["cooldown_min"] is not None:
|
||||
_set("DBBAND_COOLDOWN_SEC", int(float(p["cooldown_min"]) * 60))
|
||||
if "sl_pct" in p:
|
||||
_set("DBBAND_STOP_LOSS_PCT", abs(float(p["sl_pct"])))
|
||||
if "force_eod_exit" in p:
|
||||
_set("DBBAND_FORCE_EOD_EXIT", str(p["force_eod_exit"]).lower())
|
||||
return patch
|
||||
|
||||
|
||||
def web_body_to_dbband_env_patch(body: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""웹 saveDbBandConfig POST → DBBAND env 패치."""
|
||||
if not isinstance(body, dict):
|
||||
return {}
|
||||
|
||||
def _get(key: str) -> Any:
|
||||
v = body.get(key)
|
||||
if v is None or v == "":
|
||||
return None
|
||||
return v
|
||||
|
||||
p: Dict[str, Any] = {}
|
||||
for k in (
|
||||
"bb_period", "bb_inner_std", "bb_outer_std", "trend_ma_period",
|
||||
"side_mode", "entry_valid_bars", "entry_mode", "stop_mode",
|
||||
"stop_buffer_pct", "tp_mode", "rr_ratio",
|
||||
"cooldown_min", "max_daily", "min_price", "slot_money",
|
||||
"max_stocks", "total_budget_krw", "max_hold_bars", "timeframe",
|
||||
"min_invest_ratio_of_slot",
|
||||
):
|
||||
v = _get(k)
|
||||
if v is not None:
|
||||
p[k] = v
|
||||
|
||||
sl = _get("sl_pct")
|
||||
if sl is not None:
|
||||
p["sl_pct"] = float(sl)
|
||||
tp = _get("tp_pct")
|
||||
if tp is not None:
|
||||
p["tp_pct"] = float(tp)
|
||||
smh = _get("shoulder_min_high")
|
||||
if smh is not None:
|
||||
p["shoulder_min_high"] = float(smh)
|
||||
sc = _get("shoulder_cut_pct")
|
||||
if sc is not None:
|
||||
p["shoulder_cut_pct"] = float(sc)
|
||||
tr = _get("trail_pct")
|
||||
if tr is not None:
|
||||
p["trail_pct"] = float(tr)
|
||||
tra = _get("trail_arm_pct")
|
||||
if tra is not None:
|
||||
p["trail_arm_pct"] = float(tra)
|
||||
|
||||
utf = _get("use_trend_filter")
|
||||
if utf is not None:
|
||||
p["use_trend_filter"] = utf
|
||||
if "time_start" in body and body["time_start"] not in (None, ""):
|
||||
p["time_start_hm"] = int(float(body["time_start"]))
|
||||
if "time_end" in body and body["time_end"] not in (None, ""):
|
||||
p["time_end_hm"] = int(float(body["time_end"]))
|
||||
if "force_eod_exit" in body:
|
||||
p["force_eod_exit"] = body.get("force_eod_exit")
|
||||
|
||||
return params_to_dbband_env_patch(p)
|
||||
Reference in New Issue
Block a user