Created At: 2026-08-11T10:35:15Z
Completed At: 2026-08-11T10:35:15Z
File Path: `file:///home/hoon/kis_bot/kis_trader/main.py`
Total Lines: 2749
Total Bytes: 126718
Showing lines 1110 to 1150
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.
1110:         if not app_key or not app_secret:
1111:             logger.warning(
1112:                 "LS AppKey/Secret 미설정 → LS WS 비활성 "
1113:                 "(LS_APP_KEY_REAL / LS_APP_SECRET_REAL)"
1114:             )
1115:             return
1116:
1117:         try:
1118:             from .ws.ls_ws import LSWebSocketPriceCache
1119:             also_hoga = get_env_bool("LS_WS_ALSO_HOGA", True)
1120:             self.ls_ws = LSWebSocketPriceCache(
1121:                 app_key, app_secret, is_mock=is_mock, also_hoga=also_hoga,
1122:             )
1123:             if not self.ls_ws.start():
1124:                 logger.warning("LS WS start 실패")
1125:                 self.ls_ws = None
1126:                 return
1127:
1128:             def _on_tick(code: str, payload: dict) -> None:
1129:                 # 1) LS 전용 테이블 (백테 history_source=ls → ls_ws_ticks)
1130:                 self.db.insert_ls_ws_tick(
1131:                     code=code,
1132:                     ts=payload.get("ts"),
1133:                     price=float(payload.get("price") or 0),
1134:                     volume=payload.get("volume"),
1135:                     tot_volume=payload.get("tot_volume"),
1136:                     chetime=str(payload.get("chetime") or ""),
1137:                     tr_cd=str(payload.get("tr_cd") or ""),
1138:                 )
1139:                 # 1b) 호가 틱동기 — 체결 1건당 RAM 호가 1장 (스냅 없으면 생략=실매와 동일)
1140:                 try:
1141:                     self._maybe_save_ls_orderbook_on_tick(code, payload)
1142:                 except Exception:
1143:                     pass
1144:                 # 2) 실매 get_recent_ticks 용 RAM 만 (기본).
1145:                 #    ws_ticks 이중 INSERT 는 용량 낭비 → LS_WS_TICK_MIRROR_WS_TICKS=true 때만.
1146:                 tr = getattr(self.ws, "tick_recorder", None)
1147:                 if tr is not None:
1148:                     try:
1149:                         px = float(payload.get("price") or 0)
1150:                         vol = int(float(payload.get("volume") or 0))
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>


Created At: 2026-08-11T10:35:19Z
Completed At: 2026-08-11T10:35:19Z
File Path: `file:///home/hoon/kis_bot/kis_trader/main.py`
Total Lines: 2749
Total Bytes: 126718
Showing lines 930 to 970
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.
930:             logger.info("ℹ condition 소스 쓰는 전략 없음 → 조건검색 매니저 비활성")
931:
932:         # ── 키움 조건검색: 시세 WS 기동 후 _start_kiwoom_condition_manager() 에서 등록 ──
933:         self._pending_kiwoom_condition_configs = kiwoom_condition_configs
934:         if not kiwoom_condition_configs:
935:             logger.info("ℹ kiwoom_condition 소스 쓰는 전략 없음 → 키움 조건검색 매니저 비활성")
936:
937:         # ── LS 조건검색: start() 에서 _start_ls_condition_manager() ──
938:         self._pending_ls_condition_configs = ls_condition_configs
939:         if not ls_condition_configs:
940:             logger.info("ℹ LS 조건명 없음 → LS 조건검색 매니저 비활성")
941:
942:     # ------------------------------------------------------------------
943:     def _start_market_guard(self) -> None:
944:         """
945:         시장 급락 서킷브레이커 매니저 기동.
946:
947:         - ``MARKET_GUARD_ENABLED=false`` (기본값) 면 지수 API·봉 저장 없음(쓰레드는 대기만).
948:           → DB 에서 true 로 바꾸면 재시작 없이 감시 시작.
949:         - ``bootstrap_sync()`` 는 ``start()`` 직후·전략 기동 전 1회 (재시작 PANIC 구멍 방지).
950:         - 지수 조회 (inquire-index-price) 는 모의 도메인 미지원 가능성
951:           → market_client (실키 우선) 를 사용. 실키 없으면 self.client 폴백.
952:         - PANIC 진입 시 BaseStrategy._scan_and_buy() 가 매수 차단.
953:         """
954:         try:
955:             self.market_guard = MarketGuard(client=self.market_client, db=self.db)
956:             self.market_guard.start()
957:         except Exception as e:
958:             logger.error("MarketGuard 기동 실패 (전략은 가드 없이 동작): %s", e)
959:             self.market_guard = None
960:
961:     # ------------------------------------------------------------------
962:     def _start_ws_validator(self) -> None:
963:         """키움 WS 기동 + (선택) KIS↔키움 가격 검증기."""
964:         from .utils.env import get_env_from_db
965:
966:         minimal = get_env_bool("WS_SUBSCRIBE_KIS_MINIMAL", False)
967:         need_validator = get_env_bool("LIVE_VALIDATOR_ENABLED", False)
968:
969:         # 키움 WS는 이제 틱/호가 프로바이더 및 적재 옵션에 따라 무조건 기동
970:         need_kiwoom = 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.


Created At: 2026-08-11T10:35:23Z
Completed At: 2026-08-11T10:35:23Z
File Path: `file:///home/hoon/kis_bot/kis_trader/main.py`
Total Lines: 2749
Total Bytes: 126718
Showing lines 1000 to 1040
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.
1000:             logger.warning("키움 자격증명 로드 실패 → 키움 WS 비활성: %s", e)
1001:             return
1002:         if not app_key or not app_secret:
1003:             logger.warning(
1004:                 "키움 %s 키 미설정 → 키움 WS 비활성 (KIWOOM_APP_KEY%s 확인)",
1005:                 "실키" if force_real else "키",
1006:                 "_REAL" if force_real else "",
1007:             )
1008:             return
1009:
1010:         if self.kiwoom_ws is None:
1011:             try:
1012:                 from .ws.kiwoom_ws import KiwoomWebSocketPriceCache
1013:                 self.kiwoom_ws = KiwoomWebSocketPriceCache(
1014:                     app_key, app_secret, is_mock=is_mock,
1015:                 )
1016:                 if not self.kiwoom_ws.start():
1017:                     logger.warning("키움 WS 시작 실패")
1018:                     self.kiwoom_ws = None
1019:                     return
1020:                 else:
1021:                     # 🚀 키움 틱 적재 설정 적용
1022:                     tr = getattr(self.ws, "tick_recorder", None)
1023:                     if tr is not None and get_env_bool("WS_TICK_SAVE_KIWOOM", True):
1024:                         self.kiwoom_ws.attach_tick_recorder(tr)
1025:                         logger.info("✅ 키움 WS에 TickRecorder 부착 완료 (키움 틱 → ws_ticks 적재)")
1026:
1027:                     # 🚀 키움 호가 적재 설정 적용
1028:                     tsr = getattr(self.ws, "trigger_snapshot_recorder", None)
1029:                     if tsr is not None and get_env_bool("WS_ORDERBOOK_SAVE_KIWOOM", True):
1030:                         self.kiwoom_ws.attach_trigger_snapshot_recorder(tsr)
1031:                         logger.info("✅ 키움 WS에 TriggerSnapshotRecorder 부착 완료 (키움 호가 → ws_orderbook 적재)")
1032:             except Exception as e:
1033:                 logger.warning("키움 WS 인스턴스 생성 실패: %s", e)
1034:                 self.kiwoom_ws = None
1035:                 return
1036:
1037:         kis_ws_handle = getattr(self.ws, "ws_cache", None)
1038:
1039:         if need_validator and kis_ws_handle:
1040:             try:
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.


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


키움 캔들 어태치 부분 확인


Created At: 2026-08-11T10:35:31Z
Completed At: 2026-08-11T10:35:31Z
File Path: `file:///home/hoon/kis_bot/kis_trader/main.py`
Total Lines: 2749
Total Bytes: 126718
Showing lines 1040 to 1080
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.
1040:             try:
1041:                 from .network.ws_validator import WSPriceValidator
1042:                 self.ws_validator = WSPriceValidator(
1043:                     kis_ws=kis_ws_handle,
1044:                     kiwoom_ws=self.kiwoom_ws,
1045:                     db=self.db,
1046:                     sync_kiwoom_to_kis=not minimal,
1047:                 )
1048:                 self.ws_validator.start()
1049:                 self.ws_validator.start()
1050:                 logger.info("🔬 [시세 검증] LIVE_VALIDATOR_ENABLED=True — ws_price_validation 기록 (KIS↔키움 교차 검증)")
1051:             except Exception as e:
1052:                 logger.warning("Validator 기동 실패: %s", e)
1053:                 self.ws_validator = None
1054:         elif need_validator and not kis_ws_handle:
1055:             logger.warning("KIS WS 핸들 미발견 → Validator 비활성")
1056:
1057:     def _start_ls_ws_validation(self) -> None:
1058:         """LS 실키 WS — 검증·조건이력·AFR·ls_condition 틱.
1059:
1060:         매매 **주문** = KIS.
1061:         ``ls_condition``: LS US3 틱 + 키움 갭/분봉 (``ls_feed`` = 틱만 LS).
1062:         """
1063:         from .utils.env import get_env_from_db
1064:
1065:         validation_on = get_env_bool("LS_WS_VALIDATION_ENABLED", False)
1066:         history_on = get_env_bool("LS_CONDITION_HISTORY_ENABLED", False)
1067:         follow_on = get_env_bool("LS_WS_FOLLOW_CONDITION_UNIVERSE", True)
1068:         trade_ls = False
1069:         try:
1070:             for sid in ("SCALP", "BREAKOUT", "SHORT", "MOMENTUM"):
1071:                 if self._resolve_source(sid) == "ls_condition":
1072:                     trade_ls = True
1073:                     break
1074:         except Exception:
1075:             trade_ls = False
1076:         if not validation_on and not (history_on and follow_on) and not trade_ls:
1077:             logger.info(
1078:                 "ℹ LS WS 미기동 (VALIDATION=false, HISTORY/FOLLOW 미충족, "
1079:                 "ls_condition 전략 없음)"
1080:             )
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.


