role: user

type: text

text:
<timestamp>Friday, Aug 14, 2026, 12:21 PM (UTC+9)</timestamp>
<user_query>
@ai_chat/history_ram.jsonl 이거 제대로 된거 맞아? 로그랑 디비 보고 근본원인 찾아봐||

kis_alert_bot
BOT
오전 10:42

🚀 kis_trader 통합 봇 시작
활성 전략: SCALP, SHORT, MOMENTUM, US_MOMENTUM, BREAKOUT

【유니버스】
SCALP: kiwoom_condition
  └ kiwoom_cond · scalp_re(seq=3)
SHORT: kiwoom_condition
  └ kiwoom_cond · tail(seq=1)
MOMENTUM: kiwoom_condition
  └ kiwoom_cond · momentum(seq=2)
BREAKOUT: kiwoom_condition
  └ kiwoom_cond · breakout(seq=4)

📡 [시세웹소켓] 키움(실전), KIS(실전)

✅ [기동 계좌] 계좌[모의] 501***45-01 | 보유 27종목 / 총자산 477,772,567원 | cash_basis=dnca | 예수금 471,997,465 / D+2 460,755,812 / 주문가능 458,452,032 / 사용기준 471,997,465

kis_alert_bot
BOT
오전 10:50

🚨 [운영알림/CRITICAL] LS 시세 WS 미OPEN

코드: ws_ls_down
시각: 2026-08-14 10:50:55
상세: 장중 LS 소켓 미연결 60초+
새로운 메시지

kis_alert_bot
BOT
오전 11:03

🚨 [운영알림/CRITICAL] LS 시세 WS 미OPEN

코드: ws_ls_down
시각: 2026-08-14 11:03:40
상세: 장중 LS 소켓 미연결 60초+
kis_alert_bot
BOT
오전 11:05

🚨 [운영알림/CRITICAL] MOMENTUM 유니버스 전량탈락 1→0

코드: universe_wipe
시각: 2026-08-14 11:05:48
상세: history=kiwoom 교집합 제외 1

kis_alert_bot
BOT
오전 11:16

🚨 [운영알림/CRITICAL] LS 시세 WS 미OPEN

코드: ws_ls_down
시각: 2026-08-14 11:16:59
상세: 장중 LS 소켓 미연결 60초+

kis_alert_bot
BOT
오전 11:24

🚨 [운영알림/CRITICAL] LS 시세 WS 미OPEN

코드: ws_ls_down
시각: 2026-08-14 11:24:29
상세: 장중 LS 소켓 미연결 88초+
kis_alert_bot
BOT
오전 11:28

🚨 [운영알림/CRITICAL] SCALP 유니버스 전량탈락 1→0

코드: universe_wipe
시각: 2026-08-14 11:28:16
상세: history=kiwoom 교집합 제외 1
kis_alert_bot
BOT
오전 11:28

🚨 [오류감시] 로그 오류 매칭

시각: 2026-08-14 11:28:17
유닛: kis_trader_main.service
2026-08-14T11:28:16+09:00 ubuntu2404 python[2262017]: [11:28:16] [kis_trader.strategy.SCALP] 🔍 [T:kiwoom|O:kiwoom|유니버스슬롯] SCALP 후보 1→0 (history=kiwoom 교집합, 제외 1)

kis_alert_bot
BOT
오전 11:33

🚨 [운영알림/CRITICAL] MOMENTUM 유니버스 전량탈락 4→0

코드: universe_wipe
시각: 2026-08-14 11:33:54
상세: history=kiwoom 교집합 제외 4
kis_alert_bot
BOT
오전 11:37

🚨 [운영알림/CRITICAL] LS 시세 WS 미OPEN

코드: ws_ls_down
시각: 2026-08-14 11:37:22
상세: 장중 LS 소켓 미연결 79초+
kis_alert_bot
BOT
오전 11:42

