"""[하드닝 R3 2026-10-09] 기존 스윙 ActiveTrade.entry_price 1회성 백필 (주문가 → 체결가). 배포 후 1회 실행: docker compose exec backend python backfill_swing_entry_fill.py # dry-run(기본, SELECT 만) docker compose exec backend python backfill_swing_entry_fill.py --apply # 실제 갱신 - 대상 선정·계산은 `trade_executor.plan_swing_entry_backfill_v2`(순수 함수) 가 한다. 체결가가 확정된 스윙 BUY 와 1:1(분할매수면 체결수량 합 = 행 수량)로 대응되고, 현재 진입가가 주문가 기준 그대로인 행만 보정한다. 그 외(시딩·DT·레거시 미확인·부분체결 등)는 사유만 출력한다. - 쓰기는 db_worker 의 `upsert_active_trade` 핸들러(`_execute_db_task_impl`) 를 그대로 쓴다. 이 스크립트는 앱 밖 프로세스라 앱의 워커 큐에 넣을 수 없으므로 같은 핸들러를 직접 실행한다. - 스윙 사이클과 같은 행을 동시에 갱신하지 않도록 **장 마감 후** 실행을 권장한다. """ import argparse import asyncio import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from app.core.database import AsyncSessionLocal # noqa: E402 from app.services.trade_executor import ( # noqa: E402 apply_swing_entry_backfill_v2, collect_swing_entry_backfill_v2, ) async def _submit_direct(payload: dict) -> None: from app.services.db_worker import _execute_db_task_impl await _execute_db_task_impl({"type": "upsert_active_trade", "data": payload}) async def main(apply: bool) -> int: async with AsyncSessionLocal() as db: plans = await collect_swing_entry_backfill_v2(db) for p in plans: mark = "FIX " if p["payload"] else "SKIP" print(f"{mark} id={p['active_id']} {p['stock_code']} [{p['strategy_name']}] " f"entry={p['entry_price']} — {p['reason']}") todo = sum(1 for p in plans if p["payload"]) if not apply: print(f"dry-run: 보정 대상 {todo}건 (--apply 로 실행)") return 0 n = await apply_swing_entry_backfill_v2(plans, submit=_submit_direct) print(f"applied: {n}건") return 0 if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("--apply", action="store_true") sys.exit(asyncio.run(main(ap.parse_args().apply)))