Created At: 2026-08-11T10:35:35Z
Completed At: 2026-08-11T10:35:35Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py`
Total Lines: 987
Total Bytes: 41892
Showing lines 600 to 640
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.
600:         # 토큰 발급
601:         token = self._get_kiwoom_token()
602:         if not token:
603:             logger.warning("⚠ 키움 토큰 발급 실패 → WS 연결 보류 (60s)")
604:             try:
605:                 from kis_trader.utils.ops_alert import ops_alert
606:                 ops_alert(
607:                     "token_kiwoom",
608:                     "키움 토큰 발급 실패",
609:                     detail="시세 WS·조건검색 불가 → 60s 후 재시도",
610:                     level="critical",
611:                     session_only=False,
612:                 )
613:             except Exception:
614:                 pass
615:             time.sleep(60)
616:             return
617:
618:         # WebSocketApp 생성
619:         self._ws = self._ws_lib.WebSocketApp(
620:             self._ws_url,
621:             on_open=self._on_open(token),
622:             on_message=self._on_message,
623:             on_error=self._on_error,
624:             on_close=self._on_close,
625:         )
626:         self._last_connect_time = time.time()
627:         # blocking — 연결 종료까지 여기서 대기
628:         # ping_interval=0 : 키움은 JSON {"trnm":"PING"} keep-alive (프로토콜 ping 비호환)
629:         self._ws.run_forever(ping_interval=0)
630:
631:     def _on_open(self, token: str):
632:         """on_open 콜백 팩토리 — token 캡처 후 LOGIN 발송."""
633:         def _handler(ws):
634:             self._connected = True
635:             self._authenticated = False
636:             try:
637:                 ws.send(json.dumps({"trnm": "LOGIN", "token": token}))
638:                 logger.info("📡 키움 WS 연결 → LOGIN 발송")
639:             except Exception as e:
640:                 logger.warning("키움 WS LOGIN 발송 실패: %s", 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.


파일 없음 에러 났던 파일 재확인


Created At: 2026-08-11T10:35:39Z
Error invalid tool call: There was a problem parsing the tool call.
Error Message: model output error: invalid tool call error (invalid_args) failed to read file: open /home/hoon/kis_bot/kis_trader/ws/candle_aggregator.py: no such file or directory
Guidance: You are trying to correct your previous tool call error, you must focus on fixing the failed tool call with sequential tool calls and try again. Do not do parallel tool calls and if you are fixing multiple tool calls, do them one at a time. Do not apologize.
Retries remaining: 4.


캔들 집계기 상세 확인


Created At: 2026-08-11T10:35:43Z
Completed At: 2026-08-11T10:35:43Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1600 to 1650
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.
1600:                 "volume": int(volume) if inc else 0,
1601:                 "_acml_base": None if inc else int(volume),
1602:                 "market": market,
1603:             }
1604:             return
1605:
1606:         cur = self._current[key]
1607:         cur["market"] = market or cur.get("market") or "KR"
1608:
1609:         if new_ctime != cur["candle_time"]:
1610:             # ── 봉 확정 (다음 틱 도착 → 정상 롤오버) ────────────────
1611:             confirmed_candle = self._confirm_current_bucket(key, cur)
1612:             logger.debug(
1613:                 "🕯 [봉확정] %s %dM %s C=%.0f RSI3=%s",
1614:                 code, tf, confirmed_candle["candle_time"], confirmed_candle["close"],
1615:                 f"{confirmed_candle['rsi_3']:.1f}" if confirmed_candle["rsi_3"] is not None else "N/A",
1616:             )
1617:
1618:             # ── 새 봉 시작 (RAM만) ──────────────────────────────────
1619:             inc = self._volume_is_incremental(code)
1620:             self._current[key] = {
1621:                 "candle_time": new_ctime,
1622:                 "open": price, "high": price, "low": price,
1623:                 "close": price,
1624:                 "volume": int(volume) if inc else 0,
1625:                 "_acml_base": None if inc else int(volume),
1626:                 "market": market,
1627:             }
1628:         else:
1629:             # 같은 봉: OHLCV 갱신 (RAM만, DB 쓰기 없음)
1630:             cur["high"]   = max(cur["high"], price)
1631:             cur["low"]    = min(cur["low"], price)
1632:             cur["close"]  = price
1633:             if self._volume_is_incremental(code):
1634:                 # 키움 FID 15: 틱 체결량 합산
1635:                 cur["volume"] = int(cur.get("volume", 0) or 0) + int(volume or 0)
1636:             else:
1637:                 # KIS ACML_VOL: 봉 시작 대비 델타
1638:                 base = int(cur.get("_acml_base", 0) or 0)
1639:                 cur["volume"] = max(0, int(volume or 0) - base)
1640:
1641:     def _confirm_current_bucket(self, key: tuple, cur: Dict) -> Dict:
1642:         """
1643:         진행 중이던 ``_current[key]`` 봉을 확정봉으로 전환한다.
1644:         (``_process_tick``의 정상 롤오버 / ``flush_stale_current_candles``의
1645:         시간경과 강제확정 양쪽에서 공통으로 사용 — 락 보유 상태에서 호출)
1646:
1647:         [트랙 1] RSI 계산 + ``_confirmed`` RAM 버퍼 적재 (매수 루프 즉시 참조용)
1648:         [트랙 2] DB 기록 Queue 적재 (논블로킹)
1649:
1650:         동일 ``candle_time`` 이 이미 있으면:
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.


Created At: 2026-08-11T10:35:47Z
Completed At: 2026-08-11T10:35:47Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1640 to 1680
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.
1640:
1641:     def _confirm_current_bucket(self, key: tuple, cur: Dict) -> Dict:
1642:         """
1643:         진행 중이던 ``_current[key]`` 봉을 확정봉으로 전환한다.
1644:         (``_process_tick``의 정상 롤오버 / ``flush_stale_current_candles``의
1645:         시간경과 강제확정 양쪽에서 공통으로 사용 — 락 보유 상태에서 호출)
1646:
1647:         [트랙 1] RSI 계산 + ``_confirmed`` RAM 버퍼 적재 (매수 루프 즉시 참조용)
1648:         [트랙 2] DB 기록 Queue 적재 (논블로킹)
1649:
1650:         동일 ``candle_time`` 이 이미 있으면:
1651:         - ``WS_CANDLE_FREEZE_ON_CONFIRM``(기본 true): OHLCV 유지(첫 확정 승), holding_peak 만 갱신
1652:         - freeze OFF: volume 더 큰 쪽 upsert (레거시)
1653:         """
1654:         code, tf = key
1655:         ctime = str(cur.get("candle_time") or "")[:12]
1656:         buf = self._confirmed.setdefault(key, [])
1657:         closes = self._closes.setdefault(key, [])
1658:
1659:         new_vol = int(cur.get("volume") or 0)
1660:         idx = next(
1661:             (
1662:                 i for i, c in enumerate(buf)
1663:                 if str(c.get("candle_time") or "")[:12] == ctime
1664:             ),
1665:             -1,
1666:         )
1667:
1668:         hp = self._holding_peak_for(code)
1669:         freeze = self._ws_candle_freeze_on_confirm()
1670:         if idx >= 0:
1671:             old = buf[idx]
1672:             old_vol = int(old.get("volume") or 0)
1673:             # freeze: 이미 확정된 봉은 OHLCV 고정 (REST/재확정이 키우지 않음)
1674:             if freeze or new_vol < old_vol:
1675:                 confirmed_candle = dict(old)
1676:                 confirmed_candle["is_confirmed"] = 1
1677:                 confirmed_candle["market"] = str(
1678:                     cur.get("market") or old.get("market") or "KR"
1679:                 ).upper()[:8]
1680:                 if hp is not 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.


Created At: 2026-08-11T10:35:52Z
Completed At: 2026-08-11T10:35:52Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1680 to 1730
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.
1680:                 if hp is not None:
1681:                     confirmed_candle["holding_peak"] = max(
1682:                         float(confirmed_candle.get("holding_peak") or 0), float(hp),
1683:                     )
1684:                     # freeze 시 high 도 동결 — peak 만 메타로 보관
1685:                     if not freeze:
1686:                         confirmed_candle["high"] = max(
1687:                             float(confirmed_candle.get("high") or 0), float(hp),
1688:                         )
1689:                     buf[idx] = confirmed_candle
1690:                 return confirmed_candle
1691:
1692:             low_cands = [
1693:                 x for x in (float(old.get("low") or 0), float(cur["low"])) if x > 0
1694:             ]
1695:             confirmed_candle = {
1696:                 "code": code,
1697:                 "market": str(cur.get("market") or old.get("market") or "KR").upper()[:8],
1698:                 "tf": tf,
1699:                 "candle_time": ctime,
1700:                 "open": float(old.get("open") or cur["open"]),
1701:                 "high": max(float(old.get("high") or 0), float(cur["high"])),
1702:                 "low": min(low_cands) if low_cands else float(cur["low"]),
1703:                 "close": float(cur["close"]),
1704:                 "volume": max(old_vol, new_vol),
1705:                 "is_confirmed": 1,
1706:                 "source": str(cur.get("source") or old.get("source") or "ws"),
1707:             }
1708:             if hp is not None:
1709:                 confirmed_candle["holding_peak"] = hp
1710:                 confirmed_candle["high"] = max(
1711:                     float(confirmed_candle["high"]), float(hp),
1712:                 )
1713:             buf[idx] = confirmed_candle
1714:             closes[:] = [float(c.get("close") or 0) for c in buf]
1715:             rsi2, rsi3, rsi5 = self._compute_rsi_set(closes[: idx + 1])
1716:             confirmed_candle["rsi_2"] = rsi2
1717:             confirmed_candle["rsi_3"] = rsi3
1718:             confirmed_candle["rsi_5"] = rsi5
1719:             buf[idx] = confirmed_candle
1720:         else:
1721:             closes.append(float(cur["close"]))
1722:             if len(closes) > self._ram_buffer_max:
1723:                 closes.pop(0)
1724:             rsi2, rsi3, rsi5 = self._compute_rsi_set(closes)
1725:             confirmed_candle = {
1726:                 "code": code,
1727:                 "market": str(cur.get("market") or "KR").upper()[:8],
1728:                 "tf": tf,
1729:                 "candle_time": ctime,
1730:                 "open": cur["open"],
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.


Created At: 2026-08-11T10:35:56Z
Completed At: 2026-08-11T10:35:56Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py`
Total Lines: 987
Total Bytes: 41892
Showing lines 800 to 850
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.
800:                 tick_vol = 0
801:
802:             # ── CandleAggregator: 후보만 (tick_to_agg = 후보−보유) ─────
803:             if self._candle_agg is not None:
804:                 filt = self._candle_agg_codes
805:                 if filt is None or code in filt:
806:                     try:
807:                         self._candle_agg.on_tick(code, price, tick_vol, tick_time)
808:                     except Exception as ex:
809:                         logger.debug("키움→CandleAggregator on_tick 실패 %s: %s", code, ex)
810:
811:             # ── TickRecorder: 보유 포함 전 구독 종목 (백테 틱청산·B안 폴백) ─────
812:             if self._tick_recorder is not None:
813:                 try:
814:                     self._tick_recorder.on_tick(
815:                         code, price, tick_vol, tick_time, source="kiwoom",
816:                     )
817:                 except Exception as ex:
818:                     logger.debug("키움→TickRecorder on_tick 실패 %s: %s", code, ex)
819:
820:             # ── 호가 틱동기: 체결 1건당 RAM 0D 스냅 1장 (LS tick 모드와 동일) ──
821:             if self._trigger_snapshot_recorder is not None:
822:                 try:
823:                     max_age = float(
824:                         getattr(
825:                             self._trigger_snapshot_recorder,
826:                             "orderbook_tick_max_age_sec",
827:                             3.0,
828:                         )
829:                         or 3.0
830:                     )
831:                     snap = self.get_orderbook_snapshot(code, max_age_sec=max_age)
832:                     if snap is not None:
833:                         self._trigger_snapshot_recorder.on_orderbook_tick_sync(
834:                             code, snap, snap_time=tick_time,
835:                         )
836:                 except Exception as ex:
837:                     logger.debug("키움→호가틱동기 실패 %s: %s", code, ex)
838:         except (ValueError, TypeError) as e:
839:             logger.debug("키움 0B 파싱 오류 %s: %s", code, e)
840:
841:     def _on_error(self, ws, error) -> None:
842:         self._connected = False
843:         self._authenticated = False
844:         logger.warning("⚠ 키움 WS 오류: %s", error)
845:
846:     def _on_close(self, ws, close_status_code, close_msg) -> None:
847:         self._connected = False
848:         self._authenticated = False
849:         logger.info("🔌 키움 WS 연결 종료 (code=%s msg=%s)",
850:                     close_status_code, close_msg or "")
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.