🚨 [운영알림/CRITICAL] SCALP 유니버스 전량탈락 17→0

코드: universe_wipe
시각: 2026-08-14 11:42:21
상세: history=kiwoom 교집합 제외 17
kis_alert_bot
BOT
오전 11:43

🚨 [운영알림/CRITICAL] LS 시세 WS 미OPEN

코드: ws_ls_down
시각: 2026-08-14 11:43:12
상세: 장중 LS 소켓 미연결 83초+

kis_alert_bot
BOT
오전 11:48

🚨 [운영알림/CRITICAL] LS 시세 WS 미OPEN

코드: ws_ls_down
시각: 2026-08-14 11:48:18
상세: 장중 LS 소켓 미연결 61초+

kis_alert_bot
BOT
오전 11:53

🚨 [운영알림/CRITICAL] LS 시세 WS 미OPEN

코드: ws_ls_down
시각: 2026-08-14 11:53:48
상세: 장중 LS 소켓 미연결 60초+

kis_alert_bot
BOT
오후 12:04

🚨 [운영알림/CRITICAL] LS 시세 WS 미OPEN

코드: ws_ls_down
시각: 2026-08-14 12:04:54
상세: 장중 LS 소켓 미연결 60초+
kis_alert_bot
BOT
오후 12:05

🚨 [운영알림/CRITICAL] SCALP 유니버스 전량탈락 11→0

코드: universe_wipe
시각: 2026-08-14 12:05:51
상세: history=kiwoom 교집합 제외 11

너무 많이 빠지는데 11개 이상씩
</user_query>


role: assistant

text: 유니버스가 한 번에 11개 이상 0으로 떨어지는 현상과 LS WS 미OPEN을 로그·DB에서 원인부터 확인하겠습니다. 먼저 대화 기록과 관련 문서·코드를 대조합니다.


text: 이전 분석이 맞는지 오늘 장중 로그·DB로 다시 대조합니다. 특히 11개 이상 전량탈락이 히스토리 구멍인지, 키움 실시간 목록 공백인지부터 가릅니다.


text: 11→0·17→0은 ‘1종목 래그’로는 설명이 안 됩니다. 히스토리 스냅샷이 실시간 목록과 아예 다른 집합인지 로그·DB로 확인합니다.


Write

contents:
#!/usr/bin/env python3
"""adhoc: 오늘 SCALP/MOMENTUM history 스냅샷 vs 전량탈락 시각."""
from database import TradeDB
from kis_trader.utils.env import get_env_from_db, get_env_bool, get_env_int

db = TradeDB()
cols = [r["Field"] for r in db.conn.execute("SHOW COLUMNS FROM target_candidates_history").fetchall()]
print("COLS", cols)

keys = (
    "LIVE_UNIVERSE_SLOT_ALIGN",
    "LS_WS_PING_INTERVAL_SEC",
    "LS_WS_ENABLED",
    "UNIVERSE_SLOT_CACHE_TTL_SEC",
    "CONDITION_EXIT_GRACE_SEC",
    "SCALP_SCAN_INTERVAL_MIN",
    "MOMENTUM_SCAN_INTERVAL_MIN",
    "SCAN_INTERVAL_MIN",
    "UNIVERSE_HISTORY_SAVE",
)
ph = ",".join(["%s"] * len(keys))
ext = db.conn.execute(
    "SELECT env_key, env_value FROM env_config_ext WHERE env_key IN (" + ph + ")",
    keys,
).fetchall()
print("EXT", [(dict(r)["env_key"], dict(r)["env_value"]) for r in ext])
print("--- get_env ---")
for k in keys:
    print(k, repr(get_env_from_db(k, "")), "bool", get_env_bool(k, False), "int", get_env_int(k, -1))

need = {"strategy_id", "event_time", "code", "slot_key"}
missing = need - set(cols)
print("missing_need", missing)
if missing:
    raise SystemExit(1)

