Changes: - Introduced the `e_min_chg_pct` parameter to define the minimum price change percentage compared to the previous day's close, enhancing the momentum trading strategy. - Updated various functions and classes to incorporate this new parameter, ensuring it is utilized in both backtesting and live trading scenarios. - Improved documentation and comments to clarify the purpose and usage of the new parameter across the codebase. Impact: - This addition allows for more precise control over trading conditions, potentially increasing the effectiveness of the momentum strategy while maintaining system integrity and performance.
916 lines
47 KiB
Python
916 lines
47 KiB
Python
"""
|
||
kis_trader/web/live_config_schema.py — 실매 운영 설정 웹 탭 필드 정의
|
||
======================================================================
|
||
backtest_web ``/api/live_config`` 가 이 스키마로 폼을 생성·저장한다.
|
||
키는 env/DB 와 1:1 — 하드코딩 임계값 없음.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Dict, List, Optional, Tuple
|
||
|
||
# 웹 탭에 노출할 전략 ID (kis_trader 봇)
|
||
# ※ SCALP·RANGE_BREAK·DBBAND 는 사용자 지정으로 숨김 (strategy_ids.HIDDEN_STRATEGY_IDS)
|
||
# → 운영설정 ON/OFF·운용한도·일일익절·휩쏘 폼에서 제외. 실거래 중인 SHORT·BREAKOUT·MOMENTUM + UPDOW 만 노출.
|
||
LIVE_STRATEGY_IDS: Tuple[str, ...] = (
|
||
"SHORT",
|
||
"MOMENTUM",
|
||
"BREAKOUT",
|
||
"UPDOW",
|
||
)
|
||
|
||
LIVE_STRATEGY_LABELS: Dict[str, str] = {
|
||
"SCALP": "스캘핑",
|
||
"SHORT": "꼬리잡기",
|
||
"MOMENTUM": "모멘텀",
|
||
"BREAKOUT": "돌파",
|
||
"RANGE_BREAK": "박스권돌파",
|
||
"UPDOW": "UPDOWN 박스",
|
||
"DBBAND": "더블BB",
|
||
"DART": "DART 수주",
|
||
}
|
||
|
||
# 전략 ID → 실제 env 키 (SHORT 는 TAIL_* 가 canonical — tail_engine·config_short)
|
||
STRATEGY_BUDGET_ENV_KEY: Dict[str, Dict[str, str]] = {
|
||
"SHORT": {
|
||
"TOTAL_BUDGET_KRW": "TAIL_TOTAL_BUDGET_KRW",
|
||
"SLOT_MONEY": "TAIL_SLOT_MONEY",
|
||
"MAX_STOCKS": "TAIL_MAX_STOCKS",
|
||
},
|
||
}
|
||
|
||
# UI 조회 시 이전 키·별칭 폴백 (표시만 — 저장은 canonical)
|
||
BUDGET_READ_FALLBACKS: Dict[str, Tuple[str, ...]] = {
|
||
"TAIL_SLOT_MONEY": (
|
||
"SLOT_MONEY_DEFAULT",
|
||
"SHORT_SLOT_MONEY",
|
||
"MAX_BUY_AMOUNT_PER_STOCK",
|
||
),
|
||
"TAIL_TOTAL_BUDGET_KRW": ("SHORT_TOTAL_BUDGET_KRW",),
|
||
"TAIL_MAX_STOCKS": ("SHORT_MAX_STOCKS", "MAX_STOCKS"),
|
||
}
|
||
|
||
# TAIL canonical 저장 시 실매 루프가 읽는 별칭에 동기화
|
||
TAIL_BUDGET_SAVE_MIRROR: Dict[str, str] = {
|
||
"TAIL_SLOT_MONEY": "SLOT_MONEY_DEFAULT",
|
||
"TAIL_MAX_STOCKS": "SHORT_MAX_STOCKS",
|
||
"TAIL_TOTAL_BUDGET_KRW": "SHORT_TOTAL_BUDGET_KRW",
|
||
}
|
||
|
||
# type: bool | int | float | pct | text
|
||
FieldDef = Dict[str, Any]
|
||
GroupDef = Dict[str, Any]
|
||
|
||
|
||
def _f(key: str, label: str, ftype: str = "float", **extra: Any) -> FieldDef:
|
||
d: FieldDef = {"key": key, "label": label, "type": ftype}
|
||
d.update(extra)
|
||
return d
|
||
|
||
|
||
def _sid_key(sid: str, suffix: str) -> str:
|
||
aliases = STRATEGY_BUDGET_ENV_KEY.get(sid.upper(), {})
|
||
if suffix in aliases:
|
||
return aliases[suffix]
|
||
return f"{sid}_{suffix}"
|
||
|
||
|
||
def read_snap_value(snap: Dict[str, Any], key: str, ftype: str) -> Any:
|
||
"""canonical 키 + 폴백 키 순으로 UI 값 해석."""
|
||
keys = (key,) + BUDGET_READ_FALLBACKS.get(key, ())
|
||
for k in keys:
|
||
v = snap_value_to_ui(snap.get(k), ftype)
|
||
if v is not None:
|
||
return v
|
||
return None
|
||
|
||
|
||
def expand_live_config_save_patch(patch: Dict[str, str]) -> Dict[str, str]:
|
||
"""TAIL_* 저장 시 tail_catch·BaseStrategy 가 읽는 별칭 키 동기화."""
|
||
out = dict(patch)
|
||
for src, dst in TAIL_BUDGET_SAVE_MIRROR.items():
|
||
if src in out and dst not in out:
|
||
out[dst] = out[src]
|
||
return out
|
||
|
||
|
||
def build_live_config_groups() -> List[GroupDef]:
|
||
"""정적 그룹 + 전략 반복 그룹 메타."""
|
||
groups: List[GroupDef] = [
|
||
{
|
||
"id": "daily_profit_global",
|
||
"title": "일일 익절 — 총합 (규칙·수동중단)",
|
||
"hint": (
|
||
"손익 감시 ON/OFF 는 전략별(*_DAILY_PROFIT_TARGET_ENABLED)만 사용. "
|
||
"여기 마스터는 총합 목표금액/%·수동 매수잠금·알림·트레일 규칙. "
|
||
"① 신규매수 수동 중단 = 목표·손익과 무관 매수만 잠금(긴급용, 기본 OFF). "
|
||
"매도·손절은 항상 유지"
|
||
),
|
||
"fields": [
|
||
_f("DAILY_PROFIT_TARGET_KRW", "목표 금액(원)", "int", default=0),
|
||
_f("DAILY_PROFIT_TARGET_PCT", "목표 수익률(%)", "float", default=0,
|
||
hint="퍼센트 그대로 (예: 2.5 = 2.5%) · 0=미사용"),
|
||
_f(
|
||
"DAILY_PROFIT_TARGET_BUDGET_KRW",
|
||
"분모 운용한도(원)",
|
||
"int",
|
||
default=0,
|
||
hint="0 = ON 전략 TOTAL_BUDGET 합 자동",
|
||
),
|
||
_f(
|
||
"DAILY_PROFIT_HALT_NEW_BUYS",
|
||
"신규매수 수동 중단",
|
||
"bool",
|
||
default=False,
|
||
hint=(
|
||
"목표·손익과 무관한 수동 매수 잠금. "
|
||
"ON=즉시 신규매수 전면 차단 / OFF=정상 (기본)"
|
||
),
|
||
),
|
||
_f("DAILY_PROFIT_NOTIFY_MM", "달성 시 MM 알림", "bool", default=True),
|
||
_f("DAILY_PROFIT_MODE", "익절 방식", "text", default="fixed",
|
||
hint="fixed=고정목표 / trailing=고점대비 되돌림 / both=둘 중 먼저"),
|
||
_f("DAILY_PROFIT_TRAIL_TIERS", "다단 트레일(수익원:컷%,…)", "text", default="",
|
||
hint="벌수록 타이트하게 사수 · 예: 30000:50,100000:35,300000:25 · 비우면 단일 되돌림 사용"),
|
||
_f("DAILY_PROFIT_TRAIL_DROP_PCT", "단일 트레일 되돌림(%, 고점대비)", "float", default=0,
|
||
hint="다단 비었을 때만 · 예: 35 = 고점 대비 -35% 꺾이면 중단 · 0=미사용"),
|
||
_f("DAILY_PROFIT_TRAIL_ARM_KRW", "트레일 발동 최소수익(원)", "int", default=0,
|
||
hint="이 이상 벌어야 트레일 감시 시작 · 0=트레일 비활성(과민발동 방지)"),
|
||
_f("DAILY_PROFIT_TRAIL_ARM_PCT", "트레일 발동 최소수익(%)", "float", default=0,
|
||
hint="운용한도 대비 % · KRW와 둘 중 큰 값"),
|
||
],
|
||
},
|
||
{
|
||
"id": "risk_global",
|
||
"title": "리스크 — 계정 공통",
|
||
"hint": "env_config",
|
||
"fields": [
|
||
_f("DAILY_STOP_LOSS_PCT", "일일 손실 한도(분율, -0.05=-5%)", "float", default=0),
|
||
_f("CONSECUTIVE_LOSS_LIMIT", "연속 손실 제한(회)", "int", default=0),
|
||
_f("USE_RISK_CHECK", "리스크 체크 사용", "bool"),
|
||
_f("EXCLUDE_NON_STOCK", "비본주(우선주·ETF·ETN) 매수 제외", "bool", default=True,
|
||
hint="후보 name==code 여도 코드 끝 5/7/9·K/L… 로 우선주 차단"),
|
||
_f("NON_STOCK_PREF_CODE_SUFFIXES", "우선주 코드 끝자리(숫자)", "str", default="5,7,9",
|
||
hint="KRX 구형 우선주. 콤마 구분"),
|
||
_f("NON_STOCK_EXCLUDE_ALPHA_SUFFIX", "코드 끝 알파벳=우선주 제외", "bool", default=True,
|
||
hint="00088K·00781K 등"),
|
||
],
|
||
},
|
||
{
|
||
"id": "ws_trigger_global",
|
||
"title": "TRIGGER 필터 — WS 공통",
|
||
"hint": (
|
||
"호가(0D)·프로그램(0w) · env_config(컬럼 없으면 env_config_ext) · "
|
||
"COLLECT=판정순간1장(filter_eval) / SAVE=후보~3초시계열(kiwoom_0d) — 둘은 다름"
|
||
),
|
||
"fields": [
|
||
_f("KIWOOM_WS_ORDERBOOK_ENABLED", "호가 WS(0D) 구독", "bool", default=True,
|
||
hint="키움 실시간 호가 수신(RAM) · 꺼면 필터·수집·시계열 모두 불가"),
|
||
_f("KIWOOM_WS_PROGRAM_ENABLED", "프로그램 WS(0w) 구독", "bool", default=True,
|
||
hint="키움 프로그램매매 수신(RAM)"),
|
||
_f(
|
||
"WS_TRIGGER_EVAL_SAVE_ENABLED",
|
||
"TRIGGER 판정 DB 저장(마스터)",
|
||
"bool",
|
||
default=True,
|
||
hint="OFF면 아래 COLLECT(판정 스냅) 전부 안 씀 · SAVE(시계열)와 무관",
|
||
),
|
||
_f(
|
||
"WS_ORDERBOOK_COLLECT_ENABLED",
|
||
"호가 판정스냅(FILTER_EVAL) — 매수체크 순간 1장",
|
||
"bool",
|
||
default=True,
|
||
hint="≠ 연속시계열. TRIGGER 판정 때만 ws_orderbook source=filter_eval · 필터 OFF여도 저장 가능",
|
||
),
|
||
_f(
|
||
"WS_ORDERBOOK_SAVE_ENABLED",
|
||
"호가 시계열(KIWOOM_0D) — 구독후보 ~3초 덤프",
|
||
"bool",
|
||
default=False,
|
||
hint="틱처럼 후보·보유만 연속 저장(source=kiwoom_0d) · 파람서치 호가축용 · 봇 재시작 필요 · 필터와 무관",
|
||
),
|
||
_f(
|
||
"WS_TRIGGER_SNAPSHOT_DB_INTERVAL_SEC",
|
||
"호가/프로그램 시계열 간격(초)",
|
||
"float",
|
||
default=3.0,
|
||
hint="SAVE ON일 때만 · 종목당 최소 저장 간격 · 봇 재시작 필요",
|
||
),
|
||
# 글로벌 ORDERBOOK_FILTER / MAX_SPREAD 폐기 → 아래「호가필터 — 전략별」
|
||
_f(
|
||
"WS_PROGRAM_COLLECT_ENABLED",
|
||
"프로그램 판정스냅 — 매수체크 순간 1장",
|
||
"bool",
|
||
default=True,
|
||
hint="≠ 연속시계열. 판정 때만 ws_program · 필터 OFF여도 가능",
|
||
),
|
||
_f(
|
||
"WS_PROGRAM_SAVE_ENABLED",
|
||
"프로그램 시계열 — 구독후보 ~3초 덤프",
|
||
"bool",
|
||
default=False,
|
||
hint="SAVE와 동일 Recorder · 봇 재시작 필요 · 필터와 무관",
|
||
),
|
||
_f("PROGRAM_FILTER_ENABLED", "프로그램 필터(TRIGGER) — 실매 탈락", "bool", default=True),
|
||
_f("WHIPSAW_FILTER_ENABLED", "휩쏘 필터(글로벌)", "bool", default=True),
|
||
_f("WHIPSAW_SUBBAR_SEC", "휩쏘 미니봉(초)", "int", default=30,
|
||
hint="틱을 N초 봉으로 묶음 · 실매 기본 30 · Optuna 꼬리 TPE 탐색"),
|
||
_f("WHIPSAW_LOOKBACK_SEC", "휩쏘 룩백(초)", "int", default=90,
|
||
hint="신호 직전 몇 초를 볼지 · 실매 기본 90"),
|
||
_f("WHIPSAW_DIP_PCT", "휩쏘 하락폭(분율, 0.003=0.3%)", "float", default=0.003),
|
||
_f("WHIPSAW_RECOVERY_TOL_PCT", "휩쏘 회복 허용(분율, 0.001=0.1%)", "float", default=0.001),
|
||
],
|
||
},
|
||
]
|
||
|
||
# 호가필터 — 전략별 (글로벌 ORDERBOOK_* 수치 폐기)
|
||
# SCALP 는 LIVE_STRATEGY_IDS 에서 숨김이어도 config_scalp 키는 노출
|
||
from kis_trader.engine.orderbook_env import (
|
||
OB_DEFAULT_ASK_WALL_MAX_QTY,
|
||
OB_DEFAULT_ENTRY_ASK_MAX_MULT,
|
||
OB_DEFAULT_ENTRY_BID_DEPTH_MULT,
|
||
OB_DEFAULT_ENTRY_BID_LEVELS,
|
||
OB_DEFAULT_FILTER_ENABLED,
|
||
OB_DEFAULT_MAX_SPREAD_PCT,
|
||
OB_DEFAULT_MIN_BID_ASK_RATIO,
|
||
OB_STRATEGY_PREFIXES,
|
||
)
|
||
ob_fields: List[FieldDef] = []
|
||
_ob_lbl = {
|
||
"SCALP": "스캘핑",
|
||
"TAIL": "꼬리잡기",
|
||
"MOMENTUM": "모멘텀",
|
||
"BREAKOUT": "돌파",
|
||
}
|
||
for pfx in OB_STRATEGY_PREFIXES:
|
||
lbl = _ob_lbl.get(pfx, pfx)
|
||
ob_fields.extend([
|
||
_f(
|
||
f"{pfx}_ORDERBOOK_FILTER_ENABLED",
|
||
f"{lbl} 호가필터 ON",
|
||
"bool",
|
||
default=OB_DEFAULT_FILTER_ENABLED,
|
||
hint="ON=스프레드·잔량비·얇은호가·매도벽으로 매수 차단 · OFF=저장만",
|
||
),
|
||
_f(
|
||
f"{pfx}_ORDERBOOK_MAX_SPREAD_PCT",
|
||
f"{lbl} 스프레드상한(%)",
|
||
"float",
|
||
default=OB_DEFAULT_MAX_SPREAD_PCT,
|
||
hint="0.45=0.45% · 그리드 max_spread_pct 앵커",
|
||
),
|
||
_f(
|
||
f"{pfx}_ORDERBOOK_MIN_BID_ASK_RATIO",
|
||
f"{lbl} 매수/매도잔량비 하한",
|
||
"float",
|
||
default=OB_DEFAULT_MIN_BID_ASK_RATIO,
|
||
hint="그리드 min_bid_ask_ratio",
|
||
),
|
||
_f(
|
||
f"{pfx}_ORDERBOOK_ENTRY_ASK_MAX_MULT",
|
||
f"{lbl} 매도벽 허용배수",
|
||
"float",
|
||
default=OB_DEFAULT_ENTRY_ASK_MAX_MULT,
|
||
hint="필요수량×N · 그리드 ask_max_mult",
|
||
),
|
||
_f(
|
||
f"{pfx}_ORDERBOOK_ENTRY_BID_DEPTH_MULT",
|
||
f"{lbl} 매수호가깊이 배수",
|
||
"float",
|
||
default=OB_DEFAULT_ENTRY_BID_DEPTH_MULT,
|
||
hint="얇은호가 탈락 · 필요수량×N",
|
||
),
|
||
_f(
|
||
f"{pfx}_ORDERBOOK_ENTRY_BID_LEVELS",
|
||
f"{lbl} 호가단계(N호가)",
|
||
"int",
|
||
default=OB_DEFAULT_ENTRY_BID_LEVELS,
|
||
),
|
||
])
|
||
if pfx == "BREAKOUT":
|
||
ob_fields.append(
|
||
_f(
|
||
"BREAKOUT_ORDERBOOK_ASK_WALL_MAX_QTY",
|
||
"돌파 매도벽 상한(주)",
|
||
"int",
|
||
default=OB_DEFAULT_ASK_WALL_MAX_QTY,
|
||
hint="그리드 ask_wall_max_qty",
|
||
),
|
||
)
|
||
groups.append({
|
||
"id": "orderbook_strategy",
|
||
"title": "호가필터 — 전략별 (수치)",
|
||
"hint": (
|
||
"글로벌 ORDERBOOK_MAX_SPREAD_PCT 등 폐기 · "
|
||
"파람서치 apply-best ↔ *_ORDERBOOK_* 정합"
|
||
),
|
||
"fields": ob_fields,
|
||
})
|
||
|
||
groups.append({
|
||
"id": "strategy_switch",
|
||
"title": "전략 ON/OFF",
|
||
"hint": "config_{strategy} · 봇 재시작 없이 다음 루프 반영",
|
||
"fields": [
|
||
_f(f"STRATEGY_{sid}_ENABLED", LIVE_STRATEGY_LABELS.get(sid, sid), "bool")
|
||
for sid in LIVE_STRATEGY_IDS
|
||
],
|
||
})
|
||
|
||
# 유니버스 소스 — ranking | condition | kiwoom_condition | ls_condition
|
||
_univ_choices = [
|
||
{"value": "kiwoom_condition", "label": "키움 조건(WS)"},
|
||
{"value": "ls_condition", "label": "LS 조건(AFR)"},
|
||
{"value": "condition", "label": "한투 조건(REST)"},
|
||
{"value": "ranking", "label": "거래량순위"},
|
||
]
|
||
univ_fields: List[FieldDef] = []
|
||
for sid in LIVE_STRATEGY_IDS:
|
||
lbl = LIVE_STRATEGY_LABELS.get(sid, sid)
|
||
univ_fields.append(
|
||
_f(
|
||
f"{sid}_UNIVERSE_SOURCE",
|
||
f"{lbl} 유니버스",
|
||
"select",
|
||
default="kiwoom_condition",
|
||
choices=_univ_choices,
|
||
hint=(
|
||
"ls_condition=LS 동명 AFR · 시세는 키움/한투 유지 · "
|
||
"매니저 최초 기동은 봇 재시작 필요할 수 있음 · "
|
||
"키움으로 자동 폴백 안 함"
|
||
),
|
||
),
|
||
)
|
||
groups.append({
|
||
"id": "universe_source",
|
||
"title": "유니버스 소스",
|
||
"hint": "{SID}_UNIVERSE_SOURCE · 기본 키움 · LS는 opt-in",
|
||
"fields": univ_fields,
|
||
})
|
||
|
||
|
||
# 전략별 운용한도
|
||
budget_fields: List[FieldDef] = []
|
||
for sid in LIVE_STRATEGY_IDS:
|
||
lbl = LIVE_STRATEGY_LABELS.get(sid, sid)
|
||
budget_fields.extend([
|
||
_f(_sid_key(sid, "TOTAL_BUDGET_KRW"), f"{lbl} 총 한도(원)", "int"),
|
||
_f(_sid_key(sid, "SLOT_MONEY"), f"{lbl} 슬롯(원)", "int"),
|
||
_f(_sid_key(sid, "MAX_STOCKS"), f"{lbl} 동시 종목", "int"),
|
||
])
|
||
groups.append({
|
||
"id": "strategy_budget",
|
||
"title": "전략별 운용한도",
|
||
"hint": "config_{strategy}",
|
||
"fields": budget_fields,
|
||
})
|
||
|
||
# 전략별 일일 익절 서브
|
||
dp_fields: List[FieldDef] = []
|
||
for sid in LIVE_STRATEGY_IDS:
|
||
lbl = LIVE_STRATEGY_LABELS.get(sid, sid)
|
||
dp_fields.extend([
|
||
_f(
|
||
_sid_key(sid, "DAILY_PROFIT_TARGET_ENABLED"),
|
||
f"{lbl} 손익 감시 켜기",
|
||
"bool",
|
||
hint="OFF = 해당 전략 일일익절 비활성 (매수 차단 없음)",
|
||
),
|
||
_f(
|
||
_sid_key(sid, "DAILY_PROFIT_TARGET_KRW"),
|
||
f"{lbl} 목표(원)",
|
||
"int",
|
||
),
|
||
_f(
|
||
_sid_key(sid, "DAILY_PROFIT_TARGET_PCT"),
|
||
f"{lbl} 목표(%)",
|
||
"float",
|
||
hint="퍼센트 그대로 (예: 2.5 = 2.5%) · 0=미사용",
|
||
),
|
||
_f(
|
||
_sid_key(sid, "DAILY_PROFIT_MODE"),
|
||
f"{lbl} 익절방식",
|
||
"text",
|
||
hint="비우면 마스터 상속 · fixed/trailing/both",
|
||
),
|
||
_f(
|
||
_sid_key(sid, "DAILY_PROFIT_TRAIL_TIERS"),
|
||
f"{lbl} 다단 트레일",
|
||
"text",
|
||
hint="수익원:컷%,… · 비우면 마스터 상속 · 예: 30000:50,100000:35,300000:25",
|
||
),
|
||
_f(
|
||
_sid_key(sid, "DAILY_PROFIT_TRAIL_DROP_PCT"),
|
||
f"{lbl} 단일 트레일 되돌림(%)",
|
||
"float",
|
||
hint="다단 비었을 때만 · 고점 대비 % · 비우면 마스터 상속 · 0=미사용",
|
||
),
|
||
_f(
|
||
_sid_key(sid, "DAILY_PROFIT_TRAIL_ARM_KRW"),
|
||
f"{lbl} 트레일 발동수익(원)",
|
||
"int",
|
||
hint="이 이상 벌어야 트레일 감시 · 비우면 마스터 상속",
|
||
),
|
||
_f(
|
||
_sid_key(sid, "DAILY_PROFIT_HALT_NEW_BUYS"),
|
||
f"{lbl} 신규매수 수동 중단",
|
||
"bool",
|
||
default=False,
|
||
hint="목표 무관 수동 잠금 · 비우면 마스터 상속 · OFF=정상",
|
||
),
|
||
_f(
|
||
_sid_key(sid, "DAILY_PROFIT_RISK_BUDGET_ENABLED"),
|
||
f"{lbl} 리스크버짓(B)",
|
||
"bool",
|
||
default=False,
|
||
hint="ON=일일익절 hit 후 보유최악손절합>cushion 이면 전량정리 · 기본 OFF · 마스터 없음",
|
||
),
|
||
_f(
|
||
_sid_key(sid, "DAILY_PROFIT_RISK_BUDGET_COOLDOWN_SEC"),
|
||
f"{lbl} 리스크버짓 쿨다운(초)",
|
||
"int",
|
||
default=300,
|
||
hint="같은 날 재평가 최소 간격 · 0=당일 1회만",
|
||
),
|
||
])
|
||
groups.append({
|
||
"id": "daily_profit_strategy",
|
||
"title": "일일 익절 — 전략별 (서브)",
|
||
"hint": (
|
||
"전략별 손익만 따로 감시. 비어 있으면 마스터 값 상속. "
|
||
"손익 감시 ON이면 목표 달성 시 해당 전략 신규매수 자동 차단. "
|
||
"리스크버짓(B)은 전략별만(기본 OFF)."
|
||
),
|
||
"fields": dp_fields,
|
||
})
|
||
|
||
# 전략별 휩쏘 ON/OFF + 꼬리 초·% (Optuna TPE 축과 동일 키)
|
||
ws_fields: List[FieldDef] = []
|
||
for sid in LIVE_STRATEGY_IDS:
|
||
if sid == "SCALP":
|
||
continue
|
||
lbl = LIVE_STRATEGY_LABELS.get(sid, sid)
|
||
pfx = "TAIL" if sid == "SHORT" else sid
|
||
ws_fields.append(
|
||
_f(f"{pfx}_WHIPSAW_FILTER_ENABLED", f"{lbl} 휩쏘 필터", "bool"),
|
||
)
|
||
ws_fields.extend([
|
||
_f("TAIL_WHIPSAW_SUBBAR_SEC", "꼬리 휩쏘 미니봉(초)", "int", default=30,
|
||
hint="비우면 글로벌 WHIPSAW_SUBBAR_SEC · Optuna 꼬리 TPE 축"),
|
||
_f("TAIL_WHIPSAW_LOOKBACK_SEC", "꼬리 휩쏘 룩백(초)", "int", default=90,
|
||
hint="비우면 글로벌 WHIPSAW_LOOKBACK_SEC"),
|
||
_f("TAIL_WHIPSAW_DIP_PCT", "꼬리 휩쏘 하락폭(분율)", "float", default=0.003,
|
||
hint="0.003=0.3% · 비우면 글로벌"),
|
||
_f("TAIL_WHIPSAW_RECOVERY_TOL_PCT", "꼬리 휩쏘 회복허용(분율)", "float", default=0.001,
|
||
hint="0.001=0.1% · 비우면 글로벌"),
|
||
])
|
||
groups.append({
|
||
"id": "whipsaw_strategy",
|
||
"title": "휩쏘 — 전략별 ON/OFF · 꼬리 초/%",
|
||
"hint": (
|
||
"ON/OFF와 초·%는 별개. 꼬리 Optuna --mode tpe 가 이 축을 탐색. "
|
||
"실매 현재값(enabled=false, 30초, 90초, 0.003, 0.001)이 그리드에 포함됨."
|
||
),
|
||
"fields": ws_fields,
|
||
})
|
||
|
||
groups.append({
|
||
"id": "execution_fill",
|
||
"title": "체결 검증 (실전·모의 훈련)",
|
||
"hint": "실전은 항상 fill 확인. 모의는 STRICT_FILL_VERIFY=true 시 동일",
|
||
"fields": [
|
||
_f("STRICT_FILL_VERIFY", "모의 엄격 체결 검증", "bool", default=False,
|
||
hint="모의 실매매만 영향 · ON=체결 API 필수(모의 API 한계로 매수 0건 가능) · 실전은 항상 엄격"),
|
||
_f("BUY_DEDUP_PENDING", "매수 미체결 중복주문 방지", "bool", default=True,
|
||
hint="동일 전략·종목 PENDING_FILL 있으면 재주문 스킵"),
|
||
_f("BACKTEST_SLIP_PCT", "백테 체결 슬리피지(편도 %)", "float", default=0.0,
|
||
hint="STRICT_FILL_VERIFY ON 일 때만 백테/파람서치에 적용 · 전 전략 공통 · 0=순수 백테 · 0.2=0.2%"),
|
||
_f("BACKTEST_VOL_FILL_CAP_PCT", "백테 체결량 상한(진입봉 거래량×%)", "float", default=0.0,
|
||
hint="STRICT_FILL_VERIFY ON 일 때만 · 전 전략 공통 · 0=무제한 · 소형주 IOC 미체결 근사"),
|
||
_f("TAIL_BACKTEST_TICK_FALLBACK_OHLC", "⚠ 꼬리 OHLC 폴백 (정합위험·금지)", "bool", default=False,
|
||
danger=True,
|
||
hint="절대금지: 틱 모드에서 OHLC로 체결·고점 변조. 엔진은 틱 청산 ON 시 무시. Optuna/파람 강제 OFF"),
|
||
_f("TAIL_BACKTEST_USE_TICK_EXIT", "꼬리 틱 청산", "bool", default=True,
|
||
hint="ON=ws_ticks 시간순 청산(실매 폴링 정합) · OFF=OHLC intrabar"),
|
||
_f("TAIL_BACKTEST_POLL_MS", "꼬리 틱청산 폴링(ms)", "int", default=100,
|
||
hint="실매 STRATEGY_LOOP_SLEEP≈0.1초 정합 · 하한 50ms"),
|
||
_f("MOMENTUM_BACKTEST_TICK_FALLBACK_OHLC", "⚠ 모멘텀 OHLC 폴백 (정합위험·금지)", "bool", default=False,
|
||
danger=True,
|
||
hint="절대금지 — 틱 청산 ON 시 엔진 무시 · Optuna 강제 OFF"),
|
||
_f("MOMENTUM_BACKTEST_POLL_MS", "모멘텀 틱청산 폴링(ms)", "int", default=100,
|
||
hint="실매 STRATEGY_LOOP_SLEEP≈0.1초 정합 · 틱 사이 청산 검사 간격 · 하한 50ms"),
|
||
_f("BREAKOUT_BACKTEST_USE_TICK_EXIT", "돌파 틱 청산", "bool", default=True,
|
||
hint="ON=ws_ticks 시간순 청산 · OFF=OHLC intrabar"),
|
||
_f("BREAKOUT_BACKTEST_TICK_FALLBACK_OHLC", "⚠ 돌파 OHLC 폴백 (정합위험·금지)", "bool", default=False,
|
||
danger=True,
|
||
hint="절대금지 — 틱 진입 ON 시 봉 high 폴백 안 함"),
|
||
_f("BREAKOUT_BACKTEST_POLL_MS", "돌파 틱청산 폴링(ms)", "int", default=100,
|
||
hint="실매 폴링 정합 · 하한 50ms"),
|
||
_f("SCALP_BACKTEST_USE_TICK_EXIT", "스캘핑 틱 청산", "bool", default=True,
|
||
hint="ON=ws_ticks 시간순 청산(실매 폴링 정합) · OFF=OHLC intrabar"),
|
||
_f("SCALP_BACKTEST_USE_TICK_ENTRY", "스캘핑 틱 진입", "bool", default=True,
|
||
hint="ON=예약 체결 시 해당 분 첫 틱가 · OFF=분봉 시가"),
|
||
_f("SCALP_BACKTEST_TICK_FALLBACK_OHLC", "⚠ 스캘핑 OHLC 폴백 (정합위험·금지)", "bool", default=False,
|
||
danger=True,
|
||
hint="절대금지 — 틱 청산 ON 시 엔진 무시"),
|
||
_f("SCALP_BACKTEST_POLL_MS", "스캘핑 틱청산 폴링(ms)", "int", default=100,
|
||
hint="실매 STRATEGY_LOOP_SLEEP≈0.1초 정합 · 하한 50ms"),
|
||
_f("RANGE_BREAK_BACKTEST_USE_TICK_EXIT", "박스돌파 틱 청산", "bool", default=True,
|
||
hint="ON=ws_ticks 시간순 청산 · OFF=OHLC intrabar"),
|
||
_f("RANGE_BREAK_BACKTEST_TICK_FALLBACK_OHLC", "⚠ 박스돌파 OHLC 폴백 (정합위험·금지)", "bool", default=False,
|
||
danger=True,
|
||
hint="절대금지 — 틱 청산 ON 시 엔진 무시"),
|
||
_f("RANGE_BREAK_BACKTEST_POLL_MS", "박스돌파 틱청산 폴링(ms)", "int", default=100,
|
||
hint="실매 폴링 정합 · 하한 50ms"),
|
||
_f("DBBAND_BACKTEST_USE_TICK_EXIT", "더블BB 틱 청산", "bool", default=True,
|
||
hint="ON=ws_ticks 시간순 청산 · OFF=봉 OHLC 1회"),
|
||
_f("DBBAND_BACKTEST_TICK_FALLBACK_OHLC", "⚠ 더블BB OHLC 폴백 (정합위험·금지)", "bool", default=False,
|
||
danger=True,
|
||
hint="절대금지 — 틱 청산 ON 시 엔진 무시"),
|
||
_f("DBBAND_BACKTEST_POLL_MS", "더블BB 틱청산 폴링(ms)", "int", default=100,
|
||
hint="실매 폴링 정합 · 하한 50ms"),
|
||
_f("TAIL_PARAM_SEARCH_MIN_START", "꼬리 파라서치 최소 시작일", "str", default="2026-07-07",
|
||
hint="조건식 변경일 이후만 탐색 (YYYY-MM-DD) · CLI --start 와 max 적용"),
|
||
_f("USE_MARKET_IOC", "매수 시장가 IOC", "bool", default=True,
|
||
hint="실전 매수 ORD_DVSN=13(IOC) · OFF=01 · 모의는 01 고정"),
|
||
_f("USE_MARKET_IOC_SELL", "매도 시장가 IOC", "bool", default=True,
|
||
hint="실전 매도 ORD_DVSN=13(IOC) · OFF=01 · 모의는 01 고정 · 거절 많으면 OFF"),
|
||
_f("ORDER_FILL_WAIT_SEC", "시장가 체결 대기(초)", "int", default=2),
|
||
_f("DUPLICATE_ORDER_FILL_RECOVERY_ENABLED", "주문DB중복 체결복구", "bool", default=True,
|
||
hint="insert 실패(주문DB중복) 시 inquire-daily-ccld 1회 → active_trades 복구 · 실패 시에만 REST"),
|
||
_f("DUPLICATE_ORDER_RECOVERY_WAIT_SEC", "중복복구 체결대기(초)", "int", default=2,
|
||
hint="주문DB중복 복구 전용 · 미설정 시 ORDER_FILL_WAIT_SEC 사용"),
|
||
_f("LIMIT_ORDER_FILL_WAIT_SEC", "지정가 체결 대기(초)", "int", default=1),
|
||
_f("PENDING_FILL_POLL_INTERVAL_SEC", "미체결 재조회 주기(초)", "int", default=10),
|
||
_f("PENDING_FILL_POLL_SEC", "재조회 1회 대기(초)", "int", default=1),
|
||
_f("PENDING_POLL_BATCH_FETCH", "미체결 체결 일괄조회", "bool", default=True,
|
||
hint="실매 heartbeat 전용 · ON=poll 1회당 inquire-daily-ccld 1건(미체결 N건) · OFF=건별 조회 · 백테 무관"),
|
||
_f("PENDING_BUY_MAX_AGE_SEC", "매수 미체결 만료(초)", "int", default=120),
|
||
_f("PENDING_SELL_MAX_AGE_SEC", "익절 매도 미체결 만료(초)", "int", default=60),
|
||
_f("PENDING_SELL_STOP_MAX_AGE_SEC", "손절 매도 미체결 만료(초)", "int", default=15),
|
||
_f("SELL_PENDING_REORDER_ON_EXPIRE", "손절 만료 후 즉시 재매도", "bool", default=True),
|
||
_f("AUTO_CANCEL_PARTIAL_BUY_REMAINDER", "부분체결 잔량 자동취소", "bool", default=True),
|
||
_f(
|
||
"SELL_LIMIT_CANCEL_BEFORE_MARKET_RETRY",
|
||
"익절지정가→시장가 전 취소",
|
||
"bool",
|
||
default=True,
|
||
hint=(
|
||
"ON=지정가 미확인/부분체결 시 잔여 취소 후 ODNO 재조회, "
|
||
"취소실패·미확정이면 시장가 금지(PENDING) · "
|
||
"OFF=레거시 즉시 시장가(이중매도 위험) · 전전략 OrderManager 공통"
|
||
),
|
||
),
|
||
_f(
|
||
"SELL_LIMIT_RECHECK_WAIT_SEC",
|
||
"지정가 취소 후 재조회(초)",
|
||
"int",
|
||
default=1,
|
||
hint="SELL_LIMIT_CANCEL_BEFORE_MARKET_RETRY=ON 일 때 ODNO 재조회 대기",
|
||
),
|
||
_f(
|
||
"SELL_LIMIT_CANCEL_FAIL_BALANCE_CONFIRM",
|
||
"취소실패+잔고0→매도확정",
|
||
"bool",
|
||
default=True,
|
||
hint=(
|
||
"ON=익절지정가 취소 실패·ODNO 미확인이어도 브로커 0주면 "
|
||
"지정가 체결로 finalize(유령금지) · OFF=PENDING만"
|
||
),
|
||
),
|
||
_f(
|
||
"GHOST_PURGE_BLOCK_WHILE_PENDING_SELL",
|
||
"PENDING매도 시 유령정리 금지",
|
||
"bool",
|
||
default=True,
|
||
hint=(
|
||
"ON=미확정 매도 ODNO 있으면 ghost_purge 대신 체결재조회/잔고확정 청산"
|
||
),
|
||
),
|
||
_f(
|
||
"DAILY_CCLD_TR_ID",
|
||
"일별체결조회 TR_ID",
|
||
"str",
|
||
default="",
|
||
hint="비우면 모의 VTTC0081R / 실전 TTTC0081R (레거시 *8001R 사용 금지)",
|
||
),
|
||
],
|
||
})
|
||
|
||
groups.append({
|
||
"id": "rest_throttle",
|
||
"title": "REST 유량 제한 (한투 API)",
|
||
"hint": (
|
||
"한투 유량(계좌·앱키 단위): 실전 18건/초 · 모의 1건/초 · "
|
||
"신규 앱키는 첫 3일 3건/초 후 자동 상향 · 토큰발급 1건/초 · "
|
||
"동시호출 100~150ms 텀 권장. "
|
||
"※ 모의: 거래는 모의앱키(1건/초), 시세·랭킹·조건검색은 실전앱키(18건/초)로 분리. "
|
||
"운영(실전) 전환 시 거래+시세가 한 앱키로 합쳐져 18건/초를 공유."
|
||
),
|
||
"fields": [
|
||
_f("KIS_MIN_INTERVAL_SEC_MOCK", "모의 REST 최소간격(초)", "float", default=0.22,
|
||
hint="모의 도메인(거래: 주문·체결·잔고) · 조회·랭킹·조건은 실키라 무관 · 429 로그 없으면 0.22 유지 · 한투 공지 1건/초 엄수 필요 시 1.0 · 재시작 없이 반영(1초 TTL)"),
|
||
_f("KIS_MIN_INTERVAL_SEC_REAL", "실전 REST 최소간격(초)", "float", default=0.12,
|
||
hint="실전 도메인(시세 · 운영 시 거래+조회+취소+재주문 통합 한 앱키) · 18건/초 한도 → 0.12=약8건/초 · 신규앱키 첫 3일은 0.34(3건/초) · 재시작 없이 반영"),
|
||
_f("KIS_MIN_INTERVAL_SEC", "공통 폴백 최소간격(초)", "float", default=0.22,
|
||
hint="위 도메인별 키 미설정 시 폴백"),
|
||
_f("BALANCE_MAX_PAGES", "잔고 연속조회 최대 페이지", "int", default=20,
|
||
hint="잔고조회는 1페이지에 실전 50종목/모의 20종목만 내려옴. 보유가 많으면 다음 페이지를 이어 조회(누적 병합). 20p=최대 약 1000종목. 보유 종목이 50개를 넘으면 필수 — 부족하면 51번째부터 매도 검증에서 누락됨"),
|
||
],
|
||
})
|
||
|
||
groups.append({
|
||
"id": "scan_loop_sleep",
|
||
"title": "스캔 루프 회전율 (WS 따라가기)",
|
||
"hint": (
|
||
"REST 유량과 별개 — 전략 매수체크 루프의 sleep. 짧을수록 WS 실시간 신호를 빨리 잡지만 CPU↑. "
|
||
"탈락 경로는 REST 0건(WS·DB·연산만)이라 줄여도 한투 유량 무관. 재시작 없이 반영. "
|
||
"비우면 전략별 기본값(loop 0.1초) 사용."
|
||
),
|
||
"fields": [
|
||
_f("SCAN_REJECT_SLEEP_MIN", "탈락 후 sleep 최소(초)", "float", default=0.2,
|
||
hint="check_buy 탈락 종목마다 · REST 0건 · 0.05~0.1 로 낮추면 한 바퀴 대폭 단축"),
|
||
_f("SCAN_REJECT_SLEEP_MAX", "탈락 후 sleep 최대(초)", "float", default=0.5),
|
||
_f("SCAN_BUY_OK_SLEEP_MIN", "매수성공 후 sleep 최소(초)", "float", default=1.0,
|
||
hint="1루프 1매수 후 대기 · 포지션 과집중 방지"),
|
||
_f("SCAN_BUY_OK_SLEEP_MAX", "매수성공 후 sleep 최대(초)", "float", default=2.0),
|
||
_f("SCAN_BUY_FAIL_SLEEP_MIN", "주문실패 후 sleep 최소(초)", "float", default=0.3),
|
||
_f("SCAN_BUY_FAIL_SLEEP_MAX", "주문실패 후 sleep 최대(초)", "float", default=0.8),
|
||
_f("STRATEGY_LOOP_SLEEP_MIN", "루프 끝 sleep 최소(초)", "float", default=0.1,
|
||
hint="한 바퀴 끝 대기 · WS 구독 캐시 폴링 · 0.1=실매 반응 빠르게"),
|
||
_f("STRATEGY_LOOP_SLEEP_MAX", "루프 끝 sleep 최대(초)", "float", default=0.1),
|
||
_f("TICK_SELL_ENABLED", "틱매도(공통)", "bool", default=False,
|
||
hint="ON=WS 현재가 갱신마다 매도검사(백테 틱청산에 근접) · OFF=루프(~0.1초)만 · "
|
||
"전략별 *_TICK_SELL_ENABLED 가 비어있지 않으면 그쪽 우선 · 기본 OFF"),
|
||
_f("TICK_SELL_MIN_INTERVAL_MS", "틱매도 최소간격(ms)", "int", default=50,
|
||
hint="같은 종목 연속 틱 폭주 시 매도검사 하한 · 백테 poll≈50~100ms 정합"),
|
||
],
|
||
})
|
||
|
||
# 전략별 틱매도 스위치 (비어 있으면 공통 TICK_SELL_ENABLED)
|
||
tick_sell_fields: List[FieldDef] = []
|
||
for sid in LIVE_STRATEGY_IDS:
|
||
lbl = LIVE_STRATEGY_LABELS.get(sid, sid)
|
||
tick_sell_fields.append(
|
||
_f(f"{sid}_TICK_SELL_ENABLED", f"{lbl} 틱매도", "bool", default=False,
|
||
hint="비우면 공통 TICK_SELL_ENABLED · true/false 명시 시 전략만 덮어씀 · 기본 false"),
|
||
)
|
||
groups.append({
|
||
"id": "tick_sell_by_strategy",
|
||
"title": "전략별 틱매도 (선택)",
|
||
"hint": (
|
||
"매도 규칙은 기존 check_sell_signals 그대로 · 호출 시점만 WS 틱. "
|
||
"실매↔백테 청산 타이밍 정합용 · 기본 OFF 권장(검증 후 ON)."
|
||
),
|
||
"fields": tick_sell_fields,
|
||
})
|
||
|
||
# 전략별 후보 하드캡 (WS 100 구독 한도 안전 + 매수체크 회전율)
|
||
cand_fields: List[FieldDef] = []
|
||
for sid in LIVE_STRATEGY_IDS:
|
||
lbl = LIVE_STRATEGY_LABELS.get(sid, sid)
|
||
cand_fields.append(
|
||
_f(f"{sid}_CAND_LIMIT", f"{lbl} 후보 상한", "int", default=0,
|
||
hint="0=무제한 · 조건검색/랭킹이 N개 줘도 상위 N개만 매수체크·WS구독 · "
|
||
"전 전략 합산이 키움 WS 100종 한도를 넘지 않게 조절"),
|
||
)
|
||
groups.append({
|
||
"id": "candidate_limit",
|
||
"title": "전략별 후보 상한 (WS 100 구독 분배)",
|
||
"hint": (
|
||
"env_config · 키움 WS 실시간 구독은 계좌당 100종목이 한도. "
|
||
"전략별 후보 상한 합 + 영구구독 + 보유 여유 ≤ 100 이 되도록 분배. "
|
||
"UPDOW 는 추가로 watchlist(UPDOWN_WATCH_MAX) 안에서만 유지됨."
|
||
),
|
||
"fields": cand_fields,
|
||
})
|
||
|
||
# UPDOWN 박스권 전략 — SCAN/판별/진입/청산 파라미터 (env_config UPDOWN_BOX_*)
|
||
groups.append({
|
||
"id": "updown_box",
|
||
"title": "UPDOWN 박스권 — 판별·진입·손절 스택",
|
||
"hint": (
|
||
"env_config · 횡보(박스권) 종목을 하단에서 줍고 상단/익절·박스이탈·패닉·래칫으로 청산. "
|
||
"파라서치(param_search_updown_box) 1위와 동일 키 · 재시작 없이 다음 루프 반영."
|
||
),
|
||
"fields": [
|
||
_f("UPDOWN_SCAN_INTERVAL_SEC", "SCAN 주기(초)", "int", default=300,
|
||
hint="조건검색→박스필터→watchlist 충전 주기 · 박스는 느리게 변하므로 300(5분) 권장"),
|
||
_f("UPDOWN_SCAN_TF_MIN", "분봉(분)", "int", default=15,
|
||
hint="박스 판별·진입에 쓰는 분봉 · 수집/백테/파라서치와 동일해야 함"),
|
||
_f("UPDOWN_WATCH_MAX", "관심종목 최대 수", "int", default=30,
|
||
hint="sticky watchlist 최대 보유 · REST 1콜 30종(intstock-multprice)과 정합"),
|
||
_f("UPDOWN_BOX_LOOKBACK_BARS", "박스 lookback(봉)", "int", default=20,
|
||
hint="박스 상·하단 산출에 쓸 최근 봉 수"),
|
||
_f("UPDOWN_BOX_RANGE_MAX_PCT", "박스 최대 높이(%)", "float", default=8.0,
|
||
hint="(상단-하단)/하단 · 이보다 넓으면 박스 아님(변동성 과대)"),
|
||
_f("UPDOWN_BOX_BB_BW_MAX", "밴드폭 상한(%)", "float", default=12.0,
|
||
hint="볼린저 밴드폭(스퀴즈) · 작을수록 횡보 · 초과 시 박스 아님"),
|
||
_f("UPDOWN_BOX_MA_SLOPE_TOL_PCT", "MA 기울기 허용(%)", "float", default=0.5,
|
||
hint="|MA 기울기| 이내여야 추세없음(횡보) 인정"),
|
||
_f("UPDOWN_BOX_NEAR_LOW_PCT", "하단 근접 진입(%)", "float", default=2.0,
|
||
hint="현재가가 박스 하단에서 이 % 이내로 내려오면 매수 후보(싸게 줍기)"),
|
||
_f("UPDOWN_BOX_BODY_DROP_MIN_PCT", "직전봉 음봉 최소(%)", "float", default=0.2,
|
||
hint="되돌림 확인 · 직전봉 음봉 몸통 하락률 하한 · 0=무시"),
|
||
_f("UPDOWN_BOX_TP_PCT", "익절(%)", "float", default=3.0,
|
||
hint="진입가 대비 · 박스 상단 도달 기대치 근방"),
|
||
_f("UPDOWN_BOX_STOP_LOSS_PCT", "하드 손절(%)", "float", default=2.5,
|
||
hint="진입가 대비 · 박스이탈·패닉이 안 걸릴 때 최종 방어"),
|
||
_f("UPDOWN_BOX_BREACH_BUFFER_PCT", "박스이탈 여유(%)", "float", default=0.3,
|
||
hint="현재가 < 하단×(1-여유%) 면 구조 붕괴 즉시 손절 · 노이즈 이탈 방지"),
|
||
_f("UPDOWN_PANIC_FROM_HIGH_PCT", "패닉 손절(고점대비 %)", "float", default=3.0,
|
||
hint="보유 중 고점 대비 이 % 급락 시 무조건 긴급컷(진입가 무관)"),
|
||
_f("UPDOWN_BOX_SHOULDER_MIN_HIGH_PCT", "래칫 무장(+%)", "float", default=1.0,
|
||
hint="고점이 진입가 대비 +이 % 도달해야 어깨컷(트레일) 무장"),
|
||
_f("UPDOWN_BOX_SHOULDER_CUT_PCT", "래칫 컷(고점대비 -%)", "float", default=1.0,
|
||
hint="무장 후 고점 대비 이 % 꺾이면 청산(어깨컷)"),
|
||
_f("UPDOWN_BOX_RATCHET_TIERS", "다단 래칫(수익%:컷%,…)", "text", default="",
|
||
hint="TAIL·BREAKOUT 동일 형식. 예 '2:1.5,5:1.0' = +2%부터 고점-1.5%컷, +5%부터 -1.0%컷. "
|
||
"비우면 OFF(위 단일 어깨컷 사용)"),
|
||
_f("UPDOWN_BOX_MAX_HOLD_BARS", "보유 한도(봉)", "int", default=64,
|
||
hint="이만큼 봉이 지나면 무조건 청산"),
|
||
_f("UPDOWN_FEED_MODE", "피드 모드", "text", default="rest",
|
||
hint="ws=키움 실시간(100한도 공유) / rest=한투 30배치 폴링 · watchlist 30종이면 rest 권장"),
|
||
_f("UPDOWN_REST_POLL_SEC", "REST 폴링 주기(초)", "int", default=5,
|
||
hint="rest 모드에서 현재가 스냅샷 갱신 주기"),
|
||
],
|
||
})
|
||
|
||
groups.append({
|
||
"id": "scalp_exit",
|
||
"title": "스캘핑(SCALP) — EOD",
|
||
"hint": (
|
||
"config_scalp · 저장 후 다음 매도 루프부터 반영(재시작 권장). "
|
||
"실매 기존 하드코딩 15:25 장마감청산과 동일 기본값."
|
||
),
|
||
"fields": [
|
||
_f("SCALP_EOD_ENABLED", "EOD 당일청산 사용", "bool", default=True,
|
||
hint="ON=설정 시각 이후 보유 전량 장마감청산 · OFF=익절·손절·어깨만"),
|
||
_f("SCALP_EOD_HM", "EOD 시각 (HH:MM)", "text", default="15:25",
|
||
hint="실매 기존 15:25 · 정규장 15:30 마감 직전 순차매도"),
|
||
],
|
||
})
|
||
|
||
groups.append({
|
||
"id": "breakout_exit",
|
||
"title": "돌파(BREAKOUT) — EOD·시간컷",
|
||
"hint": (
|
||
"env_config · 저장 후 다음 매도 루프부터 반영(재시작 불필요). "
|
||
"EOD OFF 시 당일 15:15 강제청산 없음 — 손절·익절·어깨·시간컷만."
|
||
),
|
||
"fields": [
|
||
_f("BREAKOUT_EOD_ENABLED", "EOD 당일청산 사용", "bool", default=True,
|
||
hint="ON=설정 시각 이후 보유 전량 eod 청산 · OFF=익절·손절·시간컷만"),
|
||
_f("BREAKOUT_EOD_HM", "EOD 시각 (HH:MM)", "text", default="15:15",
|
||
hint="이 시각 이후 보유 전량 청산 · 예: 15:15"),
|
||
_f("BREAKOUT_MAX_HOLD_BARS", "최대보유(분)", "int", default=0,
|
||
hint="0=OFF · 진입 후 N분 경과 시 max_hold 청산"),
|
||
],
|
||
})
|
||
|
||
groups.append({
|
||
"id": "momentum_exit",
|
||
"title": "모멘텀(MOMENTUM) — EOD",
|
||
"hint": "env_config · 저장 후 다음 매도 루프부터 반영(재시작 불필요).",
|
||
"fields": [
|
||
_f("MOMENTUM_EOD_ENABLED", "EOD 당일청산 사용", "bool", default=True,
|
||
hint="ON=설정 시각 이후 보유 전량 장마감청산 · OFF=익절·손절·시간컷만"),
|
||
_f("MOMENTUM_EOD_HM", "EOD 시각 (HH:MM)", "text", default="15:20",
|
||
hint="정규장 15:30 마감 기준 최소 10분 전 · 순차매도 여유"),
|
||
],
|
||
})
|
||
|
||
groups.append({
|
||
"id": "tail_exit",
|
||
"title": "꼬리(SHORT/TAIL) — EOD",
|
||
"hint": "env_config · 저장 후 다음 매도 루프부터 반영(재시작 불필요).",
|
||
"fields": [
|
||
_f("TAIL_EOD_ENABLED", "EOD 당일청산 사용", "bool", default=True,
|
||
hint="ON=설정 시각 이후 보유 전량 장마감 청산 · OFF=익절·손절·시간컷만"),
|
||
_f("TAIL_EOD_HM", "EOD 시각 (HH:MM)", "text", default="15:20",
|
||
hint="정규장 15:30 마감 기준 최소 10분 전 · 순차매도 여유"),
|
||
],
|
||
})
|
||
|
||
groups.append({
|
||
"id": "error_watch_mm",
|
||
"title": "오류감시 → Mattermost",
|
||
"hint": (
|
||
"별도 프로세스 scripts/kis_error_watch_mm.py 가 journalctl -f 로 "
|
||
"kis_trader_main 을 감시 → Traceback/유닛다운 시 MM. 실매 봇과 분리."
|
||
),
|
||
"fields": [
|
||
_f("ERROR_WATCH_ENABLED", "오류감시 ON", "bool", default=True,
|
||
hint="감시 프로세스 시작 시 읽음 · OFF면 감시자 즉시 종료"),
|
||
_f("ERROR_WATCH_UNIT", "감시 systemd 유닛", "text",
|
||
default="kis_trader_main.service",
|
||
hint="journalctl -u 대상"),
|
||
_f("ERROR_WATCH_MM_CHANNEL", "알림 MM alias", "text", default="",
|
||
hint="비우면 KIS_SYSTEM_MM_CHANNEL(default=키스명령봇)"),
|
||
_f("ERROR_WATCH_COOLDOWN_SEC", "동일오류 쿨다운(초)", "int", default=180,
|
||
hint="같은 Traceback 지문 재알림 간격"),
|
||
_f("ERROR_WATCH_HEALTH_CHECK_SEC", "유닛생존 점검(초)", "int", default=60,
|
||
hint="systemctl is-active 주기"),
|
||
_f("ERROR_WATCH_TRACEBACK_EXTRA_LINES", "Traceback 추가줄", "int", default=12,
|
||
hint="Traceback 이후 스택 수집 줄 수"),
|
||
_f("ERROR_WATCH_STARTUP_NOTIFY", "감시자 기동 MM", "bool", default=True,
|
||
hint="감시 프로세스 시작 시 1회 알림"),
|
||
_f("ERROR_WATCH_MM_JITTER", "MM jitter", "bool", default=False,
|
||
hint="오류 알림은 즉시 권장(OFF)"),
|
||
],
|
||
})
|
||
|
||
groups.append({
|
||
"id": "portfolio_hold",
|
||
"title": "보유·매도 / 고아복구",
|
||
|
||
"hint": (
|
||
"보유·매도 탭 일괄매도·수동 보호·장마감 후 DB 동기화. "
|
||
"MANUAL_HOLD_CODES 에 넣은 종목은 일괄매도·고아복구 모두 제외."
|
||
),
|
||
"fields": [
|
||
_f("MANUAL_HOLD_CODES", "수동 보유 보호 종목", "text", default="",
|
||
hint="쉼표(,) 구분 종목코드 예: 005930,000660 · HTS 직접 매수·장기 보유분 보호 · 일괄매도·고아복구 제외"),
|
||
_f("BULK_SELL_INTERVAL_SEC", "일괄매도 종목 간격(초)", "float", default=0.3,
|
||
hint="미등록 봇고아 일괄 시장가매도 시 REST 429 방지 · 종목마다 sleep"),
|
||
_f("ORPHAN_RECONCILE_ENABLED", "장마감 고아복구", "bool", default=True,
|
||
hint="매일 15:36~16:00 1회 · 실계좌 잔고↔active_trades 대조 · orders BUY 있는 미기록분만 복구 · 수동·MANUAL_HOLD 제외"),
|
||
_f("ORPHAN_RECONCILE_PRE_EOD_ENABLED", "Pre-EOD 고아복구", "bool", default=True,
|
||
hint="활성 전략 중 가장 이른 EOD − N분 1회 · EOD 청산 전 active_trades 반영"),
|
||
_f("ORPHAN_RECONCILE_PRE_EOD_LEAD_MIN", "Pre-EOD 선행(분)", "int", default=7,
|
||
hint="예: BREAKOUT EOD 15:15 → 15:08 실행 · REST 잔고 1회/일"),
|
||
_f("ORPHAN_SKIP_AFTER_SELL_HISTORY", "청산히스토리 있으면 재등록금지", "bool", default=True,
|
||
hint="ON=매수 이후 장마감청산 등 히스토리 있으면 고아복구로 active 다시 안 넣음"),
|
||
_f("ORPHAN_SKIP_SELL_REASONS", "재등록금지 청산사유", "str", default="장마감청산,eod",
|
||
hint="쉼표 구분 키워드. 비우면 매수 이후 모든 청산 히스토리 금지"),
|
||
_f("ORPHAN_PURGE_ACTIVE_AFTER_SELL_HISTORY", "청산후 유령 active 정리", "bool", default=True,
|
||
hint="ON=active에 남았는데 매수 이후 청산 히스토리면 DB 행 삭제"),
|
||
_f("GHOST_PURGE_ON_RECONCILE", "고아복구 시 유령잔고 삭제", "bool", default=True,
|
||
hint="ON=브로커 0주인데 active_trades 남은 종목 삭제(수동보호 제외) · Pre/Post EOD 동일 잔고조회에서 처리"),
|
||
_f(
|
||
"GHOST_PURGE_RECORD_HISTORY",
|
||
"유령정리 시 거래내역 기록",
|
||
"bool",
|
||
default=True,
|
||
hint=(
|
||
"ON=유령 삭제 시 trade_history 에 sell_reason=ghost_purge "
|
||
"(realized_pnl=0) · OFF=삭제만 · 전전략 공통"
|
||
),
|
||
),
|
||
_f("INTRADAY_HOLDINGS_DRIFT_ENABLED", "장중 qty 드리프트 감시", "bool", default=False,
|
||
hint="기본 OFF · ON=N분마다 잔고 REST · 알림/자동복구는 별도 키"),
|
||
_f("INTRADAY_HOLDINGS_DRIFT_INTERVAL_SEC", "드리프트 조회주기(초)", "int", default=300,
|
||
hint="INTRADAY_HOLDINGS_DRIFT_ENABLED=ON 일 때만"),
|
||
_f("INTRADAY_HOLDINGS_DRIFT_AUTO_RECOVER", "드리프트 자동복구", "bool", default=False,
|
||
hint="OFF=알림만 · ON=active_trades qty 보정(기본 OFF)"),
|
||
],
|
||
})
|
||
|
||
return groups
|
||
|
||
|
||
def all_live_config_keys() -> List[str]:
|
||
keys: List[str] = []
|
||
for g in build_live_config_groups():
|
||
for f in g.get("fields") or []:
|
||
k = f.get("key")
|
||
if k:
|
||
keys.append(str(k))
|
||
return keys
|
||
|
||
|
||
def snap_value_to_ui(raw: Any, ftype: str) -> Any:
|
||
"""DB 문자열 → 웹 입력값."""
|
||
if raw is None or str(raw).strip() in ("", "None"):
|
||
return None
|
||
s = str(raw).strip().split("#", 1)[0].strip()
|
||
if ftype == "bool":
|
||
return s.lower() in ("1", "true", "yes", "on")
|
||
try:
|
||
if ftype == "int":
|
||
return int(float(s))
|
||
if ftype in ("float", "pct"):
|
||
v = float(s)
|
||
if ftype == "pct" and 0 < abs(v) < 0.5:
|
||
return round(abs(v) * 100, 4)
|
||
return round(abs(v), 6) if ftype == "pct" else v
|
||
except (ValueError, TypeError):
|
||
return None
|
||
return s
|
||
|
||
|
||
def ui_value_to_db(val: Any, ftype: str) -> str:
|
||
"""웹 입력 → DB 저장 문자열."""
|
||
if ftype == "bool":
|
||
if val in (True, "true", "1", 1, "on", "yes"):
|
||
return "true"
|
||
return "false"
|
||
if val is None or (isinstance(val, str) and not val.strip()):
|
||
return ""
|
||
if ftype == "int":
|
||
return str(int(float(val)))
|
||
if ftype == "pct":
|
||
return str(float(val))
|
||
if ftype == "float":
|
||
return str(float(val))
|
||
return str(val).strip()
|