import sys; sys.path.insert(0, '/home/hoon/kis_bot')
from database import TradeDB
db = TradeDB()
try:
    # 테이블명 확인
    tables = db.conn.execute("SHOW TABLES").fetchall()
    tbl_names = [list(dict(t).values())[0] for t in tables]
    env_tbl = [t for t in tbl_names if 'env' in t.lower()]
    print("env 관련 테이블:", env_tbl)

    if env_tbl:
        tbl = env_tbl[0]
        cols = [dict(r)["Field"] for r in db.conn.execute(f"SHOW COLUMNS FROM {tbl}").fetchall()]
        print(f"{tbl} 컬럼:", cols)
        rows = db.conn.execute(f"SELECT * FROM {tbl} WHERE {cols[0]} LIKE %s LIMIT 20", ('%%TICK%%',)).fetchall()
        for r in rows:
            print(dict(r))
finally:
    db.close()

env 테이블 조회 스크립트


Created At: 2026-08-13T23:25:47+09:00
Completed At: 2026-08-13T23:25:47+09:00
Created file file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/scratch/check_tick_sell.py with requested content.
If relevant, proactively run terminal commands to execute this code for the USER. Don't ask for permission.


cd /home/hoon/kis_bot && python3 /home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/scratch/check_tick_sell.py 2>&1 | grep -v "📌\|📊\|✅\|TradeDB"


Created At: 2026-08-13T23:25:54+09:00
Completed At: 2026-08-13T23:25:56+09:00

				The command exited with code 0.
				Output:
				<truncated 23 bytes>
