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,
)
@@ -49,7 +49,7 @@
5. 再次校验:预览未过期;**当前可用余额**与预览快照相对偏差 ≤ `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT`(默认 **5%**),否则拒绝执行并要求重新预览。
6. **首仓****立即市价** 开立 **总计划张数 × 50%**(不附带交易所止盈单)。
7. **止损**:撤销旧条件单后,挂 **仅止损** 的仓位触发单;之后每次补仓成交会 **刷新** 止损挂单。
7b. **手动保本**(可选):首仓完成且交易所有持仓后,可在运行中计划卡片点击「应用保本止损」——将止损移至 **持仓均价 ± 偏移%**(默认 **+0.3%** 多 / **0.3%** 空)仅当新止损 **优于** 当前止损时生效,并同步 Gate 仓位止损单
7b. **保本移交下单监控**(可选):首仓完成且交易所有持仓后,可点击「保本移交下单监控」——将止损移至 **持仓均价 ± 偏移%**(默认 **+0.3%** 多 / **0.3%** 空)仅当新止损 **优于** 当前止损时生效;**本次趋势计划随即结束**,持仓写入 **下单监控**(备注 **趋势回调计划**),交易所在 **同一时刻挂保本止损 + 计划止盈**;后续无论中控平仓或交易所手动平仓,均经下单监控轮询 **`reconcile_external_closes` / `check_order_monitors`** 写入 **交易记录**(含 `trend_plan_id`、开仓类型「趋势回调」),供人工核对
8. **补仓**:当价格 **穿越** 下一档触发价(做多为自上向下穿越,做空为自下向上穿越)时,按该档张数 **市价加仓**;直至 `N` 档执行完毕或计划结束。
9. **止盈监控**:后台线程若发现价格触及止盈,则 **市价全平**
10. **止损触发**:若仓位被交易所止损打光,本地检测到 **持仓为 0** 后记账为 **止损** 并结束计划。
@@ -88,7 +88,7 @@
| 开仓 | 单次市价 + 条件止盈+止损 | 首仓 50% 市价 + 多档补仓 + **仅止损在交易所** |
| 止盈 | 条件单 + 本地监控 | **仅本地监控市价止盈** |
| 仓位基数 | 以损定仓(表单/会话基数) | **可用余额快照 × 风险比例** 推导 |
| 移动保本 | 支持(按 R 自动上移) | **手动保本**首仓后有持仓即可点;默认均价 +0.3%,可改偏移;同步 Gate 止损单**无**自动 R 保本) |
| 移动保本 | 支持(按 R 自动上移) | **保本移交**结束计划→下单监控;交易所 TP+SL**无**自动 R 保本) |
---