on_tick 캔들 소스 파라미터 확인


Created At: 2026-08-11T10:36:00Z
Completed At: 2026-08-11T10:36:00Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1550 to 1650
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.
1550:             self._calc_rsi(closes, 3),
1551:             self._calc_rsi(closes, 5),
1552:         )
1553:
1554:     # ------------------------------------------------------------------
1555:     # 핵심: 틱 수신 처리
1556:     # ------------------------------------------------------------------
1557:
1558:     def on_tick(
1559:         self,
1560:         code: str,
1561:         price: float,
1562:         volume: int,
1563:         tick_time: str,
1564:         *,
1565:         market: str = "KR",
1566:         timeframes: Optional[list] = None,
1567:     ) -> None:
1568:         """
1569:         KISWebSocketPriceCache._parse_realtime_msg 에서 틱마다 호출됨.
1570:         기본: 등록된 모든 timeframe 봉 갱신.
1571:         timeframes: 해외 WS 등에서 1분만 넘길 때 (국내 호출부는 None 유지).
1572:         market: KR(국내) | US(해외 HDFSCNT0) — ws_candles.market 기록용.
1573:         """
1574:         if price <= 0:
1575:             return
1576:         mk = (market or "KR").strip().upper()[:8] or "KR"
1577:         tfs = list(timeframes) if timeframes is not None else self.timeframes
1578:         with self._lock:
1579:             for tf in tfs:
1580:                 self._process_tick(code, price, volume, tick_time, tf, market=mk)
1581:
1582:     def _process_tick(self, code: str, price: float, volume: int,
1583:                       tick_time: str, tf: int, *, market: str = "KR") -> None:
1584:         """
1585:         단일 timeframe 에 대한 틱 처리 (lock 내부에서 호출).
1586:
1587:         [트랙 1] RAM 갱신만 수행, DB 호출 없음 → 블로킹 0ms
1588:         [트랙 2] 봉 확정 순간에만 Queue.put_nowait() → 기록원이 비동기 배치 저장
1589:         """
1590:         key = (code, tf)
1591:         new_ctime = self._candle_key(tick_time, tf)
1592:
1593:         if key not in self._current:
1594:             # 첫 틱: 새 봉 시작 (RAM만)
1595:             inc = self._volume_is_incremental(code)
1596:             self._current[key] = {
1597:                 "candle_time": new_ctime,
1598:                 "open": price, "high": price, "low": price,
1599:                 "close": price,
1600:                 "volume": int(volume) if inc else 0,
1601:                 "_acml_base": None if inc else int(volume),
1602:                 "market": market,
1603:             }
1604:             return
1605:
1606:         cur = self._current[key]
1607:         cur["market"] = market or cur.get("market") or "KR"
1608:
1609:         if new_ctime != cur["candle_time"]:
1610:             # ── 봉 확정 (다음 틱 도착 → 정상 롤오버) ────────────────
1611:             confirmed_candle = self._confirm_current_bucket(key, cur)
1612:             logger.debug(
1613:                 "🕯 [봉확정] %s %dM %s C=%.0f RSI3=%s",
1614:                 code, tf, confirmed_candle["candle_time"], confirmed_candle["close"],
1615:                 f"{confirmed_candle['rsi_3']:.1f}" if confirmed_candle["rsi_3"] is not None else "N/A",
1616:             )
1617:
1618:             # ── 새 봉 시작 (RAM만) ──────────────────────────────────
1619:             inc = self._volume_is_incremental(code)
1620:             self._current[key] = {
1621:                 "candle_time": new_ctime,
1622:                 "open": price, "high": price, "low": price,
1623:                 "close": price,
1624:                 "volume": int(volume) if inc else 0,
1625:                 "_acml_base": None if inc else int(volume),
1626:                 "market": market,
1627:             }
1628:         else:
1629:             # 같은 봉: OHLCV 갱신 (RAM만, DB 쓰기 없음)
1630:             cur["high"]   = max(cur["high"], price)
1631:             cur["low"]    = min(cur["low"], price)
1632:             cur["close"]  = price
1633:             if self._volume_is_incremental(code):
1634:                 # 키움 FID 15: 틱 체결량 합산
1635:                 cur["volume"] = int(cur.get("volume", 0) or 0) + int(volume or 0)
1636:             else:
1637:                 # KIS ACML_VOL: 봉 시작 대비 델타
1638:                 base = int(cur.get("_acml_base", 0) or 0)
1639:                 cur["volume"] = max(0, int(volume or 0) - base)
1640:
1641:     def _confirm_current_bucket(self, key: tuple, cur: Dict) -> Dict:
1642:         """
1643:         진행 중이던 ``_current[key]`` 봉을 확정봉으로 전환한다.
1644:         (``_process_tick``의 정상 롤오버 / ``flush_stale_current_candles``의
1645:         시간경과 강제확정 양쪽에서 공통으로 사용 — 락 보유 상태에서 호출)
1646:
1647:         [트랙 1] RSI 계산 + ``_confirmed`` RAM 버퍼 적재 (매수 루프 즉시 참조용)
1648:         [트랙 2] DB 기록 Queue 적재 (논블로킹)
1649:
1650:         동일 ``candle_time`` 이 이미 있으면:
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.


Created At: 2026-08-11T10:36:16Z
Completed At: 2026-08-11T10:36:17Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py`
Total Lines: 987
Total Bytes: 41892
Showing lines 790 to 835
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.
790:             if len(tt_raw) >= 6:
791:                 tick_time = tt_raw[-6:]
792:             else:
793:                 import datetime as _dt
794:                 tick_time = _dt.datetime.now().strftime("%H%M%S")
795:             try:
796:                 tick_vol = int(
797:                     abs(float(str(values.get(self.FID_TICK_VOL, "0")).replace(",", "")))
798:                 )
799:             except (ValueError, TypeError):
800:                 tick_vol = 0
801:
802:             # ── CandleAggregator: 후보만 (tick_to_agg = 후보−보유) ─────
803:             if self._candle_agg is not None:
804:                 filt = self._candle_agg_codes
805:                 if filt is None or code in filt:
806:                     try:
807:                         self._candle_agg.on_tick(code, price, tick_vol, tick_time)
808:                     except Exception as ex:
809:                         logger.debug("키움→CandleAggregator on_tick 실패 %s: %s", code, ex)
810:
811:             # ── TickRecorder: 보유 포함 전 구독 종목 (백테 틱청산·B안 폴백) ─────
812:             if self._tick_recorder is not None:
813:                 try:
814:                     self._tick_recorder.on_tick(
815:                         code, price, tick_vol, tick_time, source="kiwoom",
816:                     )
817:                 except Exception as ex:
818:                     logger.debug("키움→TickRecorder on_tick 실패 %s: %s", code, ex)
819:
820:             # ── 호가 틱동기: 체결 1건당 RAM 0D 스냅 1장 (LS tick 모드와 동일) ──
821:             if self._trigger_snapshot_recorder is not None:
822:                 try:
823:                     max_age = float(
824:                         getattr(
825:                             self._trigger_snapshot_recorder,
826:                             "orderbook_tick_max_age_sec",
827:                             3.0,
828:                         )
829:                         or 3.0
830:                     )
831:                     snap = self.get_orderbook_snapshot(code, max_age_sec=max_age)
832:                     if snap is not None:
833:                         self._trigger_snapshot_recorder.on_orderbook_tick_sync(
834:                             code, snap, snap_time=tick_time,
835:                         )
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>
<browser_subagent_reminder>
IMPORTANT: You are shown COMPLETE details of every action the browser subagent performed:
- The subagent's final result message
- EVERY SINGLE STEP the subagent executed (numbered sequentially)
- For each step: the tool name, full JSON arguments, status, and any errors
- For screenshot steps: the absolute file path where the screenshot was saved
- For pixel click steps: the absolute file path where the click feedback screenshot was saved
- The recording path showing all browser interactions if a recording was generated

If you expected the subagent to take a specific action (e.g., capture a screenshot, click a button, navigate to a page) but you do NOT see that step type in the detailed actions list above, then the subagent did NOT perform that action. Do not assume work was done if you cannot see the corresponding step.
CRITICAL: NEVER trust the subagent's claims. After a browser subagent completes a task, IMMEDIATELY verify the screenshot BEFORE responding to the user. Look at the actual screenshot content and describe what you see. If the screenshot doesn't show the expected result, acknowledge that the task may not have completed successfully and investigate further.

- IMPORTANT: Do NOT view the webp recording. Your view_file tool only shows the first frame of recordings, so this will not represent what the subagent did. Solely rely on screenshot images.
- If there are no screenshots, you MUST ask another subagent to take screenshots to prove the task was completed successfully.
- The ONLY exception is if the output of any javascript executed by the subagent shows that the browser subagent successfully performed the action. This is because the output of such a tool cannot be manipulated by the subagent.
- Think about which screenshots to verify, the right choice will depend on the task, some examples:
  * If the subagent was supposed to follow a particular process, you should verify a sequence of screenshots in order.
  * If the subagent was supposed to navigate to a website, you should verify the final screenshot before the subagent stopped.
- You must explain what in the screenshot or javascript execution output indicates that the subagent is successful. Default to skepticism.
</browser_subagent_reminder>
</EPHEMERAL_MESSAGE>


Created At: 2026-08-11T10:36:20Z
Error invalid tool call: There was a problem parsing the tool call.
Error Message: model output error: invalid tool call error (invalid_args) failed to read file: open /home/hoon/kis_bot/kis_trader/ws/candle_aggregator.py: no such file or directory
Guidance: You are trying to correct your previous tool call error, you must focus on fixing the failed tool call with sequential tool calls and try again. Do not do parallel tool calls and if you are fixing multiple tool calls, do them one at a time. Do not apologize.
Retries remaining: 4.


Created At: 2026-08-11T10:36:24Z
Completed At: 2026-08-11T10:36:24Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1550 to 1600
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.
1550:             self._calc_rsi(closes, 3),
1551:             self._calc_rsi(closes, 5),
1552:         )
1553:
1554:     # ------------------------------------------------------------------
1555:     # 핵심: 틱 수신 처리
1556:     # ------------------------------------------------------------------
1557:
1558:     def on_tick(
1559:         self,
1560:         code: str,
1561:         price: float,
1562:         volume: int,
1563:         tick_time: str,
1564:         *,
1565:         market: str = "KR",
1566:         timeframes: Optional[list] = None,
1567:     ) -> None:
1568:         """
1569:         KISWebSocketPriceCache._parse_realtime_msg 에서 틱마다 호출됨.
1570:         기본: 등록된 모든 timeframe 봉 갱신.
1571:         timeframes: 해외 WS 등에서 1분만 넘길 때 (국내 호출부는 None 유지).
1572:         market: KR(국내) | US(해외 HDFSCNT0) — ws_candles.market 기록용.
1573:         """
1574:         if price <= 0:
1575:             return
1576:         mk = (market or "KR").strip().upper()[:8] or "KR"
1577:         tfs = list(timeframes) if timeframes is not None else self.timeframes
1578:         with self._lock:
1579:             for tf in tfs:
1580:                 self._process_tick(code, price, volume, tick_time, tf, market=mk)
1581:
1582:     def _process_tick(self, code: str, price: float, volume: int,
1583:                       tick_time: str, tf: int, *, market: str = "KR") -> None:
1584:         """
1585:         단일 timeframe 에 대한 틱 처리 (lock 내부에서 호출).
1586:
1587:         [트랙 1] RAM 갱신만 수행, DB 호출 없음 → 블로킹 0ms
1588:         [트랙 2] 봉 확정 순간에만 Queue.put_nowait() → 기록원이 비동기 배치 저장
1589:         """
1590:         key = (code, tf)
1591:         new_ctime = self._candle_key(tick_time, tf)
1592:
1593:         if key not in self._current:
1594:             # 첫 틱: 새 봉 시작 (RAM만)
1595:             inc = self._volume_is_incremental(code)
1596:             self._current[key] = {
1597:                 "candle_time": new_ctime,
1598:                 "open": price, "high": price, "low": price,
1599:                 "close": price,
1600:                 "volume": int(volume) if inc else 0,
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.


Created At: 2026-08-11T10:36:45Z
Completed At: 2026-08-11T10:36:45Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py`
Total Lines: 987
Total Bytes: 41892
Showing lines 800 to 850
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.
800:                 tick_vol = 0
801:
802:             # ── CandleAggregator: 후보만 (tick_to_agg = 후보−보유) ─────
803:             if self._candle_agg is not None:
804:                 filt = self._candle_agg_codes
805:                 if filt is None or code in filt:
806:                     try:
807:                         self._candle_agg.on_tick(code, price, tick_vol, tick_time)
808:                     except Exception as ex:
809:                         logger.debug("키움→CandleAggregator on_tick 실패 %s: %s", code, ex)
810:
811:             # ── TickRecorder: 보유 포함 전 구독 종목 (백테 틱청산·B안 폴백) ─────
812:             if self._tick_recorder is not None:
813:                 try:
814:                     self._tick_recorder.on_tick(
815:                         code, price, tick_vol, tick_time, source="kiwoom",
816:                     )
817:                 except Exception as ex:
818:                     logger.debug("키움→TickRecorder on_tick 실패 %s: %s", code, ex)
819:
820:             # ── 호가 틱동기: 체결 1건당 RAM 0D 스냅 1장 (LS tick 모드와 동일) ──
821:             if self._trigger_snapshot_recorder is not None:
822:                 try:
823:                     max_age = float(
824:                         getattr(
825:                             self._trigger_snapshot_recorder,
826:                             "orderbook_tick_max_age_sec",
827:                             3.0,
828:                         )
829:                         or 3.0
830:                     )
831:                     snap = self.get_orderbook_snapshot(code, max_age_sec=max_age)
832:                     if snap is not None:
833:                         self._trigger_snapshot_recorder.on_orderbook_tick_sync(
834:                             code, snap, snap_time=tick_time,
835:                         )
836:                 except Exception as ex:
837:                     logger.debug("키움→호가틱동기 실패 %s: %s", code, ex)
838:         except (ValueError, TypeError) as e:
839:             logger.debug("키움 0B 파싱 오류 %s: %s", code, e)
840:
841:     def _on_error(self, ws, error) -> None:
842:         self._connected = False
843:         self._authenticated = False
844:         logger.warning("⚠ 키움 WS 오류: %s", error)
845:
846:     def _on_close(self, ws, close_status_code, close_msg) -> None:
847:         self._connected = False
848:         self._authenticated = False
849:         logger.info("🔌 키움 WS 연결 종료 (code=%s msg=%s)",
850:                     close_status_code, close_msg or "")
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>


Created At: 2026-08-11T10:36:49Z
Completed At: 2026-08-11T10:36:49Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/tick_recorder.py`
Total Lines: 288
Total Bytes: 11915
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/ws/tick_recorder.py — 실시간 체결 틱 RAM 링버퍼 + ws_ticks 배치 저장 (C안)
3: ==================================================================================
4: [트랙 1] 종목별 deque 링버퍼 — B안 봉 내 돌파·get_recent_ticks() 용 (DB 대기 없음)
5: [트랙 2] 제한 큐 → 백그라운드 스레드 배치 INSERT (journalctl 파싱 아님)
6:
7: 해외 확장: market / exchange / currency / session 컬럼 (국내 기본 KR/KRX/KRW).
8: - 국내 → ``ws_ticks`` (메인 TradeDB 커넥션)
9: - 해외 US → ``ws_ticks_us`` (전용 raw 커넥션 — 앱 TradeDB 락과 분리)
10: """
11: from __future__ import annotations
12:
13: import datetime as dt
14: import queue
15: import threading
16: import time
17: from collections import deque
18: from typing import Any, Deque, Dict, List, Optional, Set
19:
20: from ..utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int
21: from ..utils.logger import get_logger
22:
23: logger = get_logger("kis_trader.tick_recorder")
24:
25:
26: class TickRecorder:
27:     """
28:     WebSocket 체결 틱 기록기.
29:
30:     - ``set_record_codes()`` 로 후보(또는 구독) 종목만 필터.
31:     - RAM: 종목당 최대 N틱 (링버퍼).
32:     - DB: BATCH_SIZE / FLUSH_SEC 마다 append INSERT
33:       (KR→ws_ticks, US→ws_ticks_us).
34:     """
35:
36:     def __init__(self, db=None) -> None:
37:         self.db = db
38:         self._enabled = get_env_bool("WS_TICK_SAVE_ENABLED", True)
39:         self._buffer_max = get_env_int("WS_TICK_BUFFER_MAX_PER_CODE", 500)
40:         self._batch_size = get_env_int("WS_TICK_DB_BATCH_SIZE", 200)
41:         self._flush_sec = get_env_float("WS_TICK_DB_FLUSH_SEC", 3.0)
42:         self._queue_max = get_env_int("WS_TICK_WRITE_QUEUE_MAX", 50000)
43:         self._keep_days = get_env_int("WS_TICK_KEEP_DAYS", 7)
44:         self._default_market = (get_env_from_db("WS_TICK_DEFAULT_MARKET", "KR") or "KR").strip().upper()
45:         self._default_exchange = (get_env_from_db("WS_TICK_DEFAULT_EXCHANGE", "KRX") or "KRX").strip()
46:         self._default_currency = (get_env_from_db("WS_TICK_DEFAULT_CURRENCY", "KRW") or "KRW").strip().upper()
47:
48:         self._lock = threading.Lock()
49:         self._buffers: Dict[str, Deque[dict]] = {}
50:         self._record_codes: Optional[Set[str]] = None
51:         self._write_queue: queue.Queue = queue.Queue(maxsize=max(1000, self._queue_max))
52:         self._writer_thread: Optional[threading.Thread] = None
53:         self._last_cleanup_ts = 0.0
54:         self._drops = 0
55:         # 해외 틱 전용 DB 커넥션 (메인 TradeDB._lock 과 분리)
56:         self._us_write_conn: Any = None
57:
58:         if self._enabled and self.db is not None:
59:             self._ensure_table()
60:             self._start_writer()
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.


