refactor: enhance Optuna backtesting framework, optimize orderbook filtering, and update database management utilities.
This commit is contained in:
@@ -18,7 +18,7 @@ import logging
|
||||
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_from_db, get_env_int
|
||||
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
|
||||
|
||||
logger = logging.getLogger("kis_trader.orphan_reconcile")
|
||||
@@ -393,6 +393,134 @@ def purge_ghost_active_trades(order_mgr, broker: Optional[Dict[str, Any]] = None
|
||||
return out
|
||||
|
||||
|
||||
def sync_active_trades_with_broker(order_mgr, broker: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
개장 전(08:35~09:15) 또는 고아복구 시 DB(active_trades)와 증권사 실계좌 잔고를 동기화.
|
||||
- 기업 이벤트(액면분할, 무상증자 등) 또는 수기 매입으로 계좌 매수단가/수량이 변경된 경우,
|
||||
DB의 avg_buy_price, current_qty, stop_price, target_price, max_price 를 비율에 맞춰 보정(Sync).
|
||||
- 오진입 손절 및 트레이링 스톱 착시 방지.
|
||||
"""
|
||||
out: Dict[str, Any] = {"synced": [], "failed": []}
|
||||
if not get_env_bool("ORPHAN_RECONCILE_ENABLED", True):
|
||||
out["msg"] = "ORPHAN_RECONCILE_ENABLED=false"
|
||||
return out
|
||||
|
||||
db = order_mgr.db
|
||||
if broker is None:
|
||||
broker = order_mgr.get_broker_holdings(force=True)
|
||||
if not getattr(order_mgr, "_holdings_last_fetch_ok", False):
|
||||
out["error"] = "실계좌 잔고 조회 실패 — 안전을 위해 동기화 스킵"
|
||||
logger.warning("🔄 [계좌동기화] 실계좌 잔고 조회 실패 — 안전을 위해 스킵")
|
||||
return out
|
||||
|
||||
manual_hold = parse_manual_hold_codes()
|
||||
min_diff_px = get_env_float("SYNC_PRICE_DIFF_MIN", 0.5)
|
||||
|
||||
try:
|
||||
rows = db.conn.execute("SELECT * FROM active_trades").fetchall() or []
|
||||
except Exception as e:
|
||||
out["error"] = str(e)
|
||||
logger.warning("🔄 [계좌동기화] active_trades 조회 실패: %s", e)
|
||||
return out
|
||||
|
||||
now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
for row in rows:
|
||||
d = dict(row)
|
||||
code = str(d.get("code") or "").strip()
|
||||
if not code:
|
||||
continue
|
||||
name = str(d.get("name") or code)
|
||||
strategy = str(d.get("strategy") or "").strip()
|
||||
|
||||
# 수동 보호 및 장기 투자 포지션은 개입 차단
|
||||
if code in manual_hold or strategy.upper() == "HOLDING":
|
||||
continue
|
||||
|
||||
br = broker.get(code)
|
||||
if not br:
|
||||
# 브로커에 0주인 종목은 purge_ghost_active_trades 가 처리하므로 여기선 스킵
|
||||
continue
|
||||
|
||||
br_qty = int(br.get("qty") or 0)
|
||||
br_avg = float(br.get("avg_price") or 0.0)
|
||||
db_qty = int(d.get("current_qty") or 0)
|
||||
db_avg = float(d.get("avg_buy_price") or 0.0)
|
||||
|
||||
if br_qty <= 0 or br_avg <= 0:
|
||||
continue
|
||||
|
||||
qty_diff = (br_qty != db_qty)
|
||||
price_diff = abs(br_avg - db_avg) >= min_diff_px
|
||||
|
||||
if not (qty_diff or price_diff):
|
||||
continue
|
||||
|
||||
# 단가/수량 변경 감지! 비율(ratio) 계산하여 손절가·익절가·고점(max_price) 비례 조정
|
||||
ratio = (br_avg / db_avg) if (db_avg > 0) else 1.0
|
||||
old_stop = float(d.get("stop_price") or 0.0)
|
||||
old_target = float(d.get("target_price") or 0.0)
|
||||
old_max = float(d.get("max_price") or 0.0)
|
||||
|
||||
new_stop = round(old_stop * ratio, 2) if old_stop > 0 else 0.0
|
||||
new_target = round(old_target * ratio, 2) if old_target > 0 else 0.0
|
||||
new_max = max(br_avg, round(old_max * ratio, 2)) if old_max > 0 else br_avg
|
||||
new_inv = br_avg * br_qty
|
||||
|
||||
try:
|
||||
if strategy:
|
||||
db.conn.execute(
|
||||
"UPDATE active_trades SET avg_buy_price=%s, current_qty=%s, target_qty=%s, "
|
||||
"stop_price=%s, target_price=%s, max_price=%s, total_invested=%s, updated_at=%s "
|
||||
"WHERE code=%s AND strategy=%s",
|
||||
(br_avg, br_qty, br_qty, new_stop, new_target, new_max, new_inv, now_str, code, strategy)
|
||||
)
|
||||
else:
|
||||
db.conn.execute(
|
||||
"UPDATE active_trades SET avg_buy_price=%s, current_qty=%s, target_qty=%s, "
|
||||
"stop_price=%s, target_price=%s, max_price=%s, total_invested=%s, updated_at=%s "
|
||||
"WHERE code=%s",
|
||||
(br_avg, br_qty, br_qty, new_stop, new_target, new_max, new_inv, now_str, code)
|
||||
)
|
||||
|
||||
msg = (
|
||||
f"🔄 [계좌 싱크 로봇] 원장 변동 동기화! {name}({code}) [{strategy}]\n"
|
||||
f" · 수량: {db_qty}주 ➔ {br_qty}주\n"
|
||||
f" · 매입단가: {db_avg:,.0f}원 ➔ {br_avg:,.0f}원 (비율 {ratio:.4f})\n"
|
||||
f" · 손절가 보정: {old_stop:,.0f}원 ➔ {new_stop:,.0f}원\n"
|
||||
f" · 고점(max) 보정: {old_max:,.0f}원 ➔ {new_max:,.0f}원"
|
||||
)
|
||||
logger.info(msg)
|
||||
out["synced"].append({
|
||||
"code": code, "name": name, "strategy": strategy,
|
||||
"old_qty": db_qty, "new_qty": br_qty,
|
||||
"old_avg": db_avg, "new_avg": br_avg,
|
||||
})
|
||||
# 알림 발송
|
||||
if hasattr(order_mgr, "notifier") and order_mgr.notifier and hasattr(order_mgr.notifier, "send_message"):
|
||||
try:
|
||||
order_mgr.notifier.send_message(msg)
|
||||
except Exception:
|
||||
pass
|
||||
elif hasattr(order_mgr, "send_telegram_msg"):
|
||||
try:
|
||||
order_mgr.send_telegram_msg(msg)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
out["failed"].append({"code": code, "name": name, "error": str(e)})
|
||||
logger.exception("🔄 [계좌동기화] UPDATE 실패 %s", code)
|
||||
|
||||
if out["synced"]:
|
||||
try:
|
||||
order_mgr.invalidate_holdings_cache()
|
||||
except Exception:
|
||||
pass
|
||||
out["synced_count"] = len(out["synced"])
|
||||
else:
|
||||
out["synced_count"] = 0
|
||||
return out
|
||||
|
||||
|
||||
def reconcile_orphan_positions(order_mgr) -> Dict[str, Any]:
|
||||
"""
|
||||
장마감 후 1회 호출 — 봇 고아만 active_trades 에 upsert.
|
||||
@@ -412,6 +540,7 @@ def reconcile_orphan_positions(order_mgr) -> Dict[str, Any]:
|
||||
"failed": [],
|
||||
"ghost_purged": [],
|
||||
"stale_purged": [],
|
||||
"synced": [],
|
||||
}
|
||||
|
||||
if not get_env_bool("ORPHAN_RECONCILE_ENABLED", True):
|
||||
@@ -544,6 +673,15 @@ def reconcile_orphan_positions(order_mgr) -> Dict[str, Any]:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 실계좌 평단가·수량 동기화 (액면조정·수기매입 등 보정)
|
||||
sync = sync_active_trades_with_broker(order_mgr, broker=broker)
|
||||
result["synced"] = list(sync.get("synced") or [])
|
||||
result["synced_count"] = int(sync.get("synced_count") or 0)
|
||||
if sync.get("error"):
|
||||
result["sync_error"] = sync["error"]
|
||||
for f in sync.get("failed") or []:
|
||||
result["failed"].append(f)
|
||||
|
||||
# 유령(DB有·브로커0) — 동일 잔고 스냅샷으로 삭제 (추가 REST 없음)
|
||||
ghost = purge_ghost_active_trades(order_mgr, broker=broker)
|
||||
result["ghost_purged"] = list(ghost.get("purged") or [])
|
||||
@@ -557,9 +695,10 @@ def reconcile_orphan_positions(order_mgr) -> Dict[str, Any]:
|
||||
result["failed_count"] = len(result["failed"])
|
||||
result["skipped_after_sell_count"] = len(result["skipped_after_sell"])
|
||||
logger.info(
|
||||
"🧩 [고아복구] 완료 — 복구 %d / 청산후스킵 %d / 청산후유령정리 %d / "
|
||||
"🧩 [고아복구] 완료 — 복구 %d / 단가·수량동기화 %d / 청산후스킵 %d / 청산후유령정리 %d / "
|
||||
"유령삭제 %d / 실패 %d / 보호(수동) %d / 이미추적 %d / 주문없음 %d",
|
||||
len(result["reconciled"]),
|
||||
result["synced_count"],
|
||||
len(result["skipped_after_sell"]),
|
||||
result["stale_purged_count"],
|
||||
result["ghost_purged_count"],
|
||||
|
||||
Reference in New Issue
Block a user