Files
kis_bot/kis_trader/web/live_config_schema.py
Your Name 1d69c217e2 fix(정합성): 틱 lag wall-clock 정합 + 3벤더 DB 저장 스위치 통일
- feed_fallback.bar_is_garbage: 봉끝 기준 → 각 틱의 recv_ts wall-clock 기준으로 정정
  실매 RAM 3초컷과 동일 논리 → 유동성 낮은 종목 부당 스킵 해소
- candle_garbage_fallback_enabled: 기본 True 복원 (wall-clock 정정 후 안전)
- param_search_optuna·run_tail_backtest_cli: CANDLE_GARBAGE_FALLBACK·BACKTEST_USE_RUST
  강제 os.environ 세팅 제거 → DB env·CLI 플래그로만 관리 (UI 존중)
- WS_TICK_DB_SAVE_LAG_CUT_ENABLED 신설 (bool, 기본 false, 3벤더 공통)
  OFF=키움/KIS/LS 모든 틱 lag 무관 전부 저장 (벤더 통계·재현·백테 정합)
  ON=lag > LIVE_FEED_FALLBACK_MAX_AGE_SEC 이면 미저장 (미래 A안)
- KIWOOM_TICK_LIVE_MAX_LAG_SEC 완전 폐기 → 위 스위치로 통일
- kiwoom_ws: _skip_persist 로직 새 스위치로 교체
- kis_ws·ls_ws: _skip_persist_kis/_ls 신규 (벤더별 상이했던 정책 통일)
- docs/정합성.md §9 신설 (문제·결정·시나리오·향후 A안 전환법)
- docs/rust_engine_parity_port_plan.md (신규 설계)

실매 스모크: logs/test_live_execution_validation_20260906_191845.log
  → 최종: 통과 · 👑 완결
브라우저 검증: http://192.168.0.149:5050/#liveconfig → 새 스위치 노출, JS 오류 없음

영향: 실매(DB 저장 정책 통일, RAM 컷 변경 없음) + 백테/Optuna(실매 정합)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 19:23:01 +09:00