Created At: 2026-08-11T10:36:53Z
Completed At: 2026-08-11T10:36:53Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/tick_recorder.py`
Total Lines: 288
Total Bytes: 11915
Showing lines 90 to 140
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.
90:         exchange: Optional[str] = None,
91:         currency: Optional[str] = None,
92:         source: str = "kis",
93:         session: Optional[str] = None,
94:         tick_seq: Optional[int] = None,
95:         persist_db: bool = True,
96:     ) -> None:
97:         """체결 틱 1건 — WS 핫패스에서 호출 (논블로킹).
98:
99:         persist_db=False 이면 RAM 링버퍼만 갱신 (DB INSERT 생략).
100:         LS 는 ``ls_ws_ticks`` 에 이미 적재하므로 ``ws_ticks`` 이중 저장 방지용.
101:         """
102:         if not self._enabled or price <= 0:
103:             return
104:         code = (code or "").strip()
105:         if not code:
106:             return
107:         mkt = (market or self._default_market).upper()[:8]
108:         # 국내: WS_TICK_RECORD_SCOPE(candidates 등) 필터 적용.
109:         # 해외(US): 영구구독이 국내 후보 집합에 없어 필터에 걸리면
110:         # 봉(ws_candles)은 쌓이는데 틱(ws_ticks_us)만 끊기는 사고가 남.
111:         # → US 는 필터 우회 (저장 ON/OFF 는 해외 WS 의 WS_TICK_SAVE_US_ENABLED).
112:         if mkt != "US":
113:             with self._lock:
114:                 filt = self._record_codes
115:             if filt is not None and code not in filt:
116:                 return
117:
118:         tick_time_full = self._normalize_tick_time(tick_time)
119:         recv_ts = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
120:         item = {
121:             "market": mkt,
122:             "exchange": (exchange or self._default_exchange)[:16] if (exchange or self._default_exchange) else None,
123:             "code": code[:32],
124:             "tick_time": tick_time_full,
125:             "price": float(price),
126:             "volume": int(volume or 0),
127:             "tick_seq": tick_seq,
128:             "session": (session or "")[:8] or None,
129:             "currency": (currency or self._default_currency).upper()[:8],
130:             "source": (source or "kis")[:16],
131:             "recv_ts": recv_ts,
132:         }
133:
134:         with self._lock:
135:             buf = self._buffers.get(code)
136:             if buf is None:
137:                 buf = deque(maxlen=max(10, self._buffer_max))
138:                 self._buffers[code] = buf
139:             buf.append(item)
140:
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.


Created At: 2026-08-11T10:36:57Z
Completed At: 2026-08-11T10:36:57Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/tick_recorder.py`
Total Lines: 288
Total Bytes: 11915
Showing lines 110 to 160
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.
110:         # 봉(ws_candles)은 쌓이는데 틱(ws_ticks_us)만 끊기는 사고가 남.
111:         # → US 는 필터 우회 (저장 ON/OFF 는 해외 WS 의 WS_TICK_SAVE_US_ENABLED).
112:         if mkt != "US":
113:             with self._lock:
114:                 filt = self._record_codes
115:             if filt is not None and code not in filt:
116:                 return
117:
118:         tick_time_full = self._normalize_tick_time(tick_time)
119:         recv_ts = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
120:         item = {
121:             "market": mkt,
122:             "exchange": (exchange or self._default_exchange)[:16] if (exchange or self._default_exchange) else None,
123:             "code": code[:32],
124:             "tick_time": tick_time_full,
125:             "price": float(price),
126:             "volume": int(volume or 0),
127:             "tick_seq": tick_seq,
128:             "session": (session or "")[:8] or None,
129:             "currency": (currency or self._default_currency).upper()[:8],
130:             "source": (source or "kis")[:16],
131:             "recv_ts": recv_ts,
132:         }
133:
134:         with self._lock:
135:             buf = self._buffers.get(code)
136:             if buf is None:
137:                 buf = deque(maxlen=max(10, self._buffer_max))
138:                 self._buffers[code] = buf
139:             buf.append(item)
140:
141:         # LS 등: 전용 테이블에 이미 쓰면 ws_ticks 중복 INSERT 생략
142:         if not persist_db or self.db is None:
143:             return
144:         try:
145:             self._write_queue.put_nowait(item)
146:         except queue.Full:
147:             self._drops += 1
148:             if self._drops == 1 or self._drops % 5000 == 0:
149:                 logger.warning(
150:                     "⚠ TickRecorder 쓰기 Queue 가득참 — 틱 DROP 누적=%d (code=%s)",
151:                     self._drops, code,
152:                 )
153:
154:     def get_recent_ticks(self, code: str, limit: int = 100) -> List[dict]:
155:         """RAM 링버퍼 최근 틱 (오래된→최신 순)."""
156:         code = (code or "").strip()
157:         if not code:
158:             return []
159:         with self._lock:
160:             buf = self._buffers.get(code)
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.