OTAL_DEPOSIT', 'ROUND_TRIP_COST_PCT', 'POP_NET_PCT', 'LOCK_NET_PCT', 'MAX_LOSS_PER_TRADE_KRW', 'MIN_DROP_PCT_FOR_LOSS_CUT', 'KIS_APP_KEY_REAL', 'KIS_APP_SECRET_REAL', 'KIS_APP_KEY_MOCK', 'KIS_APP_SECRET_MOCK', 'KIS_ACCOUNT_NO_REAL', 'KIS_ACCOUNT_CODE_REAL', 'KIS_ACCOUNT_NO_MOCK', 'KIS_ACCOUNT_CODE_MOCK', 'KIS_MOCK', 'TAKE_PROFIT_PCT', 'MIN_DROP_RATE', 'MIN_RECOVERY_RATIO_SHORT', 'QUICK_PROFIT_PROTECT_HOURS', 'QUICK_PROFIT_MAX_RATIO', 'QUICK_PROFIT_CURRENT_MIN', 'MIN_HOLD_EARLY_TAKE_PCT', 'MIN_HOLD_HIGH_PCT', 'MIN_HOLD_DROP_FROM_HIGH', 'POST_HOLD_TAKE_PCT', 'POST_HOLD_DROP_FROM_HIGH', 'MAX_PER', 'MAX_PEG', 'MIN_GROWTH_PCT', 'DCA_INTERVALS', 'DCA_AMOUNTS', 'MM_SERVER_URL', 'MM_BOT_TOKEN_', 'MATTERMOST_CHANNEL', 'GEMINI_API_KEY', 'AI_JOURNAL_LINES', 'ANTHROPIC_API_KEY', 'CLAUDE_MODEL_ID', 'CLAUDE_MAX_TOKENS', 'MM_BUTLER_CHANNEL', 'MM_DUAL_CHANNEL_ENABLED', 'OPENROUTER_API_KEY', 'OPENROUTER_MODEL_ID', 'KIS_LONG_MM_CHANNEL', 'LONG_DAILY_LOOKBACK_DAYS', 'LONG_MA_SHORT_DAYS', 'LONG_MA_LONG_DAYS', 'LONG_REPORT_AM_HOUR', 'LONG_REPORT_AM_MIN', 'LONG_REPORT_PM_HOUR', 'LONG_REPORT_PM_MIN', 'LONG_NEWS_ENABLED', 'LONG_NEWS_INTERVAL_MIN', 'LONG_NEWS_ACTIVE_START_HOUR', 'LONG_NEWS_ACTIVE_END_HOUR', 'LONG_ANALYSIS_DELAY_MIN_SEC', 'LONG_ANALYSIS_DELAY_MAX_SEC', 'RSI_PERIOD', 'KIS_WS_URL_REAL', 'KIS_WS_URL_MOCK', 'KIS_WS_MOCK_ENABLED', 'REENTRY_COOLDOWN_SEC', 'TIME_START', 'TIME_END', 'SELL_FAILURE_BACKOFF_SEC', 'BROKER_HOLDINGS_CACHE_TTL_SEC', 'GHOST_POSITION_COOLDOWN_SEC', 'WS_TICK_SAVE_ENABLED', 'WS_TICK_BUFFER_MAX_PER_CODE', 'WS_TICK_DB_BATCH_SIZE', 'WS_TICK_DB_FLUSH_SEC', 'WS_TICK_WRITE_QUEUE_MAX', 'WS_TICK_KEEP_DAYS', 'WS_TICK_RECORD_SCOPE', 'WS_TICK_DEFAULT_MARKET', 'WS_TICK_DEFAULT_EXCHANGE', 'WS_TICK_DEFAULT_CURRENCY', 'WS_ORDERBOOK_SAVE_ENABLED', 'WS_PROGRAM_SAVE_ENABLED', 'WS_TRIGGER_SNAPSHOT_DB_INTERVAL_SEC', 'WS_TRIGGER_SNAPSHOT_DB_BATCH_SIZE', 'WS_TRIGGER_SNAPSHOT_DB_FLUSH_SEC', 'WS_TRIGGER_SNAPSHOT_WRITE_QUEUE_MAX', 'WS_ORDERBOOK_KEEP_DAYS', 'WS_PROGRAM_KEEP_DAYS', 'BACKTEST_USE_TRIGGER_SNAPSHOT_DB', 'WHIPSAW_FILTER_ENABLED', 'WHIPSAW_BACKTEST_OHLC_FALLBACK', 'WHIPSAW_MODE', 'WHIPSAW_SUBBAR_SEC', 'WHIPSAW_LOOKBACK_SEC', 'WHIPSAW_DIP_PCT', 'WHIPSAW_RECOVERY_TOL_PCT', 'WHIPSAW_MIN_SUBBARS', 'WHIPSAW_TICK_BUFFER_LIMIT', 'USE_KELLY_FORMULA', 'KELLY_MULTIPLIER', 'USE_MARKET_IOC', 'KIS_PRICE_CACHE_TTL_SEC', 'MIN_HOLD_AFTER_BUY_SEC', 'MIN_HOLD_HOURS', 'MAX_RECOVERY_RATIO_3M', 'CANDIDATE_LIST_TOP_N_LIGHT', 'SCAN_UNIVERSE_MAX_CODES', 'KIS_APP_KEY', 'KIS_APP_SECRET', 'KIS_ACCOUNT_NO', 'KIS_ACCOUNT_CODE', 'GEMINI_MODEL_ID', 'MM_BUTLER_POLL_SEC', 'AI_SOURCE_MAX_CHARS', 'FEE_RATE_PCT', 'SELL_TAX_RATE_PCT', 'KIWOOM_APP_KEY_REAL', 'KIWOOM_APP_SECRET_REAL', 'KIWOOM_APP_KEY_MOCK', 'KIWOOM_APP_SECRET_MOCK', 'KIWOOM_APP_KEY', 'KIWOOM_APP_SECRET', 'PERMANENT_WS_CODES', 'USE_MARKET_REGIME_FILTER', 'MARKET_REGIME_MIN_RSI', 'USE_THEME_HEAT_FILTER', 'THEME_HEAT_RSI_MAX', 'STRATEGY_SAME_CODE_POLICY', 'ORDER_CASH_PCT', 'ORDER_CASH_FEE_BUFFER', 'ORDER_CASH_DIVIDE_BY_MAX_STOCKS', 'ACCOUNT_CASH_PERSIST_SEC', 'BACKTEST_EXIT_CHECKS_PER_BAR', 'RANKING_POLL_INTERVAL_SEC', 'KIS_HTS_ID', 'CONDITION_POLL_INTERVAL_SEC', 'CONDITION_EXIT_GRACE_SEC', 'UNIVERSE_HISTORY_SAVE', 'WS_GAP_FILL_OFF_HOURS', 'WS_GAP_FILL_LIMIT', 'WS_TIMEFRAMES', 'WS_GAP_FILL_KIS_FALLBACK', 'MAX_BUY_AMOUNT_PER_STOCK', 'KIS_MIN_INTERVAL_SEC', 'KIS_REST_MAX_RETRIES', 'KIS_REST_BACKOFF_CAP_SEC', 'KIS_REST_TIMEOUT_SEC', 'MARKET_GUARD_ENABLED', 'MARKET_GUARD_5MIN_DROP_PCT', 'MARKET_GUARD_DAILY_DROP_PCT', 'MARKET_GUARD_RECOVERY_PCT', 'MARKET_GUARD_INDEX_CODE', 'MARKET_GUARD_POLL_SEC', 'MARKET_GUARD_INDEX_CANDLE_KEEP_MIN', 'MARKET_GUARD_PERSIST_STATE', 'STOCK_SHARE_DENOM', 'EXCLUDE_NON_STOCK', 'WS_PROVIDER', 'WS_SUBSCRIBE_KIS_MINIMAL', 'WS_VALIDATION_INTERVAL_SEC', 'WS_VALIDATION_DIFF_WARN_PCT', 'KIWOOM_WS_FORCE_REAL', 'KIWOOM_WS_MAX_SUBSCRIPTIONS', 'KIWOOM_WS_REG_CHUNK_SIZE', 'KIWOOM_WS_REG_GAP_SEC', 'KIWOOM_WS_REG_DEBOUNCE_SEC', 'KIWOOM_WS_ORDERBOOK_ENABLED', 'KIWOOM_WS_PROGRAM_ENABLED', 'ORDERBOOK_FILTER_ENABLED', 'ORDERBOOK_MAX_SPREAD_PCT', 'ORDERBOOK_ENTRY_BID_LEVELS', 'ORDERBOOK_ENTRY_BID_DEPTH_MULT', 'ORDERBOOK_ENTRY_ASK_MAX_MULT', 'ORDERBOOK_MIN_BID_ASK_RATIO', 'ORDERBOOK_BREAKOUT_ASK_WALL_MAX_QTY', 'PROGRAM_FILTER_ENABLED', 'PROGRAM_MIN_NET_BUY_QTY', 'PROGRAM_MIN_NET_BUY_AMT', 'PROGRAM_MAX_SELL_BUY_QTY_RATIO', 'PROGRAM_MIN_NET_DELTA_QTY', 'PROGRAM_SNAPSHOT_MAX_AGE_SEC', 'STRICT_FILL_VERIFY', 'ORDER_FILL_WAIT_SEC', 'LIMIT_ORDER_FILL_WAIT_SEC', 'PENDING_FILL_POLL_INTERVAL_SEC', 'PENDING_FILL_POLL_SEC', 'PENDING_FILL_MAX_AGE_SEC', 'PENDING_BUY_MAX_AGE_SEC', 'PENDING_SELL_MAX_AGE_SEC', 'PENDING_SELL_STOP_MAX_AGE_SEC', 'SELL_PENDING_REORDER_ON_EXPIRE', 'AUTO_CANCEL_PARTIAL_BUY_REMAINDER', 'WS_TRIGGER_EVAL_SAVE_ENABLED', 'WS_TRIGGER_EVAL_DB_BATCH_SIZE', 'WS_TRIGGER_EVAL_DB_FLUSH_SEC', 'WS_TRIGGER_EVAL_WRITE_QUEUE_MAX', 'BACKTEST_USE_TRIGGER_EVAL_SNAPSHOT', 'DAILY_PROFIT_MODE', 'DAILY_PROFIT_TRAIL_TIERS', 'DAILY_PROFIT_TRAIL_DROP_PCT', 'DAILY_PROFIT_TRAIL_ARM_KRW', 'DAILY_PROFIT_TRAIL_ARM_PCT', 'BUY_DEDUP_PENDING', 'KIS_MIN_INTERVAL_SEC_MOCK', 'KIS_MIN_INTERVAL_SEC_REAL', 'PENDING_POLL_BATCH_FETCH', 'SCAN_REJECT_SLEEP_MIN', 'SCAN_REJECT_SLEEP_MAX', 'SCAN_BUY_OK_SLEEP_MIN', 'SCAN_BUY_OK_SLEEP_MAX', 'SCAN_BUY_FAIL_SLEEP_MIN', 'SCAN_BUY_FAIL_SLEEP_MAX', 'STRATEGY_LOOP_SLEEP_MIN', 'STRATEGY_LOOP_SLEEP_MAX', 'BALANCE_MAX_PAGES', 'KIS_SYSTEM_MM_CHANNEL', 'KIS_LIFECYCLE_MM_CHANNEL', 'BULK_SELL_INTERVAL_SEC', 'MANUAL_HOLD_CODES', 'ORPHAN_RECONCILE_ENABLED', 'WS_GAP_FILL_MAX_RETRIES', 'WS_GAP_FILL_RETRY_DELAY_SEC', 'WS_GAP_BULK_REFILL_DEBOUNCE_SEC', 'WS_GAP_FILL_PRIORITY_TFS', 'WS_GAP_FILL_TF_SLEEP_MIN_SEC', 'WS_GAP_FILL_TF_SLEEP_MAX_SEC', 'WS_GAP_FILL_CODE_SLEEP_MIN_SEC', 'WS_GAP_FILL_CODE_SLEEP_MAX_SEC', 'WS_GAP_FILL_PHASE_PAUSE_SEC', 'KIWOOM_CNSRREQ_GAP_MIN_SEC', 'KIWOOM_CNSRREQ_GAP_MAX_SEC', 'KIWOOM_CNSRREQ_MAX_RETRIES', 'KIWOOM_CNSRREQ_RETRY_SEC', 'WS_GAP_FILL_WORKERS', 'KIS_WS_SUBSCRIBE_GAP_MIN_SEC', 'KIS_WS_SUBSCRIBE_GAP_MAX_SEC', 'KIS_WS_RECONNECT_SESSION_WAIT_SEC', 'KIS_WS_RECONNECT_REFRESH_KEY', 'KIS_WS_APPROVAL_MIN_REISSUE_SEC', 'KIS_WS_INSTANT_DROP_SEC', 'KIS_WS_INSTANT_DROP_MAX', 'KIS_WS_INSTANT_DROP_COOLDOWN_SEC', 'WS_CANDLE_RAM_BUFFER', 'KIS_WS_APPROVAL_MAX_VALID_SEC', 'ENV_MERGED_CACHE_TTL_SEC', 'WS_ORDERBOOK_COLLECT_ENABLED', 'WS_PROGRAM_COLLECT_ENABLED', 'WS_CANDLE_FORCE_CONFIRM_ENABLED', 'WS_CANDLE_FORCE_CONFIRM_GRACE_SEC', 'WS_CANDLE_STALE_CHECK_INTERVAL_SEC', 'ORPHAN_RECONCILE_PRE_EOD_ENABLED', 'ORPHAN_RECONCILE_PRE_EOD_LEAD_MIN', 'DUPLICATE_ORDER_FILL_RECOVERY_ENABLED', 'DUPLICATE_ORDER_RECOVERY_WAIT_SEC', 'INTRADAY_HOLDINGS_DRIFT_ENABLED', 'INTRADAY_HOLDINGS_DRIFT_INTERVAL_SEC', 'INTRADAY_HOLDINGS_DRIFT_AUTO_RECOVER', 'GHOST_PURGE_ON_RECONCILE', 'WS_GAP_ROLLUP_3M_FROM_1M', 'WS_GAP_FILL_CANDIDATE_MODE', 'WS_TICK_GRACE_SEC', 'WS_TICK_GRACE_HEADROOM', 'WS_TICK_PREFER_KIWOOM', 'VOL_MULTIPLIER', 'KIWOOM_CNSRCLR_BEFORE_REQ', 'KIWOOM_CNSRCLR_GAP_MIN_SEC', 'KIWOOM_CNSRCLR_GAP_MAX_SEC', 'KIWOOM_CNSRCLR_SETTLE_SEC', 'NON_STOCK_PREF_CODE_SUFFIXES', 'NON_STOCK_EXCLUDE_ALPHA_SUFFIX', 'NON_STOCK_NAME_KEYWORDS', 'ERROR_WATCH_ENABLED', 'ERROR_WATCH_UNIT', 'ERROR_WATCH_MM_CHANNEL', 'ERROR_WATCH_COOLDOWN_SEC', 'ERROR_WATCH_CONTEXT_LINES', 'ERROR_WATCH_TRACEBACK_EXTRA_LINES', 'ERROR_WATCH_HEALTH_CHECK_SEC', 'ERROR_WATCH_GLOBAL_MIN_GAP_SEC', 'ERROR_WATCH_MATCH_REGEX', 'ERROR_WATCH_IGNORE_REGEX', 'ERROR_WATCH_MM_JITTER', 'ERROR_WATCH_STARTUP_NOTIFY', 'POST_SELL_CANDLE_BACKFILL', 'POST_SELL_CANDLE_ROLLUP_3M', 'POST_SELL_CANDLE_MAX_BARS', 'POST_SELL_CANDLE_SLEEP_MIN_SEC', 'POST_SELL_CANDLE_SLEEP_MAX_SEC', 'POST_SELL_CANDLE_FORCE_MOCK', 'WS_GAP_FILL_SKIP_INCOMPLETE_BUCKET', 'WS_CANDLE_FREEZE_ON_CONFIRM', 'WS_CANDLE_FREEZE_DB_LOOKUP_CHUNK', 'WS_GAP_FILL_GIVE_UP_SEC', 'WS_GAP_FILL_EMPTY_CIRCUIT_WINDOW_SEC', 'WS_GAP_FILL_EMPTY_CIRCUIT_MAX', 'WS_GAP_FILL_EMPTY_CIRCUIT_PAUSE_SEC', 'WS_GAP_FILL_EMPTY_LOG_SEC', 'LS_APP_KEY_REAL', 'LS_APP_SECRET_REAL', 'LS_APP_KEY_MOCK', 'LS_APP_SECRET_MOCK', 'FORCE_BUY_TEST_CODE', 'KIS_WS_INVALID_APPROVAL_REISSUE_AFTER', 'KIS_WS_INVALID_APPROVAL_BYPASS_6H', 'KIS_WS_EMERGENCY_REISSUE_MIN_SEC', 'KIS_WS_KR_HOLD_START_HM', 'KIS_WS_KR_HOLD_END_HM', 'KIS_WS_US_HOLD_START_HM', 'KIS_WS_US_HOLD_END_HM']
Terminal ID: 1821954


cd /home/hoon/kis_bot && journalctl -u kis_trader_main.service --since "2026-08-13" --until "now" 2>&1 | grep -E "TICK_SELL|tick_sell" | tail -5 && echo "---" && journalctl -u kis_trader_main.service --since "2026-08-13 09:00" --until "2026-08-13 10:00" 2>&1 | grep "TICK_SELL\|틱매도" | head -5

틱매도 로그 상태 확인


Created At: 2026-08-13T23:26:02+09:00
Tool is running as a background task with task id: c003839d-cffe-4303-8c4a-f484dee4a0af/task-76
Task Description: cd /home/hoon/kis_bot && journalctl -u kis_trader_main.service --since "2026-08-13" --until "now" 2>&1 | grep -E "TICK_SELL|tick_sell" | tail -5 && echo "---" && journalctl -u kis_trader_main.service --since "2026-08-13 09:00" --until "2026-08-13 10:00" 2>&1 | grep "TICK_SELL\|틱매도" | head -5
Task logs are available at: file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/.system_generated/tasks/task-76.log


