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
+30 -71
View File
@@ -60,7 +60,9 @@ from journal_chart_lib import (
from hub_auth import request_allowed as hub_request_allowed
from strategy_trade_labels import (
STRATEGY_ENTRY_REASON_OPTIONS,
handoff_trade_miss_reason,
trade_record_monitor_type as resolve_trade_record_monitor_type,
trend_plan_id_from_monitor_row,
)
from history_window_lib import (
PRESET_CUSTOM,
@@ -4151,6 +4153,7 @@ def reconcile_external_closes(conn, days=None):
conn,
symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
direction=r["direction"],
trigger_price=r["trigger_price"],
stop_loss=r["stop_loss"],
@@ -4165,7 +4168,7 @@ def reconcile_external_closes(conn, days=None):
planned_rr=calc_rr_ratio(r["direction"], r["trigger_price"], r["initial_stop_loss"] or r["stop_loss"], r["take_profit"]),
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=result,
miss_reason=miss_reason,
miss_reason=handoff_trade_miss_reason(miss_reason, r),
opened_at=opened_at,
closed_at=closed_at,
)
@@ -4545,66 +4548,6 @@ def _trend_refresh_stop_only(exchange_symbol, direction, stop_loss):
_gate_place_stop_loss_only_position(exchange_symbol, direction, stop_loss)
def apply_trend_pullback_manual_breakeven(conn, row, offset_pct=None):
"""运行中趋势计划:将交易所止损移至均价+偏移(默认 0.3%),仅当新止损更优时生效。"""
if (row["status"] or "").strip() != "active":
return False, "计划已结束"
if not int(row["first_order_done"] or 0):
return False, "尚未完成首仓,无法保本"
avg_e = float(row["avg_entry_price"] or 0)
if avg_e <= 0:
return False, "缺少有效持仓均价"
direction = (row["direction"] or "long").lower()
ex_sym = row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"])
pos = get_live_position_contracts(ex_sym, direction)
if pos is None or float(pos) <= 0:
return False, "交易所当前无该方向持仓"
new_sl_raw = calc_trend_manual_breakeven_stop(direction, avg_e, offset_pct)
if new_sl_raw is None:
return False, "保本价计算失败"
new_sl = round_price_to_exchange(ex_sym, new_sl_raw)
if new_sl is None:
return False, "保本价经交易所精度舍入后无效"
new_sl = float(new_sl)
cur_sl = float(row["stop_loss"] or 0)
if direction == "long":
if new_sl <= cur_sl:
return False, f"新止损 {new_sl} 未高于当前止损 {cur_sl}(多仓需上移)"
else:
if new_sl >= cur_sl:
return False, f"新止损 {new_sl} 未低于当前止损 {cur_sl}(空仓需下移)"
try:
_trend_refresh_stop_only(ex_sym, direction, new_sl)
except Exception as e:
return False, friendly_exchange_error(e)
now_s = app_now_str()
conn.execute(
"UPDATE trend_pullback_plans SET stop_loss=?, breakeven_applied=1, breakeven_applied_at=? WHERE id=?",
(new_sl, now_s, row["id"]),
)
pct_used = float(
offset_pct
if offset_pct is not None
else TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT
)
sym = row["symbol"]
send_wechat_msg(
"\n".join(
[
f"# ✅ {sym} 趋势回调手动保本",
f"**账户:{_wechat_account_label()}**",
f"- 计划 ID**{row['id']}**",
f"- 方向:{_wechat_direction_text(direction)}",
f"- 持仓均价:{format_price_for_symbol(sym, avg_e)}",
f"- 偏移:{pct_used}%(相对均价)",
f"- 新止损:{format_price_for_symbol(sym, new_sl)}",
f"- 交易所:已更新仓位止损触发单",
]
)
)
return True, None
def _trend_weighted_avg(old_avg, old_amt, fill_px, add_amt):
try:
oa, aa = float(old_amt), float(add_amt)
@@ -5086,6 +5029,7 @@ def check_order_monitors():
conn,
symbol=sym,
monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
direction=direction,
trigger_price=trigger_price,
stop_loss=stop_loss,
@@ -5100,7 +5044,10 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit),
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=res,
miss_reason="触发价已触达,仓位已由交易所止盈/止损或其他方式平掉(本地补记)",
miss_reason=handoff_trade_miss_reason(
"触发价已触达,仓位已由交易所止盈/止损或其他方式平掉(本地补记)",
r,
),
opened_at=opened_at,
closed_at=closed_at,
)
@@ -5144,6 +5091,7 @@ def check_order_monitors():
conn,
symbol=sym,
monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
direction=direction,
trigger_price=trigger_price,
stop_loss=stop_loss,
@@ -5158,7 +5106,7 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit),
actual_rr=calc_actual_rr(record_pnl, r["risk_amount"]),
result=record_res,
miss_reason=record_miss,
miss_reason=handoff_trade_miss_reason(record_miss, r),
opened_at=opened_at,
closed_at=record_closed,
)
@@ -5212,6 +5160,7 @@ def check_order_monitors():
conn,
symbol=sym,
monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
direction=direction,
trigger_price=trigger_price,
stop_loss=stop_loss,
@@ -5226,6 +5175,7 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit),
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=res,
miss_reason=handoff_trade_miss_reason(None, r),
opened_at=opened_at,
closed_at=closed_at,
)
@@ -5277,6 +5227,7 @@ def force_close_before_reset():
conn,
symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
direction=direction,
trigger_price=trigger_price,
stop_loss=r["stop_loss"],
@@ -5291,7 +5242,10 @@ def force_close_before_reset():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or r["stop_loss"], r["take_profit"]),
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result="强制清仓",
miss_reason=f"北京时间 {FORCE_CLOSE_BJ_HOUR}:00 整点风控清仓",
miss_reason=handoff_trade_miss_reason(
f"北京时间 {FORCE_CLOSE_BJ_HOUR}:00 整点风控清仓",
r,
),
opened_at=opened_at,
closed_at=closed_at,
)
@@ -6660,13 +6614,17 @@ def trend_pullback_breakeven(pid):
conn.close()
flash("未找到运行中的趋势回调计划")
return redirect(url_for("strategy_trading_page"))
ok, err = apply_trend_pullback_manual_breakeven(conn, row, offset_pct=offset_pct)
from strategy_trend_register import apply_manual_breakeven, build_trend_config
cfg = build_trend_config(sys.modules[__name__])
ok, err = apply_manual_breakeven(cfg, conn, row, offset_pct=offset_pct)
conn.commit()
conn.close()
if ok:
flash("手动保本:交易所止损已按均价+偏移更新")
else:
flash(err or "手动保本失败")
flash(
"已保本:趋势计划已结束,持仓已移交下单监控并挂止盈止损;平仓后将写入交易记录"
if ok
else (err or "保本移交失败")
)
return redirect(url_for("strategy_trend_page"))
@@ -6988,6 +6946,7 @@ def del_order(id):
conn,
symbol=row["symbol"],
monitor_type=trade_record_monitor_type(conn, row),
trend_plan_id=trend_plan_id_from_monitor_row(row),
direction=row["direction"],
trigger_price=row["trigger_price"],
stop_loss=row["stop_loss"],
@@ -7002,7 +6961,7 @@ def del_order(id):
planned_rr=calc_rr_ratio(row["direction"], row["trigger_price"], row["initial_stop_loss"] or row["stop_loss"], row["take_profit"]),
actual_rr=calc_actual_rr(pnl_amount, row["risk_amount"]),
result="手动平仓",
miss_reason="用户手动删除订单触发平仓",
miss_reason=handoff_trade_miss_reason("用户手动删除订单触发平仓", row),
opened_at=opened_at,
closed_at=closed_at,
)
@@ -7056,7 +7015,7 @@ def del_order(id):
planned_rr=calc_rr_ratio(row["direction"], row["trigger_price"], row["initial_stop_loss"] or row["stop_loss"], row["take_profit"]),
actual_rr=calc_actual_rr(pnl_amount, row["risk_amount"]),
result=result,
miss_reason=miss_reason,
miss_reason=handoff_trade_miss_reason(miss_reason, row),
opened_at=opened_at,
closed_at=closed_at,
)