1657 lines
86 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 봇) — 오늘 운영 탭·대시보드 순서와 동일
# ※ RANGE_BREAK·DBBAND 는 strategy_ids.HIDDEN_STRATEGY_IDS 로 숨김 유지
# ※ US_MOMENTUM 은 전략 ON/OFF 에만 노출 (유니버스/KRW 한도 폼은 국내와 축이 다름).
LIVE_STRATEGY_IDS: Tuple[str, ...] = (
"SCALP",
"SHORT",
"MOMENTUM",
"BREAKOUT",
"UPDOW",
)
# 전략 ON/OFF 전용 — 해외를 맨 위
LIVE_STRATEGY_SWITCH_IDS: Tuple[str, ...] = (
"US_MOMENTUM",
"SCALP",
"SHORT",
"MOMENTUM",
"BREAKOUT",
"UPDOW",
)
LIVE_STRATEGY_LABELS: Dict[str, str] = {
"SCALP": "스캘핑",
"SHORT": "꼬리잡기",
"MOMENTUM": "모멘텀",
"US_MOMENTUM": "해외 모멘텀",
"BREAKOUT": "돌파",
"RANGE_BREAK": "박스권돌파",
"UPDOW": "UPDOWN 박스",
"DBBAND": "더블BB",
"DART": "DART 수주",
}
# 기동 시 _register_strategies 기본값과 동일 (핫게이트 폴백)
STRATEGY_ENABLED_DEFAULTS: Dict[str, bool] = {
"SCALP": True,
"SHORT": True,
"MOMENTUM": False,
"US_MOMENTUM": True,
"BREAKOUT": False,
"RANGE_BREAK": False,
"UPDOW": False,
"DBBAND": False,
"DART": False,
}
# 전략 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_feed_settings",
"title": "📡 시세 소스 설정 (봇 재시작 필요)",
"hint": (
"키움과 KIS 중 어느 쪽 시세·호가로 매매할지 선택. "
"분봉 저장은 증권사별(source=kiwoom|kis, channel=ws|rest|rollup)로 분리. "
"갭보정은 키움 REST만. 영구구독 KR은 LS WS."
),
"fields": [
_f("LIVE_TICK_PROVIDER", "📊 실매매 틱(시세) 소스 (재시작 필요)", "select", default="kiwoom",
choices=[
{"value": "kiwoom", "label": "키움 (kiwoom)"},
{"value": "kis", "label": "한투 (kis)"},
],
hint=(
"1차 RAM. 키움=kiwoom / 한투=kis. LS는 여기 없음(3차 spill). "
"폴백 나이 넘으면 실패로 보고 2차(상대 체결소켓)→3차(LS)→매도는 4차 키움 REST(ka10007). "
"한투 60초 캐시는 안 씀. MINIMAL 이면 한투 RAM 은 보유만. "
"저장 후 kis_trader_main 재시작"
)),
_f("LIVE_OB_PROVIDER", "📊 실매매 호가(OB) 소스 (재시작 필요)", "select", default="kis",
choices=[
{"value": "kis", "label": "한투 (kis)"},
{"value": "kiwoom", "label": "키움 (kiwoom)"},
],
hint=(
"호가 1차. 한투=kis(2키 OB 세션 41) / 키움=kiwoom. LS는 여기 없음(3차 UH1). "
"폴백 나이 넘으면 그 벤더는 없는 것과 같고 2차→3차. "
"만료로 None 만 만들어서 필터 ON 통과 구멍을 만들지 않음. "
"kis 선택 시 아래 'KIS 실시간 호가 수집' ON + 2번째 앱키(KIS_APP_KEY_OB_REAL). "
"기본 체인: kis→kiwoom→ls. 저장 후 kis_trader_main 재시작"
)),
_f("LIVE_FEED_FALLBACK_MAX_AGE_SEC", "⏱ 시세·호가 읽기 폴백 나이(초)", "float", default=3.0,
hint=(
"틱: 체결시각(키움 FID20 / 한투·LS chetime) vs 서버 지금 → 초과면 RAM 미반영. "
"호가: snap_time(한투 BSOP_HOUR / LS hotime / 키움 FID20)도 동일 초로 "
"RAM 미반영·읽기 체인 실패(필터 max_age=0=마지막 RAM 은 예외). "
"1·2·3차 같음. 0이면 나이 컷 OFF. 기본 3. 적재(공책)와 분리."
)),
_f("FEED_STATS_DISCONNECT_SOFT_SEC", "📡 수집통계 끊김(소프트) 초", "int", default=10,
hint="수집통계 탭: 종목별 recv_ts 간격 ≥이 값이면 끊김 카운트. 기본 10"),
_f("FEED_STATS_DISCONNECT_HARD_SEC", "📡 수집통계 끊김(하드) 초", "int", default=60,
hint="수집통계 탭: ≥이 값이면 긴 끊김. 기본 60 (소켓 알람 체감과 유사)"),
_f("FEED_STATS_DISCONNECT_CAP_SEC", "📡 수집통계 끊김 상한(초)", "int", default=1800,
hint="이보다 긴 공백은 장외·미구독으로 보고 끊김 집계에서 제외. 기본 1800"),
_f("TRIGGER_FEED_DETAIL_LOG", "📝 트리거 로그에 틱·호가·벤더 상세", "bool", default=True,
hint=(
"ON=매수체크(🔍) 로그 끝에 틱1차설정/실제(kis|kiwoom|ls)·현재가·틱타임·"
"호가 bid/ask/잔량비·스냅시각을 붙임. OFF=접두 T/O/R 만. 재시작 없이 반영(수 초)"
)),
_f("BT_FEED_DETAIL_LOG", "📝 옵투나/백테에도 틱·호가 벤더 추적", "bool", default=True,
hint=(
"ON=옵투나·백테 로그에 틱/호가 로드 벤더 비율 + TRIGGER 샘플 "
"(실매 TRIGGER_FEED_DETAIL_LOG 과 같은 축). OFF=샘플만 끔(비율 INFO는 유지)"
)),
_f("BT_FEED_DETAIL_LOG_MAX", "옵투나 TRIGGER 샘플 상한(건)", "int", default=40,
hint="프로세스당 매수 후보 상세 샘플 최대. 0=샘플 없음. 기본 40"),
_f("CANDLE_GARBAGE_FALLBACK", "🕯 분봉 쓰레기→2차·3차 봉 통째", "bool", default=True,
hint=(
"ON(기본)=그 분 메인 틱이 0건이거나 전부 봉끝 대비 읽기나이 초과면 "
"그 봉을 구멍으로 보고 2차 WS→LS 봉 통째. 한 봉 안 혼합 롤업 없음. "
"실매 링에 그 분이 안 들어오면 판정 불가→기존 봉 유지"
)),
_f("WS_TICK_SAVE_KIWOOM", "📈 키움 체결 틱 수집", "bool", default=True,
hint="ON = 키움 체결 틱을 ws_ticks 테이블에 저장 (기본값: ON)"),
_f("KIWOOM_TICK_TIME_MAX_LAG_SEC", "⏱ 키움 체결시간(FID20) 동결 한도(초)", "int", default=120,
hint=(
"같은 FID20 가 이 초 이상 안 움직일 때만 아침 시계멈춤으로 보고 "
"RAM 유지·tick_time 을 수신시각으로 보정. "
"밀린 줄의 같은 초 연속 체결(수십 ms)은 동결이 아님"
)),
_f("WS_TICK_DB_SAVE_LAG_CUT_ENABLED", "⏱ DB 적재 시 지연 틱 컷 (3벤더 공통)", "bool", default=False,
danger=True,
hint=(
"OFF(기본)=키움/KIS/LS 모든 벤더의 틱을 lag 무관 전부 ws_ticks 에 저장. "
"벤더별 지연 통계·재현·백테 정합에 필수. "
"ON=lag(체결시각 vs 지금) > LIVE_FEED_FALLBACK_MAX_AGE_SEC(기본 3초) 인 틱은 미저장. "
"통계를 보고 확신 서면 ON — 매매 RAM 컷과 저장 컷 정합. "
"이전 KIWOOM_TICK_LIVE_MAX_LAG_SEC 대체 (2026-09-06)"
)),
_f("KIWOOM_TICK_STALE_LOG_GAP_SEC", "⏱ 키움 stale skip 로그 간격(초)", "float", default=30.0,
hint="종목당 stale skip 경고 최소 간격"),
_f("KIWOOM_WS_REG_REFRESH", "🧪 키움 REG refresh (테스트)", "int", default=1,
hint=(
"1(기본)=기존 구독 유지. 0=LOGIN 직후 일괄 REG 첫 청크만 그룹 구독 해지 후 재등록. "
"장중 종목 추가 REG는 항상 1. 장중 상시 0 비추(catch-up 재점화). "
"실매 반영은 kis_trader_main 재시작"
)),
_f("WS_ORDERBOOK_SAVE_KIWOOM", "📋 키움 실시간 호가 수집", "bool", default=True,
hint="ON = 키움 호가를 ws_orderbook 테이블에 저장 (기본값: ON)"),
_f("WS_TICK_SAVE_KIS", "📈 KIS 체결 틱 수집", "bool", default=True,
hint="ON = KIS 체결 틱을 ws_ticks 테이블에 저장 (기본값: ON)"),
_f("WS_ORDERBOOK_SAVE_KIS", "📋 KIS 실시간 호가 수집", "bool", default=True,
danger=True,
hint=(
"한투 호가는 2번째 앱키(KIS_APP_KEY_OB_REAL) 전용 세션만. "
"메인 시세 41과 합산 금지. 2키 없으면 ON 해도 호가 구독·적재 안 함. "
"2키 세션 한도는 호가 전용 41. LIVE_OB_PROVIDER=kis 일 때 ON 권장."
)),
_f("WS_SUBSCRIBE_KIS_MINIMAL", "📉 KIS 최소 구독 모드", "bool", default=False,
hint=(
"한투 소켓에 누구를 넣을지. LIVE_TICK_PROVIDER 와 무관. "
"OFF(기본)=한투도 후보·보유를 키움과 같이 구독(세션 41). "
"ON=한투는 보유만, 후보는 키움. 영구구독 KR은 LS"
)),
_f("WS_SUBSCRIBE_SPILL", "📡 구독초과/실패 즉시 벤더 spill", "bool", default=True,
hint=(
"ON(기본)=한도초과·구독실패·해당 WS 미연결 시 대기 없이 다음 벤더로 구독 "
"(틱/호가 체인 기본: LIVE_* 1차 → 상대 → ls 3차 RAM). "
"OFF=예전처럼 거절/패스. 기존 LIVE_TICK/OB_PROVIDER 값은 바꾸지 않음."
)),
_f("WS_TICK_SUBSCRIBE_CHAIN", "📡 틱 구독 spill 체인(csv)", "text", default="",
hint="비우면 LIVE_TICK_PROVIDER→나머지→ls. 예: kiwoom,kis,ls"),
_f("WS_OB_SUBSCRIBE_CHAIN", "📡 호가 구독 spill 체인(csv)", "text", default="kis,kiwoom,ls",
hint="기본 kis,kiwoom,ls. 비우면 LIVE_OB_PROVIDER→나머지→ls(3차 RAM)."),
_f("LS_WS_ENABLED", "📡 LS WS 기동 (재시작 필요)", "bool", default=False,
hint=(
"ON = LS증권 실시간 WS 기동 — 구독 spill 3차·영구구독·읽기 폴백 RAM. "
"OFF(기본)=VALIDATION/조건이력/ls_condition 전략이 있을 때만 기동. "
"키: env_config LS_APP_KEY_REAL / LS_APP_SECRET_REAL 필요."
)),
_f("LS_FEED_FALLBACK_SUBSCRIBE", "📡 LS 유니버스 RAM 미러 (레거시)", "bool", default=False,
hint=(
"OFF(기본)=KIS·키움과 동일 spill 3차만. ON=후보보유grace 전체를 LS에 "
"벌크 미러(_feed_fallback) — 200+ REG·07:00 Bye 위험. 영구는 별도(_permanent)"
)),
_f("LS_WS_HOLD_RAM_KEEP_HOLDINGS", "📡 LS hold RAM 보유 유지", "bool", default=True,
hint=(
"ON(기본)=장마감 hold 시 영구+보유 종목만 LS RAM 유지(07:00 replay 축소). "
"OFF=영구(_permanent)만 유지"
)),
_f("LS_WS_MAX_SUBSCRIPTIONS", "📡 LS 로컬 구독 한도", "int", default=100,
hint="spill 3차 LS 종목 상한. 0=무제한(비권장). 키움 100과 유사."),
_f("LS_WS_TICK_SAVE", "📈 LS 체결 틱 DB 적재", "bool", default=False,
hint=(
"ON=구독 중 종목(영구후보보유grace) US3 → ls_ws_ticks. "
"호가 DB 게이트와 동일. OFF=테이블 미적재(RAM·3차 폴백은 구독 ON이면 유지). "
"봉·VI 는 영구만. 옵투나 틱 3차는 BT_TICK_LS_THIRD_FALLBACK"
)),
_f("BT_TICK_LS_THIRD_FALLBACK", "📈 백테/옵투나 틱 3차 LS", "bool", default=True,
hint=(
"ON(기본)=ws_ticks 로드 후 같은 초에 1·2차 없으면 ls_ws_ticks 채움. "
"나이=LIVE_FEED_FALLBACK_MAX_AGE_SEC(기본3). history_source=ls 본체와 별개"
)),
_f("LS_WS_ALSO_HOGA", "📋 LS 호가(UH1) 구독 (재시작 필요)", "bool", default=True,
hint=(
"ON(기본)=LS 종목 구독 시 체결과 같이 UH1 호가도 받음. "
"OFF면 호가 RAM·ls_ws_orderbook 이 비어 3차 호가가 없음. "
"적재 스위치와 별개 — 둘 다 ON"
)),
_f("LS_WS_ORDERBOOK_SAVE", "📋 LS 호가 DB 적재", "bool", default=True,
hint=(
"ON(기본)=LS 구독 종목 호가를 ls_ws_orderbook 에 쌓음(영구만이 아님). "
"옵투나 호가 3차. tick 모드=체결 1건당 1장. "
"LS_WS_TICK_SAVE OFF 여도 이 스위치만 ON 이면 호가는 쌓임. "
"위 UH1 구독이 OFF 면 쌓을 사진이 없음"
)),
_f("LS_GAP_FILL_CANDIDATES", "⛔ 후보 LS 갭 REST (t8412)", "bool", default=False,
hint=(
"OFF(기본)=폴백 구독만, 후보 전 종목 t8412 금지. "
"영구 갭은 LS_GAP_FILL_ENABLED"
)),
_f("PERMANENT_SUBSCRIBE_ENABLED", "📡 영구구독 마스터 (재시작 필요)", "bool", default=True,
hint=(
"ON(기본)=테이블 enabled 종목을 KR=LS WS / US=해외 WS 로 구독. "
"OFF=행은 유지하고 구독만 안 함. LS 틱/봉 DB는 enabled 코드만."
)),
_f("WS_GAP_FILL_KIS_FALLBACK", "⛔ KIS REST 갭보정 (쓰지 말 것)", "bool", default=False,
hint=(
"페이지 REST라 느림. 코드가 ON이어도 호출하지 않음. "
"후보 갭보정은 키움 ka10080 → ws_candles(source=kiwoom,channel=rest)만."
)),
_f("PERM_LS_FILL_BARS", "영구구독 확정봉 가져오기 봉수", "int", default=500,
hint="키움 ka10080 → ls_ws_candles INSERT IGNORE. ws_candles 에 넣지 않음."),
_f("PERM_LS_FILL_SLEEP_MIN", "확정봉 가져오기 종목간 sleep 최소(초)", "float", default=1.0),
_f("PERM_LS_FILL_SLEEP_MAX", "확정봉 가져오기 종목간 sleep 최대(초)", "float", default=3.0),
_f("PERM_ALERT_ENABLED", "영구구독 목표가 MM알람", "bool", default=True,
hint="ON=목표가 도달 시 Mattermost(permanent 채널). 봇 루프 폴링."),
_f("PERM_ALERT_POLL_SEC", "목표가 검사 주기(초)", "int", default=3,
hint="heartbeat 에서 검사 간격. 최소 1."),
_f("KIS_PERM_SUB_MM_CHANNEL", "영구구독 MM alias", "text", default="permanent",
hint="mm_config.json channels 키. 채널 만든 뒤 ID 등록."),
_f("KIS_APP_KEY_OB_REAL", "🔑 KIS 실전 호가 전용 앱키 (선택)", "text",
hint="호가 전용 2번째 실전 앱키. 미입력 시 메인에 호가 TR 안 붙임(합산 금지). spill 틱용 아님."),
_f("KIS_APP_SECRET_OB_REAL", "🔑 KIS 실전 호가 전용 시크릿 (선택)", "text",
hint="호가 전용 2번째 실전 시크릿키."),
_f("KIS_APP_KEY_OB_MOCK", "🔑 KIS 모의 호가 전용 앱키 (선택)", "text",
hint="모의투자용 호가 전용 2번째 앱키."),
_f("KIS_APP_SECRET_OB_MOCK", "🔑 KIS 모의 호가 전용 시크릿 (선택)", "text",
hint="모의투자용 호가 전용 2번째 시크릿키."),
_f("LIVE_VALIDATOR_ENABLED", "🔬 시세 무결성 검증기 기동 (재시작 필요)", "bool", default=False,
hint="ON = 실매매 중 키움/KIS 시세 교차 검증 데몬을 백그라운드에 함께 기동 (기본값: OFF)"),
],
},
{
"id": "ws_trigger_global",
"title": "💾 TRIGGER 스냅샷 저장 (분석용 · 매수 차단 아님)",
"hint": (
"매수 검토 순간을 DB에 기록할지 설정. "
"여기 ON/OFF는 주문을 막지 않습니다. "
"진입 차단은 아래 「진입 호가필터」「프로그램 매매 필터」「휩쏘」를 보세요."
),
"fields": [
_f(
"KIWOOM_WS_PROGRAM_ENABLED",
"🤖 키움 프로그램 매매 데이터 수신",
"bool",
default=True,
hint=(
"ON = 기관·외국인 프로그램 매매 흐름을 메모리에 저장 | "
"OFF = 프로그램 매매 필터·수집 불가"
),
),
_f(
"WS_TRIGGER_EVAL_SAVE_ENABLED",
"💾 매수 판단 결과 DB 저장 (마스터 스위치)",
"bool",
default=True,
hint=(
"ON = 매수 검토할 때 호가·프로그램 스냅샷을 DB에 기록 (분석·디버깅용) | "
"OFF = 아래 모든 저장 비활성 (DB 용량 아낄 때) · 매수 차단과 무관"
),
),
_f(
"WS_ORDERBOOK_TICK_MAX_AGE_SEC",
"⏱ 호가 저장 유효 시간(초)",
"float",
default=30.0,
hint=(
"0D 덤프·시계열 저장용. 이 초보다 오래된 호가는 저장 안 함. "
"진입 필터 판정 나이와 무관(아래 FILTER_MAX_AGE). 코드 기본 30초"
),
),
_f(
"WS_ORDERBOOK_FILTER_MAX_AGE_SEC",
"⏱ 진입 호가필터 RAM 나이(초)",
"float",
default=0.0,
hint=(
"0(기본)=마지막 호가로 검사하되, 벤더가 폴백 나이 넘으면 그 사진만 버리고 2차·3차를 봄. "
"만료=None 으로 필터 ON 통과 구멍을 만들지 않음. REST 없음"
),
),
_f(
"WS_PRICE_MAX_AGE_SEC",
"⏱ WS 현재가 조회 나이(초)",
"float",
default=0.0,
hint=(
"0(기본)=읽기 폴백 나이(LIVE_FEED_FALLBACK, 기본 3초)로 메인→2차→3차. "
"양수=그 초와 폴백 나이 중 짧은 쪽. None 인자만 마지막 RAM. "
"매도 4차 REST 는 1·2·3 실패 시에만(키움 ka10007, 쿨다운)"
),
),
_f(
"SELL_WS_STALE_REST_SEC",
"⏱ 매도 죽은선 REST(초)",
"float",
default=1.0,
hint=(
"1·2·3차 WS 가 모두 없거나 2초 넘을 때 키움 ka10007 1회. "
"한투 inquire_price(60초 캐시) 안 씀. 0=REST 안 함. "
"종목당 쿨다운은 아래 간격. 매수 루프 전 종목 REST 금지"
),
),
_f(
"SELL_WS_STALE_REST_COOLDOWN_SEC",
"⏱ 매도 REST 확인 간격(초)",
"float",
default=30.0,
hint="죽은선 REST를 종목당 이 초마다 1회. 유량 보호. 코드 기본 30",
),
_f(
"SELL_WS_LAST_RAM_MAX_AGE_SEC",
"⏱ 매도 last-RAM 허용나이(초)",
"float",
default=30.0,
hint=(
"3초 체인 실패 시 마지막 WS RAM을 이 초까지 매도에 사용. "
"FID20 동결(패킷나이 초과)은 거부. 0=last-RAM 안 씀. "
"그 다음 직전가 캐시 → 키움 ka10007"
),
),
_f(
"WS_ORDERBOOK_FILTER_REJECT_IF_EMPTY",
"⛔ 호가 사진 없으면 매수 안 함",
"bool",
default=True,
hint=(
"ON(기본)=사기 직전 호가창 사진이 한 장도 없으면 이번엔 안 삼 "
"(다음 검사에서 사진 있으면 다시 봄) | "
"OFF=예전처럼 사진 없어도 통과(눈 가리고 사기). REST 없음"
),
),
_f(
"WS_ORDERBOOK_COLLECT_ENABLED",
"📸 매수 검토 순간 호가창 1장 저장",
"bool",
default=True,
hint=(
"ON = 매수 검토 순간 호가창(10호가 잔량 등) 스냅샷도 DB에 저장 | "
"→ 분석용 (용량 주의) · 진입 호가필터 ON/OFF와 별개"
),
),
_f(
"WS_PROGRAM_COLLECT_ENABLED",
"📸 매수 검토 순간 프로그램 매매 1장 저장",
"bool",
default=True,
hint=(
"ON = 매수 검토 순간 프로그램 매매 스냅샷도 DB에 저장 | "
"→ 분석용 · 프로그램 필터 차단 스위치와 별개"
),
),
_f(
"WS_PROGRAM_SAVE_ENABLED",
"📼 프로그램 매매 연속 시계열 저장",
"bool",
default=False,
hint=(
"ON = 프로그램 매매 데이터를 계속 DB에 쌓음 | "
"평소엔 OFF 권장, 변경 후 봇 재시작 필요"
),
),
],
},
{
"id": "program_entry_filter",
"title": "🚫 프로그램 매매 필터 (진입 차단)",
"hint": (
"기관·외국인 프로그램 수급이 나쁠 때 매수를 막는 스위치. "
"진입 호가필터·휩쏘·EXIT_OB(매도 호가)와 전부 별개."
),
"fields": [
_f(
"PROGRAM_FILTER_ENABLED",
"🚫 프로그램 매매 필터 (매수 차단)",
"bool",
default=True,
hint=(
"ON = 프로그램 매매 흐름이 나쁠 때 매수를 차단 | "
"OFF = 프로그램 수급 무시하고 매수 허용"
),
),
],
},
{
"id": "ws_whipsaw_global",
"title": "🌊 휩쏘 필터 (진입 차단 · 틱 급락→급반등)",
"hint": (
"호가창이 아니라 최근 틱/미니봉 가격 패턴으로 가짜 급반등을 차단. "
"예: 30초 안에 0.3% 이상 뚝 떨어졌다가 올라온 종목은 패스. "
"진입 호가필터(스프레드·잔량)·프로그램 필터와 다름."
),
"fields": [
_f(
"WHIPSAW_FILTER_ENABLED",
"🚫 휩쏘 필터 ON/OFF (전략 공통)",
"bool",
default=True,
hint="ON = 가짜 상승 종목 매수 차단 | OFF = 필터 끄고 다 매수",
),
_f(
"WHIPSAW_SUBBAR_SEC",
"⏱ 관찰 단위 시간(초)",
"int",
default=30,
hint=(
"틱을 몇 초 단위로 묶어서 미니봉을 만들지 | "
"기본 30초 = 30초짜리 막대기로 패턴 분석"
),
),
_f(
"WHIPSAW_LOOKBACK_SEC",
"⏳ 몇 초 전까지 볼지",
"int",
default=90,
hint=(
"매수 신호 직전 몇 초를 돌아볼지 | "
"기본 90초 = 1분 30초 전까지 확인"
),
),
_f(
"WHIPSAW_DIP_PCT",
"📉 이 이상 떨어졌으면 가짜 신호로 판단 (분율)",
"float",
default=0.003,
hint=(
"0.003 = 0.3% | 직전에 0.3% 이상 하락했다가 올라온 종목은 차단 | "
"더 큰 값 = 필터가 느슨해짐"
),
),
_f(
"WHIPSAW_RECOVERY_TOL_PCT",
"📈 이 정도는 회복 허용 (분율)",
"float",
default=0.001,
hint=(
"0.001 = 0.1% | 하락 후 0.1% 이내로 빠르게 회복하면 정상으로 봄 | "
"노이즈성 미세 출렁임 허용"
),
),
],
},
{
"id": "ws_universe_alert",
"title": "🔔 유니버스·알림 임계값",
"hint": "봇이 이상한 상황을 감지해서 알람을 보내기 위한 임계값 설정.",
"fields": [
_f(
"OPS_ALERT_HISTORY_OPEN_GRACE_SEC",
"🕐 개장 직후 히스토리 0건 허용 시간(초)",
"int",
default=180,
hint=(
"개장 후 N초 안에는 히스토리가 0건이어도 알람 안 보냄 | "
"기본 180초(3분) = 개장 3분까지는 정상으로 취급"
),
),
_f(
"OPS_ALERT_WS_DOWN_HOLD_SEC",
"📡 소켓 끊김 알람 기준 시간(초)",
"int",
default=60,
hint=(
"키움/LS 소켓이 이 시간(초) 이상 끊겨 있을 때만 위험 알람 발송 | "
"기본 60초 = 1분 이상 끊겨야 알람 (순간 재연결은 무시)"
),
),
],
},
]
# 호가필터 — 전략별 (글로벌 ORDERBOOK_* 수치 폐기)
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=진입은 안 막음(수치 보관만). "
"EXIT_OB/STOP_OB(매도 호가익절·손절)와 무관 · 휩쏘·프로그램 필터와도 무관"
),
),
_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,
hint="진입 필터용 · N호가 잔량 합산",
),
])
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": (
"호가창 스프레드·잔량·벽으로 매수만 막음. "
"끄면(OFF) 진입 차단만 해제 · 매도 쪽 EXIT_OB/STOP_OB는 아래 "
"「매도 호가익절·손절」카드에서 따로 킴/끔. "
"휩쏘(틱 패턴)·프로그램 필터와도 별개."
),
"fields": ob_fields,
})
groups.append({
"id": "strategy_switch",
"title": "전략 ON/OFF",
"hint": (
"config_{strategy} · STRATEGY_*_ENABLED 는 다음 루프부터 신규매수 중단/재개 "
"(보유 청산은 유지 · WS 구독 해제는 안 함). "
"기동 시 OFF 였던 전략을 켜려면 봇 재시작. "
"해외 WS 소켓은 OVERSEAS_WS_ENABLED(기동 시) · permanent US 구독과 별개."
),
"fields": [
_f(
"STRATEGY_US_MOMENTUM_ENABLED",
"해외 모멘텀",
"bool",
default=True,
hint="config_us_momentum · permanent US 유니버스 · 소켓 해제 없음",
),
_f(
"OVERSEAS_WS_ENABLED",
"해외 WS(HDFSCNT0)",
"bool",
default=True,
hint="기동 시만 · false=해외 틱 소켓 미기동 · 영구구독 US 시세/알람용(전략 OFF여도 ON 가능) · 변경 후 재시작",
),
*[
_f(
f"STRATEGY_{sid}_ENABLED",
LIVE_STRATEGY_LABELS.get(sid, sid),
"bool",
default=bool(STRATEGY_ENABLED_DEFAULTS.get(sid, False)),
hint=f"config_{sid.lower()}",
)
for sid in LIVE_STRATEGY_SWITCH_IDS
if sid != "US_MOMENTUM"
],
],
})
# 유니버스 소스 — 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,
})
groups.append({
"id": "kiwoom_condition_config",
"title": "키움 조건검색 (안정화·회복)",
"hint": "KIWOOM_CNSR* · 유니버스 0종목 공백(Blind Spot) 차단 및 3초 고정 회복",
"fields": [
_f("KIWOOM_CNSRCLR_BEFORE_REQ", "재등록 전 선행 해제(CNSRCLR)", "bool", default=True,
hint="Bye 후 유령 등록(900003) 방지용 선행 클리어"),
_f("KIWOOM_CNSRCLR_SETTLE_SEC", "CNSRCLR 해제 응답 ACK 대기(초)", "float", default=2.0,
hint="해제 응답(rc=0) 완료 대기 최대 타임아웃 (조기 ACK 수신 시 즉시 통과)"),
_f("KIWOOM_CNSRREQ_RETRY_SEC", "미응답/900003 고정 재시도 주기(초)", "float", default=3.0,
hint="지수 백오프 대신 3초 고정 주기로 빠른 회복 시도"),
_f("KIWOOM_CNSRREQ_MAX_RETRIES", "최대 재시도 상한", "int", default=30,
hint="30회(약 90초) 동안 RAM 후보를 보존하며 지속 회복 시도"),
_f("KIWOOM_CNSRCLR_GAP_MIN_SEC", "CNSRCLR 연속 발송 최소 간격(초)", "float", default=0.3),
_f("KIWOOM_CNSRCLR_GAP_MAX_SEC", "CNSRCLR 연속 발송 최대 간격(초)", "float", default=0.6),
_f("KIWOOM_CNSRREQ_GAP_MIN_SEC", "CNSRREQ 연속 발송 최소 간격(초)", "float", default=0.8),
],
})
groups.append({
"id": "ls_condition_config",
"title": "LS 조건검색 (안정화·Diff 보정)",
"hint": "LS_T1859* · 매매 전용이 아닌 조건·시세 전용 이점을 극대화한 1분 주기 0.15초 소생술",
"fields": [
_f("LS_T1859_STALE_RESYNC_SEC", "AFR 침묵 시 소켓 재동기화 주기(초)", "int", default=60,
hint="기본 60초(1분). 매매 주문을 처리하지 않으므로 429 차단 위험 0%! 하루 총 눈감는 시간 약 58초 동안 Delta Diff 1:1 보정"),
_f("LS_T1859_EMPTY_RETRY_SEC", "RAM 0종목 시 t1859 재시드 주기(초)", "int", default=1,
hint="기동 초기 0종목 시 1초마다 재시드 (영구 0종목 공백 록인 방지)"),
_f("LS_T1859_ALLOW_SHRINK", "t1859 축소 전체교체 허용", "bool", default=False,
hint="기본 false (장중 1종목 오류 응답이 20종목 덮어 쓰는 자살골 사고 차단)"),
_f("LS_CONDITION_TR_GAP_SEC", "t1859 REST 호출 최소 간격(초)", "float", default=1.1,
hint="LS 증권 공식 1초 1건 상한 보호 쿨다운"),
],
})
# 전략별 운용한도
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:
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"),
)
groups.append({
"id": "whipsaw_strategy",
"title": "휩쏘 — 전략별 ON/OFF (진입 차단)",
"hint": (
"전략별 휩쏘(틱 급락→급반등) 진입 차단 ON/OFF. "
"수치는 위 「휩쏘 필터」 글로벌. "
"진입 호가필터·프로그램 필터·EXIT_OB와 별개."
),
"fields": ws_fields,
})
groups.append({
"id": "execution_fill",
"title": "🏦 주문·체결 설정 (실전+모의 공통)",
"hint": (
"⚠️ 현재 모의(mock) 모드로 운영 중. "
"실전 전환 시 STRICT_FILL_VERIFY 는 무관 (실전은 항상 체결 확인). "
"나머지 설정은 모의·실전 모두 동일하게 적용됨."
),
"fields": [
_f(
"USE_MARKET_IOC",
"⚡ 매수 시장가 IOC (즉시체결 후 미체결 취소)",
"bool",
default=True,
hint=(
"ON(권장) = 매수 주문을 시장가로 내고, 즉시 안 채워진 나머지는 자동 취소 (실전만 해당, 모의는 항상 지정가) | "
"OFF = 시장가 보통주문 (미체결이 그냥 남아있음)"
),
),
_f(
"USE_MARKET_IOC_SELL",
"⚡ 매도 시장가 IOC",
"bool",
default=True,
hint=(
"ON = 매도도 IOC 시장가 (실전만 해당, 모의는 항상 지정가) | "
"OFF = 시장가 보통주문. 거절(주문거부) 많이 나오면 OFF로"
),
),
_f(
"STRICT_FILL_VERIFY",
"🔍 모의 체결 강제 확인 (모의 전용)",
"bool",
default=False,
hint=(
"★ 모의 모드에서만 의미 있음 (실전은 항상 체결 확인) ★ | "
"ON = 모의에서도 매수 후 체결 API 로 실제 체결 됐는지 확인. "
"모의 API 특성상 확인 실패해서 매수 0건으로 집계될 수 있음 | "
"OFF(기본) = 모의는 주문만 내고 체결됐다고 가정"
),
),
_f(
"BUY_DEDUP_PENDING",
"🚫 동일 종목 매수 중복 방지",
"bool",
default=True,
hint=(
"ON(권장) = 같은 전략·종목의 주문이 아직 체결 대기 중이면 추가 주문 안 냄 | "
"OFF = 중복 주문 허용 (같은 종목을 두 번 살 수 있음)"
),
),
_f(
"ORDER_FILL_WAIT_SEC",
"⏱ 시장가 주문 체결 대기 시간(초)",
"int",
default=2,
hint="주문 낸 후 체결됐는지 확인하기까지 기다리는 시간. 기본 2초.",
),
_f(
"PENDING_BUY_MAX_AGE_SEC",
"⏳ 매수 미체결 주문 포기 시간(초)",
"int",
default=120,
hint=(
"매수 주문을 냈는데 이 시간(초) 안에 체결이 안 되면 포기하고 주문 취소 | "
"기본 120초(2분)"
),
),
_f(
"PENDING_SELL_MAX_AGE_SEC",
"⏳ 익절 매도 미체결 주문 포기 시간(초)",
"int",
default=60,
hint="익절 매도 주문이 이 시간(초) 안에 안 채워지면 포기. 기본 60초.",
),
_f(
"PENDING_SELL_STOP_MAX_AGE_SEC",
"⏳ 손절 매도 미체결 주문 포기 시간(초)",
"int",
default=15,
hint=(
"손절은 빨리 안 팔리면 위험! 이 시간(초) 안에 체결 안 되면 재주문 | "
"기본 15초 (손절은 짧게)"
),
),
_f(
"SELL_PENDING_REORDER_ON_EXPIRE",
"🔄 손절 포기 후 즉시 재주문",
"bool",
default=True,
hint=(
"ON(권장) = 손절 주문이 시간 초과되면 즉시 새로 손절 주문 | "
"OFF = 그냥 포기"
),
),
_f(
"AUTO_CANCEL_PARTIAL_BUY_REMAINDER",
"✂ 매수 일부 체결 후 나머지 자동 취소",
"bool",
default=True,
hint=(
"ON(권장) = 100주 주문했는데 50주만 체결됐으면 나머지 50주 주문 자동 취소 | "
"OFF = 나머지 50주 주문이 그대로 남아있음"
),
),
_f(
"SELL_LIMIT_CANCEL_BEFORE_MARKET_RETRY",
"🔒 익절 지정가 취소 후 시장가 전환",
"bool",
default=True,
hint=(
"ON(권장) = 익절 지정가 매도가 안 팔리면 → 취소 확인 후 → 시장가로 바꿔서 팜. "
"안전하게 이중매도 방지 | "
"OFF = 레거시 방식 (이중매도 위험, 비권장)"
),
),
_f(
"SELL_LIMIT_CANCEL_FAIL_BALANCE_CONFIRM",
"🛡 취소 실패해도 잔고 0이면 매도 완료 처리",
"bool",
default=True,
hint=(
"ON(권장) = 취소가 실패해도 보유=0·매도가능=0·시장 미체결 매도=0 일 때만 지정가 체결로 확정 | "
"매도가능만 0인 경우는 아직 안 팔린 것 → 확정 안 함"
),
),
_f(
"GHOST_PURGE_BLOCK_WHILE_PENDING_SELL",
"👻 매도 주문 대기 중 유령 정리 금지",
"bool",
default=True,
hint=(
"ON(권장) = 아직 체결 확인 안 된 매도 주문이 있으면 유령(DB만 있는 포지션) 정리 안 함 | "
"'팔리는 중인데 유령이라고 지워버리는' 사고 방지"
),
),
_f(
"INQUIRE_PSBL_RVSECNCL_BEFORE_GHOST",
"📋 유령정리 전 정정취소가능 조회",
"bool",
default=True,
hint=(
"ON(권장) = 실전은 한투 정정취소가능주문조회, 모의는 당일체결 미체결로 "
"시장에 남은 매도를 본 뒤에만 유령 정리 | "
"매도주문 나가면 매도가능=0 이 정상. 그걸 0주=없다 로 지우지 않음 | "
"OFF = 예전처럼 잔고 맵만 봄 (비권장)"
),
),
_f(
"PSBL_RVSECNCL_CACHE_TTL_SEC",
"📋 정정취소가능 조회 캐시(초)",
"int",
default=30,
hint="같은 초 안에 종목마다 REST 연타 방지. sell_locked retry 시 캐시 hit.",
),
_f(
"CANCELABLE_CCLD_MAX_PAGES",
"📋 모의 cancelable daily-ccld 최대 페이지",
"int",
default=3,
hint="모의투자 cancelable 조회 시 daily-ccld 페이지 상한. 기본 3 (유량·지연 보호).",
),
_f(
"SELL_LOCKED_ENQUEUE_COOLDOWN_SEC",
"⏸ sell_locked enqueue 쿨다운(초)",
"int",
default=20,
hint=(
"sell_locked/sellable_zero/pending 시 tick·scan 재 enqueue 대기. "
"REST 폭주 방지 (SELL_FAILURE_BACKOFF 와 별개)."
),
),
_f(
"CANCELABLE_RECONCILE_ENABLED",
"🔄 cancelable_open DB↔브로커 reconcile",
"bool",
default=True,
hint="ON = DB CANCELLED vs 브로커 미체결 open 불일치 시 자동 조사·복구 시도.",
),
_f(
"CANCELABLE_RECONCILE_CANCEL_RETRY",
"🔄 reconcile 시 cancel_order 재시도",
"bool",
default=True,
hint="reconcile 중 브로커 미체결 ODNO 에 cancel_order 1회 재시도.",
),
_f(
"PSBL_RVSECNCL_MAX_PAGES",
"📋 정정취소가능 최대 페이지",
"int",
default=5,
hint="한 페이지 최대 50건. 5페이지면 250건. REST 유량 보호 상한.",
),
_f(
"BACKTEST_SLIP_PCT",
"📊 백테스트 슬리피지 (백테 전용)",
"float",
default=0.0,
hint=(
"★ 백테스트·파람서치에만 적용, 실제 매매 무관 ★ | "
"0.2 = 매수가에 0.2%, 매도가에서 0.2% 차감해서 현실적인 수익 계산 | "
"0 = 이상적인 체결 가정 (실제보다 성과 좋게 나옴)"
),
),
_f(
"BACKTEST_VOL_FILL_CAP_PCT",
"📊 백테스트 체결량 상한 (백테 전용)",
"float",
default=0.0,
hint=(
"★ 백테스트·파람서치에만 적용 ★ | "
"0 = 무제한 (다 체결됐다고 가정) | "
"예: 20 = 해당 봉 거래량의 20%까지만 체결 가정 (소형주 현실 반영)"
),
),
_f(
"TAIL_BACKTEST_USE_TICK_EXIT",
"📊 꼬리잡기 백테 틱 청산 (백테 전용)",
"bool",
default=True,
hint=(
"★ 백테스트에서만 사용 ★ | "
"ON(권장) = 실제 틱 데이터 시간순으로 손절/익절 판단 (실매매와 동일 방식) | "
"OFF = 봉 OHLC로 판단 (덜 정확)"
),
),
_f(
"TAIL_BACKTEST_TICK_FALLBACK_OHLC",
"⛔ 꼬리잡기 OHLC 폴백 (절대 금지)",
"bool",
default=False,
danger=True,
hint=(
"⛔ 절대 ON 하지 말 것! ⛔ | "
"ON = 틱 데이터로 청산하는 대신 봉 고점/저점으로 체결가 계산 → 실매매와 완전 다른 결과"
),
),
_f(
"TAIL_BACKTEST_POLL_MS",
"📊 꼬리잡기 틱 청산 검사 간격(ms) (백테 전용)",
"int",
default=100,
hint="백테에서 틱을 몇 ms마다 확인할지. 100ms = 0.1초 = 실매매 루프와 동일.",
),
_f(
"TAIL_PARAM_SEARCH_MIN_START",
"📅 꼬리잡기 파라미터 서치 시작일 하한",
"str",
default="2026-07-07",
hint=(
"꼬리잡기 Optuna 파람서치를 이 날짜 이전 데이터로는 탐색 안 함 | "
"조건식 바꾼 날 이후만 탐색하도록. YYYY-MM-DD 형식."
),
),
_f(
"BREAKOUT_BACKTEST_USE_TICK_EXIT",
"📊 돌파 백테 틱 청산 (백테 전용)",
"bool",
default=True,
hint=(
"★ 백테스트에서만 사용 ★ | "
"ON(권장) = 실제 틱 순서로 청산 판단 (실매매 정합)"
),
),
_f(
"BREAKOUT_BACKTEST_TICK_FALLBACK_OHLC",
"⛔ 돌파 OHLC 폴백 (절대 금지)",
"bool",
default=False,
danger=True,
hint="⛔ 절대 ON 하지 말 것! ⛔ | 봉 high/low로 체결가 계산 → 실매와 다른 가짜 결과",
),
_f(
"BREAKOUT_BACKTEST_POLL_MS",
"📊 돌파 틱 청산 검사 간격(ms) (백테 전용)",
"int",
default=100,
hint="백테에서 틱을 몇 ms마다 확인할지. 기본 100ms.",
),
_f(
"MOMENTUM_BACKTEST_TICK_FALLBACK_OHLC",
"⛔ 모멘텀 OHLC 폴백 (절대 금지)",
"bool",
default=False,
danger=True,
hint="⛔ 절대 ON 하지 말 것! ⛔",
),
_f(
"MOMENTUM_BACKTEST_POLL_MS",
"📊 모멘텀 틱 청산 검사 간격(ms) (백테 전용)",
"int",
default=100,
),
_f(
"SCALP_BACKTEST_USE_TICK_EXIT",
"📊 스캘핑 백테 틱 청산 (백테 전용)",
"bool",
default=True,
hint="ON = 실제 틱 순서로 청산 (실매매 정합)",
),
_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,
),
],
})
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("LOOP_PROFILE_ENABLED", "루프 숙제 ms 계측", "bool", default=False,
hint="ON=한 바퀴 단계별·락 wait/hold·check_buy 합/max 로그 · 병목 확정용 · 기본 OFF"),
_f("LOOP_PROFILE_EVERY_N", "계측 샘플 주기(바퀴)", "int", default=1,
hint="N바퀴마다 1회 상세 로그 · 1=매바퀴 · 로그 폭주 시 5~10"),
_f("LOOP_PROFILE_LOG_PATH", "계측 로그 파일", "str", default="logs/loop_profile.log",
hint="상대경로=레포루트 기준 · 비우면 전략 logger INFO 만"),
_f("CHECK_BUY_PROF_MIN_MS", "check_buy 상세 로그 하한(ms)", "int", default=500,
hint="이 ms 이상인 check_buy 만 [CHECK_BUY_PROF] 단계별 기록 · LOOP_PROFILE_ENABLED 필요"),
_f("HOLDINGS_DB_SYNC_INTERVAL_SEC", "보유 DB sync 안전망(초)", "int", default=60,
hint="루프마다 get_active_trades 대신 N초마다만 · 매수/매도 체결은 RAM 즉시 · "
"0=매루프(레거시) · 재시작·지정가체결은 강제 sync"),
_f("TODAY_TRADES_CACHE_TTL_SEC", "당일거래 RAM TTL(초)", "float", default=1.0,
hint="check_buy·일일한도 가드 공유 · N초 내 DB 재조회 생략 · 체결 시 즉시 무효화 · 0=매번 DB"),
_f("STRATEGY_ENV_CACHE_TTL_SEC", "전략설정 RAM TTL(초)", "float", default=1.0,
hint="config_* reload 매루프 SELECT 완화 · 기본 1초 · 웹 저장 시 즉시 무효화 · "
"유니버스/봉 분슬롯과 무관(임계값만)"),
_f("TICK_SELL_ENABLED", "틱매도(공통)", "bool", default=False,
hint="ON=WS 현재가 갱신마다 매도검사(백테 틱청산에 근접) · OFF=루프(~0.1초)만 · "
"전략별 *_TICK_SELL_ENABLED 가 비어있지 않으면 그쪽 우선 · 기본 OFF. "
"주문은 TICK_SELL_ASYNC_PLACE(기본 ON) 이면 워커"),
_f("TICK_SELL_MIN_INTERVAL_MS", "틱매도 최소간격(ms)", "int", default=50,
hint="같은 종목 연속 틱 폭주 시 매도검사 하한 · 백테 poll≈50~100ms 정합"),
_f("TICK_SELL_ASYNC_PLACE", "틱매도 주문 비동기", "bool", default=True,
hint="ON(기본)=틱 수신 스레드에서는 신호만 큐. 잔고·주문 REST는 워커. "
"OFF=예전처럼 틱 콜백에서 place()까지 동기(시세 일시정지)"),
],
})
# 전략별 틱매도 스위치 (비어 있으면 공통 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종목이 한도. "
"전략별 후보 상한 합 + 보유 + grace ≤ 100. "
"영구구독 KR은 LS(키움 100·한투 41에 안 넣음). "
"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": "breakout_exit",
"title": "📉 매도 호가익절·손절 (EXIT_OB/STOP_OB) — 돌파",
"hint": (
"보유 중 청산만. 진입 호가필터(*_ORDERBOOK_FILTER_ENABLED)를 꺼도 "
"여기 ON이면 익절/손절 호가매도는 그대로 동작. "
"꼬리·스캘프는 스위치 없음."
),
"fields": [
_f("BREAKOUT_MAX_HOLD_BARS", "최대보유(분)", "int", default=0,
hint="0=OFF · 진입 후 N분 경과 시 max_hold 청산"),
_f("BREAKOUT_EXIT_OB_ENABLED", "수익구간 호가매도 ON (익절)", "bool", default=False,
danger=True,
hint="진입 필터와 무관 · ON=이득+매수잔량 붕괴 시 즉시 탈출"),
_f("BREAKOUT_EXIT_OB_RATIO_MIN", "수익구간 OR_MA 임계", "float", default=0.4,
hint="매수잔량/매도잔량 MA 미만이면 붕괴 · 예: 0.4"),
_f("BREAKOUT_EXIT_OB_MA_WINDOW", "수익구간 OR MA 창", "int", default=5,
hint="OR 이동평균에 쓰는 최근 스냅 수"),
_f("BREAKOUT_EXIT_OB_MIN_PROFIT_PCT", "수익구간 최소이익(분율)", "float", default=0.005,
hint="0.005=+0.5% · 미달이면 호가컷 미발동"),
_f("BREAKOUT_EXIT_OB_MIN_HOLD_BARS", "수익구간 최소보유(분)", "int", default=3,
hint="보유 분(1분봉≈1) 미달이면 미발동"),
_f("BREAKOUT_STOP_OB_ENABLED", "손절호가 ON", "bool", default=False,
danger=True,
hint="진입 필터와 무관 · ON=손해+매수잔량 붕괴 시 하드손절 전 미리 탈출"),
_f("BREAKOUT_STOP_OB_RATIO_MIN", "손절호가 OR_MA 임계", "float", default=0.4,
hint="손실구간 OR_MA 임계"),
_f("BREAKOUT_STOP_OB_MA_WINDOW", "손절호가 OR MA 창", "int", default=5,
hint="손절호가 OR 이동평균 스냅 수"),
_f("BREAKOUT_STOP_OB_MIN_LOSS_PCT", "손절호가 최소손실(분율)", "float", default=0.003,
hint="0.003=0.3% · 이만큼 이상 손해일 때만"),
_f("BREAKOUT_STOP_OB_MIN_HOLD_BARS", "손절호가 최소보유(분)", "int", default=2,
hint="보유 분 미달이면 손절호가 미발동"),
],
})
groups.append({
"id": "momentum_exit",
"title": "📉 매도 호가익절·손절 (EXIT_OB/STOP_OB) — 모멘텀",
"hint": (
"보유 중 청산만. 진입 호가필터를 꺼도 여기 ON이면 그대로 동작. "
"돌파 카드와 동일 개념 · 꼬리·스캘프 없음."
),
"fields": [
_f("MOMENTUM_EXIT_OB_ENABLED", "수익구간 호가매도 ON (익절)", "bool", default=False,
danger=True,
hint="진입 필터와 무관 · ON=이득+매수잔량 붕괴 시 즉시 탈출"),
_f("MOMENTUM_EXIT_OB_RATIO_MIN", "수익구간 OR_MA 임계", "float", default=0.4,
hint="매수잔량/매도잔량 MA 미만이면 붕괴 · 예: 0.4"),
_f("MOMENTUM_EXIT_OB_MA_WINDOW", "수익구간 OR MA 창", "int", default=5,
hint="OR 이동평균에 쓰는 최근 스냅 수"),
_f("MOMENTUM_EXIT_OB_MIN_PROFIT_PCT", "수익구간 최소이익(분율)", "float", default=0.005,
hint="0.005=+0.5% · 미달이면 호가컷 미발동"),
_f("MOMENTUM_EXIT_OB_MIN_HOLD_BARS", "수익구간 최소보유(분)", "int", default=3,
hint="보유 분(1분봉≈1) 미달이면 미발동"),
_f("MOMENTUM_STOP_OB_ENABLED", "손절호가 ON", "bool", default=False,
danger=True,
hint="진입 필터와 무관 · ON=손해+매수잔량 붕괴 시 하드손절 전 미리 탈출"),
_f("MOMENTUM_STOP_OB_RATIO_MIN", "손절호가 OR_MA 임계", "float", default=0.4,
hint="총사÷총팔 MA 미만이면 붕괴 · 예: 0.4"),
_f("MOMENTUM_STOP_OB_MA_WINDOW", "손절호가 OR MA 창", "int", default=5,
hint="OR 이동평균 스냅 수"),
_f("MOMENTUM_STOP_OB_MIN_LOSS_PCT", "손절호가 최소손실(분율)", "float", default=0.003,
hint="0.003=0.3% · 이만큼 이상 손해일 때만"),
_f("MOMENTUM_STOP_OB_MIN_HOLD_BARS", "손절호가 최소보유(분)", "int", default=2,
hint="보유 분 미달이면 미발동"),
],
})
groups.append({
"id": "eod_global",
"title": "🌅 EOD 당일청산 설정 (전략 통합)",
"hint": "각 전략별로 정해진 시각이 되면 보유 물량을 전량 청산(퇴근)합니다.",
"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 마감 직전 순차매도"),
_f("BREAKOUT_EOD_ENABLED", "돌파 EOD 사용", "bool", default=True),
_f("BREAKOUT_EOD_HM", "돌파 EOD 시각 (HH:MM)", "text", default="15:15",
hint="예: 15:15 (빠르게 정리)"),
_f("MOMENTUM_EOD_ENABLED", "모멘텀 EOD 사용", "bool", default=True),
_f("MOMENTUM_EOD_HM", "모멘텀 EOD 시각 (HH:MM)", "text", default="15:20",
hint="순차매도 여유분 감안"),
_f("TAIL_EOD_ENABLED", "꼬리잡기 EOD 사용", "bool", default=True),
_f("TAIL_EOD_HM", "꼬리잡기 EOD 시각 (HH:MM)", "text", default="15:20"),
],
})
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": "ops_alert",
"title": "운영치명 알림 (봇 내부)",
"hint": (
"실매 프로세스 안에서 토큰/WS/유니버스0/주문거부/유량/VI/MarketGuard PANIC 을 "
"시스템 MM(+TG)로 보냄. journal 오류감시와 별개·보완."
),
"fields": [
_f("OPS_ALERT_ENABLED", "운영알림 ON", "bool", default=True),
_f("OPS_ALERT_MM_CHANNEL", "MM alias", "text", default="",
hint="비우면 KIS_SYSTEM_MM_CHANNEL"),
_f("OPS_ALERT_TG_ON_CRITICAL", "critical 시 TG", "bool", default=True),
_f("OPS_ALERT_COOLDOWN_SEC", "코드별 쿨다운(초)", "int", default=300),
_f("OPS_ALERT_GLOBAL_MIN_GAP_SEC", "전역 최소간격(초)", "int", default=20),
_f("OPS_ALERT_SESSION_START_HM", "장중게이트 시작 HHMM", "int", default=900),
_f("OPS_ALERT_SESSION_END_HM", "장중게이트 종료 HHMM", "int", default=1535),
_f("OPS_ALERT_UNIVERSE_ZERO_SEC", "유니버스0 지속(초)", "int", default=180,
hint="장중 유니버스 0 이 이 시간 이상이면 알림"),
_f("OPS_ALERT_HISTORY_STALE_SEC", "히스토리 공백(초)", "int", default=600,
hint="키움 history 당일 최신 스냅샷이 이보다 오래면 알림"),
_f("OPS_ALERT_ORDER_REJECT_STREAK", "주문거부 연속 N회", "int", default=3),
_f("OPS_ALERT_RATE_LIMIT_STREAK", "유량 연속 N회", "int", default=5),
_f("OPS_ALERT_VI_ENABLED", "VI 발동 알림", "bool", default=True),
_f("OPS_ALERT_MARKET_GUARD_ENABLED", "MarketGuard PANIC 알림", "bool", default=True),
],
})
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_ON_STARTUP", "기동 시 잔고=DB 맞춤", "bool", default=True,
hint="봇 시작 직후 1회 · 매도 없음 · 고아복구+유령삭제+수량/평단 sync · 전략 쓰레드보다 먼저 · OFF 시 웹 「잔고=DB 맞추기」만"),
_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주·매도가능0·정정취소가능 없을 때 active 삭제 · **현재 KIS_MOCK=is_mock 행만** (모의↔실전 교차삭제 금지) · Pre/Post/기동"),
_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)"),
],
})
# 전략 ON/OFF — 운영설정 탭 최상단
sw_idx = next((i for i, g in enumerate(groups) if g.get("id") == "strategy_switch"), None)
if sw_idx is not None:
sw = groups.pop(sw_idx)
groups.insert(0, sw)
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()