**[절대 규칙 복창]** 🚨 단일 턴 내 원인 규명 원칙에 따라, 증권사 API의 구독 취소 에러(`not found`)와 웹소켓 재연결 빈도에 대한 오해를 즉각적으로 해소해드리는 규칙을 명심하고 작업했습니다.

사용자님, 전혀 걱정하실 필요 없는 아주 정상적인 방어 로직의 흔적들입니다! 두 가지 모두 명확하게 설명해 드리겠습니다.

### 1. 🙅♂ "구독 거부 (UNSUBSCRIBE ERROR - not found)" 란?
* **상황**: 조건식에서 특정 종목이 빠져나가면 봇이 증권사 서버에 "이 종목 이제 안 볼 거니까 실시간 시세 구독 끊어줘!"라고 요청(`구독 해제`)을 보냅니다.
* **이유**: 그런데 증권사 서버가 **"어? 너 이 종목 애초에 구독 안 되어 있었는데? (not found)"** 라며 취소를 거부한 것입니다.
* **결론**: 종목이 조건식에 들어왔다가 0.1초 만에 너무 빨리 나가버려서 아직 구독 신청도 안 들어갔는데 해제부터 요청했거나, 방금 웹소켓이 끊어졌다 붙으면서 서버 쪽에서 이미 초기화된 종목을 봇이 혹시 몰라서 한 번 더 확실하게 취소(청소)하려다 발생한 해프닝입니다. **전혀 문제없는 아주 안전하고 꼼꼼한 청소 과정**입니다.

### 2. 🔌 "웹소켓 커넥트 로그가 왜 자주 나오지?"
자주 나오는 게 아닙니다! 로그를 자세히 보시면 딱 **두 번** 나왔습니다.
1. **10:52:51**: 아까 제가 0초 딜레이 튜닝 패치를 적용하고 **봇을 강제로 하드 리스타트** 시켰을 때 최초로 접속한 로그입니다.
2. **10:55:05**: 한국투자증권(KIS) 서버 측에서 핑(Ping) 응답을 안 줘서 살짝 팅겼을 때, 봇이 멈추지 않고 **스스로 5초 만에 서버를 다시 멱살 잡고 끌고 와서 재연결(자동 복구)**을 성공시킨 로그입니다.

즉, 봇이 불안정한 게 아니라 **증권사 서버가 불안정해서 팅기더라도 봇이 안 죽고 불사조처럼 계속 자동 복구**를 해내고 있다는 뜻입니다. 안심하고 켜두셔도 됩니다!

**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Analyzing Log Entries**

I'm now diving into the user's log snippets, prioritizing the "Subscription Rejected" error and the WebSocket connection frequency. My approach involves extracting timestamps and matching logs to identify patterns. Initially, I will examine the log snippets to establish the context around the error and the WebSocket connection. I'll need to carefully compare the snippets to check the WebSocket connection frequency.

**Clarifying Subscription Errors**

I'm now zeroing in on the "Subscription Rejected" error to address the user's confusion. The log indicates an unsubscribe attempt for a nonexistent subscription. This appears to be the bot cleaning up, sending an unsubscribe request for a stock it never subscribed to in the first place, which is perfectly normal. It's a harmless occurrence. It explains "구독 거부" to the user.


<USER_REQUEST>
재시작후 제대롤 잘 돌고 있나 검증해주고 지금 로그가 백단이랑 같이 나와서 매수체크로그가 드문드문 나오는거냐? 왜이렇게 못믿겟지 근본원인 찾아봐
</USER_REQUEST>
<ADDITIONAL_METADATA>
The current local time is: 2026-08-10T11:34:15+09:00.

The user's current state is as follows:
Active Document: /home/hoon/kis_bot/kis_trader/strategies/base.py (LANGUAGE_PYTHON)
Cursor is on line: 1
Other open documents:
- /home/hoon/kis_bot/kis_trader/network/ws_manager.py (LANGUAGE_PYTHON)
- /home/hoon/kis_bot/docs/like_mcp.md/code_architecture.md (LANGUAGE_MARKDOWN)
- /home/hoon/kis_bot/scratch/disable_whipsaw.py (LANGUAGE_PYTHON)
- /home/hoon/kis_bot/kis_trader/strategies/base.py (LANGUAGE_PYTHON)
- /home/hoon/kis_bot/scratch/remove_cand_limits.py (LANGUAGE_PYTHON)
</ADDITIONAL_METADATA>


The following is an <EPHEMERAL_MESSAGE> not actually sent by the user. It is provided by the system as a set of reminders and general important information to pay attention to. Do NOT respond to this message, just act accordingly.

<EPHEMERAL_MESSAGE>
<planning_mode>
You're in planning mode. Exercise judgement on whether the user's last request warrants a plan and take action accordingly.
If the user's request warrants a plan then follow this workflow:
1. Research first before taking actions. Do not use your code editing tools or run modifying commands during this time.
2. Create an implementation plan artifact with `request_feedback = true` to request approval. Include any open questions for the user directly in the implementation plan. Don't use the ask_question tool to ask these questions. Stop calling tools and allow the user to review the plan and provide feedback or approval.
3. After receiving approval, begin execution. Create and update the task artifact as you work.
4. Verify any work that you have done.
5. Create a walkthrough artifact to present your work to the user. When writing artifacts, make sure to follow the artifact formatting guidelines and use github alerts to highlight critical sections.
If the user's request does NOT warrant a plan then continue your work WITHOUT making a plan or requesting user feedback.
</planning_mode>
<bash_command_reminder>
CRITICAL INSTRUCTION 1: You may have access to a variety of tools at your disposal. Some tools may be for a specific task such as 'view_file' (for viewing contents of a file). Others may be very broadly applicable such as the ability to run a command on a terminal. Always prioritize using the most specific tool you can for the task at hand. Here are some rules: (a) NEVER run cat inside a bash command to create a new file or append to an existing file. (b) ALWAYS use grep_search instead of running grep inside a bash command unless absolutely needed. (c) DO NOT use ls for listing, cat for viewing, grep for finding, sed for replacing. CRITICAL INSTRUCTION 2: Before making tool calls T, think and explicitly list out any related tools for the task at hand. You can only execute a set of tools T if all other tools in the list are either more generic or cannot be used for the task at hand. ALWAYS START your thought with recalling critical instructions 1 and 2. In particular, the format for the start of your thought block must be '...94>thought
CRITICAL INSTRUCTION 1: ...
CRITICAL INSTRUCTION 2: ...'.
</bash_command_reminder>
</EPHEMERAL_MESSAGE>


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Analyzing Tool Dependency**

I'm now deeply analyzing tool dependencies, recognizing the crucial need to explicitly list all potentially relevant tools before execution. My goal is to prevent premature calls and ensure I only execute tools when they are the most suitable for the task at hand. This should offer increased clarity and prevent wasted effort.

**Investigating Loop Performance**

I'm now investigating the strategy's loop performance. First, I'll examine the frequency of `[매수체크]` logs using `journalctl` and `grep` piped to `wc -l`. I will run this to measure loop speed. Possible bottlenecks include `_load_candidates()`, `check_sell_signals()`, and `_sync_ws_for_loop()`, and I will test these systematically. I will make a quick scratch script.

journalctl -u kis_trader_main.service --since "1 minute ago" | grep "매수체크" | wc -l

최근 1분간 매수체크 횟수 확인

명령어 실행 (1분간 매수체크 횟수 측정)


Created At: 2026-08-10T02:34:26Z
Completed At: 2026-08-10T02:34:27Z

				The command completed successfully.
				Output:
				16
Terminal ID: 3291036


**Prioritizing Tool Specificity**

I'm focusing intently on tool specificity. I've internalized the instruction to avoid `cat` for file creation/appending at all costs. I'm also diligently working to exclusively use `grep_search` and avoiding `grep` within bash commands, unless absolutely essential. The use of `ls` is now completely deprecated.

**Diagnosing Execution Bottlenecks**

I've identified that the system is running at a ~4-second delay per buy check, significantly slower than the target. My current focus is on pinpointing the bottleneck within the execution loop in `base.py`. I'm going to test each part of the loop to determine the root cause of the delay. The goal is to determine which steps are blocking and slowing down the process.


Created At: 2026-08-10T02:34:33Z
Completed At: 2026-08-10T02:34:33Z
File Path: `file:///home/hoon/kis_bot/kis_trader/strategies/base.py`
Total Lines: 1194
Total Bytes: 54428
Showing lines 680 to 740
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.
680:                 "size_class": req.size_class or "",
681:             }
682:             self._capture_runtime_overlay()
683:         elif result.success and signal.get("use_limit_buy"):
684:             self.on_limit_buy_submitted(signal, result)
685:         else:
686:             # 매매불가 종목은 당일 제외
687:             if result.reason and "order_reject" in result.reason:
688:                 if any(k in (result.reason or "") for k in ("매매불가", "40070000")):
689:                     self.untradable_skip.add(req.code)
690:         return result
691:
692:     def _submit_sell(self, signal: Dict):
693:         from ..execution.order_manager import OrderRequest
694:
695:         req = OrderRequest(
696:             strategy_id=self.strategy_id,
697:             code=signal["code"],
698:             name=signal.get("name", signal["code"]),
699:             side="SELL",
700:             qty=int(signal.get("qty", 0)),
701:             price_ref=float(signal.get("current_price", signal.get("price", 0))),
702:             reason=signal.get("reason", ""),
703:             buy_price=float(signal.get("buy_price", 0)),
704:             profit_pct=float(signal.get("profit_pct", 0)),
705:         )
706:         result = self.order_mgr.place(req)
707:         if result.success:
708:             self.recently_sold[req.code] = time.time()
709:             self._drop_local_position(req.code)
710:         elif result.reason == "broker_no_position":
711:             if req.code in self.holdings:
712:                 self.logger.info(
713:                     "🧹 [유령정리] %s %s — 로컬 holdings 제거 (%s)",
714:                     req.name, req.code, result.reason,
715:                 )
716:             self._drop_local_position(req.code)
717:         return result
718:
719:     # ------------------------------------------------------------------
720:     # 서브클래스 공통 헬퍼
721:     # ------------------------------------------------------------------
722:     def _session_time_bounds(self) -> Tuple[int, int]:
723:         """실매 **매도·EOD** 세션 (HHMM). ``{SID}_TIME_END``(매수 종료)와 별도.
724:
725:         종료 우선순위:
726:           - SHORT: ``TAIL_TIME_START`` / ``TAIL_TIME_END``
727:           - 그 외: ``{SID}_SELL_TIME_END`` → ``MARKET_SESSION_END_HM`` → 기본 **1530**
728:         """
729:         sid = self.strategy_id.upper()
730:         if sid == "SHORT":
731:             from ..engine.tail_env_keys import tail_market_time_hm
732:             return tail_market_time_hm()
733:         start = get_env_int(f"{sid}_TIME_START", 0) or get_env_int("TIME_START", 0) or 900
734:         sell_end = (
735:             get_env_int(f"{sid}_SELL_TIME_END", 0)
736:             or get_env_int("MARKET_SESSION_END_HM", 0)
737:             or 1530
738:         )
739:         return start, sell_end
740:
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.


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

Question: def _load_candidates


Created At: 2026-08-10T02:34:39Z
Completed At: 2026-08-10T02:34:39Z
{"File":"/home/hoon/kis_bot/kis_trader/strategies/base.py","LineNumber":981,"LineContent":"    def _load_candidates(self) -\u003e List[Dict]:"}


**Prioritizing Tool Specificity**