Created At: 2026-08-11T10:37:12Z
Completed At: 2026-08-11T10:37:12Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 2250 to 2300
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.
2250:         self._expiry: Optional[_datetime] = None
2251:         self._lock   = _threading.Lock()
2252:
2253:     def _is_valid(self) -> bool:
2254:         """만료 30초 전까지 유효 (TokenManager._is_access_token_valid 와 동일)"""
2255:         if not self._token or not self._expiry:
2256:             return False
2257:         return _datetime.now() < self._expiry - _timedelta(seconds=30)
2258:
2259:     def _request_new_token(self) -> None:
2260:         """토큰 신규 발급 — 만료 시에만 호출됨"""
2261:         resp = requests.post(
2262:             f"https://{self._domain}/oauth2/token",
2263:             json={
2264:                 "grant_type": "client_credentials",
2265:                 "appkey":     self._app_key,
2266:                 "secretkey":  self._app_secret,
2267:             },
2268:             timeout=10,
2269:         )
2270:         data  = resp.json()
2271:         token = (data.get("token") or data.get("access_token") or "").strip()
2272:         if not token:
2273:             raise RuntimeError(f"키움 토큰 발급 실패 [{self._mode_str}]: {data}")
2274:
2275:         # expires_dt 정확히 파싱 (TokenManager._update_token_info 와 동일 로직)
2276:         exp_s = data.get("expires_dt", "")
2277:         try:
2278:             self._expiry = _datetime.strptime(str(exp_s), "%Y%m%d%H%M%S")
2279:         except Exception:
2280:             # expires_in 도 없으면 24h 기본 (보수적 fallback)
2281:             exp_in = data.get("expires_in", 86400)
2282:             self._expiry = _datetime.now() + _timedelta(seconds=int(exp_in))
2283:
2284:         self._token = token
2285:         logger.info(
2286:             "✅ 키움 토큰 발급 완료 [%s] (앞8자: %s…, 만료: %s)",
2287:             self._mode_str, token[:8], self._expiry.strftime("%Y-%m-%d %H:%M:%S"),
2288:         )
2289:
2290:     def invalidate(self, reason: str = "") -> None:
2291:         """서버가 토큰을 거부했을 때 로컬 캐시를 비운다 (만료 전 재사용 금지).
2292:
2293:         WS LOGIN rc=805004 등 — expires_dt 전이라도 서버 측 무효면
2294:         같은 토큰을 재전송하면 재연결만 소모하고 영구 비활성으로 간다.
2295:         """
2296:         with self._lock:
2297:             self._token = None
2298:             self._expiry = None
2299:         if reason:
2300:             logger.warning(
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.


Created At: 2026-08-11T10:37:16Z
Completed At: 2026-08-11T10:37:16Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py`
Total Lines: 987
Total Bytes: 41892
Showing lines 800 to 850
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.
800:                 tick_vol = 0
801:
802:             # ── CandleAggregator: 후보만 (tick_to_agg = 후보−보유) ─────
803:             if self._candle_agg is not None:
804:                 filt = self._candle_agg_codes
805:                 if filt is None or code in filt:
806:                     try:
807:                         self._candle_agg.on_tick(code, price, tick_vol, tick_time)
808:                     except Exception as ex:
809:                         logger.debug("키움→CandleAggregator on_tick 실패 %s: %s", code, ex)
810:
811:             # ── TickRecorder: 보유 포함 전 구독 종목 (백테 틱청산·B안 폴백) ─────
812:             if self._tick_recorder is not None:
813:                 try:
814:                     self._tick_recorder.on_tick(
815:                         code, price, tick_vol, tick_time, source="kiwoom",
816:                     )
817:                 except Exception as ex:
818:                     logger.debug("키움→TickRecorder on_tick 실패 %s: %s", code, ex)
819:
820:             # ── 호가 틱동기: 체결 1건당 RAM 0D 스냅 1장 (LS tick 모드와 동일) ──
821:             if self._trigger_snapshot_recorder is not None:
822:                 try:
823:                     max_age = float(
824:                         getattr(
825:                             self._trigger_snapshot_recorder,
826:                             "orderbook_tick_max_age_sec",
827:                             3.0,
828:                         )
829:                         or 3.0
830:                     )
831:                     snap = self.get_orderbook_snapshot(code, max_age_sec=max_age)
832:                     if snap is not None:
833:                         self._trigger_snapshot_recorder.on_orderbook_tick_sync(
834:                             code, snap, snap_time=tick_time,
835:                         )
836:                 except Exception as ex:
837:                     logger.debug("키움→호가틱동기 실패 %s: %s", code, ex)
838:         except (ValueError, TypeError) as e:
839:             logger.debug("키움 0B 파싱 오류 %s: %s", code, e)
840:
841:     def _on_error(self, ws, error) -> None:
842:         self._connected = False
843:         self._authenticated = False
844:         logger.warning("⚠ 키움 WS 오류: %s", error)
845:
846:     def _on_close(self, ws, close_status_code, close_msg) -> None:
847:         self._connected = False
848:         self._authenticated = False
849:         logger.info("🔌 키움 WS 연결 종료 (code=%s msg=%s)",
850:                     close_status_code, close_msg or "")
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.


Created At: 2026-08-11T10:37:20Z
Completed At: 2026-08-11T10:37:20Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1550 to 1600
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.
1550:             self._calc_rsi(closes, 3),
1551:             self._calc_rsi(closes, 5),
1552:         )
1553:
1554:     # ------------------------------------------------------------------
1555:     # 핵심: 틱 수신 처리
1556:     # ------------------------------------------------------------------
1557:
1558:     def on_tick(
1559:         self,
1560:         code: str,
1561:         price: float,
1562:         volume: int,
1563:         tick_time: str,
1564:         *,
1565:         market: str = "KR",
1566:         timeframes: Optional[list] = None,
1567:     ) -> None:
1568:         """
1569:         KISWebSocketPriceCache._parse_realtime_msg 에서 틱마다 호출됨.
1570:         기본: 등록된 모든 timeframe 봉 갱신.
1571:         timeframes: 해외 WS 등에서 1분만 넘길 때 (국내 호출부는 None 유지).
1572:         market: KR(국내) | US(해외 HDFSCNT0) — ws_candles.market 기록용.
1573:         """
1574:         if price <= 0:
1575:             return
1576:         mk = (market or "KR").strip().upper()[:8] or "KR"
1577:         tfs = list(timeframes) if timeframes is not None else self.timeframes
1578:         with self._lock:
1579:             for tf in tfs:
1580:                 self._process_tick(code, price, volume, tick_time, tf, market=mk)
1581:
1582:     def _process_tick(self, code: str, price: float, volume: int,
1583:                       tick_time: str, tf: int, *, market: str = "KR") -> None:
1584:         """
1585:         단일 timeframe 에 대한 틱 처리 (lock 내부에서 호출).
1586:
1587:         [트랙 1] RAM 갱신만 수행, DB 호출 없음 → 블로킹 0ms
1588:         [트랙 2] 봉 확정 순간에만 Queue.put_nowait() → 기록원이 비동기 배치 저장
1589:         """
1590:         key = (code, tf)
1591:         new_ctime = self._candle_key(tick_time, tf)
1592:
1593:         if key not in self._current:
1594:             # 첫 틱: 새 봉 시작 (RAM만)
1595:             inc = self._volume_is_incremental(code)
1596:             self._current[key] = {
1597:                 "candle_time": new_ctime,
1598:                 "open": price, "high": price, "low": price,
1599:                 "close": price,
1600:                 "volume": int(volume) if inc else 0,
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.


Created At: 2026-08-11T10:37:24Z
Completed At: 2026-08-11T10:37:24Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1600 to 1650
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.
1600:                 "volume": int(volume) if inc else 0,
1601:                 "_acml_base": None if inc else int(volume),
1602:                 "market": market,
1603:             }
1604:             return
1605:
1606:         cur = self._current[key]
1607:         cur["market"] = market or cur.get("market") or "KR"
1608:
1609:         if new_ctime != cur["candle_time"]:
1610:             # ── 봉 확정 (다음 틱 도착 → 정상 롤오버) ────────────────
1611:             confirmed_candle = self._confirm_current_bucket(key, cur)
1612:             logger.debug(
1613:                 "🕯 [봉확정] %s %dM %s C=%.0f RSI3=%s",
1614:                 code, tf, confirmed_candle["candle_time"], confirmed_candle["close"],
1615:                 f"{confirmed_candle['rsi_3']:.1f}" if confirmed_candle["rsi_3"] is not None else "N/A",
1616:             )
1617:
1618:             # ── 새 봉 시작 (RAM만) ──────────────────────────────────
1619:             inc = self._volume_is_incremental(code)
1620:             self._current[key] = {
1621:                 "candle_time": new_ctime,
1622:                 "open": price, "high": price, "low": price,
1623:                 "close": price,
1624:                 "volume": int(volume) if inc else 0,
1625:                 "_acml_base": None if inc else int(volume),
1626:                 "market": market,
1627:             }
1628:         else:
1629:             # 같은 봉: OHLCV 갱신 (RAM만, DB 쓰기 없음)
1630:             cur["high"]   = max(cur["high"], price)
1631:             cur["low"]    = min(cur["low"], price)
1632:             cur["close"]  = price
1633:             if self._volume_is_incremental(code):
1634:                 # 키움 FID 15: 틱 체결량 합산
1635:                 cur["volume"] = int(cur.get("volume", 0) or 0) + int(volume or 0)
1636:             else:
1637:                 # KIS ACML_VOL: 봉 시작 대비 델타
1638:                 base = int(cur.get("_acml_base", 0) or 0)
1639:                 cur["volume"] = max(0, int(volume or 0) - base)
1640:
1641:     def _confirm_current_bucket(self, key: tuple, cur: Dict) -> Dict:
1642:         """
1643:         진행 중이던 ``_current[key]`` 봉을 확정봉으로 전환한다.
1644:         (``_process_tick``의 정상 롤오버 / ``flush_stale_current_candles``의
1645:         시간경과 강제확정 양쪽에서 공통으로 사용 — 락 보유 상태에서 호출)
1646:
1647:         [트랙 1] RSI 계산 + ``_confirmed`` RAM 버퍼 적재 (매수 루프 즉시 참조용)
1648:         [트랙 2] DB 기록 Queue 적재 (논블로킹)
1649:
1650:         동일 ``candle_time`` 이 이미 있으면:
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.


Created At: 2026-08-11T10:37:28Z
Completed At: 2026-08-11T10:37:28Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1680 to 1730
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.
1680:                 if hp is not None:
1681:                     confirmed_candle["holding_peak"] = max(
1682:                         float(confirmed_candle.get("holding_peak") or 0), float(hp),
1683:                     )
1684:                     # freeze 시 high 도 동결 — peak 만 메타로 보관
1685:                     if not freeze:
1686:                         confirmed_candle["high"] = max(
1687:                             float(confirmed_candle.get("high") or 0), float(hp),
1688:                         )
1689:                     buf[idx] = confirmed_candle
1690:                 return confirmed_candle
1691:
1692:             low_cands = [
1693:                 x for x in (float(old.get("low") or 0), float(cur["low"])) if x > 0
1694:             ]
1695:             confirmed_candle = {
1696:                 "code": code,
1697:                 "market": str(cur.get("market") or old.get("market") or "KR").upper()[:8],
1698:                 "tf": tf,
1699:                 "candle_time": ctime,
1700:                 "open": float(old.get("open") or cur["open"]),
1701:                 "high": max(float(old.get("high") or 0), float(cur["high"])),
1702:                 "low": min(low_cands) if low_cands else float(cur["low"]),
1703:                 "close": float(cur["close"]),
1704:                 "volume": max(old_vol, new_vol),
1705:                 "is_confirmed": 1,
1706:                 "source": str(cur.get("source") or old.get("source") or "ws"),
1707:             }
1708:             if hp is not None:
1709:                 confirmed_candle["holding_peak"] = hp
1710:                 confirmed_candle["high"] = max(
1711:                     float(confirmed_candle["high"]), float(hp),
1712:                 )
1713:             buf[idx] = confirmed_candle
1714:             closes[:] = [float(c.get("close") or 0) for c in buf]
1715:             rsi2, rsi3, rsi5 = self._compute_rsi_set(closes[: idx + 1])
1716:             confirmed_candle["rsi_2"] = rsi2
1717:             confirmed_candle["rsi_3"] = rsi3
1718:             confirmed_candle["rsi_5"] = rsi5
1719:             buf[idx] = confirmed_candle
1720:         else:
1721:             closes.append(float(cur["close"]))
1722:             if len(closes) > self._ram_buffer_max:
1723:                 closes.pop(0)
1724:             rsi2, rsi3, rsi5 = self._compute_rsi_set(closes)
1725:             confirmed_candle = {
1726:                 "code": code,
1727:                 "market": str(cur.get("market") or "KR").upper()[:8],
1728:                 "tf": tf,
1729:                 "candle_time": ctime,
1730:                 "open": cur["open"],
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.


Created At: 2026-08-11T10:37:32Z
Completed At: 2026-08-11T10:37:32Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1550 to 1650
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.
1550:             self._calc_rsi(closes, 3),
1551:             self._calc_rsi(closes, 5),
1552:         )
1553:
1554:     # ------------------------------------------------------------------
1555:     # 핵심: 틱 수신 처리
1556:     # ------------------------------------------------------------------
1557:
1558:     def on_tick(
1559:         self,
1560:         code: str,
1561:         price: float,
1562:         volume: int,
1563:         tick_time: str,
1564:         *,
1565:         market: str = "KR",
1566:         timeframes: Optional[list] = None,
1567:     ) -> None:
1568:         """
1569:         KISWebSocketPriceCache._parse_realtime_msg 에서 틱마다 호출됨.
1570:         기본: 등록된 모든 timeframe 봉 갱신.
1571:         timeframes: 해외 WS 등에서 1분만 넘길 때 (국내 호출부는 None 유지).
1572:         market: KR(국내) | US(해외 HDFSCNT0) — ws_candles.market 기록용.
1573:         """
1574:         if price <= 0:
1575:             return
1576:         mk = (market or "KR").strip().upper()[:8] or "KR"
1577:         tfs = list(timeframes) if timeframes is not None else self.timeframes
1578:         with self._lock:
1579:             for tf in tfs:
1580:                 self._process_tick(code, price, volume, tick_time, tf, market=mk)
1581:
1582:     def _process_tick(self, code: str, price: float, volume: int,
1583:                       tick_time: str, tf: int, *, market: str = "KR") -> None:
1584:         """
1585:         단일 timeframe 에 대한 틱 처리 (lock 내부에서 호출).
1586:
1587:         [트랙 1] RAM 갱신만 수행, DB 호출 없음 → 블로킹 0ms
1588:         [트랙 2] 봉 확정 순간에만 Queue.put_nowait() → 기록원이 비동기 배치 저장
1589:         """
1590:         key = (code, tf)
1591:         new_ctime = self._candle_key(tick_time, tf)
1592:
1593:         if key not in self._current:
1594:             # 첫 틱: 새 봉 시작 (RAM만)
1595:             inc = self._volume_is_incremental(code)
1596:             self._current[key] = {
1597:                 "candle_time": new_ctime,
1598:                 "open": price, "high": price, "low": price,
1599:                 "close": price,
1600:                 "volume": int(volume) if inc else 0,
1601:                 "_acml_base": None if inc else int(volume),
1602:                 "market": market,
1603:             }
1604:             return
1605:
1606:         cur = self._current[key]
1607:         cur["market"] = market or cur.get("market") or "KR"
1608:
1609:         if new_ctime != cur["candle_time"]:
1610:             # ── 봉 확정 (다음 틱 도착 → 정상 롤오버) ────────────────
1611:             confirmed_candle = self._confirm_current_bucket(key, cur)
1612:             logger.debug(
1613:                 "🕯 [봉확정] %s %dM %s C=%.0f RSI3=%s",
1614:                 code, tf, confirmed_candle["candle_time"], confirmed_candle["close"],
1615:                 f"{confirmed_candle['rsi_3']:.1f}" if confirmed_candle["rsi_3"] is not None else "N/A",
1616:             )
1617:
1618:             # ── 새 봉 시작 (RAM만) ──────────────────────────────────
1619:             inc = self._volume_is_incremental(code)
1620:             self._current[key] = {
1621:                 "candle_time": new_ctime,
1622:                 "open": price, "high": price, "low": price,
1623:                 "close": price,
1624:                 "volume": int(volume) if inc else 0,
1625:                 "_acml_base": None if inc else int(volume),
1626:                 "market": market,
1627:             }
1628:         else:
1629:             # 같은 봉: OHLCV 갱신 (RAM만, DB 쓰기 없음)
1630:             cur["high"]   = max(cur["high"], price)
1631:             cur["low"]    = min(cur["low"], price)
1632:             cur["close"]  = price
1633:             if self._volume_is_incremental(code):
1634:                 # 키움 FID 15: 틱 체결량 합산
1635:                 cur["volume"] = int(cur.get("volume", 0) or 0) + int(volume or 0)
1636:             else:
1637:                 # KIS ACML_VOL: 봉 시작 대비 델타
1638:                 base = int(cur.get("_acml_base", 0) or 0)
1639:                 cur["volume"] = max(0, int(volume or 0) - base)
1640:
1641:     def _confirm_current_bucket(self, key: tuple, cur: Dict) -> Dict:
1642:         """
1643:         진행 중이던 ``_current[key]`` 봉을 확정봉으로 전환한다.
1644:         (``_process_tick``의 정상 롤오버 / ``flush_stale_current_candles``의
1645:         시간경과 강제확정 양쪽에서 공통으로 사용 — 락 보유 상태에서 호출)
1646:
1647:         [트랙 1] RSI 계산 + ``_confirmed`` RAM 버퍼 적재 (매수 루프 즉시 참조용)
1648:         [트랙 2] DB 기록 Queue 적재 (논블로킹)
1649:
1650:         동일 ``candle_time`` 이 이미 있으면:
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.


Created At: 2026-08-11T10:37:36Z
Completed At: 2026-08-11T10:37:36Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1640 to 1680
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.
1640:
1641:     def _confirm_current_bucket(self, key: tuple, cur: Dict) -> Dict:
1642:         """
1643:         진행 중이던 ``_current[key]`` 봉을 확정봉으로 전환한다.
1644:         (``_process_tick``의 정상 롤오버 / ``flush_stale_current_candles``의
1645:         시간경과 강제확정 양쪽에서 공통으로 사용 — 락 보유 상태에서 호출)
1646:
1647:         [트랙 1] RSI 계산 + ``_confirmed`` RAM 버퍼 적재 (매수 루프 즉시 참조용)
1648:         [트랙 2] DB 기록 Queue 적재 (논블로킹)
1649:
1650:         동일 ``candle_time`` 이 이미 있으면:
1651:         - ``WS_CANDLE_FREEZE_ON_CONFIRM``(기본 true): OHLCV 유지(첫 확정 승), holding_peak 만 갱신
1652:         - freeze OFF: volume 더 큰 쪽 upsert (레거시)
1653:         """
1654:         code, tf = key
1655:         ctime = str(cur.get("candle_time") or "")[:12]
1656:         buf = self._confirmed.setdefault(key, [])
1657:         closes = self._closes.setdefault(key, [])
1658:
1659:         new_vol = int(cur.get("volume") or 0)
1660:         idx = next(
1661:             (
1662:                 i for i, c in enumerate(buf)
1663:                 if str(c.get("candle_time") or "")[:12] == ctime
1664:             ),
1665:             -1,
1666:         )
1667:
1668:         hp = self._holding_peak_for(code)
1669:         freeze = self._ws_candle_freeze_on_confirm()
1670:         if idx >= 0:
1671:             old = buf[idx]
1672:             old_vol = int(old.get("volume") or 0)
1673:             # freeze: 이미 확정된 봉은 OHLCV 고정 (REST/재확정이 키우지 않음)
1674:             if freeze or new_vol < old_vol:
1675:                 confirmed_candle = dict(old)
1676:                 confirmed_candle["is_confirmed"] = 1
1677:                 confirmed_candle["market"] = str(
1678:                     cur.get("market") or old.get("market") or "KR"
1679:                 ).upper()[:8]
1680:                 if hp is not 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.


Created At: 2026-08-11T10:37:40Z
Completed At: 2026-08-11T10:37:40Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1680 to 1730
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.
1680:                 if hp is not None:
1681:                     confirmed_candle["holding_peak"] = max(
1682:                         float(confirmed_candle.get("holding_peak") or 0), float(hp),
1683:                     )
1684:                     # freeze 시 high 도 동결 — peak 만 메타로 보관
1685:                     if not freeze:
1686:                         confirmed_candle["high"] = max(
1687:                             float(confirmed_candle.get("high") or 0), float(hp),
1688:                         )
1689:                     buf[idx] = confirmed_candle
1690:                 return confirmed_candle
1691:
1692:             low_cands = [
1693:                 x for x in (float(old.get("low") or 0), float(cur["low"])) if x > 0
1694:             ]
1695:             confirmed_candle = {
1696:                 "code": code,
1697:                 "market": str(cur.get("market") or old.get("market") or "KR").upper()[:8],
1698:                 "tf": tf,
1699:                 "candle_time": ctime,
1700:                 "open": float(old.get("open") or cur["open"]),
1701:                 "high": max(float(old.get("high") or 0), float(cur["high"])),
1702:                 "low": min(low_cands) if low_cands else float(cur["low"]),
1703:                 "close": float(cur["close"]),
1704:                 "volume": max(old_vol, new_vol),
1705:                 "is_confirmed": 1,
1706:                 "source": str(cur.get("source") or old.get("source") or "ws"),
1707:             }
1708:             if hp is not None:
1709:                 confirmed_candle["holding_peak"] = hp
1710:                 confirmed_candle["high"] = max(
1711:                     float(confirmed_candle["high"]), float(hp),
1712:                 )
1713:             buf[idx] = confirmed_candle
1714:             closes[:] = [float(c.get("close") or 0) for c in buf]
1715:             rsi2, rsi3, rsi5 = self._compute_rsi_set(closes[: idx + 1])
1716:             confirmed_candle["rsi_2"] = rsi2
1717:             confirmed_candle["rsi_3"] = rsi3
1718:             confirmed_candle["rsi_5"] = rsi5
1719:             buf[idx] = confirmed_candle
1720:         else:
1721:             closes.append(float(cur["close"]))
1722:             if len(closes) > self._ram_buffer_max:
1723:                 closes.pop(0)
1724:             rsi2, rsi3, rsi5 = self._compute_rsi_set(closes)
1725:             confirmed_candle = {
1726:                 "code": code,
1727:                 "market": str(cur.get("market") or "KR").upper()[:8],
1728:                 "tf": tf,
1729:                 "candle_time": ctime,
1730:                 "open": cur["open"],
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.


on_tick 인자 부분 상세 확인


Created At: 2026-08-11T10:37:44Z
Completed At: 2026-08-11T10:37:44Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1550 to 1650
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.
1550:             self._calc_rsi(closes, 3),
1551:             self._calc_rsi(closes, 5),
1552:         )
1553:
1554:     # ------------------------------------------------------------------
1555:     # 핵심: 틱 수신 처리
1556:     # ------------------------------------------------------------------
1557:
1558:     def on_tick(
1559:         self,
1560:         code: str,
1561:         price: float,
1562:         volume: int,
1563:         tick_time: str,
1564:         *,
1565:         market: str = "KR",
1566:         timeframes: Optional[list] = None,
1567:     ) -> None:
1568:         """
1569:         KISWebSocketPriceCache._parse_realtime_msg 에서 틱마다 호출됨.
1570:         기본: 등록된 모든 timeframe 봉 갱신.
1571:         timeframes: 해외 WS 등에서 1분만 넘길 때 (국내 호출부는 None 유지).
1572:         market: KR(국내) | US(해외 HDFSCNT0) — ws_candles.market 기록용.
1573:         """
1574:         if price <= 0:
1575:             return
1576:         mk = (market or "KR").strip().upper()[:8] or "KR"
1577:         tfs = list(timeframes) if timeframes is not None else self.timeframes
1578:         with self._lock:
1579:             for tf in tfs:
1580:                 self._process_tick(code, price, volume, tick_time, tf, market=mk)
1581:
1582:     def _process_tick(self, code: str, price: float, volume: int,
1583:                       tick_time: str, tf: int, *, market: str = "KR") -> None:
1584:         """
1585:         단일 timeframe 에 대한 틱 처리 (lock 내부에서 호출).
1586:
1587:         [트랙 1] RAM 갱신만 수행, DB 호출 없음 → 블로킹 0ms
1588:         [트랙 2] 봉 확정 순간에만 Queue.put_nowait() → 기록원이 비동기 배치 저장
1589:         """
1590:         key = (code, tf)
1591:         new_ctime = self._candle_key(tick_time, tf)
1592:
1593:         if key not in self._current:
1594:             # 첫 틱: 새 봉 시작 (RAM만)
1595:             inc = self._volume_is_incremental(code)
1596:             self._current[key] = {
1597:                 "candle_time": new_ctime,
1598:                 "open": price, "high": price, "low": price,
1599:                 "close": price,
1600:                 "volume": int(volume) if inc else 0,
1601:                 "_acml_base": None if inc else int(volume),
1602:                 "market": market,
1603:             }
1604:             return
1605:
1606:         cur = self._current[key]
1607:         cur["market"] = market or cur.get("market") or "KR"
1608:
1609:         if new_ctime != cur["candle_time"]:
1610:             # ── 봉 확정 (다음 틱 도착 → 정상 롤오버) ────────────────
1611:             confirmed_candle = self._confirm_current_bucket(key, cur)
1612:             logger.debug(
1613:                 "🕯 [봉확정] %s %dM %s C=%.0f RSI3=%s",
1614:                 code, tf, confirmed_candle["candle_time"], confirmed_candle["close"],
1615:                 f"{confirmed_candle['rsi_3']:.1f}" if confirmed_candle["rsi_3"] is not None else "N/A",
1616:             )
1617:
1618:             # ── 새 봉 시작 (RAM만) ──────────────────────────────────
1619:             inc = self._volume_is_incremental(code)
1620:             self._current[key] = {
1621:                 "candle_time": new_ctime,
1622:                 "open": price, "high": price, "low": price,
1623:                 "close": price,
1624:                 "volume": int(volume) if inc else 0,
1625:                 "_acml_base": None if inc else int(volume),
1626:                 "market": market,
1627:             }
1628:         else:
1629:             # 같은 봉: OHLCV 갱신 (RAM만, DB 쓰기 없음)
1630:             cur["high"]   = max(cur["high"], price)
1631:             cur["low"]    = min(cur["low"], price)
1632:             cur["close"]  = price
1633:             if self._volume_is_incremental(code):
1634:                 # 키움 FID 15: 틱 체결량 합산
1635:                 cur["volume"] = int(cur.get("volume", 0) or 0) + int(volume or 0)
1636:             else:
1637:                 # KIS ACML_VOL: 봉 시작 대비 델타
1638:                 base = int(cur.get("_acml_base", 0) or 0)
1639:                 cur["volume"] = max(0, int(volume or 0) - base)
1640:
1641:     def _confirm_current_bucket(self, key: tuple, cur: Dict) -> Dict:
1642:         """
1643:         진행 중이던 ``_current[key]`` 봉을 확정봉으로 전환한다.
1644:         (``_process_tick``의 정상 롤오버 / ``flush_stale_current_candles``의
1645:         시간경과 강제확정 양쪽에서 공통으로 사용 — 락 보유 상태에서 호출)
1646:
1647:         [트랙 1] RSI 계산 + ``_confirmed`` RAM 버퍼 적재 (매수 루프 즉시 참조용)
1648:         [트랙 2] DB 기록 Queue 적재 (논블로킹)
1649:
1650:         동일 ``candle_time`` 이 이미 있으면:
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.


키움 source 파라미터 전달 여부 다시 확인


Created At: 2026-08-11T10:37:48Z
Completed At: 2026-08-11T10:37:49Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kiwoom_ws.py`
Total Lines: 987
Total Bytes: 41892
Showing lines 800 to 850
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.
800:                 tick_vol = 0
801:
802:             # ── CandleAggregator: 후보만 (tick_to_agg = 후보−보유) ─────
803:             if self._candle_agg is not None:
804:                 filt = self._candle_agg_codes
805:                 if filt is None or code in filt:
806:                     try:
807:                         self._candle_agg.on_tick(code, price, tick_vol, tick_time)
808:                     except Exception as ex:
809:                         logger.debug("키움→CandleAggregator on_tick 실패 %s: %s", code, ex)
810:
811:             # ── TickRecorder: 보유 포함 전 구독 종목 (백테 틱청산·B안 폴백) ─────
812:             if self._tick_recorder is not None:
813:                 try:
814:                     self._tick_recorder.on_tick(
815:                         code, price, tick_vol, tick_time, source="kiwoom",
816:                     )
817:                 except Exception as ex:
818:                     logger.debug("키움→TickRecorder on_tick 실패 %s: %s", code, ex)
819:
820:             # ── 호가 틱동기: 체결 1건당 RAM 0D 스냅 1장 (LS tick 모드와 동일) ──
821:             if self._trigger_snapshot_recorder is not None:
822:                 try:
823:                     max_age = float(
824:                         getattr(
825:                             self._trigger_snapshot_recorder,
826:                             "orderbook_tick_max_age_sec",
827:                             3.0,
828:                         )
829:                         or 3.0
830:                     )
831:                     snap = self.get_orderbook_snapshot(code, max_age_sec=max_age)
832:                     if snap is not None:
833:                         self._trigger_snapshot_recorder.on_orderbook_tick_sync(
834:                             code, snap, snap_time=tick_time,
835:                         )
836:                 except Exception as ex:
837:                     logger.debug("키움→호가틱동기 실패 %s: %s", code, ex)
838:         except (ValueError, TypeError) as e:
839:             logger.debug("키움 0B 파싱 오류 %s: %s", code, e)
840:
841:     def _on_error(self, ws, error) -> None:
842:         self._connected = False
843:         self._authenticated = False
844:         logger.warning("⚠ 키움 WS 오류: %s", error)
845:
846:     def _on_close(self, ws, close_status_code, close_msg) -> None:
847:         self._connected = False
848:         self._authenticated = False
849:         logger.info("🔌 키움 WS 연결 종료 (code=%s msg=%s)",
850:                     close_status_code, close_msg or "")
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.


Created At: 2026-08-11T10:37:53Z
Completed At: 2026-08-11T10:37:53Z
File Path: `file:///home/hoon/kis_bot/kis_trader/ws/kis_ws.py`
Total Lines: 2701
Total Bytes: 119326
Showing lines 1600 to 1650
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.
1600:                 "volume": int(volume) if inc else 0,
1601:                 "_acml_base": None if inc else int(volume),
1602:                 "market": market,
1603:             }
1604:             return
1605:
1606:         cur = self._current[key]
1607:         cur["market"] = market or cur.get("market") or "KR"
1608:
1609:         if new_ctime != cur["candle_time"]:
1610:             # ── 봉 확정 (다음 틱 도착 → 정상 롤오버) ────────────────
1611:             confirmed_candle = self._confirm_current_bucket(key, cur)
1612:             logger.debug(
1613:                 "🕯 [봉확정] %s %dM %s C=%.0f RSI3=%s",
1614:                 code, tf, confirmed_candle["candle_time"], confirmed_candle["close"],
1615:                 f"{confirmed_candle['rsi_3']:.1f}" if confirmed_candle["rsi_3"] is not None else "N/A",
1616:             )
1617:
1618:             # ── 새 봉 시작 (RAM만) ──────────────────────────────────
1619:             inc = self._volume_is_incremental(code)
1620:             self._current[key] = {
1621:                 "candle_time": new_ctime,
1622:                 "open": price, "high": price, "low": price,
1623:                 "close": price,
1624:                 "volume": int(volume) if inc else 0,
1625:                 "_acml_base": None if inc else int(volume),
1626:                 "market": market,
1627:             }
1628:         else:
1629:             # 같은 봉: OHLCV 갱신 (RAM만, DB 쓰기 없음)
1630:             cur["high"]   = max(cur["high"], price)
1631:             cur["low"]    = min(cur["low"], price)
1632:             cur["close"]  = price
1633:             if self._volume_is_incremental(code):
1634:                 # 키움 FID 15: 틱 체결량 합산
1635:                 cur["volume"] = int(cur.get("volume", 0) or 0) + int(volume or 0)
1636:             else:
1637:                 # KIS ACML_VOL: 봉 시작 대비 델타
1638:                 base = int(cur.get("_acml_base", 0) or 0)
1639:                 cur["volume"] = max(0, int(volume or 0) - base)
1640:
1641:     def _confirm_current_bucket(self, key: tuple, cur: Dict) -> Dict:
1642:         """
1643:         진행 중이던 ``_current[key]`` 봉을 확정봉으로 전환한다.
1644:         (``_process_tick``의 정상 롤오버 / ``flush_stale_current_candles``의
1645:         시간경과 강제확정 양쪽에서 공통으로 사용 — 락 보유 상태에서 호출)
1646:
1647:         [트랙 1] RSI 계산 + ``_confirmed`` RAM 버퍼 적재 (매수 루프 즉시 참조용)
1648:         [트랙 2] DB 기록 Queue 적재 (논블로킹)
1649:
1650:         동일 ``candle_time`` 이 이미 있으면:
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.