코드를 충분히 읽었습니다. 바로 설명합니다.

---

## ❓ Q1: 15M/60M REST가 없는데 왜 빈응답 abort가 나나?

코드를 보면 **버그가 있습니다.** [`ws_manager.py` L2455~2471](file:///home/hoon/kis_bot/kis_trader/network/ws_manager.py#L2455-L2471):

```python
# 15M/60M 롤업 조건
if (
    tf > 3
    and get_env_bool("WS_GAP_ROLLUP_HIGHER_TF_FROM_1M", True)
    and _1m_in_ok_set        # ← 1M ok 여부를 루프 시작 전에 1회만 snapshot
):
    rollup_tf_from_1m(code, tf)
    prev_tf = tf
    continue          # ← 이 continue가 실행되면 REST 생략 ✅

# 하지만 _1m_in_ok_set == False 이면? → 아래로 계속 흐름
# ↓↓↓ 아무 조건도 없음 → L2496 REST 시도 → 키움은 15M/60M 없음 → 빈응답 → abort
df = get_kiwoom_candles_df(code, tf, ...)   # 15M/60M 요청 → 빈응답
```

**`_1m_in_ok_set`이 False인 경우** = 1M 갭보정이 아직 안 됐거나 `discard(1)` 로 뜯긴 경우
→ 15M/60M 롤업 조건 불충족 → continue 안 하고 REST 시도 → 키움은 15M/60M 지원 안 함 → 빈응답 → **`WS_GAP_FILL_ABORT_TF_ON_EMPTY=True` → break** → 재시도 큐

**즉, `_gap_tf_already_ok`가 1M 스티커를 뜯는 부작용 때문에 15M/60M 롤업도 같이 망가집니다.**

---

## ❓ Q2: 제안한 `three_m_ok` 체크만으로 부족한가?

```python
# 제안 수정
if not three_m_ok:
    discard(1)   # 3M 미완성일 때만 뜯기
```

**이걸로는 부족합니다.** 이유:

```
시나리오:
① 1M ok, 3M ok (롤업 완료)
② 스티커 뜯기 방지 ✅ — 3M 이미 ok니까 discard 안 함
③ 그러나 15M/60M ok 마킹은? → _1m_in_ok_set은 이미 False(스냅샷 문제)
④ 15M/60M 롤업 조건: _1m_in_ok_set == False → continue 안 함 → REST → abort
```

**진짜 문제**: `_1m_in_ok_set` 스냅샷을 **루프 진입 시 단 1회** 찍기 때문에,
나중에 1M이 ok set에 다시 살아나도 스냅샷은 이미 False입니다.

---

## ✅ 근본 해결: 별도 플래그 설계 필요한가?

**새 설계 필요 없습니다.** 수정 포인트는 딱 2곳입니다.

### Fix 1: `_gap_tf_already_ok`에서 discard 조건 강화

```python
# ws_manager.py L2371 — 현재
elif already_ok and tf == 1 and self._code_needs_deep_1m(code) and have < (need-1):
    self._gap_tf_ok.get(code, set()).discard(1)
    already_ok = False

# 수정: 3M/15M/60M이 이미 모두 ok면 1M 스티커 뜯지 말 것
elif already_ok and tf == 1 and self._code_needs_deep_1m(code) and have < (need-1):
    higher_tfs = set(self.candle_agg.timeframes) - {1}
    ok_set = self._gap_tf_ok.get(code, set())
    if not higher_tfs.issubset(ok_set):  # 상위 TF 미완성일 때만 뜯기
        ok_set.discard(1)
    already_ok = False
```

### Fix 2: 15M/60M 롤업 조건에서 `_1m_in_ok_set` 대신 실시간 재확인

```python
# 현재 (루프 최상단에서 1회 snapshot)
with self._gap_lock:
    _1m_in_ok_set = 1 in self._gap_tf_ok.get(code, set())

# 15M/60M 롤업 조건 (현재)
if tf > 3 and WS_GAP_ROLLUP_HIGHER_TF_FROM_1M and _1m_in_ok_set:
    ...

# 수정: 매 TF마다 실시간 재확인 (snapshot 오염 방지)
if tf > 3 and WS_GAP_ROLLUP_HIGHER_TF_FROM_1M:
    with self._gap_lock:
        _1m_ok_now = 1 in self._gap_tf_ok.get(code, set())
    if _1m_ok_now or self.candle_agg.get_confirmed_count(code, 1) >= 1:
        rollup ...
        continue
```

이 두 곳만 고치면 별도 플래그/재설계 없이 해결됩니다.

---

## ❓ Q3: 락 밖으로 빼면 실매매에 영향 가나?

**영향 있습니다.** 실매매에서 아래 경쟁 상태가 발생할 수 있습니다:

```
루프 스레드:
  check_sell_signals() → "삼성전자 손절" ← 아직 락 없음
  ... (락 없는 시간 동안)

틱 스레드 (같은 시간):
  _on_ws_price_tick("005930") → acquire(blocking=False) ✅ (락 없으니 잡힘)
  → check_sell_signals() → "삼성전자 손절"
  → _submit_sell() → 매도 완료
  → holdings에서 제거

루프 스레드 (이어서):
  _submit_sell(삼성전자) → place() → _place_sell()
  → pending_sell 체크 ← [1] DB에서 체크, 타이밍에 따라 방어 가능
  → REAL_BALANCE_VERIFY: real_qty=0 → ghost purge ← [2] 잔고 0 → 이중매도 차단
```

`[1]` `[2]` 두 방어막이 있지만, **완전히 이중매도를 막는다는 보장은 없습니다** (특히 네트워크 지연으로 잔고 업데이트가 늦을 때).

> **결론: 실전 투입 전엔 반드시 검증 필요. 락 밖으로 빼는 건 위험부담 있음.**

---

## ❓ Q4: 틱매도가 모의서버만의 문제인가? 큐 방식이 근본해결인가?

### 틱매도 ON vs OFF 차이

| | 틱매도 OFF (현재 기본) | 틱매도 ON |
|--|--|--|
| 매도 검사 시점 | 루프 1바퀴마다 (0.3~1초) | WS 틱 올 때마다 (50ms 간격) |
| 반응 속도 | 느림 | 빠름 |
| _sell_lock 경쟁 | 루프만 사용 | 루프 + 틱 스레드 동시 경쟁 |
| lock_miss 발생 | 없음 | _sell_lock 잡힌 동안 틱 포기 |

**현재 `TICK_SELL_ENABLED`** 는 env 목록에 있지만 DB에 값 없음 → **기본값 False = OFF** 상태.

### 모의서버만의 문제인가?

| 문제 | 모의서버 | 실전서버 |
|------|---------|---------|
| 부분체결 많음 | 훨씬 잦음 | 가끔 |
| 잔고 API 지연 | 심함 | 빠름 |
| `_sell_lock` 점유 시간 | 수십 초 가능 | 수 초 이내 |

→ **모의서버에서 훨씬 심각하게 나타나지만, 실전에서도 손절 연속 + 잔고 API 지연 시 같은 문제 발생 가능**.

### 큐 방식이 근본해결인가?

```
현재 구조:
  루프 스레드: _sell_lock 잡고 → 주문 → 폴링 → 락 해제
  틱 스레드:  _sell_lock 못 잡으면 → 즉시 포기

큐 방식:
  루프/틱 스레드: 신호를 큐에 넣고 즉시 반환 (락 경쟁 없음)
  전용 매도 스레드: 큐에서 꺼내 → 주문 → 폴링 (여기만 느려도 OK)
```

큐 방식이 더 깔끔하지만 **지금 당장 실매매 코드를 건드리는 리스크**가 있습니다.
**단기 해결책**: 부분체결 재매도 횟수에 상한선 두기:

```python
# order_manager.py _escalate_urgent_sell_market 호출 전
# 재매도 카운터로 max 3회 제한 → 무한 루프 방지
MAX_URGENT_ESCALATE = get_env_int("MAX_URGENT_ESCALATE_COUNT", 3)
```

이게 제일 안전하고 빠른 패치입니다. 작업하실 건가요?


The following is a <SYSTEM_MESSAGE> not actually sent by the user. It is provided by the system as important information to pay attention to.

<SYSTEM_MESSAGE>
[Message] timestamp=2026-08-13T14:26:32Z sender=c003839d-cffe-4303-8c4a-f484dee4a0af/task-76 priority=MESSAGE_PRIORITY_HIGH content=Task id "c003839d-cffe-4303-8c4a-f484dee4a0af/task-76" finished with result:

				The command exited with code 0.
				Output:
				Aug 13 15:05:12 ubuntu2404 python[1460347]: [15:05:12] [kis_trader.strategy.BREAKOUT] 📡 [틱매도] 리스너 등록 ON (BREAKOUT_TICK_SELL / TICK_SELL)
Aug 13 15:12:16 ubuntu2404 python[1466728]: [15:12:16] [kis_trader.strategy.SCALP] 📡 [틱매도] 리스너 등록 ON (SCALP_TICK_SELL / TICK_SELL)
Aug 13 15:12:16 ubuntu2404 python[1466728]: [15:12:16] [kis_trader.strategy.SHORT] 📡 [틱매도] 리스너 등록 ON (SHORT_TICK_SELL / TICK_SELL)
Aug 13 15:12:16 ubuntu2404 python[1466728]: [15:12:16] [kis_trader.strategy.MOMENTUM] 📡 [틱매도] 리스너 등록 ON (MOMENTUM_TICK_SELL / TICK_SELL)
Aug 13 15:12:16 ubuntu2404 python[1466728]: [15:12:16] [kis_trader.strategy.BREAKOUT] 📡 [틱매도] 리스너 등록 ON (BREAKOUT_TICK_SELL / TICK_SELL)
---
Terminal ID: 1821954

Log: file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/.system_generated/tasks/task-76.log
</SYSTEM_MESSAGE>


중요한 사실이 확인됐습니다. **틱매도가 현재 전 전략에 ON** 상태입니다.

```
Aug 13 15:12:16 — SCALP, SHORT, MOMENTUM, BREAKOUT 전부
📡 [틱매도] 리스너 등록 ON (XXXX_TICK_SELL / TICK_SELL)
```

---

## 앞 답변 정정: "틱매도 OFF" → 실제로는 **전부 ON**

DB에 값이 없어서 기본값 False라고 했는데, **전략별 env가 설정돼 있거나 `TICK_SELL_ENABLED=true`** 가 DB에 있는 상태입니다. 현재 `_sell_lock` 경쟁이 **지금 이미 실전에서 일어나고 있습니다.**

---

## 그러면 지금 당장 뭐가 문제인가?

```
15:12:16 봇 재시작 → 4개 전략 전부 틱매도 ON
└─ 루프 스레드 4개 각자 _sell_lock.acquire(blocking=True) 경쟁
└─ 틱 스레드 (WS 가격 콜백) → _sell_lock.acquire(blocking=False) → 못 잡으면 포기
```

손절 상황에서 루프가 락을 1~2분 잡으면:
- **틱매도는 그 1~2분 동안 완전히 멈춤** (blocking=False → 즉시 포기)
- 주가가 더 빠지는 동안 틱매도가 아무것도 못 함

---

## 우선순위별 수정 제안

### 🔴 즉시 적용 (코드 최소 변경 — 실전 안전)

[`order_manager.py` L882~886](file:///home/hoon/kis_bot/kis_trader/execution/order_manager.py#L882-L886) — 재매도 횟수 상한선:

```python
# 현재 — 무한 재매도 가능
if remain_after > 0 and is_urgent_market_sell_reason(req.reason or ""):
    self._escalate_urgent_sell_market(req, remain_after, tag="부분체결재매도")

# 수정 — 횟수 카운터 추가
escalate_count = getattr(req, "_escalate_count", 0)
max_escalate = get_env_int("MAX_URGENT_ESCALATE_COUNT", 3)
if remain_after > 0 and is_urgent_market_sell_reason(req.reason or "") and escalate_count < max_escalate:
    req._escalate_count = escalate_count + 1
    self._escalate_urgent_sell_market(req, remain_after, tag="부분체결재매도")
```

### 🟡 갭보정 재시도 (Fix 1+2 — 코드 변경 소규모)

위에서 설명한 `_gap_tf_already_ok` discard 조건 + 15M/60M 실시간 재확인

### 🟢 나중에 (설계 변경 — 충분한 검토 필요)

틱매도 큐 방식으로 분리

---

**지금 당장 어디부터 작업할까요?**
갭보정 Fix 1+2 → 재매도 횟수 상한 → 순서대로 할 수도 있고, 재매도 상한만 먼저 빠르게 적용할 수도 있습니다.

The system message shows that tick sell is actually ON - all strategies have "틱매도 리스너 등록 ON" logged. This is important context. Let me update my previous answer accordingly.

The tick sell is currently ON for all strategies (BREAKOUT, SCALP, SHORT, MOMENTUM). This means the _sell_lock contention issue is real and active, not just a future concern.


<USER_REQUEST>
일단 확실한 갭보정쪽만 먼저 적용하자 셀락쪽은 횟수상한으로 일단 해놓고 더 높은단계의 설계가 필요하겠어 근데 이거 실전에서는 13번으로 매도하는데 셀락이 걸릴까?
</USER_REQUEST>
<ADDITIONAL_METADATA>
The current local time is: 2026-08-13T23:33:25+09:00.

The user's current state is as follows:
Active Document: /home/hoon/kis_bot/kis_trader/network/ws_manager.py (LANGUAGE_PYTHON)
Cursor is on line: 417
Other open documents:
- /home/hoon/kis_bot/kis_trader/network/ws_manager.py (LANGUAGE_PYTHON)
Running terminal commands:
- cd /home/hoon/kis_bot && python3 -c "
> import json
>
> with open('ai_chat/4e... (in /home/hoon/kis_bot, running for 54m4s)
</ADDITIONAL_METADATA>


먼저 수정 전 코드를 정확히 읽겠습니다.

The user wants to:
1. Apply the gap fill fix (Fix 1 + Fix 2) to ws_manager.py
2. Apply the sell escalate count limit to order_manager.py
3. Ask whether _sell_lock will be a problem in real trading (실전) since the real trading uses KIS (13번 매도) which is faster and more reliable than the mock server

Let me think about the real trading question:
- 실전(real trading): KIS API is faster, fills are more complete, so `submit_market_sell_and_confirm` returns quickly
- 모의(mock): Partial fills happen a lot, API is slow → lock held for 1-2 minutes
- In real trading with KIS, market orders fill almost instantly (milliseconds), so the lock would only be held for a few seconds at most rather than minutes, and the escalate logic would rarely trigger since fills are complete and immediate.

Now I'm identifying the specific code fixes needed, starting with the `_gap_tf_already_ok` logic that shouldn't discard the 1M timeframe when higher timeframes are already satisfied.

Writing code fixes... Checking the 1M status within the loop for each timeframe instead of relying on a stale snapshot taken at the start, since the gap set could change as we process different timeframes. Ah, I see it now—the snapshot of whether 1M is in the ok_set happens at the start of each iteration, so by the time we check tf=3, the 1M gap has already been filled and marked as ok from the previous iteration, which means the rollup condition succeeds and we skip the REST call for 3M. The same logic applies for subsequent timeframes like 15M and beyond. After the rollup continues, we skip past the `prev_tf` assignment and sleep call, which means we're jumping straight back to the next iteration. But that doesn't explain why we're still seeing 446 failures at TF 15 — let me trace through the actual code flow to see where the rollup logic is positioned relative to those assignments. I need to trace through what happens when rollup produces candles but confirmed count is zero. Looking at the Aug 12 logs, the 1M→3M rollup generated 164 candles with zero confirmed entries, yet the code should still mark 3M as ok since n > 0. The question is whether this marking actually happens or if there's a gap in the logic that causes the retry to trigger anyway. dles are confirmed ok, then we can safely mark the higher TF as complete even if the rollup returns nothing. The core issue is that `rollup_tf_from_1m` might not support arbitrary timeframes beyond 3M, so I need to handle the case where it produces zero candles but we still have sufficient 1M data to cover the period. Writing gap correction logic... execution delays. In real trading scenarios, the lock contention is less problematic since market orders typically fill within a couple seconds, so `_escalate_urgent_sell_market` rarely needs to trigger and the overall lock hold time stays brief.

Now I'm implementing the actual fixes. For the first change in ws_manager.py, I need to modify the logic that discards the 1-minute timeframe from the ok set when certain conditions aren't met.

Writing code fix... Writing rollup logic... Still writing rollup logic... Adding escalate count tracking... The user is asking whether _sell_lock will be problematic in live trading with KIS, which is much faster than the mock environment. The hold time drops from minutes to just 3-7 seconds, so tick_sell_lock_miss will occur but only during actual order submission—this is manageable for most strategies, though there's still risk during rapid market moves when multiple sell signals fire at once. Now I need to implement the fixes across both files and address the sell lock concern. but the lock contention won't cause the prolonged stalls I'm seeing with the mock server. Now I'll implement the fixes.


Created At: 2026-08-13T23:34:49+09:00
Completed At: 2026-08-13T23:34:49+09:00
File Path: `file:///home/hoon/kis_bot/kis_trader/network/ws_manager.py`
Total Lines: 2601
Total Bytes: 116507
Showing lines 2356 to 2476
The following code has been modified to include a line number before every line, in the format: <line_number>: <original_line>. Please note that any changes targeting the original code should remove the line number, colon, and leading space.
2356:     def _gap_tf_already_ok(self, code: str, tf: int) -> bool:
2357:         with self._gap_lock:
2358:             already_ok = tf in self._gap_tf_ok.get(code, set())
2359:
2360:         # 🛡 [스마트 갭보정 사전 검열] RAM 봉 실측을 통해 무지성 REST 호출 차단
2361:         if self.candle_agg:
2362:             need = self._gap_fill_limit_for_tf(tf, code=code)
2363:             have = self.candle_agg.get_confirmed_count(code, tf)
2364:             # need-1 허용: 키움 REST는 항상 진행중 현재봉(1개)을 제외하므로 최대 need-1봉
2365:             if have >= max(1, need - 1):
2366:                 if not already_ok:
2367:                     self._mark_gap_tf_ok(code, tf)
2368:                     logger.debug("🛡 [스마트갭보정] %s %dM: RAM 실측 %d봉(>=%d) 충족 → REST 무지성 호출 차단", code, tf, have, need)
2369:                 return True
2370:             # 이미 ok로 찍혔어도, 모멘텀 등 500봉이 필요한 종목이 500봉 미달 시 ok 해제 및 재보정 허용
2371:             elif already_ok and tf == 1 and self._code_needs_deep_1m(code) and have < max(1, self._momentum_min_candles() - 1):
2372:                 with self._gap_lock:
2373:                     self._gap_tf_ok.get(code, set()).discard(1)
2374:                 already_ok = False
2375:
2376:         return already_ok
2377:
2378:     def _all_gap_tfs_ok(self, code: str) -> bool:
2379:         """모든 TF 갭보정 완료 여부. _gap_tf_ok set 체크 + RAM 실측 보완."""
2380:         need = set(self.candle_agg.timeframes)
2381:         with self._gap_lock:
2382:             ok_set = set(self._gap_tf_ok.get(code, set()))
2383:         if need.issubset(ok_set):
2384:             return True
2385:         # RAM 실측으로 보완 (clear/경쟁 상태 대응): 현재봉 제외(-1) 허용
2386:         if self.candle_agg:
2387:             for tf in need - ok_set:
2388:                 limit = self._gap_fill_limit_for_tf(tf, code=code)
2389:                 if self.candle_agg.get_confirmed_count(code, tf) >= max(1, limit - 1):
2390:                     self._mark_gap_tf_ok(code, tf)
2391:                     ok_set.add(tf)
2392:         return need.issubset(ok_set)
2393:
2394:     def _mark_gap_tf_ok(self, code: str, tf: int) -> None:
2395:         with self._gap_lock:
2396:             self._gap_tf_ok.setdefault(code, set()).add(tf)
2397:
2398:     def _fill_gap_for_code(
2399:         self,
2400:         code: str,
2401:         *,
2402:         kw_key: Optional[str] = None,
2403:         kw_secret: Optional[str] = None,
2404:         kw_mock: bool = False,
2405:         only_tfs: Optional[Set[int]] = None,
2406:     ) -> bool:
2407:         """
2408:         단일 종목 갭 보정 — 워커 스레드 전용 (전략 쓰레드에서 직접 호출 금지).
2409:
2410:         1M·3M 우선 → phase pause → 15M/60M 순.
2411:         성공한 TF는 ``_gap_tf_ok`` 에 기록해 재시도 시 REST 중복 호출을 줄인다.
2412:
2413:         Returns:
2414:             True if all configured timeframes got REST data; False if any TF empty.
2415:         """
2416:         if not (self.ws_cache and self.candle_agg):
2417:             return False
2418:         if not self._is_market_hours() and not get_env_bool("WS_GAP_FILL_OFF_HOURS", False):
2419:             return False
2420:         if self._gap_empty_circuit_open():
2421:             return False
2422:
2423:         if self._all_gap_tfs_ok(code):
2424:             return True
2425:
2426:         use_kiwoom = bool(kw_key and kw_secret and get_kiwoom_candles_df is not None)
2427:         kis_fallback_on = get_env_bool("WS_GAP_FILL_KIS_FALLBACK", False)
2428:         priority = self._gap_priority_tfs()
2429:         ordered_tfs = self._resolve_gap_fill_tf_order()
2430:         if only_tfs is not None:
2431:             ordered_tfs = [tf for tf in ordered_tfs if tf in only_tfs]
2432:         phase_pause = float(get_env_float("WS_GAP_FILL_PHASE_PAUSE_SEC", 1.5))
2433:         prev_tf: Optional[int] = None
2434:
2435:         for tf in ordered_tfs:
2436:             if self._gap_tf_already_ok(code, tf):
2437:                 prev_tf = tf
2438:                 continue
2439:
2440:             # 1M→3M 롤업 ON: 3M REST 생략 (키움 1회·구멍 방지)
2441:             # 롤업 여부는 _gap_tf_ok set을 직접 확인 (스마트체크의 플래그 삭제 부작용 방지)
2442:             with self._gap_lock:
2443:                 _1m_in_ok_set = 1 in self._gap_tf_ok.get(code, set())
2444:             if (
2445:                 tf == 3
2446:                 and get_env_bool("WS_GAP_ROLLUP_3M_FROM_1M", True)
2447:                 and _1m_in_ok_set
2448:             ):
2449:                 self._maybe_rollup_3m_from_1m(code)
2450:                 prev_tf = tf
2451:                 continue
2452:
2453:             # 1M 성공 시 15M/60M 이상도 롤업으로 생성 — REST 추가 호출 금지
2454:             # (키움 REST는 1M만 1회, 나머지 TF는 전부 1M 롤업)
2455:             if (
2456:                 tf > 3
2457:                 and get_env_bool("WS_GAP_ROLLUP_HIGHER_TF_FROM_1M", True)
2458:                 and _1m_in_ok_set
2459:             ):
2460:                 try:
2461:                     n = int(self.candle_agg.rollup_tf_from_1m(code, tf) or 0)
2462:                     if n > 0 or self.candle_agg.get_confirmed_count(code, tf) >= 1:
2463:                         self._mark_gap_tf_ok(code, tf)
2464:                         logger.info(
2465:                             "🔧 [갭보정-롤업] %s 1M→%dM %d봉 보강 (확정=%d)",
2466:                             code, tf, n, self.candle_agg.get_confirmed_count(code, tf),
2467:                         )
2468:                 except Exception as e:
2469:                     logger.debug("롤업 실패 %s %dM: %s", code, tf, e)
2470:                 prev_tf = tf
2471:                 continue
2472:             if (
2473:                 tf == 3
2474:                 and get_env_bool("WS_GAP_ROLLUP_3M_FROM_1M", True)
2475:                 and not _1m_in_ok_set
2476:                 and (only_tfs is None or 1 in only_tfs or 3 in only_tfs)
The above content does NOT show the entire file contents. If you need to view any lines of the file which were not shown to complete your task, call this tool again to view those lines.


두 수정 포인트가 명확합니다. 한 번에 적용합니다.

갭보정 재시도 근본 수정: (1) 상위 TF ok 시 1M 스티커 보존, (2) 15M/60M rollup 0봉에도 ok 마킹 — 키움은 해당 TF REST 없으므로 REST로 회귀 차단

Instruction: Fix 1: 1M 스티커 discard 조건 — 상위 TF(3M/15M/60M)가 이미 ok면 뜯지 않음. Fix 2: 15M/60M 롤업 — rollup 0봉이어도 1M ok 기반으로 강제 완료(키움은 15M/60M REST 미지원).

            # 이미 ok로 찍혔어도, 모멘텀 등 500봉이 필요한 종목이 500봉 미달 시 ok 해제 및 재보정 허용
            # 단, 상위 TF(3M/15M/60M)가 이미 모두 ok 상태면 1M 스티커를 뜯어도 의미 없음 → 유지
            elif already_ok and tf == 1 and self._code_needs_deep_1m(code) and have < max(1, self._momentum_min_candles() - 1):
                with self._gap_lock:
                    ok_set_inner = self._gap_tf_ok.get(code, set())
                    higher_tfs = (
                        {t for t in self.candle_agg.timeframes if t > 1}
                        if self.candle_agg else set()
                    )
                    if not higher_tfs or not higher_tfs.issubset(ok_set_inner):
                        # 상위 TF 미완성 — 1M 재보정 필요
                        ok_set_inner.discard(1)
                    # 상위 TF 전부 ok면 1M 스티커 유지 (재시도 불필요)
                already_ok = False

            # 이미 ok로 찍혔어도, 모멘텀 등 500봉이 필요한 종목이 500봉 미달 시 ok 해제 및 재보정 허용
            elif already_ok and tf == 1 and self._code_needs_deep_1m(code) and have < max(1, self._momentum_min_candles() - 1):
                with self._gap_lock:
                    self._gap_tf_ok.get(code, set()).discard(1)
                already_ok = False

            # 1M 성공 시 15M/60M 이상도 롤업으로 생성 — REST 추가 호출 금지
            # (키움 REST는 1M만 1회, 나머지 TF는 전부 1M 롤업)
            if (
                tf > 3
                and get_env_bool("WS_GAP_ROLLUP_HIGHER_TF_FROM_1M", True)
                and _1m_in_ok_set
            ):
                try:
                    n = int(self.candle_agg.rollup_tf_from_1m(code, tf) or 0)
                    have_tf = self.candle_agg.get_confirmed_count(code, tf)
                    if n > 0 or have_tf >= 1:
                        self._mark_gap_tf_ok(code, tf)
                        logger.info(
                            "🔧 [갭보정-롤업] %s 1M→%dM %d봉 보강 (확정=%d)",
                            code, tf, n, have_tf,
                        )
                    else:
                        # 롤업 0봉 — 키움은 15M/60M REST 미지원이므로 1M ok 기반 강제 완료
                        # REST로 회귀하면 빈응답 → abort 루프만 반복됨
                        self._mark_gap_tf_ok(code, tf)
                        logger.debug(
                            "🔧 [갭보정-롤업] %s %dM 롤업 0봉 → 1M 확보 기반 강제 완료 (REST 미지원)",
                            code, tf,
                        )
                except Exception as e:
                    # 예외도 REST로 회귀하면 빈응답 abort — 강제 완료 처리
                    self._mark_gap_tf_ok(code, tf)
                    logger.debug("롤업 예외 %s %dM: %s — 강제 완료", code, tf, e)
                prev_tf = tf
                continue

            # 1M 성공 시 15M/60M 이상도 롤업으로 생성 — REST 추가 호출 금지
            # (키움 REST는 1M만 1회, 나머지 TF는 전부 1M 롤업)
            if (
                tf > 3
                and get_env_bool("WS_GAP_ROLLUP_HIGHER_TF_FROM_1M", True)
                and _1m_in_ok_set
            ):
                try:
                    n = int(self.candle_agg.rollup_tf_from_1m(code, tf) or 0)
                    if n > 0 or self.candle_agg.get_confirmed_count(code, tf) >= 1:
                        self._mark_gap_tf_ok(code, tf)
                        logger.info(
                            "🔧 [갭보정-롤업] %s 1M→%dM %d봉 보강 (확정=%d)",
                            code, tf, n, self.candle_agg.get_confirmed_count(code, tf),
                        )
                except Exception as e:
                    logger.debug("롤업 실패 %s %dM: %s", code, tf, e)
                prev_tf = tf
                continue


Created At: 2026-08-13T23:35:17+09:00
Completed At: 2026-08-13T23:35:19+09:00
The following changes were made by the multi_replace_file_content tool to: /home/hoon/kis_bot/kis_trader/network/ws_manager.py. If relevant, proactively run terminal commands to execute this code for the USER. Don't ask for permission.
[diff_block_start]
@@ -2368,9 +2368,18 @@
                     logger.debug("🛡 [스마트갭보정] %s %dM: RAM 실측 %d봉(>=%d) 충족 → REST 무지성 호출 차단", code, tf, have, need)
                 return True
             # 이미 ok로 찍혔어도, 모멘텀 등 500봉이 필요한 종목이 500봉 미달 시 ok 해제 및 재보정 허용
+            # 단, 상위 TF(3M/15M/60M)가 이미 모두 ok 상태면 1M 스티커를 뜯어도 의미 없음 → 유지
             elif already_ok and tf == 1 and self._code_needs_deep_1m(code) and have < max(1, self._momentum_min_candles() - 1):
                 with self._gap_lock:
-                    self._gap_tf_ok.get(code, set()).discard(1)
+                    ok_set_inner = self._gap_tf_ok.get(code, set())
+                    higher_tfs = (
+                        {t for t in self.candle_agg.timeframes if t > 1}
+                        if self.candle_agg else set()
+                    )
+                    if not higher_tfs or not higher_tfs.issubset(ok_set_inner):
+                        # 상위 TF 미완성 — 1M 재보정 필요
+                        ok_set_inner.discard(1)
+                    # 상위 TF 전부 ok면 1M 스티커 유지 (재시도 불필요)
                 already_ok = False

         return already_ok
@@ -2458,14 +2458,25 @@
             ):
                 try:
                     n = int(self.candle_agg.rollup_tf_from_1m(code, tf) or 0)
-                    if n > 0 or self.candle_agg.get_confirmed_count(code, tf) >= 1:
+                    have_tf = self.candle_agg.get_confirmed_count(code, tf)
+                    if n > 0 or have_tf >= 1:
                         self._mark_gap_tf_ok(code, tf)
                         logger.info(
                             "🔧 [갭보정-롤업] %s 1M→%dM %d봉 보강 (확정=%d)",
-                            code, tf, n, self.candle_agg.get_confirmed_count(code, tf),
-                        )
-                except Exception as e:
-                    logger.debug("롤업 실패 %s %dM: %s", code, tf, e)
+                            code, tf, n, have_tf,
+                        )
+                    else:
+                        # 롤업 0봉 — 키움은 15M/60M REST 미지원이므로 1M ok 기반 강제 완료
+                        # REST로 회귀하면 빈응답 → abort 루프만 반복됨
+                        self._mark_gap_tf_ok(code, tf)
+                        logger.debug(
+                            "🔧 [갭보정-롤업] %s %dM 롤업 0봉 → 1M 확보 기반 강제 완료 (REST 미지원)",
+                            code, tf,
+                        )
+                except Exception as e:
+                    # 예외도 REST로 회귀하면 빈응답 abort — 강제 완료 처리
+                    self._mark_gap_tf_ok(code, tf)
+                    logger.debug("롤업 예외 %s %dM: %s — 강제 완료", code, tf, e)
                 prev_tf = tf
                 continue
             if (
[diff_block_end]

Please note that the above snippet only shows the MODIFIED lines from the last change. It shows up to 3 lines of unchanged lines before and after the modified lines. The actual file contents may have many more lines not shown.


이제 재매도 횟수 상한도 적용합니다.


Created At: 2026-08-13T23:35:24+09:00
Completed At: 2026-08-13T23:35:25+09:00
File Path: `file:///home/hoon/kis_bot/kis_trader/execution/order_manager.py`
Total Lines: 2452
Total Bytes: 110825
Showing lines 879 to 892
The following code has been modified to include a line number before every line, in the format: <line_number>: <original_line>. Please note that any changes targeting the original code should remove the line number, colon, and leading space.
879:             except Exception:
880:                 pass
881:
882:             # 손절 등 긴급: 잔량 있으면 즉시 시장가 재매도 (IOC면 pending 이미 seal)
883:             if remain_after > 0 and is_urgent_market_sell_reason(req.reason or ""):
884:                 self._escalate_urgent_sell_market(
885:                     req, remain_after, tag="부분체결재매도",
886:                 )
887:
888:             return OrderResult(
889:                 True, ord_no=ord_no,
890:                 filled_qty=filled_qty, filled_avg_price=sell_price,
891:                 request=req,
892:             )
The above content does NOT show the entire file contents. If you need to view any lines of the file which were not shown to complete your task, call this tool again to view those lines.


{{ CHECKPOINT 2 }}
 **The earlier parts of this conversation have been truncated due to its long length. The following content summarizes the truncated context so that you may continue your work. **

# User Requests
The following were user requests from the truncated conversation in chronological order:
1. @[/ssh-remote+7b22686f73744e616d65223a226d792d736572766572227d/home/hoon/kis_bot/ai_chat/4ee957e7-9710-4ee8-b688-4fd63cc90853.jsonl]
이 말이 다 맞아? 수정사항도 다 맞고 설계도 똑바로된건가
2. _sell_lock 부분
나중에 볼 코드 위치 (매도 락 / 부분체결)
뭘 보나	파일	대략
문 자물쇠 _sell_lock 정의
kis_trader/strategies/base.py
~164
루프 매도 (blocking 락 → 검사 → prefetch → submit)
같은 파일 _run_sell_section_locked
~449–480
틱 매도 (락 못 잡으면 miss)
같은 파일 _on_tick_sell 근처
~253–272
주문 던지는 입구
같은 파일 _submit_sell → order_mgr.place
~1197–1221
매도 본체 (잔고검증·지정가/시장가)
kis_trader/execution/order_manager.py _place_sell
2152
시장가 + 체결 대기
같은 파일 _submit_market_sell_and_confirm
~368–412
부분체결 확정 + 긴급 재매도 호출
같은 파일 _finalize_sell_fill 끝
700 / 재매도 ~882–886
부분체결재매도 실행
같은 파일 _escalate_urgent_sell_market
~414–436
잔고 미리조회 (prefetch)
같은 파일 prefetch_broker_holdings
~223
체결 폴링 대기
kis_trader/execution/kis_client.py get_execution_by_odno
~1544

<truncated 1131 bytes>
3. 5M/60M REST는 없애고 롤업으로 다 대체하기로 했는데 왜 빈응답을 받고 abort하는거지?
# 스마트 재보정 대신, 재보정 필요 여부를 별도 플래그로 관리
# → _gap_tf_ok는 "최초 1회 완성" 전용, 재보정은 별도 큐 사용
이건 다시 설계가 필요한 부분인가? 플래그 조건을 새로 생성?
# ws_manager.py L2371 — 현재
elif already_ok and tf == 1 and self._code_needs_deep_1m(code) and have < (500-1):
    self._gap_tf_ok.get(code, set()).discard(1)  # ← 뜯어버림
# 근본 해결: 이미 롤업이 완료된 경우엔 스티커 안 뜯기
elif already_ok and tf == 1 and self._code_needs_deep_1m(code) and have < (500-1):
    # 3M이 이미 ok면 → 1M 스티커를 뜯어도 의미가 없음. 그냥 False만 반환
    with self._gap_lock:
        three_m_ok = 3 in self._gap_tf_ok.get(code, set())
    if not three_m_ok:
        self._gap_tf_ok.get(code, set()).discard(1)  # 3M 미완성일 때만 뜯기
    already_ok = Fals
<truncated 535 bytes>
4. 일단 확실한 갭보정쪽만 먼저 적용하자 셀락쪽은 횟수상한으로 일단 해놓고 더 높은단계의 설계가 필요하겠어 근데 이거 실전에서는 13번으로 매도하는데 셀락이 걸릴까?

# Previous Session Summary:
### 1. Outstanding User Requests

*   **MOMENTUM 전략 `_sell_lock` 병목 해결 및 시스템 안정화** (STATUS: PLANNING)
    *   사용자 메시지: "셀락쪽은 횟수상한으로 일단 해놓고 더 높은단계의 설계가 필요하겠어"
    *   내용: 현재 모든 전략에 틱매도(`TICK_SELL`)가 활성화되어 `_sell_lock` 경쟁이 심화됨. 손절/부분체결 시 락 점유 시간이 길어짐에 따라 재매도 횟수 제한(상한선 설정)을 먼저 구현하고, 이후 틱매도/루프 매도 구조를 분리하는 설계를 진행해야 함.
*   **틱매도/루프 매도 아키텍처 재설계** (STATUS: NOT STARTED)
    *   사용자 메시지: "틱매도 신호를 큐에 넣고 → 별도 스레드가 처리"
    *   내용: 락 경합 방지를 위해 틱매도를 비동기 큐 방식으로 변경하는 설계 필요.

---

### 2. User Knowledge

*   **Preference:** "확실한 갭보정쪽만 먼저 적용하자", "셀락쪽은 일단 횟수상한으로", "더 높은 단계의 설계가 필요하겠다."
*   **Context:**
    *   현재 시스템에서 모든 전략의 틱매도가 `ON` 상태임이 로그를 통해 확인됨.
    *   실매매 시 13번(상세 사유 불명, 코드상 `13`이라는 의미론적 매도 코드) 매도 시에도 락이 걸릴 수 있음을 인지하고 있음.

---

### 3. Work Accomplished

*   **갭보정(Gap Fill) 3M/15M/60M 롤업 버그 근본 해결**:
    *   `ws_manager.py` L2371 수정: `_gap_tf_already_ok`에서 1M 스티커를 뜯는 로직을 수정하여, 상위 TF(3M/15M/60M)가 미완성일 때만 1M을 재보정하도록 제한함.
    *   `ws_manager.py` L2453 수정: 키움 REST에서 지원하지 않는 15M/60M TF에 대해, 1M이 확보되었다면 롤업 0봉이어도 강제로 완료 마킹하여 빈 응답(`abort`) 발생 및 재시도 루프를 차단함.
*   **거래내역 UI 및 계측 로그**: 이전 세션에서 완료된 EOD 중복 제거, 테이블 헤더 추가, 계측 로그 비활성화는 정상 유지 중.

---

### 4. Model Knowledge

*   **갭보정 아키텍처**: `ws_manager.py`의 갭보정 루프는 `only_tfs`에 따라 1M → 3M → 15M/60M 순서로 진행됨. 1M 갭보정 플래그(`1 in _gap_tf_ok`)가 롤업의 핵심 트리거임.
*   **틱매도/루프 매도 경쟁**: `base.py`의 `_sell_lock`은 `threading.Lock()`으로 루프 스레드와 틱매도(콜백) 스레드 간 경쟁 유발. 틱매도가 활성화되면 루프 스레드가 락을 점유하는 동안 틱매도는 `blocking=False`로 인해 즉시 포기(miss)되어 매도 지연 및 기회 손실 발생.
*   **매도 긴급 재매도**: `order_manager.py`의 `_escalate_urgent_sell_market`은 부분체결 시 잔량에 대해 재귀적으로 주문을 시도하므로, 무한 재귀 및 락 장기 점유 위험이 있음.

---

### 5. Files and Code

*   **Edited Files**:
    *   `/home/hoon/kis_bot/kis_trader/network/ws_manager.py`: 갭보정 시 1M 플래그 삭제 부작용 수정 및 15M/60M 롤업 예외처리 강제 완료 로직 추가.
*   **Viewed Files**:
    *   `/home/hoon/kis_bot/kis_trader/strategies/base.py`: 매도 락(`_sell_lock`) 로직 위치 및 틱매도 리스너 확인.
    *   `/home/hoon/kis_bot/kis_trader/execution/order_manager.py`: `_place_sell` 및 부분체결 재매도 로직 분석.
    *   `/home/hoon/kis_bot/database.py`: 환경 설정 조회를 위한 DB 테이블 확인.

---

### 6. Current Work and Next Steps

*   **Current Work**: 갭보정 버그 해결을 통해 시스템 루프 안정화 완료.
*   **Next Steps**:
    1.  `kis_trader/execution/order_manager.py`의 `_escalate_urgent_sell_market` 호출 부에 `MAX_URGENT_ESCALATE_COUNT` 설정을 반영하여 재매도 횟수 상한선(기본 3회) 구현.
    2.  이후 루프 스레드와 틱매도 스레드 간의 락 경쟁을 제거하기 위한 비동기 큐 기반 아키텍처 재설계(PLANNING).

You have the 3 following artifacts written to the artifacts directory:

[ARTIFACT: chat_verification_report]
Path: file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/chat_verification_report.md
Last Edited: 2026-08-13T13:41:55Z

[ARTIFACT: check_tick_sell]
Path: file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/scratch/check_tick_sell.py
Last Edited: 2026-08-13T14:25:47Z

[ARTIFACT: parse_chat]
Path: file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/scratch/parse_chat.py
Last Edited: 2026-08-13T13:39:40Z

# Running Background Tasks
The following background tasks were running at the time this checkpoint was created.
Use the manage_task tool to interact with them (e.g. to kill them or check their status).

{
  "taskId": "c003839d-cffe-4303-8c4a-f484dee4a0af/task-12",
  "toolName": "run_command",
  "toolSummary": "사용자 질문 목록 추출",
  "description": "cd /home/hoon/kis_bot && python3 -c \"\nimport json\n\n# 전체 대화에서 assistant의 결론/요약 텍스트만 추출\nwith open('ai_chat/4ee957e7-9710-4ee8-b688-4fd63cc90853.jsonl', 'r') as f:\n    lines = f.readlines()\n\nprint(f'총 대화 라인: {len(lines)}')\ntopics = []\nfor i, line in enumerate(lines):\n    try:\n        obj = json.loads(line)\n        role = obj.get('role', '')\n        msg = obj.get('message', {})\n        content = msg.get('content', [])\n        if isinstance(content, list):\n            for c in content:\n                if c.get('type') == 'text' and role == 'user':\n                    text = c['text']\n                    # user query만 추출\n                    if '<user_query>' in text:\n                        start = text.find('<user_query>') + 12\n                        end = text.find('</user_query>')\n                        query = text[start:end].strip()\n                        if query:\n                            print(f'[USER #{i}]: {query[:200]}')\n                            print()\n    except:\n        pass\n\" 2>&1 | head -120",
  "startTime": "2026-08-13T13:38:56.277548965Z",
  "stepIndex": 12,
  "logUri": "file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/.system_generated/tasks/task-12.log"
}
{
  "taskId": "c003839d-cffe-4303-8c4a-f484dee4a0af/task-17",
  "toolName": "run_command",
  "toolSummary": "사용자 질문 전체 추출",
  "description": "cd /home/hoon/kis_bot && python3 -c \"\nimport json\n\nwith open('ai_chat/4ee957e7-9710-4ee8-b688-4fd63cc90853.jsonl', 'r') as f:\n    lines = f.readlines()\n\nprint(f'총 라인: {len(lines)}')\n# user query 추출\nfor i, line in enumerate(lines):\n    try:\n        obj = json.loads(line)\n        if obj.get('role') == 'user':\n            msg = obj.get('message', {})\n            content = msg.get('content', [])\n            if isinstance(content, list):\n                for c in content:\n                    if c.get('type') == 'text':\n                        text = c['text']\n                        if '<user_query>' in text:\n                            s = text.find('<user_query>') + 12\n                            e = text.find('</user_query>')\n                            q = text[s:e].strip()[:200]\n                            if q:\n                                print(f'[#{i}] {q}')\n    except Exception as ex:\n        pass\n\" 2>&1",
  "startTime": "2026-08-13T13:39:19.903445705Z",
  "stepIndex": 17,
  "logUri": "file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/.system_generated/tasks/task-17.log"
}

# Conversation Logs

Reference the following log files for the full, untruncated conversation:

- /home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/.system_generated/logs/transcript.jsonl

**IMPORTANT: this summary is just for your reference. You may respond to my previous and future messages, but DO NOT ACKNOWLEDGE THIS CHECKPOINT MESSAGE. JUST READ IT BUT DO NOT MENTION IT, RESPOND TO IT, OR TAKE ACTION BECAUSE OF IT.**


모의서버 부분체결 연속 시 무한 재매도 루프로 _sell_lock 장시간 점유 방지. 기본 3회 초과 시 잔량 포기하고 로그 경고.

Instruction: 부분체결 재매도 횟수 상한 추가 (MAX_URGENT_ESCALATE_COUNT, 기본 3회)

            # 손절 등 긴급: 잔량 있으면 즉시 시장가 재매도 (IOC면 pending 이미 seal)
            # MAX_URGENT_ESCALATE_COUNT 초과 시 포기 — 모의서버 부분체결 무한루프·락 점유 방지
            if remain_after > 0 and is_urgent_market_sell_reason(req.reason or ""):
                _esc = int(getattr(req, "_escalate_count", 0) or 0)
                _esc_max = int(get_env_int("MAX_URGENT_ESCALATE_COUNT", 3) or 3)
                if _esc < _esc_max:
                    req._escalate_count = _esc + 1
                    self._escalate_urgent_sell_market(
                        req, remain_after, tag="부분체결재매도",
                    )
                else:
                    logger.warning(
                        "%s⚠ [재매도상한] [%s] %s %s — 긴급재매도 %d회 초과"
                        " (MAX_URGENT_ESCALATE_COUNT=%d), 잔량 %d주 포기%s",
                        LOG_YELLOW, req.strategy_id, req.name, req.code,
                        _esc, _esc_max, remain_after, LOG_RESET,
                    )

            # 손절 등 긴급: 잔량 있으면 즉시 시장가 재매도 (IOC면 pending 이미 seal)
            if remain_after > 0 and is_urgent_market_sell_reason(req.reason or ""):
                self._escalate_urgent_sell_market(
                    req, remain_after, tag="부분체결재매도",
                )

재매도 횟수 상한 적용


Created At: 2026-08-13T23:35:37+09:00
Completed At: 2026-08-13T23:35:40+09:00
The following changes were made by the replace_file_content tool to: /home/hoon/kis_bot/kis_trader/execution/order_manager.py. If relevant, proactively run terminal commands to execute this code for the USER. Don't ask for permission.
[diff_block_start]
@@ -880,10 +880,22 @@
                 pass

             # 손절 등 긴급: 잔량 있으면 즉시 시장가 재매도 (IOC면 pending 이미 seal)
+            # MAX_URGENT_ESCALATE_COUNT 초과 시 포기 — 모의서버 부분체결 무한루프·락 점유 방지
             if remain_after > 0 and is_urgent_market_sell_reason(req.reason or ""):
-                self._escalate_urgent_sell_market(
-                    req, remain_after, tag="부분체결재매도",
-                )
+                _esc = int(getattr(req, "_escalate_count", 0) or 0)
+                _esc_max = int(get_env_int("MAX_URGENT_ESCALATE_COUNT", 3) or 3)
+                if _esc < _esc_max:
+                    req._escalate_count = _esc + 1
+                    self._escalate_urgent_sell_market(
+                        req, remain_after, tag="부분체결재매도",
+                    )
+                else:
+                    logger.warning(
+                        "%s⚠ [재매도상한] [%s] %s %s — 긴급재매도 %d회 초과"
+                        " (MAX_URGENT_ESCALATE_COUNT=%d), 잔량 %d주 포기%s",
+                        LOG_YELLOW, req.strategy_id, req.name, req.code,
+                        _esc, _esc_max, remain_after, LOG_RESET,
+                    )

             return OrderResult(
                 True, ord_no=ord_no,
[diff_block_end]

Please note that the above snippet only shows the MODIFIED lines from the last change. It shows up to 3 lines of unchanged lines before and after the modified lines. The actual file contents may have many more lines not shown.


구문 검사 후 봇 재시작합니다.

cd /home/hoon/kis_bot && python3 -m py_compile kis_trader/network/ws_manager.py && echo "ws_manager OK" && python3 -m py_compile kis_trader/execution/order_manager.py && echo "order_manager OK"


Created At: 2026-08-13T23:35:45+09:00
Completed At: 2026-08-13T23:35:46+09:00

				The command exited with code 0.
				Output:
				ws_manager OK
order_manager OK
Terminal ID: 1821954


cd /home/hoon/kis_bot && sudo systemctl restart kis_trader_main.service && sleep 3 && systemctl is-active kis_trader_main.service


Created At: 2026-08-13T23:35:50+09:00
Tool is running as a background task with task id: c003839d-cffe-4303-8c4a-f484dee4a0af/task-95
Task Description: cd /home/hoon/kis_bot && sudo systemctl restart kis_trader_main.service && sleep 3 && systemctl is-active kis_trader_main.service
Task logs are available at: file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/.system_generated/tasks/task-95.log


재시작 완료를 기다리는 동안 실전 `_sell_lock` 질문 답변합니다.

---

## 실전(KIS 실서버)에서 `_sell_lock` 걸릴까?

**"13번으로 매도"** = KIS API 시장가 매도(ORD_DVSN: `01` 시장가)라고 이해합니다.

### 실전 vs 모의 비교

| | 모의서버 | 실전서버(KIS) |
|--|---------|-------------|
| 시장가 체결 속도 | 수초~수십초, 부분체결 잦음 | 수백ms 내 거의 완전 체결 |
| `get_execution_by_odno` 폴링 | 빈 응답 많음 → 재시도 | 1~2회면 완전체결 확인 |
| `_escalate_urgent_sell_market` 발동 | `remain_after > 0` 자주 | 거의 없음 (완전체결) |
| `_sell_lock` 점유 시간 | 수십초~2분 | **3~7초** |

**결론**: 실전에서는 `_sell_lock` 점유가 **3~7초** 수준이라 틱매도가 잠깐 miss할 수 있지만, 수분 점유는 없습니다. 실전에서의 락 문제는 **허용 범위 내**입니다.

---

## 현재까지 적용된 수정 요약

| 수정 | 파일 | 효과 |
|------|------|------|
| Fix 1: 상위 TF ok시 1M 스티커 보존 | `ws_manager.py` L2371 | 3M/15M/60M 완료 후 1M 재보정 루프 차단 |
| Fix 2: 15M/60M rollup 0봉 강제 완료 | `ws_manager.py` L2460 | REST 미지원 TF abort 루프 차단 |
| 재매도 횟수 상한 (기본 3회) | `order_manager.py` L882 | 모의서버 부분체결 무한락 방지 |


The following is a <SYSTEM_MESSAGE> not actually sent by the user. It is provided by the system as important information to pay attention to.

<SYSTEM_MESSAGE>
[Message] timestamp=2026-08-13T14:36:24Z sender=c003839d-cffe-4303-8c4a-f484dee4a0af/task-95 priority=MESSAGE_PRIORITY_HIGH content=Task id "c003839d-cffe-4303-8c4a-f484dee4a0af/task-95" finished with result:

				The command exited with code 0.
				Output:
				active
Terminal ID: 1821954

Log: file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/.system_generated/tasks/task-95.log
</SYSTEM_MESSAGE>


봇이 정상 기동됐습니다. 갭보정 수정이 실제로 효과가 있는지 로그로 확인합니다.

The bot restarted successfully (active). Let me check the logs to verify the gap fill fix is working properly.

cd /home/hoon/kis_bot && sleep 30 && journalctl -u kis_trader_main.service --since "$(date +'%Y-%m-%d %H:%M:%S' -d '40 seconds ago')" 2>&1 | grep -E "TF 실패|갭보정.*재시도|갭보정-롤업|강제 완료|ERROR|Traceback" | head -30

수정 후 갭보정 로그 확인


Created At: 2026-08-13T23:36:32+09:00
Tool is running as a background task with task id: c003839d-cffe-4303-8c4a-f484dee4a0af/task-99
Task Description: cd /home/hoon/kis_bot && sleep 30 && journalctl -u kis_trader_main.service --since "$(date +'%Y-%m-%d %H:%M:%S' -d '40 seconds ago')" 2>&1 | grep -E "TF 실패|갭보정.*재시도|갭보정-롤업|강제 완료|ERROR|Traceback" | head -30
Task logs are available at: file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/.system_generated/tasks/task-99.log


30초 대기 중입니다. 결과가 오면 확인하겠습니다.

---

## 적용 완료 — 변경사항 요약

### `ws_manager.py` — 갭보정 Fix 1+2

**Fix 1** ([L2370](file:///home/hoon/kis_bot/kis_trader/network/ws_manager.py#L2370)): `_gap_tf_already_ok` 내 1M 스티커 뜯기 조건 강화
```
Before: 499봉 미달이면 무조건 discard(1)
After:  3M/15M/60M 중 하나라도 미완성일 때만 discard(1)
        상위 TF 전부 ok → 1M 스티커 보존 (재시도 불필요)
```

**Fix 2** ([L2455](file:///home/hoon/kis_bot/kis_trader/network/ws_manager.py#L2455)): 15M/60M rollup 0봉 강제 완료
```
Before: rollup 0봉 → ok 마킹 안 함 → 다음 루프에서 REST 시도 → 빈응답 → abort → 재시도
After:  rollup 0봉이어도 1M ok 기반 강제 완료 처리
        (키움은 15M/60M REST 자체가 없으므로 REST 회귀 = abort 루프 보장)
```

### `order_manager.py` — 재매도 상한 ([L882](file:///home/hoon/kis_bot/kis_trader/execution/order_manager.py#L882))
```
긴급 재매도 3회 초과 시 잔량 포기 + 경고 로그
→ 모의서버 부분체결 연속 시 _sell_lock 장기 점유 방지
env: MAX_URGENT_ESCALATE_COUNT (기본 3)
```


The following is a <SYSTEM_MESSAGE> not actually sent by the user. It is provided by the system as important information to pay attention to.

<SYSTEM_MESSAGE>
[Message] timestamp=2026-08-13T14:37:03Z sender=c003839d-cffe-4303-8c4a-f484dee4a0af/task-99 priority=MESSAGE_PRIORITY_HIGH content=Task id "c003839d-cffe-4303-8c4a-f484dee4a0af/task-99" finished with result:

				The command exited with code 0.
				No output
Terminal ID: 1821954

Log: file:///home/hoon/.gemini/antigravity-ide/brain/c003839d-cffe-4303-8c4a-f484dee4a0af/.system_generated/tasks/task-99.log
</SYSTEM_MESSAGE>


출력이 없다는 건 **TF 실패 / 재시도 / ERROR 0건** 입니다. 지금은 장외 시간이라 갭보정이 동작 안 하니, 기동 직후 로그를 확인합니다.

No output means no TF 실패, no 갭보정 재시도, and no ERROR/Traceback in the 30 seconds after restart. This is a good sign - the gap fill retries are gone. But 30 seconds right after restart might not show gap fill activity yet (market is closed now at 23:37 KST). Let me check the startup logs to verify the rollup is working.

cd /home/hoon/kis_bot && journalctl -u kis_trader_main.service --since "23:35" 2>&1 | grep -E "TF 실패|갭보정.*재시도|갭보정-롤업|강제 완료|ERROR|Traceback|active_trades|heartbeat" | head -40


Created At: 2026-08-13T23:37:15+09:00
Completed At: 2026-08-13T23:37:17+09:00

				The command exited with code 0.
				Output:
				Aug 13 23:36:34 ubuntu2404 python[1887217]: [23:36:34] [kis_trader.main] ❤ heartbeat ws=IDLE rank[off] cond[off] kwcond[SCALP=0,SHORT=0,BREAKOUT=0,MOMENTUM=0] lscond[off] alive=['Strat-SCALP', 'Strat-SHORT', 'Strat-MOMENTUM', 'Strat-US_MOMENTUM', 'Strat-BREAKOUT'] dead=[]
Terminal ID: 1821954


cd /home/hoon/kis_bot && journalctl -u kis_trader_main.service --since "23:35:30" --until "23:36:40" 2>&1 | grep -E "갭보정|rollup|TF|gap|ERROR" | head -40

기동 구간 갭보정 로그 확인


Created At: 2026-08-13T23:37:21+09:00
Completed At: 2026-08-13T23:37:23+09:00

				The command exited with code 0.
				Output:
				Aug 13 23:36:22 ubuntu2404 python[1887217]: [23:36:22] [kis_trader.ws] ✅ 갭보정 워커 2개 시작 (queue 병렬, WS_GAP_FILL_WORKERS=2)
Aug 13 23:36:22 ubuntu2404 python[1887217]: [23:36:22] [kis_trader.ws] ✅ LS 갭보정 워커 시작 (t8412, workers=1)
Aug 13 23:36:22 ubuntu2404 python[1887217]: [23:36:22] [kis_trader.ws] ✅ WSManager 활성 (tfs=[1, 3], permanent=7, gap_workers=2)
Terminal ID: 1821954


지금은 장외 시간이라 갭보정이 트리거되지 않습니다 (`WS_GAP_FILL_OFF_HOURS=False` 기본값). 내일 장 시작 시 확인이 필요합니다.

한 가지 중요한 점이 눈에 띕니다:

```
WSManager 활성 (tfs=[1, 3], permanent=7, gap_workers=2)
```

**`tfs=[1, 3]`** — 현재 설정된 타임프레임이 **1M, 3M 두 개뿐**입니다.
15M, 60M은 `WS_TIMEFRAMES` env에 설정돼 있지 않습니다.

즉, **Fix 2의 15M/60M 강제 완료 로직은 현재 환경에선 트리거 자체가 안 됩니다.** Fix 1 (1M 스티커 보존)이 핵심 수정이 됩니다.

---

## 최종 정리

**오늘 이 대화에서 파악하고 적용한 것**:

| 구분 | 상태 |
|------|------|
| 갭보정 1M 스티커 조건 강화 (Fix 1) | ✅ 적용 완료 — 내일 장 시작 후 재시도 건수 확인 |
| 15M/60M rollup 강제 완료 (Fix 2) | ✅ 적용 완료 — 현재 tfs=[1,3]이라 해당없음, 향후 TF 추가 시 동작 |
| 재매도 횟수 상한 3회 (order_manager) | ✅ 적용 완료 — 모의서버 부분체결 무한락 방지 |
| MOMENTUM `_sell_lock` 근본 설계 | ⏸ 유보 — 실전에선 3~7초 수준, 장기 과제 |

내일 장 시작 후 `journalctl -u kis_trader_main.service | grep -E "TF 실패|갭보정.*재시도"` 로 재시도가 줄었는지 확인해 보세요.