print("\n=== snapshot sizes today SCALP/MOMENTUM/SHORT/BREAKOUT ===")
rows = db.conn.execute(
    """
    SELECT strategy_id, event_time, COUNT(*) AS n
    FROM target_candidates_history
    WHERE event_time >= %s AND event_time < %s
    GROUP BY strategy_id, event_time
    ORDER BY strategy_id, event_time
    """,
    ("2026-08-14 10:40:00", "2026-08-14 12:20:00"),
).fetchall()
by_sid = {}
for r in rows:
    d = dict(r)
    by_sid.setdefault(d["strategy_id"], []).append(d)

for sid, snaps in by_sid.items():
    print(f"\n-- {sid} snaps={len(snaps)} --")
    prev_n = None
    prev_et = None
    for d in snaps:
        n = int(d["n"])
        et = d["event_time"]
        dn = "" if prev_n is None else f" dN={n - prev_n:+d}"
        flag = ""
        if prev_n is not None and prev_n >= 8 and n <= 2:
            flag = "  << COLLAPSE"
        if prev_n is not None and prev_n <= 2 and n >= 8:
            flag = "  << RECOVER"
        print(f"  {et} n={n}{dn}{flag}")
        prev_n, prev_et = n, et

# Jaccard at wipe windows
windows = [
    ("SCALP", "2026-08-14 11:41:00", "2026-08-14 11:44:00"),
    ("SCALP", "2026-08-14 12:04:00", "2026-08-14 12:07:00"),
    ("MOMENTUM", "2026-08-14 11:32:00", "2026-08-14 11:36:00"),
    ("MOMENTUM", "2026-08-14 11:04:00", "2026-08-14 11:07:00"),
]
print("\n=== consecutive snapshot Jaccard (wipe windows) ===")
for sid, t0, t1 in windows:
    snaps = db.conn.execute(
        """
        SELECT event_time, GROUP_CONCAT(code ORDER BY code) AS codes
        FROM target_candidates_history
        WHERE strategy_id=%s AND event_time >= %s AND event_time <= %s
        GROUP BY event_time
        ORDER BY event_time
        """,
        (sid, t0, t1),
    ).fetchall()
    print(f"\n[{sid}] {t0}..{t1} events={len(snaps)}")
    prev = None
    prev_et = None
    for r in snaps:
        d = dict(r)
        codes = set((d["codes"] or "").split(",")) if d["codes"] else set()
        codes.discard("")
        if prev is None:
            print(f"  {d['event_time']} n={len(codes)}")
        else:
            inter = prev & codes
            uni = prev | codes
            jac = (len(inter) / len(uni)) if uni else 1.0
            print(
                f"  {d['event_time']} n={len(codes)} "
                f"vs_prev inter={len(inter)} only_new={len(codes-prev)} "
                f"only_old={len(prev-codes)} jac={jac:.2f}"
            )
        prev, prev_et = codes, d["event_time"]


Shell

command: python3 -m py_compile /home/hoon/kis_bot/logs/_adhoc_univ_wipe_diag.py && cd /home/hoon/kis_bot && PYTHONPATH=/home/hoon/kis_bot python3 logs/_adhoc_univ_wipe_diag.py

description: Compile and run universe wipe DB diag

command: journalctl -u kis_trader_main.service --since '2026-08-14 10:40:00' --until '2026-08-14 12:10:00' --no-pager | grep -E '유니버스슬롯|universe_wipe|ENTER|EXIT|🔄 \[SCALP\]|🔄 \[MOMENTUM\]|LS WS|opcode=8|ping=|Bye|ws_ls_down|_opened|조건검색' | head -200

description: Journal logs around wipe and LS WS


