grid_keys=tail_tpe_axis_keys(tpe_entry_mode) if mode == "tpe" else tail_grid_axis_keys(mode),

grid_keys=tail_tpe_axis_keys() if mode == "tpe" else tail_grid_axis_keys(mode),

        help="꼬리 TPE만. 한 스터디에 한 모드(미지정=TAIL_PARAM_SEARCH_ENTRY_MODE 또는 align). "
             "둘 다 보려면 스터디를 나눠 두 번 실행.",
    )
    parser.add_argument(
        "--sl-mode",
        default=None,
        dest="sl_mode",
        choices=["fixed", "atr"],
        help="돌파 TPE만. 한 스터디에 한 손절모드(미지정=fixed). "
             "fixed=sl_pct 탐색, atr=atr_sl_mult 탐색. 둘 다 보려면 스터디를 나눠 두 번.",
    )

        help="꼬리 TPE만. 한 스터디에 한 모드(미지정=TAIL_PARAM_SEARCH_ENTRY_MODE 또는 align). "
             "둘 다 보려면 스터디를 나눠 두 번 실행.",
    )

    if strategy == "tail" and mode == "tpe":
        _study_extra = normalize_tpe_tail_entry_mode(
            getattr(args, "entry_mode", None) or "align",
        )
    elif strategy == "breakout" and mode == "tpe":
        from kis_trader.backtest.optuna_breakout_tpe_space import (
            normalize_tpe_breakout_sl_mode,
        )
        _study_extra = normalize_tpe_breakout_sl_mode(
            getattr(args, "sl_mode", None) or "fixed",
        )

    if strategy == "tail" and mode == "tpe":
        _study_extra = normalize_tpe_tail_entry_mode(
            getattr(args, "entry_mode", None) or "align",
        )

            ctx_b = prepare_breakout_search_context(
                args.start, args.end, mode,
                use_fallback_universe=use_fallback,
                time_start_hm=args.time_start,
                time_end_hm=args.time_end,
                slot_money=args.slot_money,
                max_stocks=args.max_stocks,
                total_budget_krw=args.total_budget,
                orderbook_filter=args.orderbook_filter,
                history_source=args.universe_history_source,
                sl_mode=getattr(args, "sl_mode", None),
            )

            ctx_b = prepare_breakout_search_context(
                args.start, args.end, mode,
                use_fallback_universe=use_fallback,
                time_start_hm=args.time_start,
                time_end_hm=args.time_end,
                slot_money=args.slot_money,
                max_stocks=args.max_stocks,
                total_budget_krw=args.total_budget,
                orderbook_filter=args.orderbook_filter,
                history_source=args.universe_history_source,
            )


StrReplace

def _normalize_breakout_sl_modes(raw: Any) -> List[str]:
    """웹/CLI: fixed, atr. 빈값이면 fixed 1개."""
    from kis_trader.backtest.optuna_breakout_tpe_space import (
        normalize_tpe_breakout_sl_mode,
    )

    items: List[str] = []
    if raw is None or raw is False:
        seq: List[Any] = []
    elif isinstance(raw, str):
        seq = [x for x in raw.replace(",", " ").split() if x.strip()]
    elif isinstance(raw, (list, tuple)):
        seq = list(raw)
    else:
        seq = [raw]
    for x in seq:
        sm = normalize_tpe_breakout_sl_mode(x)
        if sm not in items:
            items.append(sm)
    return items or ["fixed"]

def _normalize_tail_entry_modes(raw: Any) -> List[str]:


    return items or ["fixed"]

def _normalize_tail_entry_modes(raw: Any) -> List[str]:
    """웹/CLI: align, limit_atr. 빈값이면 align 1개(기존 TPE와 동일)."""
    from kis_trader.backtest.optuna_tail_tpe_space import normalize_tpe_tail_entry_mode

    items: List[str] = []
    if raw is None or raw is False:
        seq: List[Any] = []
    elif isinstance(raw, str):
        seq = [x for x in raw.replace(",", " ").split() if x.strip()]
    elif isinstance(raw, (list, tuple)):
        seq = list(raw)
    else:
        seq = [raw]
    for x in seq:
        em = normalize_tpe_tail_entry_mode(x)
        if em not in items:
            items.append(em)
    return items or ["align"]

    return items or ["fixed"]
    """웹/CLI: align, limit_atr. 빈값이면 align 1개(기존 TPE와 동일)."""
    from kis_trader.backtest.optuna_tail_tpe_space import normalize_tpe_tail_entry_mode

    items: List[str] = []
    if raw is None or raw is False:
        seq: List[Any] = []
    elif isinstance(raw, str):
        seq = [x for x in raw.replace(",", " ").split() if x.strip()]
    elif isinstance(raw, (list, tuple)):
        seq = list(raw)
    else:
        seq = [raw]
    for x in seq:
        em = normalize_tpe_tail_entry_mode(x)
        if em not in items:
            items.append(em)
    return items or ["align"]


    ob_source: Optional[str] = None,
    entry_modes: Optional[Any] = None,
    sl_modes: Optional[Any] = None,
) -> Dict[str, Any]:

    ob_source: Optional[str] = None,
    entry_modes: Optional[Any] = None,
) -> Dict[str, Any]:

    entry_modes: 꼬리 TPE 고정 진입 align|limit_atr. 둘 다=순차 2스터디(한 스터디에 섞지 않음).
    sl_modes: 돌파 TPE 고정 손절 fixed|atr. 둘 다=순차 2스터디.

entry_modes: 꼬리 TPE 고정 진입 align|limit_atr. 둘 다=순차 2스터디(한 스터디에 섞지 않음).

    tail_ems = _normalize_tail_entry_modes(entry_modes)
    if "tail" not in picked:
        tail_ems = ["align"]
    tail_dual = "tail" in picked and len(tail_ems) >= 2
    bo_sms = _normalize_breakout_sl_modes(sl_modes)
    if "breakout" not in picked:
        bo_sms = ["fixed"]
    bo_dual = "breakout" in picked and len(bo_sms) >= 2
    use_seq = len(picked) >= 2 or tail_dual or bo_dual

    tail_ems = _normalize_tail_entry_modes(entry_modes)
    if "tail" not in picked:
        tail_ems = ["align"]
    tail_dual = "tail" in picked and len(tail_ems) >= 2
    use_seq = len(picked) >= 2 or tail_dual

        env["TAIL_OPTUNA_ENTRY_MODES"] = " ".join(tail_ems if "tail" in picked else ["align"])
        env["BREAKOUT_OPTUNA_SL_MODES"] = " ".join(bo_sms if "breakout" in picked else ["fixed"])

