이번에 들어간 내용
한투 호가 = 2번째 앱키 전용 키 없거나 start 실패 시 메인에 H0STASP0 안 붙임. 운영설정 WS_ORDERBOOK_SAVE_KIS 빨간 danger. LS RAM 합집합 후보∪보유∪영구∪grace. sync_targets와 split reconcile 둘 다. 틱 DB 영구 게이트는 그대로. 분봉 쓰레기 → 다음 소스 봉 통째 그 분 틱 0건이거나 전부 봉끝 대비 LIVE_FEED_FALLBACK_MAX_AGE_SEC 초과면 구멍. 메인 WS → 2차 → LS → REST → rollup. CANDLE_GARBAGE_FALLBACK 기본 true. 파일: feed_fallback.py(신규), ws_manager.py, kis_ws.py, candle_series.py, bt_candle_source.py, live_config_schema.py, database.py, 스모크, MD 2개. 같은 ws_manager/database/kis_ws/live_config에는 직전 커밋 이후 쌓여 있던 시세 폴백·ENV 키 정리도 같이 들어갔습니다. 파일 단위로 나눌 수 없어서입니다.
This commit is contained in:
@@ -5,6 +5,7 @@ kis_trader/execution/orphan_reconcile.py — 봇 고아 포지션 복구 (장마
|
||||
「봇이 매수했는데 active_trades 미기록」 고아만 복구한다.
|
||||
이어서 GHOST_PURGE_ON_RECONCILE 시 「DB有·브로커0」 유령 행을 삭제한다.
|
||||
|
||||
- 기동: 전략 쓰레드 전 1회 (ORPHAN_RECONCILE_ON_STARTUP, 기본 ON)
|
||||
- Pre-EOD: 활성 전략 중 가장 이른 EOD 시각 − N분 (기본 7분) 1회
|
||||
- Post-EOD: 15:36~ 장마감 후 1회 (기존)
|
||||
- 수동매수(orders BUY 없음) · MANUAL_HOLD_CODES · strategy=HOLDING 은 건드리지 않음.
|
||||
@@ -20,6 +21,11 @@ from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
from ..engine.strategy_eod import _STRATEGY_EOD_SPEC, parse_eod_hm, resolve_strategy_eod_params
|
||||
from ..utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int
|
||||
from ..utils.strategy_ids import canonical_strategy_id
|
||||
from .kis_client import (
|
||||
broker_row_hldg_qty,
|
||||
broker_row_sellable_qty,
|
||||
cancelable_remainder_qty,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("kis_trader.orphan_reconcile")
|
||||
|
||||
@@ -167,13 +173,14 @@ def _num(v, default: float = 0.0) -> float:
|
||||
|
||||
|
||||
def _broker_qty(broker: Dict[str, Any], code: str) -> int:
|
||||
"""보유수량. 없으면 매도가능(구맵). 매도가능 0이어도 보유>0 이면 0이 아님."""
|
||||
br = (broker or {}).get(code) or (broker or {}).get(str(code).strip())
|
||||
if not br:
|
||||
return 0
|
||||
try:
|
||||
return int((br or {}).get("qty") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
hldg = broker_row_hldg_qty(br)
|
||||
if hldg > 0:
|
||||
return hldg
|
||||
return broker_row_sellable_qty(br)
|
||||
|
||||
|
||||
def _orphan_skip_sell_reasons() -> Optional[List[str]]:
|
||||
@@ -328,6 +335,18 @@ def purge_ghost_active_trades(order_mgr, broker: Optional[Dict[str, Any]] = None
|
||||
out["error"] = "실계좌 잔고 조회 실패"
|
||||
return out
|
||||
|
||||
cancelable = None
|
||||
if get_env_bool("INQUIRE_PSBL_RVSECNCL_BEFORE_GHOST", True):
|
||||
try:
|
||||
cancelable = order_mgr.get_cancelable_sells(force=True)
|
||||
except Exception as e:
|
||||
logger.warning("유령정리 전 정정취소가능 조회 예외: %s", e)
|
||||
cancelable = None
|
||||
if cancelable is None:
|
||||
out["error"] = "정정취소가능 조회 실패 — 유령삭제 스킵"
|
||||
logger.warning("🧹 [유령잔고] 정정취소가능조회 실패 → 이번 회차 삭제 안 함")
|
||||
return out
|
||||
|
||||
manual_hold = parse_manual_hold_codes()
|
||||
try:
|
||||
rows = db.conn.execute(
|
||||
@@ -354,6 +373,22 @@ def purge_ghost_active_trades(order_mgr, broker: Optional[Dict[str, Any]] = None
|
||||
continue
|
||||
if _broker_qty(broker, code) > 0:
|
||||
continue
|
||||
if cancelable_remainder_qty(cancelable or {}, code) > 0:
|
||||
logger.info(
|
||||
"⏸ [유령스킵] [%s] %s %s — 정정취소가능 매도 잔량 있음",
|
||||
strategy, name, code,
|
||||
)
|
||||
continue
|
||||
try:
|
||||
pending = db.get_pending_sell_order(strategy, code)
|
||||
except Exception:
|
||||
pending = None
|
||||
if pending:
|
||||
logger.info(
|
||||
"⏸ [유령스킵] [%s] %s %s — DB 미체결 매도 ODNO=%s",
|
||||
strategy, name, code, pending.get("ord_no"),
|
||||
)
|
||||
continue
|
||||
try:
|
||||
# OrderManager 와 동일: trade_history 에 ghost_purge 남긴 뒤 정리
|
||||
recorded = False
|
||||
|
||||
Reference in New Issue
Block a user