Fix hub full-close double-booking trend plans.

Sync active plans after hub position close, merge final close snapshots per plan, and backfill missing trade records when ending an already-stopped plan.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-08 09:06:36 +08:00
parent e71bfe095c
commit cfa28e7f4e
6 changed files with 344 additions and 7 deletions
+145
View File
@@ -657,6 +657,136 @@ def _call_insert_trade_record(m, plan_id: int, kwargs: dict) -> None:
fn(**call)
def _best_trend_close_snapshot(conn, plan_id: int) -> dict | None:
from strategy_snapshot_lib import (
FINAL_TREND_CLOSE_LABELS,
STRATEGY_TREND,
_final_trend_close_rank,
)
rows = conn.execute(
f"""SELECT * FROM strategy_trade_snapshots
WHERE strategy_type=? AND source_id=?
AND result_label IN ({",".join("?" * len(FINAL_TREND_CLOSE_LABELS))})""",
(STRATEGY_TREND, int(plan_id), *FINAL_TREND_CLOSE_LABELS),
).fetchall()
if not rows:
return None
parsed = [_row_dict(row) for row in rows]
return max(
parsed,
key=lambda d: (
_final_trend_close_rank(str(d.get("result_label") or "")),
int(d.get("id") or 0),
),
)
def _ensure_trend_plan_trade_record(
cfg: dict, conn, plan_id: int, *, prefer_label: str = "手动平仓"
) -> bool:
"""计划已结束但 trade_records 缺失时,从策略快照补录一条。"""
if _trend_plan_trade_exists(conn, plan_id):
return True
m = _m(cfg)
plan = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE id=?", (int(plan_id),)
).fetchone()
if not plan:
return False
plan_d = _row_dict(plan)
snap = _best_trend_close_snapshot(conn, plan_id)
if not snap:
return False
try:
payload = json.loads(snap.get("snapshot_json") or "{}")
except Exception:
payload = {}
sym = snap.get("symbol") or plan_d.get("symbol") or payload.get("symbol")
direction = snap.get("direction") or plan_d.get("direction") or "long"
result = (prefer_label or "").strip() or (snap.get("result_label") or "").strip() 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")
avg_e = float(payload.get("avg_entry_price") or plan_d.get("avg_entry_price") or 0)
margin_cap = trend_effective_margin_capital(plan_d)
lev = int(plan_d.get("leverage") or 1)
hold_seconds = m.calc_hold_seconds(
opened_at or "",
m.parse_dt_for_trading_day(closed_at) or m.app_now(),
)
res = m.normalize_result_with_pnl(result, float(pnl_amount or 0))
risk_amt = m.calc_risk_amount_from_plan(
direction,
float(plan_d.get("add_upper") or 0),
float(plan_d.get("stop_loss") or 0),
float(plan_d.get("plan_margin_capital") or 0),
lev,
)
planned_rr = m.calc_rr_ratio(
direction,
avg_e,
float(plan_d.get("stop_loss") or 0),
float(plan_d.get("take_profit") or 0),
)
session_date = plan_d.get("session_date") or m.get_trading_day()
_bump_session_capital_no_commit(m, conn, session_date, float(pnl_amount or 0))
_call_insert_trade_record(
m,
plan_id,
dict(
conn=conn,
symbol=sym,
monitor_type=MONITOR_TYPE_TREND,
direction=direction,
trigger_price=avg_e,
stop_loss=float(plan_d.get("stop_loss") or 0),
initial_stop_loss=float(plan_d.get("initial_stop_loss") or plan_d.get("stop_loss") or 0),
take_profit=float(plan_d.get("take_profit") or 0),
margin_capital=margin_cap,
leverage=lev,
pnl_amount=pnl_amount,
hold_seconds=hold_seconds,
trade_style="trend_pullback",
risk_amount=risk_amt,
planned_rr=planned_rr,
actual_rr=m.calc_actual_rr(pnl_amount, risk_amt),
result=res,
opened_at=opened_at,
closed_at=closed_at,
entry_reason=ENTRY_REASON_TREND_PULLBACK,
),
)
conn.commit()
return True
def sync_trend_plans_after_external_close(
cfg: dict, conn, symbol: str, direction: str
) -> dict[str, Any]:
"""中控/外部全平后:结束仍 active 的同币种同向趋势计划(避免监控再记一条止损)。"""
m = _m(cfg)
sym = m.normalize_symbol_input(symbol) if hasattr(m, "normalize_symbol_input") else (symbol or "").strip()
if not sym:
return {"ok": False, "msg": "symbol 无效", "finalized": 0}
direction = (direction or "long").strip().lower()
rows = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE status='active' AND symbol=? AND direction=?",
(sym, direction),
).fetchall()
finalized = 0
for row in rows:
px = m.get_price(row["symbol"])
exit_p = float(px) if px is not None else 0.0
before = _trend_plan_trade_exists(conn, int(row["id"]))
_finalize_plan(cfg, conn, row, "手动平仓", exit_p)
if not before:
finalized += 1
return {"ok": True, "finalized": finalized, "symbol": sym, "direction": direction}
def _trend_plan_trade_exists(conn, plan_id: int) -> bool:
try:
return conn.execute(
@@ -1664,6 +1794,21 @@ def register_trend_routes(app: Flask, cfg: dict) -> None:
"SELECT * FROM trend_pullback_plans WHERE id=? AND status='active'", (pid,)
).fetchone()
if not row:
stopped = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE id=? "
"AND status IN ('stopped_sl','stopped_tp','stopped_manual')",
(pid,),
).fetchone()
if stopped and not _trend_plan_trade_exists(conn, pid):
try:
if _ensure_trend_plan_trade_record(cfg, conn, pid, prefer_label="手动平仓"):
conn.close()
flash("计划已结束,已补录缺失的交易记录")
return _redirect_trend()
except Exception as e:
conn.close()
flash(f"补录交易记录失败:{e}")
return _redirect_trend()
conn.close()
flash("未找到运行中的趋势回调计划")
return _redirect_trend()