feat(trend): 趋势回调保本移交下单监控并统一写交易记录

保本后结束趋势计划,持仓转入下单监控(备注趋势回调),交易所同时挂保本止损与计划止盈;中控或交易所平仓均经下单监控写入交易记录(trend_plan_id、开仓类型),四所共用 strategy_trend_register。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-03 17:01:34 +08:00
parent d9b1b324f9
commit e2bf58cfd3
11 changed files with 334 additions and 102 deletions
+184 -6
View File
@@ -18,6 +18,7 @@ from strategy_trend_exchange import (
trend_market_add,
trend_market_close,
trend_refresh_stop_only,
trend_replace_tpsl,
)
from strategy_trend_lib import (
build_grid_prices,
@@ -28,6 +29,8 @@ from strategy_trend_lib import (
from strategy_trade_labels import (
ENTRY_REASON_TREND_PULLBACK,
MONITOR_TYPE_TREND_PULLBACK,
TREND_HANDOFF_KEY_SIGNAL,
TREND_HANDOFF_TRADE_NOTE,
)
MONITOR_TYPE_TREND = MONITOR_TYPE_TREND_PULLBACK
@@ -600,7 +603,117 @@ def check_trend_pullback_plans(cfg: dict) -> None:
conn.close()
TREND_PLAN_STATUS_HANDOFF = "stopped_handoff"
def _order_monitor_manual_type(m) -> str:
return getattr(m, "ORDER_MONITOR_TYPE_MANUAL", None) or "下单监控"
def _insert_trend_handoff_order_monitor(
cfg: dict,
conn,
plan_row,
*,
new_sl: float,
pos_amt: float,
) -> int:
m = _m(cfg)
sym = plan_row["symbol"]
direction = (plan_row["direction"] or "long").lower()
ex_sym = plan_row["exchange_symbol"] or m.normalize_exchange_symbol(sym)
plan_id = int(plan_row["id"])
avg_e = float(plan_row["avg_entry_price"] or 0)
tp = float(plan_row["take_profit"] or 0)
lev = int(plan_row["leverage"] or 1)
margin_cap = float(plan_row["plan_margin_capital"] or 0)
init_sl = float(
plan_row["initial_stop_loss"]
if plan_row["initial_stop_loss"] not in (None, "")
else plan_row["stop_loss"]
or 0
)
risk_pct = float(plan_row["risk_percent"] or 5)
risk_amt = None
calc_risk = getattr(m, "calc_risk_amount_from_plan", None)
if callable(calc_risk):
try:
risk_amt = calc_risk(direction, avg_e, init_sl, margin_cap, lev)
except Exception:
risk_amt = None
be_rr = float(getattr(m, "BREAKEVEN_RR_TRIGGER", 1) or 1)
be_off = float(getattr(m, "BREAKEVEN_OFFSET_PCT", 0.3) or 0.3)
be_step = float(getattr(m, "BREAKEVEN_STEP_R", 1) or 1)
if direction == "short":
be_price = round(avg_e * (1 - be_off / 100.0), 8)
else:
be_price = round(avg_e * (1 + be_off / 100.0), 8)
rp = getattr(m, "round_price_to_exchange", None)
if callable(rp):
try:
be_price = float(rp(ex_sym, be_price) or be_price)
except Exception:
pass
opened_at = plan_row["opened_at"] or m.app_now_str()
to_ms = getattr(m, "_to_ms_with_fallback", None)
opened_ms = to_ms(plan_row["opened_at_ms"] if "opened_at_ms" in plan_row.keys() else None, opened_at) if callable(to_ms) else None
trading_day = plan_row["session_date"] or getattr(m, "get_trading_day", lambda: None)()
if not trading_day and callable(getattr(m, "get_trading_day", None)):
trading_day = m.get_trading_day()
notional = margin_cap * lev if margin_cap and lev else None
monitor_type = _order_monitor_manual_type(m)
conn.execute(
"INSERT INTO order_monitors "
"(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, "
"margin_capital, leverage, trade_style, risk_percent, risk_amount, "
"breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, "
"breakeven_enabled, notional_value, position_ratio, base_amount, order_amount, exchange_order_id, "
"opened_at, opened_at_ms, session_date, monitor_type, key_signal_type, trend_plan_id) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
sym,
ex_sym,
direction,
avg_e,
new_sl,
init_sl,
tp,
margin_cap,
lev,
"trend_pullback_handoff",
risk_pct,
risk_amt,
be_rr,
be_off,
be_step,
0,
be_price,
0,
notional,
None,
None,
float(pos_amt),
"",
opened_at,
opened_ms,
trading_day,
monitor_type,
TREND_HANDOFF_KEY_SIGNAL,
plan_id,
),
)
new_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
persist = getattr(m, "try_persist_exchange_margin_for_order", None)
if callable(persist):
try:
persist(conn, new_id, ex_sym, direction, order_leverage=lev)
except Exception:
pass
return new_id
def apply_manual_breakeven(cfg: dict, conn, row, offset_pct=None) -> tuple[bool, Optional[str]]:
"""保本:结束趋势计划,持仓移交下单监控(备注趋势回调),交易所同时挂保本止损与止盈。"""
m = _m(cfg)
if (row["status"] or "").strip() != "active":
return False, "计划已结束"
@@ -610,10 +723,18 @@ def apply_manual_breakeven(cfg: dict, conn, row, offset_pct=None) -> tuple[bool,
if avg_e <= 0:
return False, "缺少有效持仓均价"
direction = (row["direction"] or "long").lower()
ex_sym = row["exchange_symbol"] or m.normalize_exchange_symbol(row["symbol"])
sym = row["symbol"]
ex_sym = row["exchange_symbol"] or m.normalize_exchange_symbol(sym)
pos = m.get_live_position_contracts(ex_sym, direction)
if pos is None or float(pos) <= 0:
return False, "交易所当前无该方向持仓"
pos_amt = float(pos)
dup = conn.execute(
"SELECT id FROM order_monitors WHERE status='active' AND symbol=? AND direction=? LIMIT 1",
(sym, direction),
).fetchone()
if dup:
return False, "该币种已有运行中的下单监控,请先结束后再保本移交"
be_fn = getattr(m, "calc_trend_manual_breakeven_stop", None)
if not callable(be_fn):
pct = float(offset_pct if offset_pct is not None else cfg["breakeven_offset_pct"])
@@ -629,6 +750,9 @@ def apply_manual_breakeven(cfg: dict, conn, row, offset_pct=None) -> tuple[bool,
if new_sl is None:
return False, "保本价经交易所精度舍入后无效"
new_sl = float(new_sl)
tp = float(row["take_profit"] or 0)
if tp <= 0:
return False, "计划止盈价无效"
cur_sl = float(row["stop_loss"] or 0)
if direction == "long":
if new_sl <= cur_sl:
@@ -636,15 +760,65 @@ def apply_manual_breakeven(cfg: dict, conn, row, offset_pct=None) -> tuple[bool,
else:
if new_sl >= cur_sl:
return False, f"新止损 {new_sl} 未低于当前止损 {cur_sl}(空仓需下移)"
ok_live, live_reason = m.ensure_exchange_live_ready()
if not ok_live:
return False, live_reason or "实盘未就绪"
plan_id = int(row["id"])
handoff_row = {
"symbol": sym,
"exchange_symbol": ex_sym,
"direction": direction,
"order_amount": pos_amt,
}
try:
trend_refresh_stop_only(cfg, ex_sym, direction, new_sl)
trend_replace_tpsl(cfg, handoff_row, new_sl, tp)
except Exception as e:
fe = getattr(m, "friendly_exchange_error", None)
return False, fe(e) if callable(fe) else str(e)
conn.execute(
"UPDATE trend_pullback_plans SET stop_loss=?, breakeven_applied=1, breakeven_applied_at=? WHERE id=?",
(new_sl, m.app_now_str(), row["id"]),
now_s = m.app_now_str()
_TREND_FLAT_STREAK.pop(plan_id, None)
cur = conn.execute(
"UPDATE trend_pullback_plans SET status=?, message=?, stop_loss=?, "
"breakeven_applied=1, breakeven_applied_at=? WHERE id=? AND status='active'",
(
TREND_PLAN_STATUS_HANDOFF,
f"保本移交下单监控({TREND_HANDOFF_TRADE_NOTE}",
new_sl,
now_s,
plan_id,
),
)
if not getattr(cur, "rowcount", 0):
return False, "计划状态更新失败(可能已被其他操作结束)"
try:
mon_id = _insert_trend_handoff_order_monitor(
cfg, conn, row, new_sl=new_sl, pos_amt=pos_amt
)
except Exception as e:
conn.execute(
"UPDATE trend_pullback_plans SET status='active', message=? WHERE id=?",
(f"移交下单监控失败:{e}", plan_id),
)
return False, f"移交下单监控失败:{e}"
pct_used = float(
offset_pct if offset_pct is not None else cfg["breakeven_offset_pct"]
)
extra = getattr(m, "build_wechat_close_message", None)
send = getattr(m, "send_wechat_msg", None)
pf = getattr(m, "format_price_for_symbol", None)
fmt = (lambda s, p: pf(s, p)) if callable(pf) else (lambda _s, p: str(p))
if callable(send):
lines = [
f"# ✅ {sym} 趋势回调保本移交",
f"- 计划 ID**{plan_id}** → 下单监控 **#{mon_id}**",
f"- 备注:**{TREND_HANDOFF_TRADE_NOTE}**",
f"- 保本止损:{fmt(sym, new_sl)} 止盈:{fmt(sym, tp)}",
f"- 交易所:已挂止盈止损;平仓后将写入交易记录({ENTRY_REASON_TREND_PULLBACK}",
]
wl = getattr(m, "_wechat_account_label", None)
if callable(wl):
lines.insert(1, f"**账户:{wl()}**")
send("\n".join(lines))
return True, None
@@ -933,7 +1107,11 @@ def register_trend_routes(app: Flask, cfg: dict) -> None:
ok, err = apply_manual_breakeven(cfg, conn, row, offset_pct=offset_pct)
conn.commit()
conn.close()
flash("已手动保本" if ok else (err or "手动保本失败"))
flash(
"已保本:趋势计划已结束,持仓已移交下单监控并挂止盈止损;平仓后将写入交易记录"
if ok
else (err or "保本移交失败")
)
return _redirect_trend()
@app.route("/stop_trend_pullback/<int:pid>")