I'm focusing intently on tool specificity. I've internalized the instruction to avoid `cat` for file creation/appending at all costs. I'm also diligently working to exclusively use `grep_search` and avoiding `grep` within bash commands, unless absolutely essential. The use of `ls` is now completely deprecated.


Created At: 2026-08-10T02:34:46Z
Completed At: 2026-08-10T02:34:46Z
File Path: `file:///home/hoon/kis_bot/kis_trader/strategies/base.py`
Total Lines: 1194
Total Bytes: 54428
Showing lines 980 to 1030
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.
980:
981:     def _load_candidates(self) -> List[Dict]:
982:         """
983:         후보 로드 우선순위:
984:           1) universe_source == "ranking"           → VolumeRankManager
985:           2) universe_source == "condition"         → ConditionSearchManager (KIS REST)
986:           3) universe_source == "kiwoom_condition"    → KiwoomConditionSearchManager (키움 WS)
987:           4) universe_source == "ls_condition"       → LsConditionSearchManager (LS AFR)
988:
989:         ⚡ 운영 스위치는 **{SID}_UNIVERSE_SOURCE 하나** 만 바꾸면 됨.
990:           - condition         + CONDITION_{SID}_NAME/SEQ     → KIS 조건 (REST 폴링)
991:           - kiwoom_condition  + CONDITION_{SID}_NAME         → 키움 조건 (WS 실시간, seq 자동)
992:           - ls_condition      + CONDITION_{SID}_NAME/LS_NAME → LS 동명 조건 (AFR, 시세는 키움/한투)
993:           - ranking           → 거래량/거래대금 순위
994:           (키움 seq 를 고정하려면 CONDITION_{SID}_KIWOOM_SEQ 선택 설정)
995:         """
996:         key = f"{self.strategy_id}_UNIVERSE_SOURCE"
997:         default = self.DEFAULT_UNIVERSE_SOURCES.get(self.strategy_id, "ranking")
998:         current_src = (get_env_from_db(key, default) or default).strip().lower()
999:         if current_src not in (
1000:             "ranking", "condition", "kiwoom_condition", "ls_condition",
1001:         ):
1002:             current_src = default
1003:         if current_src != self.universe_source:
1004:             self.logger.info(
1005:                 "🔄 [유니버스 소스 전환] %s: %s → %s (재시작 없이 즉시 반영)",
1006:                 self.strategy_id, self.universe_source, current_src,
1007:             )
1008:             self.universe_source = current_src
1009:
1010:         # 소스명 → 매니저 매핑. primary(선택 소스) 먼저, 나머지는 폴백 순.
1011:         # ※ ls_condition 선택 시 키움/KIS 로 자동 폴백하지 않음 (의도 스위치 존중).
1012:         mgr_by_src = {
1013:             "ranking": self.ranking_mgr,
1014:             "condition": self.condition_mgr,
1015:             "kiwoom_condition": self.kiwoom_condition_mgr,
1016:             "ls_condition": self.ls_condition_mgr,
1017:         }
1018:         if self.universe_source == "ls_condition":
1019:             order = ["ls_condition"]
1020:         else:
1021:             order = [self.universe_source] + [
1022:                 s for s in ("condition", "kiwoom_condition", "ranking")
1023:                 if s != self.universe_source
1024:             ]
1025:
1026:         candidates: List[Dict] = []
1027:         for tag in order:
1028:             mgr = mgr_by_src.get(tag)
1029:             if mgr is None:
1030:                 continue
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.


**Prioritizing Tool Specificity**

I'm focusing intently on tool specificity. I've internalized the instruction to avoid `cat` for file creation/appending at all costs. I'm also diligently working to exclusively use `grep_search` and avoiding `grep` within bash commands, unless absolutely essential. The use of `ls` is now completely deprecated.

**Analyzing Execution Time**

I'm now analyzing the execution time of the `_load_candidates` method and the broader `while self._running:` loop. I've determined that the `_load_candidates()` method is fast because it pulls data from a cached dictionary, whether the source is Kiwoom or KIS REST API. I'm now turning to other aspects of the loop for potential time bottlenecks. I'm focusing on the explicit listing of related tools before execution, and seeking to optimize any related calls.


