Changes: - Introduced new files for strategy definitions and study names. - Enhanced `backtest_web.py` with functions to handle integer display prices and trade data formatting. - Updated backtesting logic to incorporate end-of-day (EOD) parameters for breakout and momentum strategies. - Added EOD configuration options in the database and parameter search files. Impact: - These changes improve the modularity and usability of the backtesting framework, allowing for better integration of EOD strategies and clearer trade data presentation.
530 lines
27 KiB
Python
530 lines
27 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",
|
|
}
|
|
|
|
# 전략 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). 매도·손절은 항상 유지"
|
|
),
|
|
"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=미사용"),
|
|
_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"),
|
|
],
|
|
},
|
|
{
|
|
"id": "ws_trigger_global",
|
|
"title": "TRIGGER 필터 — WS 공통",
|
|
"hint": "호가(0D)·프로그램(0w) · env_config(컬럼 없으면 env_config_ext)",
|
|
"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("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
|
|
],
|
|
},
|
|
]
|
|
|
|
# 전략별 운용한도
|
|
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=정상",
|
|
),
|
|
])
|
|
groups.append({
|
|
"id": "daily_profit_strategy",
|
|
"title": "일일 익절 — 전략별 (서브)",
|
|
"hint": (
|
|
"전략별 손익만 따로 감시. 비어 있으면 마스터 값 상속. "
|
|
"손익 감시 ON이면 목표 달성 시 해당 전략 신규매수 자동 차단"
|
|
),
|
|
"fields": dp_fields,
|
|
})
|
|
|
|
# 전략별 휩쏘 ON/OFF (간단)
|
|
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"),
|
|
)
|
|
groups.append({
|
|
"id": "whipsaw_strategy",
|
|
"title": "휩쏘 — 전략별 ON/OFF",
|
|
"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("USE_MARKET_IOC", "시장가 IOC", "bool", default=True,
|
|
hint="실전 시장가 주문 방식 · 모의는 일반 시장가(01) 고정"),
|
|
_f("ORDER_FILL_WAIT_SEC", "시장가 체결 대기(초)", "int", default=2),
|
|
_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),
|
|
],
|
|
})
|
|
|
|
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 1~2.5초) 사용."
|
|
),
|
|
"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=1.0,
|
|
hint="비우면 전략별 기본값 · 한 바퀴 끝 대기"),
|
|
_f("STRATEGY_LOOP_SLEEP_MAX", "루프 끝 sleep 최대(초)", "float", default=2.0),
|
|
],
|
|
})
|
|
|
|
# 전략별 후보 하드캡 (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": "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:25",
|
|
hint="이 시각 이후 보유 전량 청산 · 예: 15:25"),
|
|
],
|
|
})
|
|
|
|
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:25",
|
|
hint="이 시각 이후 보유 전량 청산 · 예: 15:25"),
|
|
],
|
|
})
|
|
|
|
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 제외"),
|
|
],
|
|
})
|
|
|
|
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()
|