feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선

변경 사항 (Changes):

구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함.

스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함.

코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함.

시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함.

기대 효과 (Impact):

이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
This commit is contained in:
Your Name
2026-07-17 01:09:09 +09:00
parent a4626e0351
commit fc27e726f9
151 changed files with 20718 additions and 6450 deletions

View File

@@ -97,20 +97,14 @@ def build_live_config_groups() -> List[GroupDef]:
groups: List[GroupDef] = [
{
"id": "daily_profit_global",
"title": "일일 익절 — 총합 (마스터)",
"title": "일일 익절 — 총합 (규칙·수동중단)",
"hint": (
"손익 감시 켜기 = 당일 실현손익을 목표와 비교 "
"(ON이면 목표 달성 시 신규매수 자동 차단) "
" 신규매수 수동 중단 = 목표·손익과 무관하게 매수만 잠금 "
"(긴급 일시정지용, 기본 OFF). 매도·손절은 항상 유지"
"손익 감시 ON/OFF 는 전략별(*_DAILY_PROFIT_TARGET_ENABLED)만 사용. "
"여기 마스터는 총합 목표금액/%·수동 매수잠금·알림·트레일 규칙. "
" 신규매수 수동 중단 = 목표·손익과 무관 매수만 잠금(긴급용, 기본 OFF). "
"매도·손절은 항상 유지"
),
"fields": [
_f(
"DAILY_PROFIT_TARGET_ENABLED",
"손익 감시 켜기 (일일익절)",
"bool",
hint="ON = 목표 달성 시 신규매수 자동 차단 · OFF = 목표와 무관하게 매수 계속",
),
_f("DAILY_PROFIT_TARGET_KRW", "목표 금액(원)", "int", default=0),
_f("DAILY_PROFIT_TARGET_PCT", "목표 수익률(%)", "float", default=0,
hint="퍼센트 그대로 (예: 2.5 = 2.5%) · 0=미사용"),
@@ -152,34 +146,172 @@ def build_live_config_groups() -> List[GroupDef]:
_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)",
"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),
_f("KIWOOM_WS_PROGRAM_ENABLED", "프로그램 WS(0w)", "bool", default=True),
_f("ORDERBOOK_FILTER_ENABLED", "호가 필터(TRIGGER)", "bool", default=True),
_f("ORDERBOOK_MAX_SPREAD_PCT", "호가 최대 스프레드(%, 0.45=0.45%)", "float", default=0.45),
_f("PROGRAM_FILTER_ENABLED", "프로그램 필터(TRIGGER)", "bool", default=True),
_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_DIP_PCT", "휩쏘 하락폭(분율, 0.003=0.3%)", "float", default=0.003),
_f("WHIPSAW_RECOVERY_TOL_PCT", "휩쏘 회복 허용(분율, 0.001=0.1%)", "float", default=0.001),
],
},
{
"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
],
},
]
# 호가필터 — 전략별 (글로벌 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
],
})
# 전략별 운용한도
budget_fields: List[FieldDef] = []
for sid in LIVE_STRATEGY_IDS:
@@ -289,9 +421,51 @@ def build_live_config_groups() -> List[GroupDef]:
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,
hint="OFF=틱 없는 구간 체결 스킵(실매 정합) · ON=3분봉 OHLC 추측(유령거래)"),
_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,
hint="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,
hint="OFF 권장 — B안 틱 없으면 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,
hint="OFF 권장 — 틱 없으면 청산 스킵"),
_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,
hint="OFF 권장 — 틱 없으면 청산 스킵"),
_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,
hint="OFF 권장 — 틱 없으면 청산 스킵"),
_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),
@@ -333,7 +507,7 @@ def build_live_config_groups() -> List[GroupDef]:
"hint": (
"REST 유량과 별개 — 전략 매수체크 루프의 sleep. 짧을수록 WS 실시간 신호를 빨리 잡지만 CPU↑. "
"탈락 경로는 REST 0건(WS·DB·연산만)이라 줄여도 한투 유량 무관. 재시작 없이 반영. "
"비우면 전략별 기본값(loop 1~2.5초) 사용."
"비우면 전략별 기본값(loop 0.1초) 사용."
),
"fields": [
_f("SCAN_REJECT_SLEEP_MIN", "탈락 후 sleep 최소(초)", "float", default=0.2,
@@ -344,9 +518,9 @@ def build_live_config_groups() -> List[GroupDef]:
_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=1.0,
hint="비우면 전략별 기본값 · 한 바퀴 끝 대기"),
_f("STRATEGY_LOOP_SLEEP_MAX", "루프 끝 sleep 최대(초)", "float", default=2.0),
_f("STRATEGY_LOOP_SLEEP_MIN", "루프 끝 sleep 최소(초)", "float", default=0.1,
hint="한 바퀴 끝 대기 · WS 구독 캐시 폴링 · 0.1=실매 반응 빠르게"),
_f("STRATEGY_LOOP_SLEEP_MAX", "루프 끝 sleep 최대(초)", "float", default=0.1),
],
})
@@ -445,8 +619,8 @@ def build_live_config_groups() -> List[GroupDef]:
"fields": [
_f("MOMENTUM_EOD_ENABLED", "EOD 당일청산 사용", "bool", default=True,
hint="ON=설정 시각 이후 보유 전량 장마감청산 · OFF=익절·손절·시간컷만"),
_f("MOMENTUM_EOD_HM", "EOD 시각 (HH:MM)", "text", default="15:25",
hint="이 시각 이후 보유 전량 청산 · 예: 15:25"),
_f("MOMENTUM_EOD_HM", "EOD 시각 (HH:MM)", "text", default="15:20",
hint="정규장 15:30 마감 기준 최소 10분 전 · 순차매도 여유"),
],
})
@@ -457,14 +631,43 @@ def build_live_config_groups() -> List[GroupDef]:
"fields": [
_f("TAIL_EOD_ENABLED", "EOD 당일청산 사용", "bool", default=True,
hint="ON=설정 시각 이후 보유 전량 장마감 청산 · OFF=익절·손절·시간컷만"),
_f("TAIL_EOD_HM", "EOD 시각 (HH:MM)", "text", default="15:25",
hint="이 시각 이후 보유 전량 청산 · 예: 15:25"),
_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 에 넣은 종목은 일괄매도·고아복구 모두 제외."
@@ -476,6 +679,18 @@ def build_live_config_groups() -> List[GroupDef]:
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("GHOST_PURGE_ON_RECONCILE", "고아복구 시 유령잔고 삭제", "bool", default=True,
hint="ON=브로커 0주인데 active_trades 남은 종목 삭제(수동보호 제외) · Pre/Post EOD 동일 잔고조회에서 처리"),
_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)"),
],
})