env["TAIL_OPTUNA_ENTRY_MODES"] = " ".join(tail_ems if "tail" in picked else ["align"])

        if "tail" in picked and tail_ems:
            _lab = _lab.replace("꼬리", "꼬리(" + "+".join(tail_ems) + ")")
        if "breakout" in picked and bo_sms:
            _lab = _lab.replace("돌파", "돌파(" + "+".join(bo_sms) + ")")

        if "tail" in picked and tail_ems:
            _lab = _lab.replace("꼬리", "꼬리(" + "+".join(tail_ems) + ")")

            label = f"꼬리({_em})"
        elif strat == "breakout":
            _sm = bo_sms[0]
            study_name = (
                f"{strat}_{_sm}_{mode}_{start.replace('-', '')}_{end.replace('-', '')}_{ts}"
            )
            log_path = ROOT / "logs" / f"optuna_web_{strat}_{_sm}_{ts}.log"
            label = f"돌파({_sm})"
        else:

            label = f"꼬리({_em})"
        else:

        if strat == "tail":
            cmd.extend(["--entry-mode", tail_ems[0]])
        if strat == "breakout":
            cmd.extend(["--sl-mode", bo_sms[0]])

        if strat == "tail":
            cmd.extend(["--entry-mode", tail_ems[0]])

        "tail_entry_modes": tail_ems if "tail" in picked else None,
        "breakout_sl_modes": bo_sms if "breakout" in picked else None,

"tail_entry_modes": tail_ems if "tail" in picked else None,

        label = f"꼬리({em})" if em else "꼬리"
    elif strat == "breakout":
        sm = ""
        if "_atr_" in study or study.endswith("_atr"):
            sm = "atr"
        elif "_fixed_" in study:
            sm = "fixed"
        else:
            rows = list(data.get("results_all") or data.get("results") or [])
            p0 = (rows[0] or {}).get("params") or {} if rows else {}
            sm = str(p0.get("sl_mode") or "").strip()
        label = f"돌파({sm})" if sm else "돌파"

    if strat not in ("tail", "breakout"):
        label = _labels_imp.get(strat, strat)

    if strat != "tail":
        label = _labels_imp.get(strat, strat)


TAIL_OPTUNA_ENTRY_MODES="${TAIL_OPTUNA_ENTRY_MODES:-align}"
# 돌파 TPE 손절모드 — 한 스터디에 섞지 않음. 공백 구분 시 순차 2회.
#   BREAKOUT_OPTUNA_SL_MODES="fixed atr"
BREAKOUT_OPTUNA_SL_MODES="${BREAKOUT_OPTUNA_SL_MODES:-fixed}"

  echo "TAIL_OPTUNA_ENTRY_MODES=$TAIL_OPTUNA_ENTRY_MODES"
  echo "BREAKOUT_OPTUNA_SL_MODES=$BREAKOUT_OPTUNA_SL_MODES"

run_one() {
  local strat="$1"
  local entry_mode="${2:-}"
  local sl_mode="${3:-}"
  local ts study log sort_by
  ts="$(date +%Y%m%d_%H%M%S)"
  study="${strat}_tpe_${START//-/}_${END//-/}_${ts}"
  log="logs/optuna_${strat}_tpe_${ts}.log"
  if [[ "$strat" == "tail" && -n "$entry_mode" ]]; then
    study="${strat}_${entry_mode}_tpe_${START//-/}_${END//-/}_${ts}"
    log="logs/optuna_${strat}_${entry_mode}_tpe_${ts}.log"
  fi
  if [[ "$strat" == "breakout" && -n "$sl_mode" ]]; then
    study="${strat}_${sl_mode}_tpe_${START//-/}_${END//-/}_${ts}"
    log="logs/optuna_${strat}_${sl_mode}_tpe_${ts}.log"
  fi

run_one() {
  local strat="$1"
  local entry_mode="${2:-}"
  local ts study log sort_by
  ts="$(date +%Y%m%d_%H%M%S)"
  study="${strat}_tpe_${START//-/}_${END//-/}_${ts}"
  log="logs/optuna_${strat}_tpe_${ts}.log"
  if [[ "$strat" == "tail" && -n "$entry_mode" ]]; then
    study="${strat}_${entry_mode}_tpe_${START//-/}_${END//-/}_${ts}"
    log="logs/optuna_${strat}_${entry_mode}_tpe_${ts}.log"
  fi

  {
    echo ""
    echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}] START $(date -Is) study=$study univ=$UNIVERSE_HISTORY_SOURCE --------"
  } | tee -a "$MASTER"

  {
    echo ""
    echo "-------- [$strat${entry_mode:+/$entry_mode}] START $(date -Is) study=$study univ=$UNIVERSE_HISTORY_SOURCE --------"
  } | tee -a "$MASTER"

  if [[ "$strat" == "tail" && -n "$entry_mode" ]]; then
    cmd_args+=(--entry-mode "$entry_mode")
  fi
  if [[ "$strat" == "breakout" && -n "$sl_mode" ]]; then
    cmd_args+=(--sl-mode "$sl_mode")
  fi

  if [[ "$strat" == "tail" && -n "$entry_mode" ]]; then
    cmd_args+=(--entry-mode "$entry_mode")
  fi

echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}] END rc=$rc $(date -Is) --------"

