feat(trend): 趋势回调保本移交下单监控并统一写交易记录
保本后结束趋势计划,持仓转入下单监控(备注趋势回调),交易所同时挂保本止损与计划止盈;中控或交易所平仓均经下单监控写入交易记录(trend_plan_id、开仓类型),四所共用 strategy_trend_register。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+184
-6
@@ -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>")
|
||||
|
||||
Reference in New Issue
Block a user