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:
590
kis_trader/strategies/updow_holding_cfg.py
Normal file
590
kis_trader/strategies/updow_holding_cfg.py
Normal file
@@ -0,0 +1,590 @@
|
||||
"""
|
||||
kis_trader/strategies/updow_holding_cfg.py
|
||||
==========================================
|
||||
UPDOW(하락매수) 종목별 파라미터 — **``updow_stock_config`` 단일 테이블** (웹·백테·실매·JSON 적용 동일).
|
||||
|
||||
- 종목 행이 있으면 그 값만 사용 (``env_config`` 의 ``UPDOW_*`` 로 덮지 않음).
|
||||
- 행이 없을 때만 env ``UPDOW_*`` 폴백 (신규 종목·그리드 끝값 등 전역 설정용).
|
||||
- ``holding_stock_config`` 의 ``updow_*`` 는 **최초 1회 마이그레이션** 용도만 (이후 미사용).
|
||||
|
||||
분봉(tf): ``updow_stock_config.tf_min`` 양수 → 해당 분봉, 아니면 env ``UPDOW_TF_MIN``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from ..engine.limit_entry_common import ENTRY_ALIGN, ENTRY_LIMIT_ATR
|
||||
from ..utils.env import get_env_int
|
||||
from .updow_buy import CFG_ENGINE_KEYS, DEFAULT_UPDOW_CONFIG
|
||||
|
||||
logger = logging.getLogger("kis_trader.updow_holding_cfg")
|
||||
|
||||
_UPDOW_STOCK_DDL = """
|
||||
CREATE TABLE IF NOT EXISTS updow_stock_config (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(10) NOT NULL,
|
||||
market_type VARCHAR(8) NOT NULL DEFAULT 'KR',
|
||||
exchange VARCHAR(16) NOT NULL DEFAULT 'KRX',
|
||||
symbol VARCHAR(32) NOT NULL DEFAULT '',
|
||||
name VARCHAR(50) DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
body_drop_min_pct DOUBLE NOT NULL,
|
||||
body_drop_max_pct DOUBLE NOT NULL DEFAULT 0,
|
||||
tp_pct DOUBLE NOT NULL,
|
||||
stop_loss_pct DOUBLE NOT NULL,
|
||||
max_hold_bars INT NOT NULL,
|
||||
exit_on_green DOUBLE NOT NULL DEFAULT 0,
|
||||
tf_min INT NOT NULL DEFAULT 60,
|
||||
slot_money DOUBLE NOT NULL DEFAULT 3000000,
|
||||
regime_ma_bars INT NOT NULL DEFAULT 0,
|
||||
regime_ma_ease_pct DOUBLE NOT NULL DEFAULT 0,
|
||||
atr_use_dynamic DOUBLE NOT NULL DEFAULT 0,
|
||||
atr_period INT NOT NULL DEFAULT 14,
|
||||
atr_sl_mult DOUBLE NOT NULL DEFAULT 2,
|
||||
atr_tp_mult DOUBLE NOT NULL DEFAULT 4,
|
||||
atr_sl_min_pct DOUBLE NOT NULL DEFAULT 0.8,
|
||||
atr_sl_max_pct DOUBLE NOT NULL DEFAULT 6,
|
||||
atr_tp_min_pct DOUBLE NOT NULL DEFAULT 1.5,
|
||||
atr_tp_max_pct DOUBLE NOT NULL DEFAULT 12,
|
||||
shoulder_min_high_pct DOUBLE NOT NULL DEFAULT 0.3,
|
||||
shoulder_cut_pct DOUBLE NOT NULL DEFAULT 0.2,
|
||||
limit_atr_mult DOUBLE NOT NULL DEFAULT 1.5,
|
||||
entry_mode VARCHAR(16) NOT NULL DEFAULT '',
|
||||
KEY idx_updow_stock_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='UPDOW 하락매수 종목별 파라미터'
|
||||
"""
|
||||
|
||||
|
||||
def _trade_db_core(db: Any):
|
||||
"""TradeDBExt 이면 ``.raw``, 아니면 그대로."""
|
||||
return getattr(db, "raw", db)
|
||||
|
||||
|
||||
def ensure_updow_stock_config_table(db: Any) -> None:
|
||||
"""``updow_stock_config`` 테이블 생성."""
|
||||
raw = _trade_db_core(db)
|
||||
raw.conn.execute(_UPDOW_STOCK_DDL.strip())
|
||||
# 구버전 테이블(시장 컬럼 없음) 호환 마이그레이션
|
||||
alters = (
|
||||
"ADD COLUMN market_type VARCHAR(8) NOT NULL DEFAULT 'KR' AFTER code",
|
||||
"ADD COLUMN exchange VARCHAR(16) NOT NULL DEFAULT 'KRX' AFTER market_type",
|
||||
"ADD COLUMN symbol VARCHAR(32) NOT NULL DEFAULT '' AFTER exchange",
|
||||
"ADD COLUMN shoulder_min_high_pct DOUBLE NOT NULL DEFAULT 0.3 AFTER atr_tp_max_pct",
|
||||
"ADD COLUMN shoulder_cut_pct DOUBLE NOT NULL DEFAULT 0.2 AFTER shoulder_min_high_pct",
|
||||
"ADD COLUMN body_drop_max_pct DOUBLE NOT NULL DEFAULT 0 AFTER body_drop_min_pct",
|
||||
"ADD COLUMN limit_atr_mult DOUBLE NOT NULL DEFAULT 1.5 AFTER shoulder_cut_pct",
|
||||
# 종목별 진입모드(align/limit_atr) — '' 이면 글로벌 env UPDOW_ENTRY_MODE 사용
|
||||
"ADD COLUMN entry_mode VARCHAR(16) NOT NULL DEFAULT '' AFTER limit_atr_mult",
|
||||
)
|
||||
for frag in alters:
|
||||
try:
|
||||
raw.conn.execute(f"ALTER TABLE updow_stock_config {frag}")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
raw.conn.execute(
|
||||
"UPDATE updow_stock_config "
|
||||
"SET symbol = code "
|
||||
"WHERE (symbol IS NULL OR symbol = '') AND code IS NOT NULL AND code != ''"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# 영문 티커(QQQM 등)가 KR/KRX 로 저장된 구버전 행 보정
|
||||
try:
|
||||
raw.conn.execute(
|
||||
"UPDATE updow_stock_config "
|
||||
"SET market_type='US', exchange='NASD', symbol=UPPER(code) "
|
||||
"WHERE code REGEXP '^[A-Za-z]{1,8}$' "
|
||||
"AND (market_type='KR' OR exchange='KRX' OR exchange='')"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
raw.conn.commit()
|
||||
|
||||
|
||||
def ensure_updow_backtest_tables(db: Any) -> None:
|
||||
"""
|
||||
UPDOW 백테·탐색·웹용 DB 준비.
|
||||
|
||||
- ``holding_min_candles`` 등 분봉 저장 테이블 (``holding_bot.ensure_holding_tables``)
|
||||
- ``updow_stock_config`` 종목별 파라미터 (하락매수 전용, ``holding_stock_config`` 와 분리)
|
||||
"""
|
||||
try:
|
||||
import holding_bot as hb
|
||||
|
||||
hb.ensure_holding_tables(db)
|
||||
except Exception as e:
|
||||
logger.warning("holding 분봉 테이블 확인 경고: %s", e)
|
||||
ensure_updow_stock_config_table(db)
|
||||
logger.info("✅ UPDOW DB 확인: holding_min_candles(분봉) + updow_stock_config(종목 파라미터)")
|
||||
|
||||
|
||||
def _engine_cfg_from_row(row: Dict[str, Any]) -> Dict[str, Any]:
|
||||
out: Dict[str, Any] = {}
|
||||
for k in CFG_ENGINE_KEYS:
|
||||
if k in row and row[k] is not None:
|
||||
out[k] = float(row[k])
|
||||
else:
|
||||
# 구버전 행·신규 추가 키 누락 시 엔진 기본값으로 보강 (KeyError 방지)
|
||||
out[k] = float(DEFAULT_UPDOW_CONFIG.get(k, 0.0))
|
||||
# 종목별 진입모드(문자열) — 유효값일 때만 주입.
|
||||
# 빈 문자열을 out["entry_mode"]에 넣으면 updow_entry_mode()가 align 으로 오인하므로
|
||||
# 반드시 valid 일 때만 키를 세팅하고, 아니면 키 자체를 넣지 않아 글로벌 env 로 폴백한다.
|
||||
em = str(row.get("entry_mode") or "").strip().lower()
|
||||
if em in (ENTRY_ALIGN, ENTRY_LIMIT_ATR):
|
||||
out["entry_mode"] = em
|
||||
return out
|
||||
|
||||
|
||||
def _normalize_market_meta(
|
||||
market_type: Optional[str],
|
||||
exchange: Optional[str],
|
||||
symbol: Optional[str],
|
||||
code: str,
|
||||
) -> tuple[str, str, str]:
|
||||
mt = str(market_type or "KR").strip().upper()
|
||||
if mt not in ("KR", "US"):
|
||||
mt = "KR"
|
||||
ex = str(exchange or ("KRX" if mt == "KR" else "NASD")).strip().upper()
|
||||
sym = str(symbol or code or "").strip().upper()
|
||||
if not sym:
|
||||
sym = str(code or "").strip().upper()
|
||||
if not ex:
|
||||
ex = "KRX" if mt == "KR" else "NASD"
|
||||
return mt, ex, sym
|
||||
|
||||
|
||||
def resolve_market_meta(
|
||||
code: str,
|
||||
market_type: Optional[str] = None,
|
||||
exchange: Optional[str] = None,
|
||||
symbol: Optional[str] = None,
|
||||
) -> tuple[str, str, str]:
|
||||
"""
|
||||
시장 메타 정규화 + 티커 형태 보정.
|
||||
|
||||
DB에 QQQM 등이 market_type=KR 로 잘못 들어가도,
|
||||
6자리 숫자가 아닌 영문 티커는 해외(US)로 간주한다.
|
||||
"""
|
||||
c = str(code or symbol or "").strip().upper()
|
||||
mt, ex, sym = _normalize_market_meta(market_type, exchange, symbol, c)
|
||||
if not c:
|
||||
return mt, ex, sym
|
||||
if c.isdigit() and len(c) == 6:
|
||||
return "KR", ex if ex not in ("NASD", "NAS", "NYSE", "NYS", "AMEX", "AMS") else "KRX", sym or c
|
||||
if c.isalpha() and 1 <= len(c) <= 8:
|
||||
if mt == "KR" or ex in ("", "KRX"):
|
||||
return "US", "NASD", sym or c
|
||||
return mt, ex, sym
|
||||
|
||||
|
||||
def get_updow_stock_config_row(db: Any, code: str) -> Optional[Dict[str, Any]]:
|
||||
"""``updow_stock_config`` 최신 1행 → 엔진 cfg dict (없으면 None)."""
|
||||
ensure_updow_stock_config_table(db)
|
||||
code = str(code or "").strip()
|
||||
if not code:
|
||||
return None
|
||||
raw = _trade_db_core(db)
|
||||
row = raw.conn.execute(
|
||||
"SELECT * FROM updow_stock_config "
|
||||
"WHERE code=%s OR symbol=%s "
|
||||
"ORDER BY id DESC LIMIT 1",
|
||||
[code, code],
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
d = dict(row) if not isinstance(row, dict) else dict(row)
|
||||
return _engine_cfg_from_row(d)
|
||||
|
||||
|
||||
def get_updow_stock_meta(db: Any, code: str) -> Optional[Dict[str, Any]]:
|
||||
"""종목명·tf 포함 메타 (최신 행)."""
|
||||
ensure_updow_stock_config_table(db)
|
||||
code = str(code or "").strip()
|
||||
if not code:
|
||||
return None
|
||||
raw = _trade_db_core(db)
|
||||
row = raw.conn.execute(
|
||||
"SELECT code, market_type, exchange, symbol, name, tf_min, created_at "
|
||||
"FROM updow_stock_config "
|
||||
"WHERE code=%s OR symbol=%s ORDER BY id DESC LIMIT 1",
|
||||
[code, code],
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return dict(row) if not isinstance(row, dict) else dict(row)
|
||||
|
||||
|
||||
def set_updow_stock_config(
|
||||
db: Any,
|
||||
code: str,
|
||||
name: str,
|
||||
engine_cfg: Dict[str, Any],
|
||||
tf_min: Optional[int] = None,
|
||||
market_type: Optional[str] = None,
|
||||
exchange: Optional[str] = None,
|
||||
symbol: Optional[str] = None,
|
||||
) -> None:
|
||||
"""UPDOW 종목 파라미터 INSERT (최신 row 방식)."""
|
||||
ensure_updow_stock_config_table(db)
|
||||
code = str(code or symbol or "").strip().upper()
|
||||
if not code:
|
||||
raise ValueError("code 필수")
|
||||
mt, ex, sym = _normalize_market_meta(market_type, exchange, symbol, code)
|
||||
base = dict(DEFAULT_UPDOW_CONFIG)
|
||||
for k in CFG_ENGINE_KEYS:
|
||||
if k in engine_cfg and engine_cfg[k] is not None:
|
||||
base[k] = float(engine_cfg[k])
|
||||
try:
|
||||
tfv = int(float(tf_min if tf_min is not None else engine_cfg.get("tf_min", base.get("tf_min", 60))))
|
||||
except (TypeError, ValueError):
|
||||
tfv = int(get_env_int("UPDOW_TF_MIN", 60))
|
||||
if tfv < 1:
|
||||
tfv = int(get_env_int("UPDOW_TF_MIN", 60))
|
||||
# 진입모드(문자열) — align/limit_atr 만 허용, 그 외는 '' (글로벌 env 폴백)
|
||||
em = str(engine_cfg.get("entry_mode") or "").strip().lower()
|
||||
if em not in (ENTRY_ALIGN, ENTRY_LIMIT_ATR):
|
||||
em = ""
|
||||
cols = ["code", "market_type", "exchange", "symbol", "name", "tf_min"] + list(CFG_ENGINE_KEYS) + ["entry_mode"]
|
||||
vals = [code, mt, ex, sym, str(name or "").strip() or code, tfv]
|
||||
for k in CFG_ENGINE_KEYS:
|
||||
vals.append(base[k])
|
||||
vals.append(em)
|
||||
placeholders = ", ".join(["%s"] * len(cols))
|
||||
col_sql = ", ".join(cols)
|
||||
raw = _trade_db_core(db)
|
||||
raw.conn.execute(
|
||||
f"INSERT INTO updow_stock_config ({col_sql}) VALUES ({placeholders})",
|
||||
vals,
|
||||
)
|
||||
raw.conn.commit()
|
||||
|
||||
|
||||
def load_updow_engine_cfg(db: Any, code: str, env_fallback: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
UPDOW 종목 cfg — ``updow_stock_config`` 우선, 없으면 ``env_fallback`` 만.
|
||||
(holding_stock_config·env 이중 병합 없음)
|
||||
"""
|
||||
row_cfg = get_updow_stock_config_row(db, code)
|
||||
if row_cfg:
|
||||
return row_cfg
|
||||
return dict(env_fallback)
|
||||
|
||||
|
||||
def fetch_latest_updow_stock_config_by_code(db: Any) -> Dict[str, Dict[str, Any]]:
|
||||
"""종목코드별 ``updow_stock_config`` 최신 1행 (실매 유니버스·웹 목록)."""
|
||||
ensure_updow_stock_config_table(db)
|
||||
raw = _trade_db_core(db)
|
||||
sql = """
|
||||
SELECT u.* FROM updow_stock_config u
|
||||
INNER JOIN (
|
||||
SELECT code, MAX(id) AS mx FROM updow_stock_config GROUP BY code
|
||||
) t ON u.code = t.code AND u.id = t.mx
|
||||
ORDER BY u.code
|
||||
"""
|
||||
cur = raw.conn.execute(sql)
|
||||
rows = cur.fetchall() if cur else []
|
||||
out: Dict[str, Dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
d = dict(row) if not isinstance(row, dict) else dict(row)
|
||||
code = str(d.get("code", "")).strip()
|
||||
if not code:
|
||||
continue
|
||||
eng = _engine_cfg_from_row(d)
|
||||
eng["name"] = str(d.get("name") or code).strip() or code
|
||||
eng["tf_min"] = int(float(d.get("tf_min") or get_env_int("UPDOW_TF_MIN", 60)))
|
||||
eng["market_type"] = str(d.get("market_type") or "KR").strip().upper() or "KR"
|
||||
eng["exchange"] = str(d.get("exchange") or ("KRX" if eng["market_type"] == "KR" else "NASD")).strip().upper()
|
||||
eng["symbol"] = str(d.get("symbol") or code).strip().upper() or code
|
||||
out[code] = eng
|
||||
return out
|
||||
|
||||
|
||||
def list_updow_stock_codes(db: Any) -> list:
|
||||
"""웹 종목 셀렉트용 [{code, name}, ...]."""
|
||||
by_code = fetch_latest_updow_stock_config_by_code(db)
|
||||
out = []
|
||||
for c in sorted(by_code.keys()):
|
||||
mt, ex, sym = resolve_market_meta(
|
||||
c,
|
||||
by_code[c].get("market_type"),
|
||||
by_code[c].get("exchange"),
|
||||
by_code[c].get("symbol"),
|
||||
)
|
||||
out.append({
|
||||
"code": c,
|
||||
"name": by_code[c].get("name", c),
|
||||
"market_type": mt,
|
||||
"exchange": ex,
|
||||
"symbol": sym,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def migrate_updow_from_holding(db: Any, code: str, env_fallback: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
``holding_stock_config`` → ``updow_stock_config`` 1회 복사 (이미 있으면 False).
|
||||
구버전 holding ``updow_*`` + 익절/손절 병합값을 그대로 옮김.
|
||||
"""
|
||||
if get_updow_stock_config_row(db, code):
|
||||
return False
|
||||
try:
|
||||
import holding_bot as hb
|
||||
except ImportError:
|
||||
return False
|
||||
hold_row = hb.get_stock_config_row(db, code)
|
||||
if not hold_row:
|
||||
return False
|
||||
merged = merge_holding_row_into_updow_cfg(hold_row, env_fallback)
|
||||
tf = effective_updow_tf_min(hold_row, int(env_fallback.get("tf_min", get_env_int("UPDOW_TF_MIN", 60))))
|
||||
name = str(hold_row.get("name") or code).strip() or code
|
||||
set_updow_stock_config(db, code, name, merged, tf_min=tf)
|
||||
logger.info("updow_stock_config 마이그레이션: %s ← holding_stock_config", code)
|
||||
return True
|
||||
|
||||
|
||||
def migrate_all_updow_from_holding(db: Any, env_fallback: Dict[str, Any]) -> int:
|
||||
"""holding 에 등록된 모든 코드를 updow_stock_config 로 (없는 것만)."""
|
||||
try:
|
||||
import holding_bot as hb
|
||||
except ImportError:
|
||||
return 0
|
||||
n = 0
|
||||
try:
|
||||
items = hb.load_watchlist()
|
||||
except Exception:
|
||||
items = []
|
||||
codes = {str(it.get("code", "")).strip() for it in items if it.get("code")}
|
||||
raw = _trade_db_core(db)
|
||||
cur = raw.conn.execute("SELECT DISTINCT code FROM holding_stock_config")
|
||||
for row in (cur.fetchall() if cur else []):
|
||||
c = str(dict(row).get("code", "")).strip()
|
||||
if c:
|
||||
codes.add(c)
|
||||
for code in sorted(codes):
|
||||
if migrate_updow_from_holding(db, code, env_fallback):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def ensure_holding_updow_columns(db: Any) -> None:
|
||||
"""holding_stock_config 에 UPDOW 전용 선택 컬럼 추가(이미 있으면 무시)."""
|
||||
raw = _trade_db_core(db)
|
||||
alters = (
|
||||
"ADD COLUMN updow_body_drop_min_pct DOUBLE NULL "
|
||||
"COMMENT 'UPDOW 직전봉 몸통하락%% 하한, NULL=env UPDOW_BODY_DROP_MIN_PCT'",
|
||||
"ADD COLUMN updow_max_hold_bars INT NULL "
|
||||
"COMMENT 'UPDOW 최대 보유 봉 수, NULL=env UPDOW_MAX_HOLD_BARS'",
|
||||
"ADD COLUMN updow_exit_on_green DOUBLE NULL "
|
||||
"COMMENT 'UPDOW 양봉청산 1=ON 0=OFF, NULL=env UPDOW_EXIT_ON_GREEN'",
|
||||
"ADD COLUMN updow_tf_min INT NULL "
|
||||
"COMMENT 'UPDOW 분봉 tf, NULL 또는 1미만=env UPDOW_TF_MIN'",
|
||||
"ADD COLUMN updow_regime_ma_bars INT NULL "
|
||||
"COMMENT 'UPDOW KOSPI프록시 1분 MA봉수 0=OFF NULL=env UPDOW_REGIME_MA_BARS'",
|
||||
"ADD COLUMN updow_atr_use_dynamic DOUBLE NULL "
|
||||
"COMMENT 'UPDOW ATR 동적 청산 1=ON 0=OFF NULL=env UPDOW_ATR_USE_DYNAMIC'",
|
||||
"ADD COLUMN updow_atr_period INT NULL "
|
||||
"COMMENT 'UPDOW ATR 기간(봉), NULL=env UPDOW_ATR_PERIOD'",
|
||||
"ADD COLUMN updow_atr_sl_mult DOUBLE NULL "
|
||||
"COMMENT 'UPDOW ATR 손절 배수, NULL=env UPDOW_ATR_SL_MULT'",
|
||||
"ADD COLUMN updow_atr_tp_mult DOUBLE NULL "
|
||||
"COMMENT 'UPDOW ATR 익절 배수, NULL=env UPDOW_ATR_TP_MULT'",
|
||||
"ADD COLUMN updow_atr_sl_min_pct DOUBLE NULL "
|
||||
"COMMENT 'UPDOW ATR 손절 하한(%) NULL=env UPDOW_ATR_SL_MIN_PCT'",
|
||||
"ADD COLUMN updow_atr_sl_max_pct DOUBLE NULL "
|
||||
"COMMENT 'UPDOW ATR 손절 상한(%) NULL=env UPDOW_ATR_SL_MAX_PCT'",
|
||||
"ADD COLUMN updow_atr_tp_min_pct DOUBLE NULL "
|
||||
"COMMENT 'UPDOW ATR 익절 하한(%) NULL=env UPDOW_ATR_TP_MIN_PCT'",
|
||||
"ADD COLUMN updow_atr_tp_max_pct DOUBLE NULL "
|
||||
"COMMENT 'UPDOW ATR 익절 상한(%) NULL=env UPDOW_ATR_TP_MAX_PCT'",
|
||||
"ADD COLUMN updow_shoulder_min_high_pct DOUBLE NULL "
|
||||
"COMMENT 'UPDOW 어깨 발동(%%) NULL=env UPDOW_SHOULDER_MIN_HIGH_PCT'",
|
||||
"ADD COLUMN updow_shoulder_cut_pct DOUBLE NULL "
|
||||
"COMMENT 'UPDOW 어깨 폭(%%) NULL=env UPDOW_SHOULDER_CUT_PCT'",
|
||||
)
|
||||
for frag in alters:
|
||||
try:
|
||||
raw.conn.execute(f"ALTER TABLE holding_stock_config {frag}")
|
||||
raw.conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def fetch_latest_holding_stock_config_by_code(db: Any) -> Dict[str, Dict[str, Any]]:
|
||||
"""종목코드별 ``holding_stock_config`` 최신 1행 (id DESC)."""
|
||||
raw = _trade_db_core(db)
|
||||
sql = """
|
||||
SELECT h.* FROM holding_stock_config h
|
||||
INNER JOIN (
|
||||
SELECT code, MAX(id) AS mx FROM holding_stock_config GROUP BY code
|
||||
) t ON h.code = t.code AND h.id = t.mx
|
||||
ORDER BY h.code
|
||||
"""
|
||||
cur = raw.conn.execute(sql)
|
||||
rows = cur.fetchall() if cur else []
|
||||
out: Dict[str, Dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
d = dict(row) if not isinstance(row, dict) else dict(row)
|
||||
code = str(d.get("code", "")).strip()
|
||||
if not code:
|
||||
continue
|
||||
d.pop("id", None)
|
||||
d.pop("created_at", None)
|
||||
out[code] = d
|
||||
return out
|
||||
|
||||
|
||||
def merge_holding_row_into_updow_cfg(
|
||||
row: Optional[Dict[str, Any]],
|
||||
env_fallback: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
``holding_stock_config`` 한 행을 ``updow_buy`` 엔진 cfg 형식으로 병합.
|
||||
|
||||
- ``take_profit_pct`` → ``tp_pct``
|
||||
- ``stop_loss_pct`` → ``stop_loss_pct`` (%% 양수, 백테와 동일)
|
||||
- ``slot_money`` → ``slot_money``
|
||||
- ``updow_body_drop_min_pct`` / ``updow_max_hold_bars`` / ``updow_exit_on_green`` /
|
||||
``updow_regime_ma_bars``
|
||||
가 NULL 이 아니고 유효하면 env 를 덮어씀.
|
||||
"""
|
||||
out: Dict[str, Any] = dict(env_fallback)
|
||||
if not row:
|
||||
return out
|
||||
|
||||
def _f(key: str) -> Optional[float]:
|
||||
v = row.get(key)
|
||||
if v is None or (isinstance(v, str) and not str(v).strip()):
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
try:
|
||||
tp = _f("take_profit_pct")
|
||||
if tp is not None and tp > 0:
|
||||
out["tp_pct"] = tp
|
||||
sl = _f("stop_loss_pct")
|
||||
if sl is not None and sl > 0:
|
||||
out["stop_loss_pct"] = sl
|
||||
sm = _f("slot_money")
|
||||
if sm is not None and sm > 0:
|
||||
out["slot_money"] = sm
|
||||
|
||||
bd = _f("updow_body_drop_min_pct")
|
||||
if bd is not None and bd >= 0.0:
|
||||
out["body_drop_min_pct"] = bd
|
||||
|
||||
mh = _f("updow_max_hold_bars")
|
||||
if mh is not None and mh >= 1.0:
|
||||
out["max_hold_bars"] = float(int(mh))
|
||||
|
||||
eg = _f("updow_exit_on_green")
|
||||
# -1 = 미설정(장기홀딩 기본값) → env 유지. 0~1 은 UPDOW 양봉청산 on/off.
|
||||
if eg is not None and eg >= 0.0:
|
||||
out["exit_on_green"] = 1.0 if eg >= 0.5 else 0.0
|
||||
|
||||
rm = _f("updow_regime_ma_bars")
|
||||
if rm is not None and rm >= 0.0:
|
||||
out["regime_ma_bars"] = float(int(rm))
|
||||
|
||||
aud = _f("updow_atr_use_dynamic")
|
||||
if aud is not None and aud >= 0.0:
|
||||
out["atr_use_dynamic"] = 1.0 if aud >= 0.5 else 0.0
|
||||
ap = _f("updow_atr_period")
|
||||
if ap is not None and ap >= 1.0:
|
||||
out["atr_period"] = float(int(ap))
|
||||
asm = _f("updow_atr_sl_mult")
|
||||
if asm is not None and asm > 0.0:
|
||||
out["atr_sl_mult"] = asm
|
||||
atm = _f("updow_atr_tp_mult")
|
||||
if atm is not None and atm > 0.0:
|
||||
out["atr_tp_mult"] = atm
|
||||
asmin = _f("updow_atr_sl_min_pct")
|
||||
if asmin is not None and asmin > 0.0:
|
||||
out["atr_sl_min_pct"] = asmin
|
||||
asmax = _f("updow_atr_sl_max_pct")
|
||||
if asmax is not None and asmax > 0.0:
|
||||
out["atr_sl_max_pct"] = asmax
|
||||
atmin = _f("updow_atr_tp_min_pct")
|
||||
if atmin is not None and atmin > 0.0:
|
||||
out["atr_tp_min_pct"] = atmin
|
||||
atmax = _f("updow_atr_tp_max_pct")
|
||||
if atmax is not None and atmax > 0.0:
|
||||
out["atr_tp_max_pct"] = atmax
|
||||
usmh = _f("updow_shoulder_min_high_pct")
|
||||
if usmh is not None and usmh > 0.0:
|
||||
out["shoulder_min_high_pct"] = usmh
|
||||
usc = _f("updow_shoulder_cut_pct")
|
||||
if usc is not None and usc > 0.0:
|
||||
out["shoulder_cut_pct"] = usc
|
||||
except Exception as e:
|
||||
logger.debug("merge_holding_row_into_updow_cfg 경고: %s", e)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def updow_param_sources_for_code(
|
||||
db: Any,
|
||||
code: str,
|
||||
env_fallback: Dict[str, Any],
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
"""웹 표시: ``updow_stock_config`` 행 있으면 전부 종목, 없으면 env 폴백."""
|
||||
stock_cfg = get_updow_stock_config_row(db, code)
|
||||
applied = load_updow_engine_cfg(db, code, env_fallback)
|
||||
src = "updow_stock" if stock_cfg else "env"
|
||||
out: Dict[str, Dict[str, Any]] = {}
|
||||
for k in CFG_ENGINE_KEYS:
|
||||
out[k] = {
|
||||
"from": src,
|
||||
"applied": applied.get(k),
|
||||
"updow_stock": stock_cfg.get(k) if stock_cfg else None,
|
||||
"env": env_fallback.get(k),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def effective_updow_tf_for_code(db: Any, code: str, env_tf: int) -> int:
|
||||
"""``updow_stock_config.tf_min`` 우선."""
|
||||
meta = get_updow_stock_meta(db, code)
|
||||
if meta:
|
||||
try:
|
||||
iv = int(float(meta.get("tf_min", 0)))
|
||||
if iv >= 1:
|
||||
return iv
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
et = int(env_tf)
|
||||
except (TypeError, ValueError):
|
||||
et = 0
|
||||
if et < 1:
|
||||
et = get_env_int("UPDOW_TF_MIN", 60)
|
||||
return et
|
||||
|
||||
|
||||
def effective_updow_tf_min(row: Optional[Dict[str, Any]], env_tf: int) -> int:
|
||||
"""
|
||||
종목별 ``holding_stock_config.updow_tf_min`` — 양의 정수면 해당 분봉,
|
||||
NULL·1미만·변환 실패 시 ``env_tf`` (보통 env ``UPDOW_TF_MIN`` 스냅샷 값).
|
||||
"""
|
||||
try:
|
||||
et = int(env_tf)
|
||||
except (TypeError, ValueError):
|
||||
et = 0
|
||||
if et < 1:
|
||||
et = get_env_int("UPDOW_TF_MIN", 60)
|
||||
if not row:
|
||||
return et
|
||||
raw = row.get("updow_tf_min")
|
||||
if raw is None:
|
||||
return et
|
||||
try:
|
||||
iv = int(float(raw))
|
||||
except (TypeError, ValueError):
|
||||
return et
|
||||
if iv < 1:
|
||||
return et
|
||||
return iv
|
||||
Reference in New Issue
Block a user