fix(trend): write trade_records when hub closes plan on gate_bot
Gate_bot insert_trade_record lacked entry_reason, causing _finalize_plan to save strategy snapshots but fail trade insert. Filter kwargs by signature, insert before plan commit, and add backfill script. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""补录缺失的趋势回调 trade_records(策略快照已有、交易记录漏写)。
|
||||
|
||||
典型原因:gate_bot insert_trade_record 曾不接受 entry_reason,_finalize_plan 写快照后插入失败。
|
||||
|
||||
用法:
|
||||
python scripts/backfill_trend_trade_records.py --db crypto_monitor_gate_bot/crypto.db --dry-run
|
||||
python scripts/backfill_trend_trade_records.py --db crypto_monitor_gate_bot/crypto.db --apply
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(_REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO_ROOT))
|
||||
|
||||
from strategy_snapshot_lib import STRATEGY_TREND # noqa: E402
|
||||
from strategy_trade_labels import ENTRY_REASON_TREND_PULLBACK, MONITOR_TYPE_TREND_PULLBACK # noqa: E402
|
||||
|
||||
STATUS_TO_RESULT = {
|
||||
"stopped_sl": "止损",
|
||||
"stopped_tp": "止盈",
|
||||
"stopped_manual": "手动平仓",
|
||||
}
|
||||
|
||||
|
||||
def _row_dict(row) -> dict:
|
||||
if row is None:
|
||||
return {}
|
||||
try:
|
||||
return dict(row)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _hold_minutes(hold_seconds: int) -> int:
|
||||
try:
|
||||
return max(0, int(round(float(hold_seconds) / 60.0)))
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def backfill_one(conn: sqlite3.Connection, snap: dict, *, apply: bool) -> dict:
|
||||
plan_id = int(snap.get("source_id") or 0)
|
||||
if plan_id <= 0:
|
||||
return {"plan_id": plan_id, "skipped": True, "reason": "invalid source_id"}
|
||||
exists = conn.execute(
|
||||
"SELECT id FROM trade_records WHERE trend_plan_id=? LIMIT 1", (plan_id,)
|
||||
).fetchone()
|
||||
if exists:
|
||||
return {"plan_id": plan_id, "skipped": True, "reason": "trade_exists"}
|
||||
|
||||
try:
|
||||
payload = json.loads(snap.get("snapshot_json") or "{}")
|
||||
except Exception:
|
||||
payload = {}
|
||||
|
||||
plan = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE id=?", (plan_id,)
|
||||
).fetchone()
|
||||
plan_d = _row_dict(plan)
|
||||
|
||||
symbol = snap.get("symbol") or plan_d.get("symbol") or payload.get("symbol")
|
||||
direction = snap.get("direction") or plan_d.get("direction") or payload.get("direction") or "long"
|
||||
result = (snap.get("result_label") or "").strip() or STATUS_TO_RESULT.get(
|
||||
plan_d.get("status") or "", "手动平仓"
|
||||
)
|
||||
opened_at = snap.get("opened_at") or plan_d.get("opened_at")
|
||||
closed_at = snap.get("closed_at")
|
||||
pnl_amount = snap.get("pnl_amount")
|
||||
if pnl_amount is None:
|
||||
pnl_amount = payload.get("pnl_amount")
|
||||
|
||||
trigger_price = payload.get("avg_entry_price") or plan_d.get("avg_entry_price")
|
||||
stop_loss = payload.get("stop_loss") or plan_d.get("stop_loss")
|
||||
take_profit = payload.get("take_profit") or plan_d.get("take_profit")
|
||||
margin_capital = payload.get("plan_margin_capital") or plan_d.get("plan_margin_capital")
|
||||
leverage = payload.get("leverage") or plan_d.get("leverage")
|
||||
|
||||
opened_ms = plan_d.get("opened_at_ms")
|
||||
closed_ms = None
|
||||
|
||||
hold_seconds = 0
|
||||
if opened_at and closed_at:
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
fmt = "%Y-%m-%d %H:%M:%S"
|
||||
o = datetime.strptime(str(opened_at).strip()[:19], fmt)
|
||||
c = datetime.strptime(str(closed_at).strip()[:19], fmt)
|
||||
hold_seconds = max(0, int((c - o).total_seconds()))
|
||||
except Exception:
|
||||
hold_seconds = 0
|
||||
|
||||
row = {
|
||||
"symbol": symbol,
|
||||
"monitor_type": MONITOR_TYPE_TREND_PULLBACK,
|
||||
"direction": direction,
|
||||
"trigger_price": trigger_price,
|
||||
"stop_loss": stop_loss,
|
||||
"initial_stop_loss": plan_d.get("initial_stop_loss") or stop_loss,
|
||||
"take_profit": take_profit,
|
||||
"margin_capital": margin_capital,
|
||||
"leverage": leverage,
|
||||
"pnl_amount": pnl_amount,
|
||||
"hold_seconds": hold_seconds,
|
||||
"trade_style": "trend_pullback",
|
||||
"result": result,
|
||||
"opened_at": opened_at,
|
||||
"opened_at_ms": opened_ms,
|
||||
"closed_at": closed_at,
|
||||
"closed_at_ms": closed_ms,
|
||||
"entry_reason": ENTRY_REASON_TREND_PULLBACK,
|
||||
"trend_plan_id": plan_id,
|
||||
}
|
||||
|
||||
if not apply:
|
||||
return {"plan_id": plan_id, "dry_run": True, "row": row}
|
||||
|
||||
conn.execute(
|
||||
"""INSERT INTO trade_records (
|
||||
symbol, monitor_type, direction, trigger_price, stop_loss, initial_stop_loss,
|
||||
take_profit, margin_capital, leverage, pnl_amount, hold_seconds, trade_style,
|
||||
hold_minutes, opened_at, opened_at_ms, closed_at, closed_at_ms, result,
|
||||
entry_reason, trend_plan_id
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
row["symbol"],
|
||||
row["monitor_type"],
|
||||
row["direction"],
|
||||
row["trigger_price"],
|
||||
row["stop_loss"],
|
||||
row["initial_stop_loss"],
|
||||
row["take_profit"],
|
||||
row["margin_capital"],
|
||||
row["leverage"],
|
||||
row["pnl_amount"],
|
||||
row["hold_seconds"],
|
||||
row["trade_style"],
|
||||
_hold_minutes(hold_seconds),
|
||||
row["opened_at"],
|
||||
row["opened_at_ms"],
|
||||
row["closed_at"],
|
||||
row["closed_at_ms"],
|
||||
row["result"],
|
||||
row["entry_reason"],
|
||||
row["trend_plan_id"],
|
||||
),
|
||||
)
|
||||
return {"plan_id": plan_id, "inserted": True}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--db", required=True, help="实例 sqlite 路径")
|
||||
ap.add_argument("--apply", action="store_true", help="写入数据库(默认 dry-run)")
|
||||
args = ap.parse_args()
|
||||
db_path = Path(args.db)
|
||||
if not db_path.is_file():
|
||||
print(f"数据库不存在: {db_path}")
|
||||
return 1
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
snaps = conn.execute(
|
||||
"""SELECT * FROM strategy_trade_snapshots
|
||||
WHERE strategy_type=? ORDER BY id DESC""",
|
||||
(STRATEGY_TREND,),
|
||||
).fetchall()
|
||||
out = []
|
||||
for s in snaps:
|
||||
r = backfill_one(conn, _row_dict(s), apply=args.apply)
|
||||
out.append(r)
|
||||
print(r)
|
||||
if args.apply:
|
||||
conn.commit()
|
||||
conn.close()
|
||||
inserted = sum(1 for x in out if x.get("inserted"))
|
||||
print(f"done: inserted={inserted} total_snapshots={len(snaps)} apply={args.apply}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user