refactor: enhance Optuna backtesting framework, optimize orderbook filtering, and update database management utilities.

This commit is contained in:
Your Name
2026-08-12 10:19:19 +09:00
parent cb7e5037a0
commit c6bd62a25f
218 changed files with 31613 additions and 759 deletions

View File

@@ -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"],