echo "-------- [$strat${entry_mode:+/$entry_mode}] END rc=$rc $(date -Is) --------"

  if [[ "$s" == "tail" ]]; then
    for em in $TAIL_OPTUNA_ENTRY_MODES; do
      run_one tail "$em"
    done
  elif [[ "$s" == "breakout" ]]; then
    for sm in $BREAKOUT_OPTUNA_SL_MODES; do
      run_one breakout "" "$sm"
    done
  else
    run_one "$s"
  fi

  if [[ "$s" == "tail" ]]; then
    for em in $TAIL_OPTUNA_ENTRY_MODES; do
      run_one tail "$em"
    done
  else
    run_one "$s"
  fi

              돌파 손절

                <input class="form-check-input opt-bo-sl" type="checkbox" id="opt_bo_sl_fixed" value="fixed"> fixed
                고정%

                <input class="form-check-input opt-bo-sl" type="checkbox" id="opt_bo_sl_atr" value="atr"> atr
                ATR배수

            <input class="form-check-input opt-strat-cb" type="checkbox" value="scalp"> 스캘핑

<input class="form-check-input opt-strat-cb" type="checkbox" value="breakout"> 돌파
            <input class="form-check-input opt-strat-cb" type="checkbox" value="scalp"> 스캘핑

<small class="text-muted d-block mt-2">탐색 게이트 WR/PF=0 · 사후=results_gated · 안정=results_stable · DB 자동적용 안 함 · 2개 이상=순차(병렬 아님) · 이력소스=저장 후보 테이블 · 꼬리: align/limit_atr 중 켠 것만 · 돌파: fixed/atr 중 켠 것만(둘 다=스터디 2개 순차). 별도 「꼬리」「돌파」체크는 없음</small>

<small class="text-muted d-block mt-2">탐색 게이트 WR/PF=0 · 사후=results_gated · 안정=results_stable · DB 자동적용 안 함 · 2개 이상=순차(병렬 아님) · 이력소스=저장 후보 테이블 · 꼬리: align/limit_atr 중 켠 것만 실행(둘 다=스터디 2개 순차). 별도 「꼬리」체크는 없음</small>


  if (optunaTailEntryModes().length) checked.add('tail');
  if (optunaBreakoutSlModes().length) checked.add('breakout');
  return order.filter(s => checked.has(s));
}

  if (optunaTailEntryModes().length) checked.add('tail');
  return order.filter(s => checked.has(s));
}

  if ($('opt_tail_em_align')) $('opt_tail_em_align').checked = true;
  if ($('opt_tail_em_limit')) $('opt_tail_em_limit').checked = false;
  if ($('opt_bo_sl_fixed')) $('opt_bo_sl_fixed').checked = true;
  if ($('opt_bo_sl_atr')) $('opt_bo_sl_atr').checked = false;
}

  if ($('opt_tail_em_align')) $('opt_tail_em_align').checked = true;
  if ($('opt_tail_em_limit')) $('opt_tail_em_limit').checked = false;
}

  document.querySelectorAll('.opt-tail-em').forEach(cb => { cb.checked = false; });
  document.querySelectorAll('.opt-bo-sl').forEach(cb => { cb.checked = false; });
}

  document.querySelectorAll('.opt-tail-em').forEach(cb => { cb.checked = false; });
}

function optunaBreakoutSlModes() {
  const out = [];
  if ($('opt_bo_sl_fixed')?.checked) out.push('fixed');
  if ($('opt_bo_sl_atr')?.checked) out.push('atr');
  return out;
}