command: journalctl -u kis_trader_main.service --since '2026-08-14 11:41:50' --until '2026-08-14 11:43:00' --no-pager | grep -E '유니버스슬롯|universe_wipe|🔄 \[SCALP\]|ENTER:|EXIT :' | head -80; echo '==== 12:05 ===='; journalctl -u kis_trader_main.service --since '2026-08-14 12:05:20' --until '2026-08-14 12:06:20' --no-pager | grep -E '유니버스슬롯|universe_wipe|🔄 \[SCALP\]|ENTER:|EXIT :' | head -80; echo '==== LS ping after 10:42 ===='; journalctl -u kis_trader_main.service --since '2026-08-14 10:42:00' --until '2026-08-14 10:55:00' --no-pager | grep -E 'LS WS|ping=|opcode=8|Bye|ws_ls_down|통합 봇 시작' | head -40

description: Wipe windows and LS ping after restart


text:
**결론부터:** `history_ram.jsonl`의 큰 그림(슬롯 교집합이 전량탈락을 만든다)은 맞습니다. 다만 **「5분 스캔 때문에 빠진다」는 틀렸고**, 오늘 11개·17개가 한 번에 0이 된 **진짜 원인**은 그게 아닙니다.

---

## 1. 이전 분석에서 맞는 것 / 틀린 것

| 이전 말 | 판정 | 오늘 로그·DB |
|---|---|---|
| `LIVE_UNIVERSE_SLOT_ALIGN=true` → 실시간 ∩ history | **맞음** | 알림 문구 그대로 `history=kiwoom 교집합` |
| 스캔이 5분이라 새 종목이 5분 못 산다 | **틀림** | `SCAN_INTERVAL_MIN` 비어 있음 → **기본 1분**. 히스토리는 **변동 즉시** INSERT |
| 히스토리가 늦어서 구멍이 난다 | **반만 맞음** | 저장 자체는 초 단위. 구멍은 **저장이 한 줄씩 커밋**되면서 생김 |
| LS 핑 0 → 1분마다 Bye | **어제까지는 맞음** | 10:42 재시작 후 `ping=20s`로 바뀜. **그런데도** `ws_ls_down`이 계속 뜸 |
| 키움 HTS가 죽어서 0 | **아님** | 17→0 친 **같은 초**에 키움 RAM은 **19종목** |

---

## 2. 11개·17개 전량탈락의 근본원인 (초등 설명)

키움 조건검색은 **초마다** 종목이 들어왔다 나갑니다. 오늘 10:40~12:20만 SCALP 스냅샷이 **1325번**입니다. 그건 “유니버스가 진짜로 비었다”가 아니라 **HTS가 깜빡이는 것**입니다.

그다음 봇이 하는 일은 이렇습니다.

1. RAM(지금 키움 목록)을 가져온다.
2. DB에 방금 적힌 history와 **겹치는 종목만** 남긴다.

문제는 history 저장 방식입니다.

```525:557:kis_trader/database/db_manager.py
            with self.conn:
                self.conn.execute(
                    "DELETE FROM target_candidates_history "
                    "WHERE strategy_id=%s AND event_time=%s",
                    (strategy_id, event_time),
                )
                for it in items:
                    ...
                    self.conn.execute(
                        """
                        INSERT INTO target_candidates_history
                        ...
```

`database.py`에 **`autocommit=True`**, `with self.conn`은 **실제 트랜잭션이 아닙니다**.
그래서 한 스냅샷을 19종목 넣을 때:

1. 같은 초 `DELETE` (이 시각 행만)
2. **1종목 INSERT → 바로 커밋**
3. 2종목 INSERT → 커밋
4. … 19종목까지 반복

그 사이에 매수 루프가 `get_universe_at`(최신 `event_time`의 종목 전부)를 치면, **최신 시각에는 아직 1~몇 종목만** 있습니다.

- 실시간: 17개 (캡 등으로 19→17)
- history가 “최신”이라고 준 것: 방금 INSERT된 **1개**
- 교집합: **0**
- 알림: `SCALP 전량탈락 17→0`

증거:

