ls증권 히스토리 구독 넣음

This commit is contained in:
Your Name
2026-07-30 18:05:07 +09:00
parent 61bec4bd1d
commit 67eab24603
1593 changed files with 135733 additions and 1232 deletions

View File

@@ -0,0 +1,378 @@
"""
kis_trader/strategies/us_momentum_stock_cfg.py
==============================================
해외 모멘텀 종목별 파라미터 — ``us_momentum_stock_config`` (박스엔진/DBBAND 패턴).
- 유니버스는 계속 ``permanent_subscriptions`` (market=US).
- 종목 행이 있으면 TRIGGER/청산 축만 덮어씀. 없으면 전역 ``US_MOMENTUM_*``.
- 포트 한도(TOTAL_BUDGET/MAX_STOCKS)·세션 공통은 전역 유지.
"""
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
logger = logging.getLogger("kis_trader.us_momentum_stock_cfg")
# 엔진 params 키 ↔ 테이블 컬럼 (UI % 숫자로 저장하는 축은 *_pct)
STOCK_ENGINE_FLOAT_PCT = (
# DB에 퍼센트 숫자(1.5)로 저장 → 엔진 비율(0.015)
("sl_pct", "sl_pct"),
("tp_pct", "tp_pct"),
("tp_max_pct", "tp_max_pct"),
("shoulder_min_high", "shoulder_min_high_pct"),
("shoulder_cut_pct", "shoulder_cut_pct"),
("trail_pct", "trail_pct"),
("trail_arm_pct", "trail_arm_pct"),
)
STOCK_ENGINE_FLOAT = (
("mom_rsi_min", "rsi_min"),
("mom_rsi_max", "rsi_max"),
("mom_vol_mult", "vol_mult"),
("pullback_min_pct", "pullback_min_pct"),
("pullback_max_pct", "pullback_max_pct"),
("setup_vol_max_mult", "setup_vol_max_mult"),
("high_chase_thr", "high_chase_thr"),
("max_daily_chg", "max_daily_chg"),
("min_price", "min_price"),
)
STOCK_ENGINE_INT = (
("mom_vol_win", "vol_win"),
("chase_lookback_min", "chase_lookback_min"),
("pullback_lookback_min", "pullback_lookback_min"),
("setup_bear_bars_min", "setup_bear_bars_min"),
("max_hold_bars", "max_hold_bars"),
("max_daily", "max_daily"),
("ema_fast_period", "ema_fast_period"),
("ema_slow_period", "ema_slow_period"),
("slot_money", "slot_money"),
("cooldown_sec", "cooldown_sec"), # → cooldown_min
)
STOCK_ENGINE_BOOL = (
("use_defense_filters", "use_defense_filters"),
("use_high_chase_filter", "use_high_chase_filter"),
("use_daily_range_filter", "use_daily_range_filter"),
("use_ema_filter", "use_ema_filter"),
("use_rsi_max_filter", "use_rsi_max_filter"),
("pattern_breakout", "pattern_breakout"),
("pattern_pullback", "pattern_pullback"),
)
_DDL = """
CREATE TABLE IF NOT EXISTS us_momentum_stock_config (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(32) NOT NULL,
exchange VARCHAR(16) NOT NULL DEFAULT 'NASD',
symbol VARCHAR(32) NOT NULL DEFAULT '',
name VARCHAR(64) DEFAULT '',
stock_group VARCHAR(16) NOT NULL DEFAULT 'STOCK',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
sl_pct DOUBLE NULL,
tp_pct DOUBLE NULL,
tp_max_pct DOUBLE NULL,
shoulder_min_high_pct DOUBLE NULL,
shoulder_cut_pct DOUBLE NULL,
trail_pct DOUBLE NULL,
trail_arm_pct DOUBLE NULL,
ratchet_tiers VARCHAR(255) NULL,
max_hold_bars INT NULL,
cooldown_sec INT NULL,
max_daily INT NULL,
slot_money DOUBLE NULL,
rsi_min DOUBLE NULL,
rsi_max DOUBLE NULL,
vol_mult DOUBLE NULL,
vol_win INT NULL,
chase_lookback_min INT NULL,
pullback_lookback_min INT NULL,
pullback_min_pct DOUBLE NULL,
pullback_max_pct DOUBLE NULL,
setup_vol_max_mult DOUBLE NULL,
setup_bear_bars_min INT NULL,
high_chase_thr DOUBLE NULL,
max_daily_chg DOUBLE NULL,
min_price DOUBLE NULL,
ema_fast_period INT NULL,
ema_slow_period INT NULL,
use_defense_filters TINYINT NULL,
use_high_chase_filter TINYINT NULL,
use_daily_range_filter TINYINT NULL,
use_ema_filter TINYINT NULL,
use_rsi_max_filter TINYINT NULL,
pattern_breakout TINYINT NULL,
pattern_pullback TINYINT NULL,
UNIQUE KEY uk_usmom_code (code),
KEY idx_usmom_group (stock_group)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='US_MOMENTUM 종목별 TRIGGER/청산 (NULL=전역 US_MOMENTUM_* 폴백)'
"""
def _trade_db_core(db: Any):
return getattr(db, "raw", db)
def ensure_us_momentum_stock_config_table(db: Any) -> None:
raw = _trade_db_core(db)
raw.conn.execute(_DDL.strip())
try:
raw.conn.commit()
except Exception:
pass
def _pct_to_ratio(x: float) -> float:
"""UI/테이블 % (1.5) → 엔진 비율 (0.015). 이미 소수면 그대로."""
ax = abs(float(x))
if ax == 0:
return 0.0
return ax if ax < 0.5 else ax / 100.0
def get_us_momentum_stock_row(db: Any, code: str) -> Optional[Dict[str, Any]]:
ensure_us_momentum_stock_config_table(db)
cu = str(code or "").strip().upper()
if not cu:
return None
raw = _trade_db_core(db)
try:
row = raw.conn.execute(
"SELECT * FROM us_momentum_stock_config WHERE code=%s",
(cu,),
).fetchone()
return dict(row) if row else None
except Exception as e:
logger.debug("get_us_momentum_stock_row 실패 %s: %s", cu, e)
return None
def list_us_momentum_stock_rows(db: Any) -> List[Dict[str, Any]]:
ensure_us_momentum_stock_config_table(db)
raw = _trade_db_core(db)
try:
rows = raw.conn.execute(
"SELECT * FROM us_momentum_stock_config ORDER BY stock_group, code"
).fetchall()
return [dict(r) for r in (rows or [])]
except Exception as e:
logger.warning("list_us_momentum_stock_rows 실패: %s", e)
return []
def apply_us_momentum_stock_overlay(
db: Any,
code: str,
base_params: Dict[str, Any],
) -> Dict[str, Any]:
"""전역 엔진 params 위에 종목 행(NULL 아닌 컬럼만) 덮어씀."""
out = dict(base_params or {})
row = get_us_momentum_stock_row(db, code)
if not row:
return out
out["_us_stock_cfg"] = True
out["_us_stock_group"] = str(row.get("stock_group") or "STOCK")
if row.get("exchange"):
out["_us_exchange"] = str(row["exchange"]).strip().upper()
for eng, col in STOCK_ENGINE_FLOAT_PCT:
v = row.get(col)
if v is None or v == "":
continue
try:
out[eng] = _pct_to_ratio(float(v))
except (TypeError, ValueError):
pass
for eng, col in STOCK_ENGINE_FLOAT:
v = row.get(col)
if v is None or v == "":
continue
try:
out[eng] = float(v)
except (TypeError, ValueError):
pass
for eng, col in STOCK_ENGINE_INT:
v = row.get(col)
if v is None or v == "":
continue
try:
if eng == "cooldown_sec":
out["cooldown_min"] = float(v) / 60.0
else:
out[eng] = int(float(v))
except (TypeError, ValueError):
pass
for eng, col in STOCK_ENGINE_BOOL:
v = row.get(col)
if v is None or v == "":
continue
try:
out[eng] = bool(int(v))
except (TypeError, ValueError):
out[eng] = bool(v)
rt = row.get("ratchet_tiers")
if rt is not None and str(rt).strip() != "":
out["ratchet_tiers"] = str(rt).strip()
return out
def upsert_us_momentum_stock_config(
db: Any,
code: str,
*,
exchange: str = "NASD",
symbol: str = "",
name: str = "",
stock_group: str = "STOCK",
fields: Optional[Dict[str, Any]] = None,
) -> bool:
"""종목 행 UPSERT. fields 의 키=테이블 컬럼명, None 은 NULL(전역 폴백)."""
ensure_us_momentum_stock_config_table(db)
cu = str(code or "").strip().upper()
if not cu:
return False
sym = str(symbol or cu).strip().upper() or cu
exch = str(exchange or "NASD").strip().upper() or "NASD"
grp = str(stock_group or "STOCK").strip().upper() or "STOCK"
if grp not in ("ETF", "STOCK", "OTHER"):
grp = "STOCK"
fields = dict(fields or {})
raw = _trade_db_core(db)
# 기존 행 — Optuna 부분 apply 시 미전달 컬럼을 NULL 로 지우지 않음
existing = get_us_momentum_stock_row(db, cu) or {}
cols = [
"code", "exchange", "symbol", "name", "stock_group",
"sl_pct", "tp_pct", "tp_max_pct",
"shoulder_min_high_pct", "shoulder_cut_pct",
"trail_pct", "trail_arm_pct", "ratchet_tiers",
"max_hold_bars", "cooldown_sec", "max_daily", "slot_money",
"rsi_min", "rsi_max", "vol_mult", "vol_win",
"chase_lookback_min", "pullback_lookback_min",
"pullback_min_pct", "pullback_max_pct",
"setup_vol_max_mult", "setup_bear_bars_min",
"high_chase_thr", "max_daily_chg", "min_price",
"ema_fast_period", "ema_slow_period",
"use_defense_filters", "use_high_chase_filter",
"use_daily_range_filter", "use_ema_filter", "use_rsi_max_filter",
"pattern_breakout", "pattern_pullback",
]
vals: Dict[str, Any] = {
"code": cu,
"exchange": exch,
"symbol": sym,
"name": str(name or cu),
"stock_group": grp,
}
for c in cols:
if c in ("code", "exchange", "symbol", "name", "stock_group"):
continue
if c in fields:
vals[c] = fields[c]
elif c in existing:
vals[c] = existing.get(c)
else:
vals[c] = None
placeholders = ", ".join(["%s"] * len(cols))
col_sql = ", ".join(cols)
updates = ", ".join(
f"{c}=VALUES({c})" for c in cols if c != "code"
)
try:
raw.conn.execute(
f"INSERT INTO us_momentum_stock_config ({col_sql}) VALUES ({placeholders}) "
f"ON DUPLICATE KEY UPDATE {updates}",
tuple(vals.get(c) for c in cols),
)
raw.conn.commit()
return True
except Exception as e:
logger.error("upsert_us_momentum_stock_config 실패 %s: %s", cu, e)
return False
def delete_us_momentum_stock_config(db: Any, code: str) -> bool:
ensure_us_momentum_stock_config_table(db)
cu = str(code or "").strip().upper()
if not cu:
return False
raw = _trade_db_core(db)
try:
raw.conn.execute(
"DELETE FROM us_momentum_stock_config WHERE code=%s", (cu,),
)
raw.conn.commit()
return True
except Exception as e:
logger.warning("delete_us_momentum_stock_config 실패 %s: %s", cu, e)
return False
def seed_us_momentum_stock_from_permanent(db: Any) -> int:
"""permanent US 활성 종목 중 행 없는 것만 STOCK/ETF 추정으로 빈 행 추가. 반환=신규 수."""
ensure_us_momentum_stock_config_table(db)
try:
from permanent_subs import codes_by_market
us_list = codes_by_market(db, "US", enabled_only=True) or []
except Exception as e:
logger.debug("seed permanent 조회 실패: %s", e)
return 0
n = 0
etf_hint = {"QQQ", "QQQM", "SPY", "SPYM", "IVV", "VOO", "DIA", "IWM", "TQQQ", "SQQQ"}
for row in us_list:
code = str(row.get("code") or row.get("symbol") or "").strip().upper()
if not code:
continue
if get_us_momentum_stock_row(db, code):
continue
grp = "ETF" if code in etf_hint or code.endswith("M") and len(code) <= 5 else "STOCK"
# QQQM/SPYM 명시
if code in ("QQQM", "SPYM", "QQQ", "SPY"):
grp = "ETF"
ok = upsert_us_momentum_stock_config(
db,
code,
exchange=str(row.get("exchange") or "NASD"),
symbol=str(row.get("symbol") or code),
name=str(row.get("note") or row.get("name") or code),
stock_group=grp,
fields={}, # 전부 NULL → 전역 폴백
)
if ok:
n += 1
return n
def row_to_ui_dict(row: Dict[str, Any]) -> Dict[str, Any]:
"""웹 폼용 — NULL 유지(빈칸=전역)."""
if not row:
return {}
out = {
"code": row.get("code"),
"exchange": row.get("exchange"),
"symbol": row.get("symbol"),
"name": row.get("name"),
"stock_group": row.get("stock_group") or "STOCK",
}
for k in (
"sl_pct", "tp_pct", "tp_max_pct",
"shoulder_min_high_pct", "shoulder_cut_pct",
"trail_pct", "trail_arm_pct", "ratchet_tiers",
"max_hold_bars", "cooldown_sec", "max_daily", "slot_money",
"rsi_min", "rsi_max", "vol_mult", "vol_win",
"chase_lookback_min", "pullback_lookback_min",
"pullback_min_pct", "pullback_max_pct",
"setup_vol_max_mult", "setup_bear_bars_min",
"high_chase_thr", "max_daily_chg", "min_price",
"ema_fast_period", "ema_slow_period",
"use_defense_filters", "use_high_chase_filter",
"use_daily_range_filter", "use_ema_filter", "use_rsi_max_filter",
"pattern_breakout", "pattern_pullback",
):
out[k] = row.get(k)
return out