async function optunaStart() {

  const entryModes = optunaTailEntryModes();
  const slModes = optunaBreakoutSlModes();

const entryModes = optunaTailEntryModes();

  const emLabel = strategies.includes('tail')
    ? `\n꼬리진입=${entryModes.join('+')}` + (entryModes.length >= 2 ? ' (순차 2스터디)' : '')
    : '';
  const slLabel = strategies.includes('breakout')
    ? `\n돌파손절=${slModes.join('+')}` + (slModes.length >= 2 ? ' (순차 2스터디)' : '')
    : '';
  if (!confirm(`Optuna 시작?\n${how}: ${strategies.join(', ')}\n${start}~${end} trials=${trials}\n이력소스=${srcLabel}${emLabel}${slLabel}\n(DB 미적용)`)) return;

  const emLabel = strategies.includes('tail')
    ? `\n꼬리진입=${entryModes.join('+')}` + (entryModes.length >= 2 ? ' (순차 2스터디)' : '')
    : '';
  if (!confirm(`Optuna 시작?\n${how}: ${strategies.join(', ')}\n${start}~${end} trials=${trials}\n이력소스=${srcLabel}${emLabel}\n(DB 미적용)`)) return;

        entry_modes: strategies.includes('tail') ? entryModes : undefined,
        sl_modes: strategies.includes('breakout') ? slModes : undefined,

entry_modes: strategies.includes('tail') ? entryModes : undefined,

    entry_modes = body.get("entry_modes")
    if entry_modes is None and request.form.get("entry_modes"):
        entry_modes = request.form.get("entry_modes")
    sl_modes = body.get("sl_modes")
    if sl_modes is None and request.form.get("sl_modes"):
        sl_modes = request.form.get("sl_modes")

    entry_modes = body.get("entry_modes")
    if entry_modes is None and request.form.get("entry_modes"):
        entry_modes = request.form.get("entry_modes")

            entry_modes=entry_modes,
            sl_modes=sl_modes,

꼬리 TPE 진입모드는 탐색 축이 아님(웹 체크 → 스터디 고정/순차 2회).
돌파 TPE 손절모드(fixed/atr)도 탐색 축이 아님(웹 체크 → 스터디 고정/순차 2회).
align 스터디는 `limit_atr_mult` 제외. fixed 스터디는 `atr_sl_*` 제외.
`*_SKIP_HTS_SCAN_DUPES` true 금지.

꼬리 TPE 진입모드는 탐색 축이 아님(웹 체크 → 스터디 고정/순차 2회). `*_SKIP_HTS_SCAN_DUPES` true 금지.


BREAKOUT_TPE_AXIS_KEYS: List[str] = list(_BREAKOUT_BASE_KEYS) + list(RATCHET_TPE_AXIS_KEYS)

def normalize_tpe_breakout_sl_mode(raw: Optional[Any] = None) -> str:

BREAKOUT_TPE_AXIS_KEYS: List[str] = list(_BREAKOUT_BASE_KEYS) + list(RATCHET_TPE_AXIS_KEYS)

def normalize_tpe_breakout_sl_mode(raw: Optional[Any] = None) -> str:

Shell

python3 -m py_compile \
  /home/hoon/kis_bot/kis_trader/backtest/optuna_tail_tpe_space.py \
  /home/hoon/kis_bot/kis_trader/backtest/optuna_breakout_tpe_space.py \
  /home/hoon/kis_bot/kis_trader/backtest/optuna_breakout.py \
  /home/hoon/kis_bot/kis_trader/backtest/param_search_optuna.py \
  /home/hoon/kis_bot/kis_trader/backtest/optuna_web_jobs.py \
  /home/hoon/kis_bot/backtest_web.py && python3 - <<'PY'
import optuna
from kis_trader.backtest.optuna_tail_tpe_space import suggest_tail_params_tpe, tail_tpe_axis_keys
from kis_trader.backtest.optuna_breakout_tpe_space import suggest_breakout_params_tpe, breakout_tpe_axis_keys

s = optuna.create_study()
# dummy trials via sampler
def run(fn, **kw):
    t = s.ask()
    try:
        c = fn(t, **kw)
    except optuna.TrialPruned:
        return 'pruned'
    return sorted(c.keys())

print('tail align has limit', 'limit_atr_mult' in tail_tpe_axis_keys('align'), run(suggest_tail_params_tpe, entry_mode='align'))
print('tail limit has limit', 'limit_atr_mult' in tail_tpe_axis_keys('limit_atr'))
print('bo fixed atr_sl', 'atr_sl_mult' in breakout_tpe_axis_keys('fixed'), 'sl_pct' in breakout_tpe_axis_keys('fixed'))
print('bo atr atr_sl', 'atr_sl_mult' in breakout_tpe_axis_keys('atr'), 'sl_pct' in breakout_tpe_axis_keys('atr'))
print('bo suggest fixed', run(suggest_breakout_params_tpe, sl_mode='fixed'))
print('bo suggest atr', run(suggest_breakout_params_tpe, sl_mode='atr'))
PY

Compile and smoke TPE axis keys per mode


sudo systemctl restart kis_backtest_web.service; sleep 2; systemctl is-active kis_backtest_web.service; curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5050/

Restart backtest web after Optuna mode UI change


CallMcpTool

Open dashboard to verify breakout sl-mode checks


Retry navigate to backtest dashboard


Lock browser for Optuna tab click


Open Optuna tab


Read Optuna strategy checkbox labels


Unlock browser after Optuna UI check


**맞습니다. 한 스터디에 섞으면 안 되고, 보고 싶은 모드만 따로 돌리는 게 맞습니다.** 다만 돌파의 “모드 두 개”는 꼬리와 **종류가 다릅니다.**

### 돌파 모드가 두 겹임

| 구분 | 무엇 | TPE |
|------|------|-----|
| **손절 `sl_mode`** | `fixed` = 진입가 대비 고정% 손절 · `atr` = ATR×배수 손절 | **이제 탐색 축 아님.** 스터디마다 하나 고정 |
| **진입 `entry_mode`** | 웹 백테의 봉 안/시가 등 (언제 사느냐) | TPE가 **원래 안 굴림** (실매/폼 값) |

꼬리 `align` vs `limit_atr`은 **어떻게 사느냐**입니다.
돌파 `fixed` vs `atr`은 **어떻게 손절하느냐**입니다. 배타인 점은 같고, 그래서 **둘 다 보려면 스터디 2번**이 맞습니다. 한 trial에서 `sl_mode=fixed`인데 `atr_sl_mult`를 같이 뽑으면 죽은 축입니다.

### 이번에 한 일 (실매 엔진 그대로)

- **꼬리 align:** `limit_atr_mult` TPE에서 제거. `limit_atr` 스터디에서만 탐색
- **돌파:** 웹에 꼬리처럼 **「돌파 손절」 fixed / atr** 체크. 켠 것만 실행, 둘 다면 순차 2스터디
  - `fixed` → `sl_pct`만
  - `atr` → `atr_sl_mult`만
- Grid(`fast` 등)는 예전처럼 `sl_mode`를 격자에 넣을 수 있음. 웹 기본 경로인 **TPE만** 바꿈

브라우저 `http://192.168.0.149:5050/` Optuna 탭: **돌파 손절 · fixed 고정% · atr ATR배수** 확인. 웹 `active` · HTTP 200.

둘 다 켜면 시간이 두 배입니다. 실매가 고정%면 **fixed만** 켜도 됩니다.


<timestamp>Sunday, Aug 16, 2026, 1:03 AM (UTC+9)</timestamp>
<user_query>
유령 atr 문제도 해결해야되는데 정리좀 다시 초등생수준으로
</user_query>


유령 ATR이 어디 이름만 남았는지 다시 짚고, 초등생이 이해하게 정리한 뒤 이름·죽은 코드만 정리하겠습니다.


**유령 ATR**은 “ATR로 손절한다”가 아니라, **이름만 ATR인 빈 상자**입니다. 상자에 과자를 더 넣는 게 해결이 아닙니다. **이름표를 고치거나, 안 쓰는 숫자를 탐색에서 빼는 것**이 해결입니다.

초등생 버전으로만 다시 적습니다.

---

### ATR이 뭐야?

주가가 **하루(또는 몇 봉) 동안 얼마나 출렁이는지** 재는 자입니다.
출렁임이 크면 손절을 조금 넓히고, 작으면 좁히는 식으로 **쓸 수는** 있습니다.

우리 봇에서 **진짜 그 자로 재서 쓰는 곳**은 적습니다.

---

### 유령이란?

교실에 “소방 호스”라고 적힌 박스가 있는데, 뚜껑을 열면 **호스가 없거나 물총**인 것과 같습니다.

- 화면·DB 이름이 `ATR`이라서 “변동성 손절이 켜져 있다”고 착각함
- 실제 매매는 **고정% 손절**이나 **어깨컷**(고점에서 조금 떨어지면 팔기)을 씀
- Optuna가 그 빈 숫자를 바꿔 가며 “잘했다”고 착각하면 **가짜 1등**이 나옴

---

### 전략별로 한 줄

| 전략 | 진짜 ATR | 유령 |
|------|----------|------|
| **꼬리** | **진짜.** 손절·익절을 ATR×배수로 잡음. 지정가(`limit_atr`)도 ATR | **align**(다음봉 시가)일 때 `limit_atr_mult`만 유령 축이었음 → **이미 TPE에서 뺌** |
| **돌파** | **`atr` 손절모드일 때만 진짜** | **`fixed`(고정%)일 때 ATR 배수**는 안 씀 → **이미 모드별로 스터디 분리** |
| **스캘핑** | **없음** | 예전에 ATR 단타로 쓰던 이름 `SCALP_ATR_UP/DOWN_MULT`가 **DB에 남음**. 웹은 그걸 “트레일%”로 저장하지만, **실제 1순위 청산은 어깨컷**. 코드에 `ATR_DOWN_MULT`를 읽기만 하고 **안 쓰는 줄**도 있음 |
| **모멘텀·해외·레인지** | **없음** | 포지션에 `atr_entry=0`만 찍힘. 빈 칸 |

스캘핑·모멘텀에 ATR을 **엔진에 새로 넣으면** 지금 잘 도는 어깨·%손절과 **다른 매매**가 됩니다. 그건 유령 퇴치가 아니라 **새 전략**입니다.

---

### 그래서 해결은?

1. **이미 한 것 (가짜 1등 방지)**
   - 꼬리 align에서 지정가 ATR 배수 탐색 제거
   - 돌파 `fixed` / `atr`를 한 스터디에 섞지 않음

2. **하면 안 되는 것**
   - 스캘핑·모멘텀 엔진에 ATR 손절을 “이름이 있으니까” 넣기

3. **아직 남은 것 (이름표)**
   - 스캘핑 DB 키 이름이 ATR인데 내용은 예전 트레일/%
   - 모멘텀 `atr_entry=0`은 장부 칸만 있음
   - 스캘핑이 `ATR_DOWN_MULT`를 읽고 안 씀

3번은 **매매 숫자를 안 바꾸고** 죽은 읽기 제거·화면에 “레거시 이름, 실제 청산은 어깨”라고 쓰는 쪽으로 정리하면 됩니다. DB 키 이름 자체를 `SCALP_TRAIL_*`로 바꾸면 실매 설정 이사가 필요해서 **그건 별도 승인**이 맞습니다.

원하면 다음으로 **3번만**(죽은 코드 + 라벨, 엔진 ATR 추가 없음) 하면 됩니다.


<timestamp>Sunday, Aug 16, 2026, 1:09 AM (UTC+9)</timestamp>
<user_query>
모멘텀은 atr 자체를 엔진에서도  tpe에서도 안쓴다는거고
스캘핑은 그럼 뭐야 atr 전략이 잇긴했어? 지금 엔진에 atr 추가하는건 의미가 없나? 코드에는 있다면서 tpe에는 없고?
</user_query>


스캘핑에 ATR이 예전에 있었고 지금 엔진·TPE에 남아 있는지를 코드에서 딱 집어서 확인하겠습니다.


맞습니다. **모멘텀은 엔진에도 TPE에도 ATR이 없습니다.** `atr_entry=0`은 장부 칸만 있고, 손절·어깨·트레일은 **%** 로 갑니다.

스캘핑은 이렇게 나뉩니다.

### 예전에 ATR 스캘핑이 있었나?

**옛 단독 봇에는 있었습니다.** `remove/legacy_standalone_bots` 쪽에 `SCALP_ATR_UP/DOWN/DROP_MULT`로 “고점 대비 ATR×배수만큼 떨어지면 팔기”가 있었습니다.
**지금 돌리는 `scalping_engine`은 그 전략이 아닙니다.** RSI 반등 + **고정% 손절/익절** + **어깨컷**입니다.

### “코드에 있다” vs “TPE에 없다”

둘 다 맞는데, **같은 ATR이 아닙니다.**

| 있는 것 | 뭐냐 |
|--------|------|
| DB 키 `SCALP_ATR_UP/DOWN_MULT` | **옛 이름.** 웹/적용 코드가 이걸 `trail_trigger` / `trail_stop`이라는 **%** 로 읽고 씀. 변동성(ATR) 계산 아님 |
| `scalping.py`의 `ATR_DOWN_MULT` | **오타 키**를 읽기만 하고, 매수·매도에 **안 씀** |
| `atr_entry: 0.0` | 포지션 칸만 0 |
| 엔진 청산 | 어깨는 `shoulder_cut_pct`. `trail_stop` 변수를 읽어도 **실제 컷 가격은 어깨%** |
| **TPE** | ATR 축 **없음**. RSI, `sl_pct`, `tp_pct`, **어깨**만 굴림 → 지금 엔진과 맞음 |

그래서 “코드에 ATR이 있다” = **이름표·유령 변수**. “TPE에 없다” = **지금 엔진이 ATR을 안 쓰니까 안 넣는 게 정상**.

### 지금 엔진에 ATR 넣는 게 의미 있나?

**지금 스캘핑을 고치려고 ATR을 넣는 건 의미 없습니다.**
이미 어깨 + %손절이 있고, TPE도 그걸 봅니다. ATR을 넣으면 **옛 단타 봇을 새로 이식하는 것**이고, 실매·백테·Optuna를 한 세트로 다시 설계해야 합니다.

정리:

- **모멘텀:** ATR 안 씀 (엔진·TPE 둘 다). 넣을 이유 없음.
- **스캘핑:** 옛날엔 ATR 단타가 있었고, **지금은 폐기**. 남은 건 이름뿐. TPE에 없는 게 맞고, 엔진에 다시 넣는 건 새 전략입니다.


<timestamp>Sunday, Aug 16, 2026, 1:12 AM (UTC+9)</timestamp>
<user_query>
그럼 둘 다 필요없는거네 tpe에도 없으면 쓸데없이 낭비도아니고 그냥 헷갈리게 남은거뿐?
</user_query>


**맞습니다.** 모멘텀·스캘핑 ATR은 TPE가 안 굴리니 **탐색 낭비는 아닙니다.** 숫자로 가짜 1등을 만들지도 않습니다.

남은 건 **헷갈리게 하는 이름표**뿐입니다.

- DB/웹에 `SCALP_ATR_*` 같은 옛 이름
- 코드가 `ATR_DOWN_MULT`를 읽고 안 씀
- 포지션 `atr_entry=0`

매매·Optuna 점수에는 안 들어갑니다. 치우면 **읽기 쉬워지는 청소**이지, 수익 로직을 고치는 일이 아닙니다.


<timestamp>Sunday, Aug 16, 2026, 1:24 AM (UTC+9)</timestamp>
<user_query>
그럼 디비에 ema rsi atr  ma 저장하는건 어디에 쓰이나 옵투나에 ma도 찾는 전략이 잇어?
</user_query>


If the available MCP tools do not fully support what the user asked you to do, complete the work you can with the current tool set. In your work summary, include what you were unable to do with MCP and why. Do not use browser automation to work around missing or unavailable MCP tools unless the user explicitly asks you to use the browser.


DB에 있는 EMA·RSI·ATR·MA 키가 실매·백테·Optuna 어디에 붙는지 전략별로 찾겠습니다.


DB에 있는 값은 **두 종류**입니다. Optuna가 MA(이평) 기간을 굴리는 전략은 **지금 TPE에는 없습니다.**

---

### 1) `ws_candles`에 찍힌 RSI / EMA 숫자

봉마다 `rsi_2`, `rsi_3`, `rsi_5`(실시간 INSERT)와 `rsi_7/14/21`, `ema_5/9/12/15/21/34`(배치 materialize)가 있습니다.

**용도:** 지표를 매번 다시 안 돌리려고 미리 넣어 둔 캐시입니다. 실매 TRIGGER·백테·Optuna가 `indicator_cache`로 읽어서 **같은 봉의 RSI/EMA**를 씁니다. 매매 규칙을 바꾸는 설정이 아니라 **계산 가속**입니다.

**ATR / MA20 시계열은 이 테이블에 안 쌓습니다.** ATR·SMA20은 엔진이 OHLC로 그 자리에서 계산합니다.

---

### 2) `config_*`에 있는 EMA / RSI / ATR / MA **설정값**

이건 “봉의 지표 값”이 아니라 **켜기/끄기·기간·배수·임계값**입니다. 실매·웹 폼·Optuna `--apply-best`가 여기를 읽습니다.

| 전략 | RSI | EMA | MA(SMA) | ATR |
|------|-----|-----|---------|-----|
| **꼬리** | 필터 ON이 기본. 임계값 DB | 없음 | **MA20 이격** (`TAIL_USE_MA20_FILTER`, 기본 OFF) | 손절/익절 배수+캡. 지정가(`limit_atr`)도 ATR |
| **모멘텀** | min/max, 필터 스위치 | **추세 EMA** 기간·ON/OFF | 없음 (호가 MA 윈도는 별개) | 엔진 안 씀 |
| **돌파** | 없음 | EMA 옵션, 기본 OFF | 없음 | `sl_mode=atr`일 때만 ATR 손절 |
| **스캘핑** | 기간·과매도·과열 | 추세 EMA 없음. MACD 켤 때만 내부 EMA | 없음 | 엔진 안 씀 (옛 키 이름만 남음) |

---

### Optuna가 실제로 **찾는** 것 (웹 기본 TPE)

**MA(SMA) 기간·MA20 ON/OFF를 TPE가 탐색하는 전략은 없습니다.**

- **꼬리:** RSI 임계, ATR 배수/캡, (지정가 스터디만) `limit_atr_mult`. **MA20 축 없음.** Grid(`fast` 등)에만 `ma20_max_above`가 있음.
- **모멘텀:** RSI min/max는 찾음. **EMA 기간·EMA 필터 ON/OFF는 TPE에 없음.** DB의 `MOMENTUM_EMA_*`는 실매/웹 기본값으로만 고정 적용.
- **돌파:** ATR 손절 스터디에서 `atr_sl_mult`만. **EMA 없음.**
- **스캘핑:** RSI 기간·과매도·과열. **MA/EMA 없음.** MACD는 TPE에서 꺼 둠.

정리하면, **봉에 저장된 RSI/EMA는 “미리 계산해 둔 시세”**이고, **config의 ATR/MA는 “그 숫자를 어떻게 쓸지”**입니다. Optuna TPE는 RSI·ATR(쓰는 전략만)을 찾고, **이평선 기간을 찾는 모드는 없습니다.**


<timestamp>Sunday, Aug 16, 2026, 1:31 AM (UTC+9)</timestamp>
<user_query>
아니고 스위치만 잇고 안찾으면 무슨 의미가 잇냐 다시 정리해줘 엔진에 잇어야되는 전략이랑 지금 엔진에 있는지 없는지 tpe에 넣어야되는지 저장은 틱 들어올때 계산해서 같이 넣는지
</user_query>


스위치만 있고 TPE가 안 찾는 축이 엔진에 실제로 붙는지, 틱 저장 시점과 맞춰 다시 확인합니다.


스위치만 있고 TPE가 안 찾는 건 **Optuna용 의미가 거의 없습니다.** 실매에서 손으로 켜고 끄는 손잡이일 뿐입니다.

---

### 스위치만 있으면 뭐가 되나

Optuna trial은 **안 찾는 키는 DB/폼 기본값을 그대로 고정**합니다.

| 상태 | 결과 |
|------|------|
| 스위치 **OFF** + 숫자도 안 찾음 | 그 기능은 학습에 **한 번도 안 들어감**. 웹에 체크만 있는 수준. |
| 스위치 **OFF** + 숫자만 찾음 | **낭비.** 엔진이 숫자를 안 보는데 trial만 굴림. (국장 모멘텀 RSI가 이쪽에 가깝다) |
| 스위치 **ON** + 숫자 찾음 | **의미 있음.** 꼬리 RSI가 이쪽. |
| 스위치 ON/OFF를 **스터디로 나눔** | 꼬리 `align`/`limit_atr`, 돌파 `fixed`/`atr`처럼 **배타 모드만** 이렇게 함. |

그래서 “엔진에 코드는 있는데 TPE에 없다” = **실매 옵션이지, 학습 축이 아님.**

---

### 저장은 틱마다 같이 넣나 → **아니다**

틱이 올 때마다 EMA·ATR·MA를 계산해서 `ws_candles`에 넣는 구조가 **아닙니다.**

| 무엇 | 언제 계산 | DB에 남나 |
|------|-----------|-----------|
| 봉 OHLC | 틱으로 **형성 중 봉**을 갱신 | 봉 upsert |
| **RSI 2/3/5** | **봉을 쓸 때** 종가 시리즈로 계산 (확정/쓰기 큐). 틱 한 건마다 컬럼 INSERT 아님 | `rsi_2/3/5` |
| **RSI 7/14/21, EMA 5/9/…** | 장중 실시간 아님. **배치 materialize** (백테/파람 가속) | `rsi_N` / `ema_N` |
| **ATR** | 엔진이 분봉 OHLC로 **그때 계산** | 컬럼 없음 |
| **MA20 (SMA)** | 꼬리 엔진이 종가 20개로 **그때 계산** | 컬럼 없음 |
| `holding_peak` | 보유 중 틱 고점 | 봉에 같이 갱신 (지표 아님) |

백테/Optuna는 이 봉+캐시를 읽고, 없는 ATR/MA20은 같은 공식으로 다시 계산합니다.

---

### 전략별: 엔진에 있어야 하나 / 지금 있나 / TPE에 넣나

**넣어야 한다** = 그 전략의 타점·청산이 그 지표에 달려 있으면 엔진에 있어야 함.
**TPE에 넣는다** = 엔진이 **실제로 쓰는 상태**(스위치 ON)일 때만 숫자를 찾는다. OFF면 스터디를 나누거나 아예 빼야 함.

#### 꼬리

| 지표 | 엔진에 있어야 | 지금 엔진 | 지금 TPE | 정리 |
|------|---------------|-----------|----------|------|
| ATR 손절/익절 | **필수** | 있음 (배수+캡) | **찾음** | 맞음 |
| 지정가 ATR | `limit_atr`일 때만 | 있음 | 그 스터디만 찾음 | 맞음 |
| RSI | 필터로 쓰면 필수 | 있음, 스위치 기본 **ON** | 임계값 **찾음**, 스위치는 안 찾음 | 스위치 ON 고정이면 숫자 찾는 게 맞음 |
| MA20 | 필수는 아님 | 있음, 스위치 기본 **OFF** | **안 찾음** | 학습 의미 **없음**. 실매에서 켤 때만 동작 |

#### 모멘텀

| 지표 | 엔진에 있어야 | 지금 엔진 | 지금 TPE | 정리 |
|------|---------------|-----------|----------|------|
| EMA 추세 | 추세필터가 정체성이면 **있어야** | 있음, 웹 기본 **ON** | 기간/ON **안 찾음** | 실매 손잡이만. 학습은 DB EMA에 **고정** |
| RSI | 필수는 아님 | 있음, 국장 기본 **OFF** | min/max **찾음** | **스위치 OFF면 TPE RSI는 의미 없음** |
| ATR | **없어도 됨** | 없음 | 없음 | 맞음. 넣을 이유 없음 |
| MA | **없어도 됨** | 추세는 EMA지 SMA 아님 | 없음 | 맞음 |

#### 돌파

| 지표 | 엔진에 있어야 | 지금 엔진 | 지금 TPE | 정리 |
|------|---------------|-----------|----------|------|
| ATR 손절 | `atr` 모드일 때만 | 있음, 기본은 **fixed %** | `atr` 스터디만 배수 찾음 | 맞음 |
| EMA | 필수는 아님 | 있음, 기본 **OFF** | **안 찾음** | 학습 의미 없음 (꼬리 MA20과 같음) |
| RSI / MA | 없어도 됨 | RSI 없음 | 없음 | 맞음 |

#### 스캘핑

| 지표 | 엔진에 있어야 | 지금 엔진 | 지금 TPE | 정리 |
|------|---------------|-----------|----------|------|
| RSI | **필수** | 있음 | 기간·과매도·과열 **찾음** | 맞음 |
| ATR | **없어도 됨** | 없음 (옛 키만) | 없음 | 맞음 |
| EMA/MA | MACD 쓸 때만 | MACD 기본 OFF | MACD **강제 OFF** | 추세이평 학습 없음 |

---

### 한 줄

- **엔진에 있는 것** = 실매가 그 스위치/숫자로 거를 수 있다.
- **TPE에 있는 것** = 그 숫자를 바꿔가며 수익을 찾는다.
- **스위치만 있고 TPE가 안 찾음** = 학습에 안 들어간다. 켤 생각 없으면 Optuna 관점에선 죽은 옵션이다.
- **저장** = 틱마다 ATR/EMA/MA를 같이 넣는 게 아니라, **봉 쓸 때 RSI 2/3/5만** 넣고 긴 EMA/RSI는 배치, ATR·MA20은 **엔진 계산**이다.

TPE에 넣으려면 원칙은 하나다. **엔진이 켜서 쓰는 축만 넣고, 꺼져 있으면 빼거나 스터디를 나눈다.** 모멘텀 RSI(기본 OFF인데 숫자는 찾음)와 꼬리 MA20/돌파 EMA(코드만 있고 학습 안 함)가 지금 그 원칙에서 어긋난 두 패턴이다. 엔진 로직을 바꾸거나 TPE에 축을 넣는 건 별도 승인이 필요하다.


<timestamp>Sunday, Aug 16, 2026, 1:38 AM (UTC+9)</timestamp>
<user_query>
그럼 필요한 축은 무슨전략에 무슨 지수야? 그리고 낭비인애는 누구야
</user_query>


필요한 축은 **엔진이 켜서 쓰는 지표**이고, 낭비는 **엔진이 안 보는데 TPE가 굴리거나, 꺼진 스위치 뒤에만 있는 숫자**입니다.

---

### 필요한 축 (전략 → 지표)

| 전략 | 지표 | 왜 필요 | 지금 TPE |
|------|------|---------|----------|
| **꼬리** | **ATR** | 손절·익절(지정가면 진입가까지) | 찾음. 지정가는 `limit_atr` 스터디만 |
| **꼬리** | **RSI** | 필터 기본 ON, 과열 차단 | 임계값 찾음 |
| **모멘텀** | **EMA** | 추세필터 기본 ON | **안 찾음** (실매 고정값만) |
| **돌파** | **ATR** | `atr` 손절 스터디일 때만 | 그 스터디만 배수 찾음 |
| **돌파** | **고정% 손절** | `fixed` 스터디 | `sl_pct` 찾음 |
| **스캘핑** | **RSI** | 과매도 진입·과열 차단 | 기간·과매도·과열 찾음 |

이 여섯이 “있어야 하는 축”입니다.
MA(SMA)는 **어느 전략의 정체성 축도 아닙니다.** 꼬리에 MA20 옵션만 있고 기본 OFF입니다.

---

### 낭비

| 누구 | 뭐가 낭비 | 이유 |
|------|-----------|------|
| **모멘텀 × RSI** | TPE가 `mom_rsi_min/max`를 찾음 | 국장 스위치 **기본 OFF** → 엔진이 RSI를 안 보는데 trial만 움직임 |
| **꼬리 × MA20** | 코드·스위치만 있음 | 기본 OFF + TPE 안 찾음 → **학습 0**. 실매에서 안 켜면 죽은 옵션 |
| **돌파 × EMA** | 위와 같음 | 기본 OFF + TPE 안 찾음 |
| **모멘텀 × ATR** | 없음이 맞음 | 엔진에 ATR 없음. 찾으면 그게 낭비 |
| **스캘핑 × ATR** | DB 키 이름만 남음 | 엔진 ATR 없음, TPE도 없음. **라벨 쓰레기** |
| **스캘핑 × EMA/MA** | TPE가 MACD 꺼 둠 | 추세이평 안 씀. 찾는 게 낭비라서 안 넣는 게 맞음 |

---

### 한 줄로

- **찾아라:** 꼬리 ATR·RSI, 스캘핑 RSI, 돌파는 스터디별로 고정% 또는 ATR, 모멘텀은 EMA(지금은 안 찾고 있음).
- **버려라(학습):** 모멘텀 RSI(꺼진 채 숫자만 굴림), 꼬리 MA20, 돌파 EMA, 모멘텀/스캘핑 ATR.

모멘텀 EMA는 낭비라기보다 **필요한데 TPE에 빠진 축**입니다. RSI처럼 꺼진 채 숫자를 넣는 것과는 반대입니다.


<timestamp>Sunday, Aug 16, 2026, 1:41 AM (UTC+9)</timestamp>
<user_query>
아 개 어렵게 정리하네 전략 특성상 있어야되는데 없는거랑  아 답답하다 무슨 기준인지 모르겠냐?
</user_query>


기준은 하나다. **그 전략이 원래 그걸로 먹고사는가.**

- **꼬리** = 떨어진 뒤 반등. 손절·익절은 **ATR**. 과열이면 안 삼 = **RSI**. 이평 추세 아님.
- **모멘텀** = 세게 오르는 추세. 정체성은 **EMA**. 손절은 %/래칫이지 ATR 아님. RSI는 HTS가 이미 거름.
- **돌파** = 전고 뚫기. 정체성은 돌파+거래량. 손절은 **%** 가 기본, ATR은 대안일 뿐. 이평 아님.
- **스캘핑** = 급락 후 짧은 반등. 정체성은 **RSI**. ATR·이평 아님.

---

**특성상 있어야 하는데 없는 것 — 하나다.**

모멘텀 **EMA**를 TPE가 안 찾음. 엔진에는 있고, 학습만 안 함.

---

**낭비 (특성도 아닌데 있거나 찾고 있음)**

- 모멘텀 **RSI** — 꺼져 있는데 TPE가 숫자를 굴림
- 꼬리 **MA20**, 돌파 **EMA** — 옵션만 있고 기본 꺼짐, 학습도 안 함
- 모멘텀·스캘핑 **ATR** — 그 전략 아님. 엔진에도 없음

나머지는 이미 맞게 있다. 꼬리 ATR·RSI, 스캘핑 RSI, 돌파 손절(% 또는 ATR).


<timestamp>Sunday, Aug 16, 2026, 1:46 AM (UTC+9)</timestamp>
<user_query>
모멘텀 EMA를 TPE가 안 찾음. 엔진에는 있고, 학습만 안 함. 모멘텀 RSI — 꺼져 있는데 TPE가 숫자를 굴림
일단 모멘텀부터 정리하자 tpe 찾고 rsi는 필요없는 전략이라는거지? 필요한데 끈건아니야? tpe가 찾는데?
</user_query>


모멘텀에서 RSI가 ‘필요해서 끈 것’인지, ‘전략에 없어서 끈 것’인지 엔진이 실제로 보는 분기만 확인합니다.


국장 모멘텀 기준으로 말하면 이렇게다.

**RSI는 필요한데 끈 게 아니다.** 이 전략 본체는 HTS 후보 + 전일종가 돌파 + 거래량 + **EMA 추세**다. 코드에도 RSI는 「SCAN 이후 보조, 기본 OFF」로 적혀 있다. 스캘핑처럼 RSI로 먹고사는 전략이 아니다.

그래서 지금 상태는:

- 스위치 OFF = **의도적으로 안 쓰는 것**
- 그런데 TPE는 `mom_rsi_min` / `mom_rsi_max`를 **매번 굴린다**
- 엔진이 그 숫자를 안 보니, 국장 trial에서는 **빈 축**이다

해외만 예외다. HTS가 없어서 추격 패턴에 **RSI 하한은 쓴다.** 국장 이야기와 섞이면 안 된다.

정리하면, 국장 모멘텀 TPE는 **EMA를 찾고 RSI min/max는 빼는 게** 전략이랑 맞다. 지금은 그 반대다.