- **11:42:21** 키움 로그 `현재 19종목` + `+1 ENTER`
- **같은 초** `universe_wipe 17→0`
- DB 완성 스냅샷은 `n=19`, 직전 대비 Jaccard **0.95** (거의 같은 목록)

완성된 DB만 보면 17개가 사라진 게 아닙니다. **저장 도중에 읽어서** 17개가 전부 “history에 없다”고 잘린 겁니다.
12:05:51 `11→0`도 같은 패턴입니다. (키움은 12종목인데 알림만 11→0)

HTS가 진짜로 목록을 갈아끼운 건 그 다음입니다. **12:06:19** SCALP가 `+4 / -9`, 연속 스냅샷 Jaccard **0.00**. 그건 별개(조건식 결과 급변)입니다.

**기회 박탈:** 몇 분 대기가 아니라, **스냅샷 저장이 끝나는 동안(보통 1초 안팎)** 매수 후보가 0이 됩니다. 알림만 치명으로 크게 울립니다.

---

## 3. LS `ws_ls_down` — 핑 20으로 고친 뒤에도 남는 이유

10:42 기동 로그: `ping=20s`.
그런데 **약 50초마다** `LS WS ERROR ping/pong timed out` → CLOSE → 재OPEN.

DB:

- `LS_WS_PING_INTERVAL_SEC=20`
- `LS_WS_PING_TIMEOUT_SEC=30`

웹소켓 라이브러리는 **타임아웃 < 핑 주기**여야 합니다. 지금은 30 > 20이라, 핑을 켜도 서버 응답을 30초나 기다리다 스스로 끊습니다.
이전 수정은 핑만 켰고, **타임아웃이 더 긴 상태**를 그대로 둔 겁니다. 국장 매매 본체(키움)와는 다른 소켓이지만, 알림은 계속 CRITICAL입니다.

---

## 4. 영향 분류

| 항목 | 실매 | 백테/파람 |
|---|---|---|
| history INSERT가 한 줄씩 커밋 | **실매 유니버스 순간 0** (매수 기회 순간 차단) | 완성 스냅샷만 보면 백테는 덜 깨짐 |
| ALIGN ON 자체 | 정합용. 끄면 기회는 늘고 실매≠백테 | 유지 권고는 그대로 |
| LS ping timeout | 시세 그림자가 끊김. 키움 본체와 별개 | 해당 없음 |
| 알림 색 / 오류감시 | UX | 해당 없음 |

---

## 5. 고치려면 (아직 코드는 안 건드림)

핵심 매매/인프라라 **승인 후** 가는 게 맞습니다.

1. **history 스냅샷을 한 트랜잭션으로** (autocommit 끄고 DELETE+다중 INSERT 후 한 번 커밋). 읽는 쪽은 완성본만 보게.
2. LS: `LS_WS_PING_TIMEOUT_SEC`를 **10 이하**(주기 20보다 작게).
3. `universe_wipe`는 “RAM은 있는데 교집합만 0”이면 CRITICAL 남발입니다. 알림 완화는 그다음.

원하시면 **1번(저장 원자화)** 부터 패치하겠습니다. ALIGN은 끄지 않습니다.


text:
<timestamp>Friday, Aug 14, 2026, 12:29 PM (UTC+9)</timestamp>
<user_query>
이거 뭐 저장을 그냥 hts 종목이랑 1:1로  푸시팝 순차큐로 하면 안되나 뭐 이거 정합맞추기 개 빡세네
</user_query>


text: HTS 푸시/팝을 진실로 두고 교집합을 빼는 쪽이 맞는지, 백테가 스냅샷을 어떻게 쓰는지부터 짧게 확인하겠습니다.


text:
**됩니다. 그리고 실매 RAM은 이미 그렇게 하고 있습니다.** 빡센 건 HTS 큐가 아니라, 그 큐를 DB에 통째로 찍고 다시 교집합하는 쪽입니다.

