914 lines
47 KiB
Python
914 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="실전 시장가 주문 방식 · 모의는 일반 시장가(01) 고정"),
|
||
_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()
|