Created At: 2026-08-10T02:34:52Z
Completed At: 2026-08-10T02:34:52Z
File Path: `file:///home/hoon/kis_bot/kis_trader/strategies/base.py`
Total Lines: 1194
Total Bytes: 54428
Showing lines 360 to 430
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.
360:                     if sell_signals and get_env_bool("REAL_BALANCE_VERIFY_BEFORE_SELL", True):
361:                         self.order_mgr.prefetch_broker_holdings()
362:                     for sig in sell_signals:
363:                         self._submit_sell(sig)
364:
365:                 # ── [2] 후보 구독 동기화 (공유 WS) ────────────────
366:                 candidates = self._load_candidates()
367:                 # 중분 편입 시가 애매 가드용 — 후보 ENTER 시각(초) 기록
368:                 self._note_candidate_enters(candidates)
369:                 cand_codes = [c.get("code") for c in candidates if c.get("code")]
370:                 hold_codes = list(self.holdings.keys())
371:                 # KIS 최소 구독 모드: 후보=키움 WS, KIS=영구+보유 (WSManager.sync_targets_split)
372:                 # US_MOMENTUM 등은 _sync_ws_for_loop 오버라이드로 해외 WS 만 사용
373:                 self._sync_ws_for_loop(cand_codes, hold_codes)
374:
375:                 # ── [2b] 미체결 지정가 만료 취소 ───────────────────
376:                 self.manage_pending_orders()
377:
378:                 # ── [3] 매수 체크 (매수 종료 TIME_END 와 매도 세션 분리) ──
379:                 max_stocks = self._max_stocks()
380:                 active_cnt = len(self.holdings)
381:                 if candidates and active_cnt < max_stocks and self.check_buy_allowed():
382:                     self._scan_and_buy(candidates, max_stocks, active_cnt)
383:                 elif candidates and active_cnt >= max_stocks:
384:                     # 보유만석이면 _scan_and_buy 미진입 → 매수체크 로그가 안 나와 "멈춘 것"처럼 보임
385:                     now_m = time.time()
386:                     last_m = float(getattr(self, "_last_full_skip_log_ts", 0) or 0)
387:                     if now_m - last_m >= 60.0:
388:                         self._last_full_skip_log_ts = now_m
389:                         self.logger.info(
390:                             "🔍 [매수체크 스킵] 보유만석 %d/%d codes=%s",
391:                             active_cnt, max_stocks,
392:                             ",".join(list(self.holdings.keys())[:12]),
393:                         )
394:
395:                 # 고점·세션저점 등 런타임 오버레이 저장 (다음 루프 DB sync 시 max merge)
396:                 self._capture_runtime_overlay()
397:
398:                 time.sleep(self._scan_sleep("loop"))
399:
400:             except KeyboardInterrupt:
401:                 self._running = False
402:                 break
403:             except Exception as e:
404:                 self.logger.error("루프 예외: %s", e)
405:                 time.sleep(5)
406:
407:     def _sync_ws_for_loop(self, cand_codes: List[str], hold_codes: List[str]) -> None:
408:         """후보·보유 WS 구독 동기화 — 해외 전략은 오버라이드.
409:
410:         ``ls_condition``:
411:           - LS US3 = 틱·현재가 (히스토리와 한 묶음)
412:           - 키움 = 갭보정·분봉 (기존 잘 되는 경로)
413:         """
414:         ls_feed = str(getattr(self, "universe_source", "") or "") == "ls_condition"
415:         self.ws.sync_targets_split(
416:             self.strategy_id, cand_codes, hold_codes, ls_feed=ls_feed,
417:         )
418:
419:     def manage_pending_orders(self) -> None:
420:         """미체결 ATR 지정가 만료 시 취소 — 서브클래스에서 구현."""
421:         return None
422:
423:     def on_limit_buy_submitted(self, signal: Dict, result) -> None:
424:         """지정가 접수 성공 — 체결 전까지 holdings 미반영 (서브클래스)."""
425:         return None
426:
427:     def _resolve_buy_qty_live(
428:         self,
429:         curr_price: float,
430:         *,
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.


Question: def sync_targets


Created At: 2026-08-10T02:35:01Z
Completed At: 2026-08-10T02:35:01Z
No results found


Question: def set_candle_tick_codes

정규식 검색 (kiwoom_ws.py set_candle_tick_codes)


Created At: 2026-08-10T02:35:07Z
Completed At: 2026-08-10T02:35:07Z
{"File":"/home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py","LineNumber":559,"LineContent":"    def set_candle_tick_codes(self, codes: Optional[Set[str]]) -\u003e None:"}


파일 보기 (kiwoom_ws.py set_candle_tick_codes)


Created At: 2026-08-10T02:35:13Z
Completed At: 2026-08-10T02:35:13Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py`
Total Lines: 980
Total Bytes: 41365
Showing lines 550 to 600
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.
550:     def attach_candle_aggregator(self, agg: Any) -> None:
551:         """KIS ``CandleAggregator`` 연결 — 키움 0B 틱으로 분봉 RAM 집계.
552:
553:         ``set_candle_tick_codes()`` 로 후보 종목만 필터링하지 않으면
554:         구독된 모든 종목 틱이 집계기로 들어감 (검증 전용 모드에서는 필터 권장).
555:         """
556:         self._candle_agg = agg
557:         logger.info("✅ KiwoomWebSocket → CandleAggregator 연결")
558:
559:     def set_candle_tick_codes(self, codes: Optional[Set[str]]) -> None:
560:         """집계기로 보낼 종목 코드. None=전체 구독 종목, 비어있지 않은 Set=해당 코드만."""
561:         self._candle_agg_codes = codes
562:
563:     def attach_tick_recorder(self, recorder: Any) -> None:
564:         """TickRecorder 연결 — 키움 0B 체결 틱."""
565:         self._tick_recorder = recorder
566:         logger.info("✅ KiwoomWebSocket → TickRecorder 연결")
567:
568:     def attach_trigger_snapshot_recorder(self, recorder: Any) -> None:
569:         """TriggerSnapshotRecorder 연결 — 키움 0D/0w 스냅샷 DB."""
570:         self._trigger_snapshot_recorder = recorder
571:         logger.info("✅ KiwoomWebSocket → TriggerSnapshotRecorder 연결")
572:
573:     # ------------------------------------------------------------------
574:     # 내부: 메인 수신 루프
575:     # ------------------------------------------------------------------
576:     def _run_loop(self) -> None:
577:         while self._running:
578:             try:
579:                 self._connect_and_serve()
580:             except Exception as e:
581:                 logger.warning("키움 WS 루프 예외: %s", e)
582:             finally:
583:                 self._connected = False
584:                 self._authenticated = False
585:                 if self._running:
586:                     self._reconnect_with_backoff()
587:
588:     def _connect_and_serve(self) -> None:
589:         """단일 연결 수명. blocking. 끊기면 반환 → 호출자가 backoff 후 재호출."""
590:         if not self._ws_lib:
591:             return
592:
593:         # 토큰 발급
594:         token = self._get_kiwoom_token()
595:         if not token:
596:             logger.warning("⚠ 키움 토큰 발급 실패 → WS 연결 보류 (60s)")
597:             try:
598:                 from kis_trader.utils.ops_alert import ops_alert
599:                 ops_alert(
600:                     "token_kiwoom",
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.


Question: def sync_targets


Created At: 2026-08-10T02:35:20Z
Completed At: 2026-08-10T02:35:20Z
{"File":"/home/hoon/kis_bot/kis_trader/network/ws_manager.py","LineNumber":281,"LineContent":"    def sync_targets_split("}
{"File":"/home/hoon/kis_bot/kis_trader/network/ws_manager.py","LineNumber":740,"LineContent":"    def sync_targets(self, owner: str, codes: Iterable[str]) -\u003e None:"}


Created At: 2026-08-10T02:35:26Z
Completed At: 2026-08-10T02:35:26Z
File Path: `file:///home/hoon/kis_bot/kis_trader/network/ws_manager.py`
Total Lines: 1891
Total Bytes: 84258
Showing lines 740 to 800
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.
740:     def sync_targets(self, owner: str, codes: Iterable[str]) -> None:
741:         """
742:         한 전략의 관심 종목 목록을 통째로 동기화.
743:         - 기존 관심 종목 중 없어진 것은 unsubscribe
744:         - 새로 추가된 것은 subscribe
745:         """
746:         if self._split_feed_active:
747:             return
748:         new_set = {c for c in codes if c}
749:         with self._lock:
750:             cur = set(self._owner_codes.get(owner, set()))
751:
752:         for code in sorted(cur - new_set):
753:             self.unsubscribe(code, owner)
754:         for code in sorted(new_set - cur):
755:             self.subscribe(code, owner)
756:         self._sync_tick_record_codes()
757:
758:     def get_recent_ticks(self, code: str, limit: int = 100) -> list:
759:         """RAM 링버퍼 최근 체결 틱 (B안 봉 내 돌파 등)."""
760:         if self.tick_recorder:
761:             try:
762:                 return self.tick_recorder.get_recent_ticks(code, limit=limit)
763:             except Exception:
764:                 return []
765:         return []
766:
767:     # ------------------------------------------------------------------
768:     # 조회 헬퍼 (전략이 쓰는 API)
769:     # ------------------------------------------------------------------
770:     def get_price(self, code: str, max_age_sec: float = 5.0) -> Optional[dict]:
771:         # ls_condition 전략 코드 → LS WS 우선
772:         if self.is_ls_feed_code(code):
773:             ls_ws = self._get_ls_ws()
774:             if ls_ws is not None:
775:                 try:
776:                     p = ls_ws.get_price(code, max_age_sec=max_age_sec)
777:                     if p:
778:                         return p
779:                 except Exception:
780:                     pass
781:             else:
782:                 logger.debug("LS 피드 코드이나 LS WS 없음: %s", code)
783:         from ..utils.env import get_env_from_db
784:         is_kiwoom_main = (get_env_from_db("WS_PROVIDER", "") or "").strip().lower() == "kiwoom_main"
785:
786:         if is_kiwoom_main and self._kiwoom_ws:
787:             try:
788:                 p = self._kiwoom_ws.get_price(code, max_age_sec=max_age_sec)
789:                 if p:
790:                     return p
791:             except Exception:
792:                 pass
793:
794:         if self.ws_cache:
795:             try:
796:                 p = self.ws_cache.get_price(code, max_age_sec=max_age_sec)
797:                 if p:
798:                     return p
799:             except Exception:
800:                 pass
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.


Question: def subscribe


Created At: 2026-08-10T02:35:31Z
Completed At: 2026-08-10T02:35:31Z
{"File":"/home/hoon/kis_bot/kis_trader/network/ws_manager.py","LineNumber":697,"LineContent":"    def subscribe(self, code: str, owner: str) -\u003e None:"}


Created At: 2026-08-10T02:35:37Z
Completed At: 2026-08-10T02:35:37Z
File Path: `file:///home/hoon/kis_bot/kis_trader/network/ws_manager.py`
Total Lines: 1891
Total Bytes: 84258
Showing lines 695 to 730
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.
695:     # 구독 관리 (Reference Counting)
696:     # ------------------------------------------------------------------
697:     def subscribe(self, code: str, owner: str) -> None:
698:         """
699:         한 전략(owner)이 해당 종목에 관심 등록.
700:         ※ 갭보정은 **백그라운드 워커 큐**로 위임하여 전략 쓰레드를 블록하지 않음.
701:           (예전: 여기서 REST 4개 TF 동기 호출 → 매수 체크 2~3분 지연)
702:         """
703:         if self._split_feed_active:
704:             return
705:         if not code or not owner:
706:             return
707:         with self._lock:
708:             self._owner_codes[owner].add(code)
709:             first_ref = not self._code_refs[code]
710:             self._code_refs[code].add(owner)
711:
712:         if first_ref and self.ws_cache:
713:             self.ws_cache.subscribe(code)
714:             # 신규 구독 → 워커에게 갭보정 위임 (논블로킹) — 키움/KIS 경로
715:             self._enqueue_gap_fill(code)
716:         self._sync_tick_record_codes()
717:
718:     def unsubscribe(self, code: str, owner: str) -> None:
719:         """한 전략(owner)이 관심 해제. 다른 전략이 아직 들고 있으면 WS 는 유지."""
720:         if self._split_feed_active:
721:             return
722:         if not code or not owner:
723:             return
724:         with self._lock:
725:             self._owner_codes[owner].discard(code)
726:             if owner in self._code_refs.get(code, set()):
727:                 self._code_refs[code].discard(owner)
728:             still_refs = bool(self._code_refs.get(code))
729:             is_permanent = code in self._permanent_codes
730:
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.


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Analyzing Execution Bottlenecks**

I'm now zeroing in on a performance bottleneck. The journalctl logs are suggesting an unexpected delay in strategy execution, despite non-blocking operations and a low sleep interval in the database. I'm focusing on the interplay between database updates and application behavior. Specifically, I need to check how the database value is being picked up, and evaluated, by `base.py`. I'll verify if the expected sleep interval is being honored.


Created At: 2026-08-10T02:35:43Z
Completed At: 2026-08-10T02:35:43Z
File Path: `file:///home/hoon/kis_bot/kis_trader/strategies/base.py`
Total Lines: 1194
Total Bytes: 54428
Showing lines 240 to 270
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.
240:     # ------------------------------------------------------------------
241:     # 스캔 루프 sleep (env 핫리로드 — 재시작 없이 반영)
242:     # ------------------------------------------------------------------
243:     # REST 유량과는 별개의 "스캔 회전율" 조절. WS 따라가기 속도를 결정한다.
244:     # 전략별 클래스 기본값(loop_min_sleep 등)을 폴백으로 두고, env 가 있으면 우선.
245:     def _scan_sleep(self, kind: str) -> float:
246:         """kind: 'loop'(루프끝) | 'reject'(탈락) | 'buy_ok'(매수성공) | 'buy_fail'(주문실패)."""
247:         from ..utils.env import get_env_float
248:         if kind == "loop":
249:             lo = get_env_float("STRATEGY_LOOP_SLEEP_MIN", self.loop_min_sleep)
250:             hi = get_env_float("STRATEGY_LOOP_SLEEP_MAX", self.loop_max_sleep)
251:         elif kind == "reject":
252:             lo = get_env_float("SCAN_REJECT_SLEEP_MIN", 0.2)
253:             hi = get_env_float("SCAN_REJECT_SLEEP_MAX", 0.5)
254:         elif kind == "buy_ok":
255:             lo = get_env_float("SCAN_BUY_OK_SLEEP_MIN", 1.0)
256:             hi = get_env_float("SCAN_BUY_OK_SLEEP_MAX", 2.0)
257:         else:  # buy_fail
258:             lo = get_env_float("SCAN_BUY_FAIL_SLEEP_MIN", 0.3)
259:             hi = get_env_float("SCAN_BUY_FAIL_SLEEP_MAX", 0.8)
260:         lo = max(0.0, lo)
261:         hi = max(lo, hi)
262:         return random.uniform(lo, hi)
263:
264:     # ------------------------------------------------------------------
265:     # 하락매수(dip) 종목 제외 — 대형주에 떨어지는 칼날 잡기 방지
266:     # ------------------------------------------------------------------
267:     def is_dip_buy_excluded(self, code: str) -> bool:
268:         """
269:         하락매수 계열(UPDOW·SHORT) 이 매수하면 안 되는 종목인지 판정.
270:         - ``DIP_BUY_EXCLUDE_CODES`` : 콤마구분 종목코드 화이트리스트 제외 (기본 빈 값 → 무효)
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.


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Refining Tool Choice**

I'm now carefully considering tool alternatives before executing any. My focus is on ensuring the selected tools are not only suitable but also the most appropriate for the specific task. I have identified the tools needed, like `cat`, `grep`, and `sed`, along with a critical instruction to list all available tools and evaluate each to decide the final set. I'm also ensuring the loop should spin up fast, as I review the expected default behavior. I'm now verifying log behavior to ensure accuracy and frequency.

Question: def check_buy_allowed


Created At: 2026-08-10T02:35:51Z
Completed At: 2026-08-10T02:35:51Z
{"File":"/home/hoon/kis_bot/kis_trader/strategies/base.py","LineNumber":768,"LineContent":"    def check_buy_allowed(self) -\u003e bool:"}


Created At: 2026-08-10T02:35:59Z
Completed At: 2026-08-10T02:35:59Z
File Path: `file:///home/hoon/kis_bot/kis_trader/strategies/base.py`
Total Lines: 1194
Total Bytes: 54428
Showing lines 765 to 790
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.
765:         start, end = self._session_time_bounds()
766:         return start <= hhmm_now <= end
767:
768:     def check_buy_allowed(self) -> bool:
769:         """신규 매수 허용 시간 — ``{SID}_TIME_END`` / ``TIME_END`` 기준 (매수 종료).
770:
771:         EOD(``{SID}_EOD_HM``) 시각 이후에는 신규매수 금지.
772:         벽시계 비교라 익일 장중(EOD 전)에는 자동으로 다시 허용된다.
773:         전략에 ``eod_enabled`` 가 없거나 false 면 EOD 매수차단 없음
774:         (예: 해외모멘텀 기본 ``US_MOMENTUM_EOD_ENABLED=false``).
775:         """
776:         if get_env_bool("FORCE_MARKET_OPEN", False):
777:             return True
778:         if not self.check_market_status():
779:             return False
780:         now = dt.now()
781:         hhmm_now = now.hour * 100 + now.minute
782:         start, buy_end = self._buy_time_bounds()
783:         if not (start <= hhmm_now <= buy_end):
784:             return False
785:         # EOD 이후 신규매수 차단 — SCALP 15:25 청산 직후 재매수(003470) 재발 방지
786:         # 해외모멘텀은 check_buy_allowed 오버라이드 + eod 기본 OFF → 국장 EOD에 안 걸림
787:         if is_live_eod_now(
788:             bool(getattr(self, "eod_enabled", False)),
789:             str(getattr(self, "eod_hm", "15:20") or "15:20"),
790:             now,
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.


journalctl -u kis_trader_main.service --since "1 minute ago" | grep "BREAKOUT" | grep "매수체크"

BREAKOUT 전략 매수체크 로그 주기 확인

명령어 실행 (BREAKOUT 매수체크 로그 필터)


Created At: 2026-08-10T02:36:05Z
Completed At: 2026-08-10T02:36:06Z

				The command completed successfully.
				Output:
				Aug 10 11:36:02 ubuntu2404 python[3336514]: [11:36:02] [kis_trader.strategy.BREAKOUT] 🔍 [매수체크/시세:kiwoom_main] 후보 22 (보유 3/8)
Terminal ID: 3291036


The following is an <EPHEMERAL_MESSAGE> not actually sent by the user. It is provided by the system as a set of reminders and general important information to pay attention to. Do NOT respond to this message, just act accordingly.

<EPHEMERAL_MESSAGE>
<planning_mode>
You're in planning mode. Exercise judgement on whether the user's last request warrants a plan and take action accordingly.
If the user's request warrants a plan then follow this workflow:
1. Research first before taking actions. Do not use your code editing tools or run modifying commands during this time.
2. Create an implementation plan artifact with `request_feedback = true` to request approval. Include any open questions for the user directly in the implementation plan. Don't use the ask_question tool to ask these questions. Stop calling tools and allow the user to review the plan and provide feedback or approval.
3. After receiving approval, begin execution. Create and update the task artifact as you work.
4. Verify any work that you have done.
5. Create a walkthrough artifact to present your work to the user. When writing artifacts, make sure to follow the artifact formatting guidelines and use github alerts to highlight critical sections.
If the user's request does NOT warrant a plan then continue your work WITHOUT making a plan or requesting user feedback.
</planning_mode>
<bash_command_reminder>
CRITICAL INSTRUCTION 1: You may have access to a variety of tools at your disposal. Some tools may be for a specific task such as 'view_file' (for viewing contents of a file). Others may be very broadly applicable such as the ability to run a command on a terminal. Always prioritize using the most specific tool you can for the task at hand. Here are some rules: (a) NEVER run cat inside a bash command to create a new file or append to an existing file. (b) ALWAYS use grep_search instead of running grep inside a bash command unless absolutely needed. (c) DO NOT use ls for listing, cat for viewing, grep for finding, sed for replacing. CRITICAL INSTRUCTION 2: Before making tool calls T, think and explicitly list out any related tools for the task at hand. You can only execute a set of tools T if all other tools in the list are either more generic or cannot be used for the task at hand. ALWAYS START your thought with recalling critical instructions 1 and 2. In particular, the format for the start of your thought block must be '...94>thought
CRITICAL INSTRUCTION 1: ...
CRITICAL INSTRUCTION 2: ...'.
</bash_command_reminder>
</EPHEMERAL_MESSAGE>


Question: def check_buy


Created At: 2026-08-10T02:36:13Z
Completed At: 2026-08-10T02:36:13Z
{"File":"/home/hoon/kis_bot/kis_trader/strategies/breakout.py","LineNumber":577,"LineContent":"def check_buy_signal_breakout_intrabar_live("}
{"File":"/home/hoon/kis_bot/kis_trader/strategies/breakout.py","LineNumber":1161,"LineContent":"def check_buy_signal_breakout_live("}
{"File":"/home/hoon/kis_bot/kis_trader/strategies/breakout.py","LineNumber":1706,"LineContent":"    def check_buy(self, code: str, name: str) -\u003e Optional[Dict]:"}


Created At: 2026-08-10T02:36:19Z
Completed At: 2026-08-10T02:36:19Z
File Path: `file:///home/hoon/kis_bot/kis_trader/strategies/breakout.py`
Total Lines: 2133
Total Bytes: 102526
Showing lines 1700 to 1750
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.
1700:                 if p3 > 0:
1701:                     return p3
1702:         except Exception:
1703:             pass
1704:         return 0.0
1705:
1706:     def check_buy(self, code: str, name: str) -> Optional[Dict]:
1707:         # 골든타임 가드 (_candidate_filter 가 미리 거른 뒤에도 방어용 재검사)
1708:         if not self._is_golden_time():
1709:             return None
1710:
1711:         need_n = 1 if self.skip_hts_scan_dupes else max(self.lookback_min, self.vol_window) + 2
1712:         if self.use_ema_filter and not self.skip_hts_scan_dupes:
1713:             need_n = max(need_n, self.ema_slow_period + 5)
1714:         confirmed = self.ws.get_candles(code, self.candle_tf, n=need_n + 5)
1715:         if len(confirmed) < need_n - 1:
1716:             # force 연타 금지 — 시세불가·특수코드는 fail_max 후에도 REST 폭주
1717:             force_sec = max(30, get_env_int("BREAKOUT_CANDLE_GAP_FORCE_SEC", 120))
1718:             now_g = time.time()
1719:             if not hasattr(self, "_candle_gap_force_ts"):
1720:                 self._candle_gap_force_ts = {}
1721:             last_g = float(self._candle_gap_force_ts.get(code, 0) or 0)
1722:             do_force = (now_g - last_g) >= float(force_sec)
1723:             if do_force:
1724:                 self._candle_gap_force_ts[code] = now_g
1725:             try:
1726:                 self.ws.fill_gap([code], force=do_force)
1727:             except Exception:
1728:                 pass
1729:             log_sec = max(15, get_env_int("BREAKOUT_CANDLE_SHORT_LOG_SEC", 60))
1730:             if not hasattr(self, "_candle_short_log_ts"):
1731:                 self._candle_short_log_ts = {}
1732:             last_l = float(self._candle_short_log_ts.get(code, 0) or 0)
1733:             if now_g - last_l >= float(log_sec):
1734:                 self._candle_short_log_ts[code] = now_g
1735:                 self.logger.info(
1736:                     "🔍 [캔들부족] %s(%s) need>=%d have=%d",
1737:                     name, code, need_n - 1, len(confirmed),
1738:                 )
1739:             return None
1740:
1741:         params = {
1742:             "lookback_min":  self.lookback_min,
1743:             "vol_window":    self.vol_window,
1744:             "vol_mult":      self.vol_mult,
1745:             "prev_chg_min":  self.prev_chg_min,
1746:             "prev_chg_max":  self.prev_chg_max,
1747:             "max_daily_chg": self.max_daily_chg,
1748:             "min_price":     self.min_price,
1749:             "min_bar_trade_value_krw": self.min_bar_trade_value_krw,
1750:             "min_turnover_1m_pct": self.min_turnover_1m_pct,
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.


Question: def fill_gap


Created At: 2026-08-10T02:36:25Z
Completed At: 2026-08-10T02:36:25Z
{"File":"/home/hoon/kis_bot/kis_trader/network/ws_manager.py","LineNumber":920,"LineContent":"    def fill_gap("}


Created At: 2026-08-10T02:36:30Z
Completed At: 2026-08-10T02:36:30Z
File Path: `file:///home/hoon/kis_bot/kis_trader/network/ws_manager.py`
Total Lines: 1891
Total Bytes: 84258
Showing lines 920 to 950
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.
920:     def fill_gap(
921:         self,
922:         codes: Optional[Iterable[str]] = None,
923:         *,
924:         force: bool = False,
925:     ) -> None:
926:         """갭 보정 — 전부 키움/KIS 기존 경로 (LS t8412 안 씀).
927:
928:         force=True: 이미 ``_gap_filled`` 여도 재큐.
929:         """
930:         self._maybe_arm_session_gap_refill()
931:         if codes is None:
932:             self._trigger_bulk_refill_async()
933:             return
934:         for c in codes:
935:             self._enqueue_gap_fill(c, force=bool(force), priority=bool(force))
936:
937:     def _maybe_arm_session_gap_refill(self) -> None:
938:         """평일 장시작 세션 1회: 장외 거짓완료 마커 제거 + 구독 종목 bulk refill.
939:
940:         주말/장외에 ``_gap_filled`` 만 찍히면 개장 직후 REST 없이 have=0 레이스가 난다.
941:         """
942:         if not self._is_market_hours():
943:             return
944:         day = time.strftime("%Y%m%d")
945:         if self._gap_session_day == day:
946:             return
947:         self._gap_session_day = day
948:         logger.info(
949:             "🔄 [갭보정-장시작] 세션 %s 오픈 → 완료마커 클리어 + bulk refill",
950:             day,
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.


**Prioritizing Tool Specificity**

I'm focusing intently on tool specificity. I've internalized the instruction to avoid `cat` for file creation/appending at all costs. I'm also diligently working to exclusively use `grep_search` and avoiding `grep` within bash commands, unless absolutely essential. The use of `ls` is now completely deprecated.

**Analyzing Execution Stoppage**

I'm now deeply engaged in troubleshooting a specific execution stoppage. Initial hypothesis regarding a full stock portfolio was debunked by log analysis. The logs clearly show active_cnt is well below the max_stocks limit, so it's not a capacity issue. I'm moving toward an understanding of the issue with `_enqueue_gap_fill`, since it is non-blocking, and questioning why the log in question was only printed once in a minute.

journalctl -u kis_trader_main.service --since "1 minute ago" | grep "BREAKOUT"

최근 1분간 BREAKOUT 전략의 모든 로그 확인


Created At: 2026-08-10T02:36:39Z
Completed At: 2026-08-10T02:36:40Z

				The command completed successfully.
				Output:
				Aug 10 11:35:41 ubuntu2404 python[3336514]: [11:35:41] [kis_trader.cond] 🔄 [BREAKOUT] +1 / -0  (현재 22종목, push/pop)
Aug 10 11:35:41 ubuntu2404 python[3336514]: [11:35:41] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|탈락-직전봉약세] 저스템(417840) prev=-0.62% < 0.30%
Aug 10 11:35:46 ubuntu2404 python[3336514]: [11:35:46] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|탈락-직전봉약세] 에코프로머티(450080) prev=0.00% < 0.30%
Aug 10 11:35:46 ubuntu2404 python[3336514]: [11:35:46] [kis_trader.cond] 🔄 [BREAKOUT] +0 / -1  (현재 21종목, push/pop)
Aug 10 11:35:52 ubuntu2404 python[3336514]: [11:35:52] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|탈락-직전봉약세] 454910(454910) prev=-0.13% < 0.30%
Aug 10 11:35:53 ubuntu2404 python[3336514]: [11:35:53] [kis_trader.cond] 🔄 [BREAKOUT] +1 / -0  (현재 22종목, push/pop)
Aug 10 11:35:57 ubuntu2404 python[3336514]: [11:35:57] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|탈락-직전봉약세] 082740(082740) prev=-0.27% < 0.30%
Aug 10 11:35:59 ubuntu2404 python[3336514]: [11:35:59] [kis_trader.cond] 🔄 [BREAKOUT] +0 / -1  (현재 21종목, push/pop)
Aug 10 11:36:02 ubuntu2404 python[3336514]: [11:36:02] [kis_trader.strategy.BREAKOUT] 🔍 [매수체크/시세:kiwoom_main] 후보 22 (보유 3/8)
Aug 10 11:36:05 ubuntu2404 python[3336514]: [11:36:05] [kis_trader.cond] 🔄 [BREAKOUT] +1 / -0  (현재 22종목, push/pop)
Aug 10 11:36:06 ubuntu2404 python[3336514]: [11:36:06] [kis_trader.main] ❤ heartbeat ws=OPEN rank[off] cond[off] kwcond[SCALP=3,SHORT=1,BREAKOUT=22,MOMENTUM=2] lscond[off] alive=['Strat-SCALP', 'Strat-SHORT', 'Strat-MOMENTUM', 'Strat-US_MOMENTUM', 'Strat-BREAKOUT'] dead=[]
Aug 10 11:36:06 ubuntu2404 python[3336514]: [11:36:06] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|보유중] 비에이치(090460) 현재=15050 매수=15220 (-1.12%) 손절=14611 익절=16894 어깨=0 트레일=0 보유=88분 시간컷잔여=62분 [REST]
Aug 10 11:36:10 ubuntu2404 python[3336514]: [11:36:10] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|탈락-직전봉약세] 272210(272210) prev=-0.26% < 0.30%
Aug 10 11:36:15 ubuntu2404 python[3336514]: [11:36:15] [kis_trader.cond] 🔄 [BREAKOUT] +0 / -1  (현재 21종목, push/pop)
Aug 10 11:36:16 ubuntu2404 python[3336514]: [11:36:16] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|탈락-직전봉약세] 001440(001440) prev=0.00% < 0.30%
Aug 10 11:36:23 ubuntu2404 python[3336514]: [11:36:23] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|탈락-직전봉약세] 005090(005090) prev=-0.35% < 0.30%
Aug 10 11:36:25 ubuntu2404 python[3336514]: [11:36:25] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|보유중] 코리아써키트(007810) 현재=58000 매수=60100 (-3.49%) 손절=57696 익절=66711 어깨=0 트레일=0 보유=81분 시간컷잔여=69분 [REST]
Aug 10 11:36:25 ubuntu2404 python[3336514]: [11:36:25] [kis_trader.cond] 🔄 [BREAKOUT] +1 / -0  (현재 22종목, push/pop)
Aug 10 11:36:30 ubuntu2404 python[3336514]: [11:36:30] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|탈락-직전봉약세] 005290(005290) prev=0.12% < 0.30%
Aug 10 11:36:30 ubuntu2404 python[3336514]: [11:36:30] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|보유중] 파두(440110) 현재=60500 매수=60800 (-0.49%) 손절=58368 익절=67488 어깨=0 트레일=0 보유=26분 시간컷잔여=124분 [REST]
Aug 10 11:36:33 ubuntu2404 python[3336514]: [11:36:33] [kis_trader.strategy.BREAKOUT] 🔍 [kiwoom_main|탈락-직전봉약세] 006360(006360) prev=-0.29% < 0.30%
Aug 10 11:36:34 ubuntu2404 python[3336514]: [11:36:34] [kis_trader.cond] 🔄 [BREAKOUT] +0 / -1  (현재 21종목, push/pop)
Terminal ID: 3291036


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Refining Tool Choices**

I'm now prioritizing tool selection and explicitly listing related tools before execution, as a critical check. I'm focusing on `cat`, `grep`, and `sed` for viewing, finding, and replacing, respectively. The log data requires specific attention, I need to analyze `[매수체크/시세:kiwoom` events. I must verify the logic of tool selection before continuing.

매수체크 스로틀링 재확인

파일 보기 (base.py 매수체크 로그)


Created At: 2026-08-10T02:36:48Z
Completed At: 2026-08-10T02:36:49Z
File Path: `file:///home/hoon/kis_bot/kis_trader/strategies/base.py`
Total Lines: 1194
Total Bytes: 54428
Showing lines 540 to 580
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.
540:                     if now_ts - getattr(self, "_profit_halt_log_ts", 0.0) >= 60.0:
541:                         self._profit_halt_log_ts = now_ts
542:                         self.logger.info(
543:                             "⛔ [매수차단] %s (보유 %d, 후보 %d)",
544:                             halt_msg or "일일익절", active_cnt, len(candidates),
545:                         )
546:                     return
547:             except Exception as ex:
548:                 self.logger.debug("일일익절 가드 예외(매수 계속): %s", ex)
549:
550:         _codes = [
551:             str(c.get("code") or c.get("stk_cd") or "").strip()
552:             for c in (candidates or [])
553:         ]
554:         _codes = [c for c in _codes if c]
555:         if _codes and str(getattr(self, "strategy_id", "")).upper().startswith("US_"):
556:             self.logger.info(
557:                 "🔍 [매수체크/시세:%s] 후보 %d (보유 %d/%d) codes=%s",
558:                 self.db.get_merged_env_snapshot().get("WS_PROVIDER", "unknown"),
559:                 len(candidates), active_cnt, max_stocks,
560:                 ",".join(_codes[:12]),
561:             )
562:         else:
563:             self.logger.info(
564:                 "🔍 [매수체크/시세:%s] 후보 %d (보유 %d/%d)",
565:                 self.db.get_merged_env_snapshot().get("WS_PROVIDER", "unknown"),
566:                 len(candidates), active_cnt, max_stocks,
567:             )
568:         for c in candidates:
569:             if not self._running:
570:                 return
571:             code = c.get("code") or c.get("stk_cd", "")
572:             name = c.get("name") or c.get("stk_nm", code)
573:             if name == code or not str(name or "").strip():
574:                 try:
575:                     from ..utils.stock_name import resolve_stock_display_name
576:
577:                     name = resolve_stock_display_name(self.db, code, name)
578:                 except Exception:
579:                     name = code
580:             if not code or code in self.holdings:
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.


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Analyzing Buy Trigger Logic**

Okay, I'm now digging into the `_scan_and_buy` function, especially the buy trigger logic for the BREAKOUT strategy. I realize the logging is key to understanding the frequency of calls. My focus now is on what populates the 'candidates' variable. It seems the timing of this data holds the key. The logic hinges on whether there's data in 'candidates' from `kiwoom_condition`

**Unraveling Buy Trigger Logic**

I'm deep in the code now, focusing specifically on how the buy trigger within the `_scan_and_buy` function works, especially the BREAKOUT strategy. I now see the logging as key to understanding why the function is not being called at the expected frequency. It's clear that the 'candidates' variable is always populated with 22 items. However, I am now interested in understanding how `self.check_buy` is used, and why so many items in the candidate list are rejected via `탈락-직전봉약세`.

python3 -c 'from database import TradeDB; db=TradeDB(); print(db.get_merged_env_snapshot().get("SCAN_REJECT_SLEEP_MIN")); print(db.get_merged_env_snapshot().get("SCAN_REJECT_SLEEP_MAX"))'

명령어 실행 (DB 딜레이 설정 확인)


Created At: 2026-08-10T02:37:03Z
Completed At: 2026-08-10T02:37:05Z

				The command completed successfully.
				Output:
				[11:37:05] [TradeDB] 📌 target_candidates_history 테이블 확인/생성
[11:37:05] [TradeDB] 📌 ws_price_validation 테이블 확인/생성
[11:37:05] [TradeDB] 📌 ls_ws_ticks 테이블 확인/생성
[11:37:05] [TradeDB] 📌 ls_ws_candles 테이블 확인/생성
[11:37:05] [TradeDB] 📌 ls_ws_orderbook 테이블 확인/생성
[11:37:05] [TradeDB] 📌 kis_ws_orderbook 테이블 확인/생성
[11:37:05] [TradeDB] 📌 ls_ws_vi 테이블 확인/생성
[11:37:05] [TradeDB] 📌 ws_price_validation_ls 테이블 확인/생성
[11:37:05] [TradeDB] 📌 ls_universe_history 테이블 확인/생성
[11:37:05] [TradeDB] 📌 condition_job_events 테이블 확인/생성
[11:37:05] [TradeDB] 📌 stock_share_meta 테이블 확인/생성
[11:37:05] [TradeDB] 📊 DB 테이블 생성/확인 완료
[11:37:05] [TradeDB] ✅ TradeDB 초기화 완료: MariaDB 192.168.0.141:3306/kis_quant_db
0.0
0.0
Terminal ID: 3291036


**Prioritizing Tool Specificity**

I'm focusing intently on tool specificity. I've internalized the instruction to avoid `cat` for file creation/appending at all costs. I'm also diligently working to exclusively use `grep_search` and avoiding `grep` within bash commands, unless absolutely essential. The use of `ls` is now completely deprecated.

**Analyzing Execution Bottlenecks**

I'm now zeroing in on a puzzling 5-second delay. I've re-examined `breakout.py`, specifically the `fill_gap` function call and subsequent return of `None`. It should be evaluating the next candidate immediately, as `_scan_sleep` returns `0.0`. I'm going to determine if `_is_golden_time()` or `get_candles` are blocking, because something is causing this bottleneck. The use of `cat` is forbidden for file viewing. I will also be listing out all related tools before making calls.

Question: def get_candles


Created At: 2026-08-10T02:37:15Z
Completed At: 2026-08-10T02:37:15Z
{"File":"/home/hoon/kis_bot/kis_trader/network/ws_manager.py","LineNumber":898,"LineContent":"    def get_candles(self, code: str, tf: int, n: int = 50) -\u003e list:"}


Created At: 2026-08-10T02:37:21Z
Completed At: 2026-08-10T02:37:21Z
File Path: `file:///home/hoon/kis_bot/kis_trader/network/ws_manager.py`
Total Lines: 1891
Total Bytes: 84258
Showing lines 895 to 925
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.
895:                 pass
896:         return None
897:
898:     def get_candles(self, code: str, tf: int, n: int = 50) -> list:
899:         # ls_condition 도 분봉은 키움 갭·candle_agg (틱만 LS)
900:         if self.candle_agg:
901:             try:
902:                 return self.candle_agg.get_candles(code, tf, n)
903:             except Exception:
904:                 return []
905:         # CandleAggregator 없으면 DB 폴백
906:         try:
907:             return self.db.get_ws_candles(code, tf, limit=n, confirmed_only=True)
908:         except Exception:
909:             return []
910:
911:     def get_current_candle(self, code: str, tf: int) -> Optional[dict]:
912:         """진행 중 봉 (RAM, is_confirmed=0) — B안 거래량·양봉 판정용."""
913:         if self.candle_agg:
914:             try:
915:                 return self.candle_agg.get_current_candle(code, tf)
916:             except Exception:
917:                 return None
918:         return None
919:
920:     def fill_gap(
921:         self,
922:         codes: Optional[Iterable[str]] = None,
923:         *,
924:         force: bool = False,
925:     ) -> None:
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.


Question: def get_candles


Created At: 2026-08-10T02:37:27Z
Completed At: 2026-08-10T02:37:27Z
Encountered error in step execution: search path file:///home/hoon/kis_bot/kis_trader/ws/candle_agg.py does not exist


Question: def get_candles


Created At: 2026-08-10T02:37:33Z
Completed At: 2026-08-10T02:37:33Z
Encountered error in step execution: search path file:///home/hoon/kis_bot/kis_trader/ws/candle_aggregator.py does not exist


디렉토리 탐색 (ws/)


Created At: 2026-08-10T02:37:38Z
Completed At: 2026-08-10T02:37:38Z
{"name":"__init__.py","sizeBytes":"567"}
{"name":"__pycache__","isDir":true}
{"name":"kis_ws.py","sizeBytes":"118916"}
{"name":"kis_ws_overseas.py","sizeBytes":"25412"}
{"name":"kiwoom_ws.py","sizeBytes":"41365"}
{"name":"ls_ws.py","sizeBytes":"63799"}
{"name":"orderbook_cache.py","sizeBytes":"9666"}
{"name":"program_cache.py","sizeBytes":"4988"}
{"name":"tick_recorder.py","sizeBytes":"11915"}
{"name":"trigger_eval_recorder.py","sizeBytes":"10934"}
{"name":"trigger_snapshot_recorder.py","sizeBytes":"12546"}

Summary: This directory contains 1 subdirectories and 10 files.


Question: def get_candles


Created At: 2026-08-10T02:37:44Z
Completed At: 2026-08-10T02:37:45Z
{"File":"/home/hoon/kis_bot/kis_trader/ws/kis_ws.py","LineNumber":2127,"LineContent":"    def get_candles(self, code: str, tf: int, n: int = 10) -\u003e list:"}
{"File":"/home/hoon/kis_bot/kis_trader/ws/ls_ws.py","LineNumber":647,"LineContent":"    def get_candles(self, code: str, tf: int, n: int = 50) -\u003e list:"}
{"File":"/home/hoon/kis_bot/kis_trader/strategies/updown_feed.py","LineNumber":175,"LineContent":"    def get_candles(self, code: str, tf: int, n: int = 50) -\u003e list:"}
{"File":"/home/hoon/kis_bot/kis_trader/network/ws_manager.py","LineNumber":898,"LineContent":"    def get_candles(self, code: str, tf: int, n: int = 50) -\u003e list:"}


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Analyzing Database Queries**

I've discovered a critical performance bottleneck. The `BREAKOUT` strategy's buy-check loop is performing redundant MariaDB queries, specifically within `check_buy()`. This is happening because it calls `self.ws.get_candles()` which, in turn, hits the database via `self.db.get_ws_candles()`. The impact of this is magnified as it queries for each candidate and each iteration.

**Investigating Query Performance**

I'm now zeroing in on the performance impact of database queries, specifically related to the `get_candles` function. My initial calculation of 4.5 seconds per loop seems right. But the question is: is the `candle_agg` set? If it's set correctly, it should be using a RAM cache, which is instant. If it is NOT set, or if something is broken with the cache, that is the core issue to investigate. I've narrowed it down to `kis_ws.py`, line 2127, which defines `get_candles`, so I'll double-check all references to the `candle_agg` attribute within the `ws_manager.py` file.

kis_ws.py get_candles 동작 확인 (블로킹/DB 호출 여부)


Created At: 2026-08-10T02:37:57Z
Completed At: 2026-08-10T02:37:57Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2690
Total Bytes: 118916
Showing lines 2120 to 2150
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.
2120:
2121:     def get_prev_confirmed(self, code: str, tf: int) -> Optional[dict]:
2122:         """직전 확정봉 (최신에서 2번째). 패턴 확인용 (현재봉 - 1)."""
2123:         with self._lock:
2124:             buf = self._confirmed.get((code, tf))
2125:             return buf[-2] if buf and len(buf) >= 2 else None
2126:
2127:     def get_candles(self, code: str, tf: int, n: int = 10) -> list:
2128:         """최근 n개 확정 봉 리스트 반환 (오래된→최신 순)."""
2129:         with self._lock:
2130:             buf = self._confirmed.get((code, tf), [])
2131:             return list(buf[-n:])
2132:
2133:     def get_confirmed_count(self, code: str, tf: int) -> int:
2134:         """확정된 봉 수 (RSI 안정화 여부 확인용)."""
2135:         with self._lock:
2136:             return len(self._confirmed.get((code, tf), []))
2137:
2138:     def get_current_candle(self, code: str, tf: int) -> Optional[dict]:
2139:         """
2140:         현재 진행 중인 봉(미확정, is_confirmed=0) 반환.
2141:         RSI는 포함되지 않음 (확정 봉 기준으로만 계산).
2142:         """
2143:         with self._lock:
2144:             return dict(self._current.get((code, tf), {})) or None
2145:
2146:     def get_rsi(self, code: str, tf: int, period: int = 3) -> Optional[float]:
2147:         """
2148:         최신 확정 봉의 RSI(period) 값 반환.
2149:         period: 2, 3, 5 중 하나 (스캘핑 단타용 초단기 RSI)
2150:         """
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.


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Analyzing Execution Delays**

I'm now investigating a significant performance bottleneck. While `get_candles` is instant, `BREAKOUT` only logged a buy signal once in a minute. Looking at the user's `journalctl` logs, it seems the code spends its time on `탈락-직전봉약세` checks. It's likely this filtering process is where the delay lies.

**Investigating Threading Block**

I'm now zeroing in on a potential blocking issue related to the Global Interpreter Lock (GIL) in Python. The other strategies (SCALP, SHORT, MOMENTUM) could be the culprits. I suspect that a heavy synchronous operation, such as `pandas` dataframe operations or `requests.get()` without yielding, might be blocking other threads. I'm focusing on finding any synchronous network requests or potential sleep calls in the codebase, particularly in `kis_trader/strategies/base.py`. I noticed that scan sleeps are set to 0, though I'm still trying to figure out if there's any chance those are the issue.

파일 보기 (env.py 우선순위 확인)


Created At: 2026-08-10T02:38:10Z
Completed At: 2026-08-10T02:38:10Z
File Path: `file:///home/hoon/kis_bot/kis_trader/utils/env.py`
Total Lines: 160
Total Bytes: 5855
Showing lines 1 to 60
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.
1: """
2: kis_trader/utils/env.py — DB(config_* + env_config 병합) 우선 + os.environ 폴백 환경변수 헬퍼
3: ===========================================================================
4: 기존 kis_scalping_ver2 / kis_short_ver3 에 흩어져 있던 `get_env_*` 를 통합.
5: - DB get_merged_env_snapshot / get_strategy_env_dict → os.environ → default 순으로 조회.
6: - 하드코딩 금지 원칙에 맞춰 전 모듈에서 이 함수들만 사용하도록 한다.
7: """
8: from __future__ import annotations
9:
10: import logging
11: import os
12: import time
13: from typing import Any, Dict, Optional
14:
15: logger = logging.getLogger("kis_trader.env")
16:
17: # 순환 import 방지용 레이지 TradeDB 참조
18: _db_instance = None
19: # get_merged_env_snapshot() — TTL 내 재사용 (실매: 웹에서 MAX_DAILY 등 변경 즉시 반영)
20: _merged_env_cache: Optional[Dict[str, str]] = None
21: _merged_env_cache_ts: float = 0.0
22: # env 캐시 세대 카운터 — invalidate 시 +1. 파생 캐시(예: whipsaw 파라미터)가
23: # 이 값으로 무효화를 감지해 안전하게 재계산한다(값은 그대로, 재계산 시점만 동일).
24: _env_generation: int = 0
25:
26:
27: def _get_db():
28:     """TradeDB 싱글톤. 최초 호출 시 MariaDB 연결."""
29:     global _db_instance
30:     if _db_instance is None:
31:         try:
32:             from database import TradeDB  # 프로젝트 루트의 기존 모듈
33:             _db_instance = TradeDB()
34:         except Exception as e:
35:             logger.warning("TradeDB 초기화 실패(%s) → env_config 조회 불가 → os.environ 폴백", e)
36:             _db_instance = False  # 실패 기록 (None과 구분)
37:     return _db_instance or None
38:
39:
40: def set_db(db_obj) -> None:
41:     """외부에서 이미 만든 TradeDB 인스턴스를 재사용할 때 주입."""
42:     global _db_instance
43:     _db_instance = db_obj
44:
45:
46: def invalidate_merged_env_cache() -> None:
47:     """env/config 저장 후 스냅샷 캐시 무효화 (insert_env_snapshot 등)."""
48:     global _merged_env_cache, _merged_env_cache_ts, _env_generation
49:     _merged_env_cache = None
50:     _merged_env_cache_ts = 0.0
51:     _env_generation += 1
52:
53:
54: def _merged_env_cache_ttl_sec() -> float:
55:     """DB env 스냅샷 메모리 TTL(초). os.environ 만 사용(순환 import 방지)."""
56:     try:
57:         return max(0.0, float(os.environ.get("ENV_MERGED_CACHE_TTL_SEC", "60")))
58:     except (ValueError, TypeError):
59:         return 60.0
60:
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.


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Analyzing Sleep Logic**

I've traced the unexpected 5-second pauses to an exception handler within `base.py`. Specifically, a `time.sleep(5)` call inside a loop exception handler is causing these gaps. This directly impacts the `_scan_and_buy` function, leading to the observed delays. I'm focusing on why these exceptions are occurring. I'm prioritizing understanding what's causing those errors to then resolve this.

**Investigating Logging Discrepancies**

I'm now investigating a discrepancy in the logs. While I initially suspected "루프 예외" related delays, the logs from `11:35:41` to `11:36:34` don't contain that specific phrase. I've noted a correlation between the `[kis_trader.cond]` updates and the timing of `[탈락-]` log entries. These logs appear *before* and *after* the stock evaluations. I've realized that the loop is not necessarily throwing an exception, but it *only* prints a `[탈락-]` message under certain conditions within a specific test. I think I'm getting closer to solving this!

**Unveiling Candle Dependencies**

I've made a crucial breakthrough! The silent returns I was seeing are due to insufficient candle data. Specifically, if the WebSockets haven't yet retrieved enough candles (`len(confirmed) < need_n - 1`), `check_buy()` silently returns `None`! This is because the initial candle loading uses REST, and `fill_gap` fetches sequentially for each stock, which can take ~5 seconds. That explains the timing of the logs!

**Confirming Async Candle Fetch**

I've confirmed my hypothesis! The 5-second intervals perfectly align with the asynchronous candle retrieval. The bot processes a stock, prints a `[탈락-]` log, and then waits for the next set of candles to be delivered by the background `fill_gap` process. The rate limits introduce this 5-second delay. I can see the logs now: a `갭보정` message from the WebSockets, followed shortly by a REST `insert` or `update`. I now understand the logging spacing!

**Quantifying Background Delays**

I've quantified the background delay! It's clear now: a single stock's candle adjustment takes about 2 seconds. With 22 stocks in the queue, that translates to a total wait time of 44 seconds due to sequential processing in the `_enqueue_gap_fill` queue. The strategy silently ignores these stocks during this interval, only evaluating them when they are completed. I can now relate this to the timing of the [탈락-] logs.

_scan_and_buy 매수체크 로그 스로틀링 여부 재확인

파일 보기 (base.py 매수체크 로그 주변)


Created At: 2026-08-10T02:38:32Z
Completed At: 2026-08-10T02:38:32Z
File Path: `file:///home/hoon/kis_bot/kis_trader/strategies/base.py`
Total Lines: 1194
Total Bytes: 54428
Showing lines 520 to 565
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.
520:         # ── 시장 급락 서킷브레이커 가드 ────────────────────────────────
521:         # MarketGuard 가 PANIC 모드면 신규 매수 전면 차단.
522:         # 매도는 평소처럼 동작 (포지션 정리·손실 확대 방지).
523:         if self.market_guard is not None and self.market_guard.is_panic():
524:             now_ts = time.time()
525:             if now_ts - self._panic_log_ts >= 60:  # 분당 1회만 로그
526:                 self._panic_log_ts = now_ts
527:                 self.logger.warning(
528:                     "⛔ [매수차단] MarketGuard PANIC: %s (보유 %d, 후보 %d)",
529:                     self.market_guard.panic_reason(), active_cnt, len(candidates),
530:                 )
531:             return
532:
533:         guard = getattr(self, "daily_profit_halt", None)
534:         if guard is not None:
535:             try:
536:                 allowed, halt_msg = guard.buy_allowed(self.strategy_id)
537:                 if not allowed:
538:                     now_ts = time.time()
539:                     log_key = f"profit_halt_{self.strategy_id}"
540:                     if now_ts - getattr(self, "_profit_halt_log_ts", 0.0) >= 60.0:
541:                         self._profit_halt_log_ts = now_ts
542:                         self.logger.info(
543:                             "⛔ [매수차단] %s (보유 %d, 후보 %d)",
544:                             halt_msg or "일일익절", active_cnt, len(candidates),
545:                         )
546:                     return
547:             except Exception as ex:
548:                 self.logger.debug("일일익절 가드 예외(매수 계속): %s", ex)
549:
550:         _codes = [
551:             str(c.get("code") or c.get("stk_cd") or "").strip()
552:             for c in (candidates or [])
553:         ]
554:         _codes = [c for c in _codes if c]
555:         if _codes and str(getattr(self, "strategy_id", "")).upper().startswith("US_"):
556:             self.logger.info(
557:                 "🔍 [매수체크/시세:%s] 후보 %d (보유 %d/%d) codes=%s",
558:                 self.db.get_merged_env_snapshot().get("WS_PROVIDER", "unknown"),
559:                 len(candidates), active_cnt, max_stocks,
560:                 ",".join(_codes[:12]),
561:             )
562:         else:
563:             self.logger.info(
564:                 "🔍 [매수체크/시세:%s] 후보 %d (보유 %d/%d)",
565:                 self.db.get_merged_env_snapshot().get("WS_PROVIDER", "unknown"),
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.


The following is an <EPHEMERAL_MESSAGE> not actually sent by the user. It is provided by the system as a set of reminders and general important information to pay attention to. Do NOT respond to this message, just act accordingly.

<EPHEMERAL_MESSAGE>
<planning_mode>
You're in planning mode. Exercise judgement on whether the user's last request warrants a plan and take action accordingly.
If the user's request warrants a plan then follow this workflow:
1. Research first before taking actions. Do not use your code editing tools or run modifying commands during this time.
2. Create an implementation plan artifact with `request_feedback = true` to request approval. Include any open questions for the user directly in the implementation plan. Don't use the ask_question tool to ask these questions. Stop calling tools and allow the user to review the plan and provide feedback or approval.
3. After receiving approval, begin execution. Create and update the task artifact as you work.
4. Verify any work that you have done.
5. Create a walkthrough artifact to present your work to the user. When writing artifacts, make sure to follow the artifact formatting guidelines and use github alerts to highlight critical sections.
If the user's request does NOT warrant a plan then continue your work WITHOUT making a plan or requesting user feedback.
</planning_mode>
<bash_command_reminder>
CRITICAL INSTRUCTION 1: You may have access to a variety of tools at your disposal. Some tools may be for a specific task such as 'view_file' (for viewing contents of a file). Others may be very broadly applicable such as the ability to run a command on a terminal. Always prioritize using the most specific tool you can for the task at hand. Here are some rules: (a) NEVER run cat inside a bash command to create a new file or append to an existing file. (b) ALWAYS use grep_search instead of running grep inside a bash command unless absolutely needed. (c) DO NOT use ls for listing, cat for viewing, grep for finding, sed for replacing. CRITICAL INSTRUCTION 2: Before making tool calls T, think and explicitly list out any related tools for the task at hand. You can only execute a set of tools T if all other tools in the list are either more generic or cannot be used for the task at hand. ALWAYS START your thought with recalling critical instructions 1 and 2. In particular, the format for the start of your thought block must be '...94>thought
CRITICAL INSTRUCTION 1: ...
CRITICAL INSTRUCTION 2: ...'.
</bash_command_reminder>
</EPHEMERAL_MESSAGE>


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Refining Analysis Workflow**

I'm now zeroing in on workflow analysis. My focus is now on mapping out the tools related to a task *before* execution. I'm carefully avoiding 'cat', 'grep', or 'sed', etc., unless they are best. It seems my loop has a *significant* blocker – the `check_buy` function. I am digging into how long my loop takes to run, and the root cause of the delay, particularly line 1726.

**Investigating Data Retrieval Lag**

I'm now tracing the potential bottleneck in data retrieval. It seems `self.ws.fill_gap` is non-blocking, so the issue might lie in `self.ws.get_candles`. Specifically, I'm questioning whether the call on line 907 of `ws_manager.py` to `self.db.get_ws_candles` is the root cause of the delay. The `BREAKOUT` module may not have `self.ws.candle_agg` set correctly. My investigation is focused on how `candle_agg` is used in `ws_manager.py` relative to its definition in `kis_ws.py`.

Question: self.candle_agg =


Created At: 2026-08-10T02:38:43Z
Completed At: 2026-08-10T02:38:43Z
{"File":"/home/hoon/kis_bot/kis_trader/network/ws_manager.py","LineNumber":188,"LineContent":"            self.candle_agg = CandleAggregator(db=self.db, timeframes=tfs)"}
{"File":"/home/hoon/kis_bot/kis_trader/network/ws_manager.py","LineNumber":221,"LineContent":"                self.candle_agg = None"}
{"File":"/home/hoon/kis_bot/kis_trader/network/ws_manager.py","LineNumber":253,"LineContent":"            self.candle_agg = None"}


Created At: 2026-08-10T02:38:50Z
Completed At: 2026-08-10T02:38:50Z
File Path: `file:///home/hoon/kis_bot/kis_trader/network/ws_manager.py`
Total Lines: 1891
Total Bytes: 84258
Showing lines 215 to 260
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.
215:                     self.trigger_snapshot_recorder = None
216:
217:             ok = self.ws_cache.start()
218:             if not ok:
219:                 logger.warning("WS start() 실패 → 비활성")
220:                 self.ws_cache = None
221:                 self.candle_agg = None
222:                 self.tick_recorder = None
223:                 return False
224:
225:             # 갭보정 · 유통주식수 백그라운드 워커 (subscribe 논블로킹)
226:             self._load_share_cache_from_db()
227:             self._start_share_meta_worker()
228:             self._start_gap_worker()
229:             self._start_ls_gap_worker()
230:
231:             # 영구 구독 (KOSPI/KOSDAQ ETF 등)
232:             self._load_permanent_codes()
233:             for code in sorted(self._permanent_codes):
234:                 self.ws_cache.subscribe(code)
235:                 self._enqueue_gap_fill(code)
236:                 logger.info("📡 [영구구독] %s", code)
237:             with self._lock:
238:                 self._gap_refill_codes = set(self._permanent_codes)
239:
240:             # 연결 성공 후 자동 갭 보정 등록 (WS 재접속 시 전체 재갭보정)
241:             self.ws_cache.set_on_connected_callback(self._trigger_bulk_refill_async)
242:             self._reattach_all_price_listeners()
243:
244:             logger.info(
245:                 "✅ WSManager 활성 (tfs=%s, permanent=%d, gap_workers=%d)",
246:                 tfs, len(self._permanent_codes),
247:                 max(1, min(get_env_int("WS_GAP_FILL_WORKERS", 2), 4)),
248:             )
249:             return True
250:         except Exception as e:
251:             logger.error("WS 초기화 예외: %s", e)
252:             self.ws_cache = None
253:             self.candle_agg = None
254:             return False
255:
256:     def stop(self) -> None:
257:         try:
258:             if self.ws_cache:
259:                 self.ws_cache.stop(clear_subscriptions=True)
260:         except Exception as e:
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.


Question: class CandleAggregator


Created At: 2026-08-10T02:38:58Z
Completed At: 2026-08-10T02:38:59Z
{"File":"/home/hoon/kis_bot/kis_trader/ws/kis_ws.py","LineNumber":1137,"LineContent":"class CandleAggregator:"}


**Prioritizing Tool Specificity**

I'm focusing intently on tool specificity. I've internalized the instruction to avoid `cat` for file creation/appending at all costs. I'm also diligently working to exclusively use `grep_search` and avoiding `grep` within bash commands, unless absolutely essential. The use of `ls` is now completely deprecated.

**Analyzing Tool Applicability**

I'm now deeply engaged in a thorough analysis of tool applicability. I've consciously expanded the earlier instruction, and am now making an explicit list of relevant tools *before* executing any set of them. This is to ensure the absolute most appropriate tool is chosen. I've just located `get_candles` within `CandleAggregator` in `kis_ws.py`, and confirmed this is purely a memory read. I am diligently avoiding any inappropriate tool calls.

**Investigating Log Inconsistencies**

I'm now investigating a puzzling log inconsistency. I've tracked the execution time of individual functions, and they're all incredibly fast; however, the `[매수체크]` log appears only once per minute. I've re-examined the code, specifically line 383 of `base.py`, and have found the issue. The code skips `_scan_and_buy` if the stock holdings are at maximum capacity, which suppresses the log.

**Unraveling Log Discrepancy**

I'm now unraveling a persistent log discrepancy, focusing on the timing of the `[매수체크]` messages. I've re-examined the control flow, specifically the interaction between `check_buy_allowed()` and the logging within `_scan_and_buy`. It seems a single candidate, code 22, is processed, but subsequent log updates occur at intervals, suggesting a timing issue that I still need to resolve, which is not due to skipping execution.

**Re-Examining Function Behavior**

I'm re-examining `_scan_and_buy`, suspecting a rate limiter might be suppressing the `[매수체크]` log. I've used `view_file` to review lines 520-580 of `kis_trader/strategies/base.py`, specifically lines 555-567 which were already reviewed. I will need to look deeper into `self.db.get_merged_env_snapshot()`.

Question: def get_merged_env_snapshot


Created At: 2026-08-10T02:39:15Z
Completed At: 2026-08-10T02:39:15Z
{"File":"/home/hoon/kis_bot/database.py","LineNumber":4103,"LineContent":"    def get_merged_env_snapshot(self) -\u003e Dict[str, str]:"}
{"File":"/home/hoon/kis_bot/database.py","LineNumber":4175,"LineContent":"    def get_merged_env_snapshot_as_of(self, as_of: str) -\u003e Dict[str, str]:"}


파일 보기 (database.py get_merged_env_snapshot)


Created At: 2026-08-10T02:39:20Z
Completed At: 2026-08-10T02:39:20Z
File Path: `file:///home/hoon/kis_bot/database.py`
Total Lines: 5187
Total Bytes: 241126
Showing lines 4100 to 4130
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.
4100:             logger.info("📌 env_config_ext overflow 저장 %d키", saved)
4101:         return saved
4102:
4103:     def get_merged_env_snapshot(self) -> Dict[str, str]:
4104:         """env_config + config_* 최신 행 병합 flat dict (get_env_from_db 호환)."""
4105:         merged: Dict[str, str] = {}
4106:         for tbl, keys in CONFIG_TABLE_KEYS.items():
4107:             merged.update(self._read_latest_config_table(tbl, keys))
4108:         # 레거시 env_config(381컬럼) 에만 있는 값 — 신규 테이블에 없을 때 폴백
4109:         try:
4110:             row = self.conn.execute(
4111:                 "SELECT * FROM env_config ORDER BY id DESC LIMIT 1"
4112:             ).fetchone()
4113:             if row:
4114:                 rk = row.keys() if hasattr(row, "keys") else []
4115:                 for k in ENV_CONFIG_KEYS:
4116:                     if k not in merged and k in rk:
4117:                         v = row[k]
4118:                         if v is not None and str(v).strip() != "":
4119:                             merged[k] = str(v)
4120:         except Exception:
4121:             pass
4122:         # env_config_ext — 컬럼 한도 초과 키 + 레거시 overflow
4123:         try:
4124:             ext_rows = self.conn.execute(
4125:                 "SELECT env_key, env_value FROM env_config_ext"
4126:             ).fetchall()
4127:             env_cols = self._env_config_column_set()
4128:             for er in ext_rows or []:
4129:                 ek = er["env_key"] if isinstance(er, dict) else er[0]
4130:                 ev = er["env_value"] if isinstance(er, dict) else er[1]
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.


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".

**Analyzing Database Efficiency**

I've hit a performance bottleneck, realizing `get_merged_env_snapshot` in `database.py` is making a ton of MariaDB calls without caching. That's a minimum of 12 SELECT queries. It's called directly in the loop, where logging occurs. It's time to investigate how frequently `_scan_and_buy` runs. I need to quantify the impact on performance.

**Investigating Performance Bottleneck**

I realize that `_scan_and_buy` *is* running, and logging, but only once a minute. I need to understand why. It seems to be related to the `check_buy_allowed()` call, specifically in `kis_trader/strategies/base.py` line 383, which may be limiting the call. The earlier calculations about the number of queries is not correct. I need to understand what's in place.

Question: AntiSpam


Created At: 2026-08-10T02:39:33Z
Completed At: 2026-08-10T02:39:33Z
No results found


**Prioritizing Tool Specificity**

I'm focusing intently on tool specificity. I've internalized the instruction to avoid `cat` for file creation/appending at all costs. I'm also diligently working to exclusively use `grep_search` and avoiding `grep` within bash commands, unless absolutely essential. The use of `ls` is now completely deprecated.

**Analyzing Rate Limiting**

I'm now deeply immersed in analyzing potential rate limiting issues. I've been tracing the execution path of `_scan_and_buy`, starting with the absence of an obvious anti-spam filter in `logger.py`. I've revisited `base.py` line 560, and confirmed the lack of a time-based rate limit for the `[매수체크]` log. The `time.sleep` call within the main loop is 0.0s, which is a key observation. This raises serious questions about the expected logging behavior. I am investigating the `_load_candidates()` call now to see if it's the blocking operation I need. I'm also ensuring that I only use tools I need, and not those I don't.

Question: def get_candidates


Created At: 2026-08-10T02:39:41Z
Completed At: 2026-08-10T02:39:41Z
Encountered error in step execution: search path file:///home/hoon/kis_bot/kis_trader/cond/kiwoom_cond.py does not exist


Created At: 2026-08-10T02:39:47Z
Completed At: 2026-08-10T02:39:47Z
{"name":"README.md","sizeBytes":"7742"}
{"name":"__init__.py","sizeBytes":"94"}
{"name":"__pycache__","isDir":true}
{"name":"backtest","isDir":true}
{"name":"data","isDir":true}
{"name":"database","isDir":true}
{"name":"engine","isDir":true}
{"name":"execution","isDir":true}
{"name":"main.py","sizeBytes":"126599"}
{"name":"network","isDir":true}
{"name":"output.log","sizeBytes":"46398"}
{"name":"output2.log","sizeBytes":"26487"}
{"name":"output3.log","sizeBytes":"25483"}
{"name":"scan","isDir":true}
{"name":"scripts","isDir":true}
{"name":"share","isDir":true}
{"name":"strategies","isDir":true}
{"name":"utils","isDir":true}
{"name":"web","isDir":true}
{"name":"ws","isDir":true}

Summary: This directory contains 14 subdirectories and 6 files.


Question: def get_candidates

정규식 검색 (kis_trader/engine/ get_candidates)


Created At: 2026-08-10T02:39:52Z
Completed At: 2026-08-10T02:39:53Z
No results found


Question: def get_candidates

정규식 검색 (kis_trader/scan/ get_candidates)


Created At: 2026-08-10T02:40:00Z
Completed At: 2026-08-10T02:40:00Z
No results found


Question: class KiwoomConditionSearchManager

정규식 검색 (kis_trader 내 KiwoomConditionSearchManager)


Created At: 2026-08-10T02:40:06Z
Completed At: 2026-08-10T02:40:07Z
{"File":"/home/hoon/kis_bot/kis_trader/network/kiwoom_condition_manager.py","LineNumber":87,"LineContent":"class KiwoomConditionSearchManager(ConditionSearchManager):"}


Question: def get_candidates

정규식 검색 (kiwoom_condition_manager get_candidates)


Created At: 2026-08-10T02:40:16Z
Completed At: 2026-08-10T02:40:16Z
No results found


Question: def get_candidates

정규식 검색 (kis_trader/network/ get_candidates)


Created At: 2026-08-10T02:40:22Z
Completed At: 2026-08-10T02:40:22Z
{"File":"/home/hoon/kis_bot/kis_trader/network/ranking_manager.py","LineNumber":161,"LineContent":"    def get_candidates_for(self, strategy_id: str) -\u003e List[Dict]:"}
{"File":"/home/hoon/kis_bot/kis_trader/network/ranking_manager.py","LineNumber":190,"LineContent":"    def get_candidates(self) -\u003e List[Dict]:"}
{"File":"/home/hoon/kis_bot/kis_trader/network/condition_manager.py","LineNumber":182,"LineContent":"    def get_candidates_for(self, strategy_id: str) -\u003e List[Dict]:"}
{"File":"/home/hoon/kis_bot/kis_trader/network/condition_manager.py","LineNumber":213,"LineContent":"    def get_candidates(self) -\u003e List[Dict]:"}


**Prioritizing Tool Specificity**

I'm focusing intently on tool specificity. I've internalized the instruction to avoid `cat` for file creation/appending at all costs. I'm also diligently working to exclusively use `grep_search` and avoiding `grep` within bash commands, unless absolutely essential. The use of `ls` is now completely deprecated.

**Analyzing Bottlenecks Meticulously**

I've homed in on a potential 5-second bottleneck, initially suspected in `get_candidates_for`, but quickly ruled out due to caching. Now, my focus is on `breakout.py` and the `fill_gap` function call. I am carefully assessing the websocket integration, scrutinizing the call in `breakout.py`, and tracing its execution through `ws_manager.py` to identify any blocking operations or potential exceptions that might be causing the delay. I'm verifying the websocket object, and re-evaluating the exception handling.

**Investigating the Logging Discrepancy**

I've hit a puzzling issue where logging frequency doesn't match the expected execution rate. I've pinpointed a rate limiter on the `[매수체크]` log within `kis_trader/strategies/base.py`. Specifically, line 386 shows a deliberate skip log triggered when the stock count is maxed out. This perfectly explains the observed discrepancy, and offers a resolution, the need to increase the `max_stocks` config.

base.py 매수체크 로그 스로틀링 유무 완벽 확인


Created At: 2026-08-10T02:40:34Z
Completed At: 2026-08-10T02:40:35Z
File Path: `file:///home/hoon/kis_bot/kis_trader/strategies/base.py`
Total Lines: 1194
Total Bytes: 54428
Showing lines 540 to 580
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.
540:                     if now_ts - getattr(self, "_profit_halt_log_ts", 0.0) >= 60.0:
541:                         self._profit_halt_log_ts = now_ts
542:                         self.logger.info(
543:                             "⛔ [매수차단] %s (보유 %d, 후보 %d)",
544:                             halt_msg or "일일익절", active_cnt, len(candidates),
545:                         )
546:                     return
547:             except Exception as ex:
548:                 self.logger.debug("일일익절 가드 예외(매수 계속): %s", ex)
549:
550:         _codes = [
551:             str(c.get("code") or c.get("stk_cd") or "").strip()
552:             for c in (candidates or [])
553:         ]
554:         _codes = [c for c in _codes if c]
555:         if _codes and str(getattr(self, "strategy_id", "")).upper().startswith("US_"):
556:             self.logger.info(
557:                 "🔍 [매수체크/시세:%s] 후보 %d (보유 %d/%d) codes=%s",
558:                 self.db.get_merged_env_snapshot().get("WS_PROVIDER", "unknown"),
559:                 len(candidates), active_cnt, max_stocks,
560:                 ",".join(_codes[:12]),
561:             )
562:         else:
563:             self.logger.info(
564:                 "🔍 [매수체크/시세:%s] 후보 %d (보유 %d/%d)",
565:                 self.db.get_merged_env_snapshot().get("WS_PROVIDER", "unknown"),
566:                 len(candidates), active_cnt, max_stocks,
567:             )
568:         for c in candidates:
569:             if not self._running:
570:                 return
571:             code = c.get("code") or c.get("stk_cd", "")
572:             name = c.get("name") or c.get("stk_nm", code)
573:             if name == code or not str(name or "").strip():
574:                 try:
575:                     from ..utils.stock_name import resolve_stock_display_name
576:
577:                     name = resolve_stock_display_name(self.db, code, name)
578:                 except Exception:
579:                     name = code
580:             if not code or code in self.holdings:
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.


**Prioritizing Tool Specificity**

I'm focusing intently on tool specificity. I've internalized the instruction to avoid `cat` for file creation/appending at all costs. I'm also diligently working to exclusively use `grep_search` and avoiding `grep` within bash commands, unless absolutely essential. The use of `ls` is now completely deprecated.

**Analyzing Execution Frequency**

I'm now deep-diving into the execution frequency of the `_scan_and_buy` function. I've pinpointed a crucial log statement and determined that it prints only once every 60 seconds. This is directly linked to the 60-second completion time of the `while self._running:` loop in `base.py`. I'm starting to understand this critical, and potentially undesired, rate-limiting behavior. I am focusing on what takes 60 seconds within `base.py`.

**Investigating Blocking Operations**

I'm now zeroing in on the potential culprits behind the 60-second delay. Since `_scan_and_buy` appears fast, I'm suspecting `self._sync_ws_for_loop`. Although `sync_targets_split` seemed non-blocking, I'm now focused on the possibility that `self._kiwoom_ws.sync_targets()` inside *it* might be the bottleneck. I'm carefully reviewing the call stack, specifically `WSManager.sync_targets` and `self.subscribe` to trace possible blocking operations. I have yet to find the smoking gun.

Question: def subscribe


Created At: 2026-08-10T02:40:47Z
Completed At: 2026-08-10T02:40:47Z
{"File":"/home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py","LineNumber":378,"LineContent":"    def subscribe_many(self, codes: Iterable[str]) -\u003e List[str]:"}
{"File":"/home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py","LineNumber":401,"LineContent":"    def subscribe(self, code: str) -\u003e bool:"}
{"File":"/home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py","LineNumber":546,"LineContent":"    def subscribed_count(self) -\u003e int:"}


**Prioritizing Tool Specificity**

I'm focusing intently on tool selection to streamline my approach. I'm actively avoiding 'cat' for file manipulation, recognizing its inefficiency. Also, grep_search is the go-to alternative to embedded grep, except when it is absolutely required. Listing is being approached with a new and better perspective - I'm no longer reaching for "ls".


Created At: 2026-08-10T02:40:53Z
Completed At: 2026-08-10T02:40:53Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py`
Total Lines: 980
Total Bytes: 41365
Showing lines 401 to 440
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.
401:     def subscribe(self, code: str) -> bool:
402:         """단일 종목 등록. REG 는 짧게 디바운스 후 묶어 전송."""
403:         code = (code or "").strip()
404:         if not code:
405:             return False
406:         with self._sub_lock:
407:             if code in self._subscribed:
408:                 return True
409:             if len(self._subscribed) >= self._max_subscriptions():
410:                 logger.warning(
411:                     "⚠ 키움 WS 구독 한도 초과 (%d/%d) — %s 등록 거절",
412:                     len(self._subscribed), self._max_subscriptions(), code,
413:                 )
414:                 return False
415:             self._subscribed.add(code)
416:             # LOGIN 전에는 집합만 쌓고, REG 는 LOGIN OK 후 일괄 전송(중복·순서 레이스 방지)
417:             need_reg = self._connected and self._authenticated
418:             if need_reg:
419:                 self._reg_batch_codes.add(code)
420:         if need_reg:
421:             self._schedule_reg_debounce()
422:         return True
423:
424:     def _schedule_reg_debounce(self) -> None:
425:         deb = self._reg_debounce_sec()
426:         if deb <= 0:
427:             with self._sub_lock:
428:                 pending = sorted(self._reg_batch_codes)
429:                 self._reg_batch_codes.clear()
430:             if pending:
431:                 self._send_reg_chunked(pending)
432:             return
433:         with self._reg_timer_lock:
434:             if self._reg_timer:
435:                 try:
436:                     self._reg_timer.cancel()
437:                 except Exception:
438:                     pass
439:             self._reg_timer = threading.Timer(deb, self._flush_reg_debounced)
440:             self._reg_timer.daemon = True
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.