---

## 지금 구조 (한 줄)

키움 AFR → RAM에서 **ENTER 푸시 / EXIT 팝** (`_apply_result`, grace=0) → 변동마다 DB에 **전체 명단 스냅샷** → 실매가 그 스냅샷과 **또 교집합**.

정합을 맞추려고 **같은 목록을 세 번** 씁니다.

1. HTS가 준 RAM
2. DB 풀 스냅샷
3. RAM ∩ DB

3번이 오늘 17→0의 원인입니다. DB가 한 줄씩 커밋되는 동안 3번이 “최신 명단 = 1종목”으로 읽습니다.

---

## 초등으로

HTS가 “이 종목 들어옴 / 나감”을 말해 줍니다.
봇 메모장(RAM)에 그대로 적으면 그게 유니버스입니다.

지금은 메모장을 사진으로 찍어서(DB) 앨범에 넣고, 매수 직전에 **메모장 ∩ 앨범 사진**만 삽니다.
사진이 아직 현상 중이면 앨범에는 얼굴 한 장만 있어서, 메모장 17명이 전부 “사진에 없음”으로 잘립니다.

**정합의 본뜻은 “실매가 본 목록 = 백테가 재생하는 목록”이지, “실매가 DB를 다시 거르는 것”이 아닙니다.**

---

## 그래서 어떻게 하면 되나

| 역할 | 하면 됨 | 하면 안 됨 |
|---|---|---|
| **실매** | HTS RAM 푸시/팝 **그대로** 매수 후보 | RAM ∩ history 교집합 (ALIGN) |
| **백테** | 그 RAM이 변한 기록을 **재생** | 실매가 DB를 한 번 더 거르길 기대 |

푸시/팝 순차 큐를 **새로 만들 필요는 없습니다.** 이미 `_current_order`가 그 큐입니다.

필요한 선택은 두 가지뿐입니다.

**A. 최소 (권장)**
- 실매 ALIGN 교집합 **끔** → 후보는 HTS RAM만.
- history는 백테용으로만 남김.
- 저장만 **한 트랜잭션**(19종목 한꺼번에 커밋)으로 고침.
- 백테는 지금처럼 `get_universe_at` / `UniverseTimeline` 유지.

실매↔백테 정합: “둘 다 같은 스냅샷 시각의 완성본”이면 됨. 실매가 DB를 다시 읽을 필요 없음.

**B. 이벤트 로그 (ENTER/EXIT만 INSERT)**
말씀하신 순차 큐를 DB에 그대로 쌓는 방식.
재생은 깔끔하지만 `get_universe_at`, 웹 정합 탭, Optuna 유니버스가 **전부 스냅샷 전제**라 이식이 큽니다. 지금 아픈 곳을 고치려고 하기엔 과합니다.

---

## 남는 리스크 (ALIGN을 꺼도)

HTS가 초마다 깜빡이면 RAM도 초마다 바뀝니다. 그건 정합 버그가 아니라 **조건식 결과 자체가 출렁이는 것**입니다.
12:06:19 Jaccard 0.00 같은 급변은 ALIGN과 무관합니다. 그건 grace/디바운스 문제이고, 지금은 `CONDITION_EXIT_GRACE_SEC=0`입니다.

---

**정리:** 실매는 HTS 1:1 푸시/팝이 맞고, 그게 더 쉽습니다. 정합은 “실매가 history를 거르는 것”이 아니라 “백테가 그 RAM 변화를 재생하는 것”으로 두면 됩니다.

승인 주시면 **A**만 하겠습니다.
1) 스냅샷 INSERT 원자화
2) 실매 `LIVE_UNIVERSE_SLOT_ALIGN` 교집합 제거(또는 기본 false)
3) 백테 타임라인은 그대로

ALIGN을 웹/Optuna 표시용으로만 남길지, DB 키까지 false로 내릴지는 같이 정하면 됩니다.