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
+22 -6
View File
@@ -50,7 +50,9 @@ from fib_key_monitor_lib import (
from strategy_trade_labels import ( from strategy_trade_labels import (
STRATEGY_ENTRY_REASON_OPTIONS, STRATEGY_ENTRY_REASON_OPTIONS,
entry_reason_for_monitor_type, entry_reason_for_monitor_type,
handoff_trade_miss_reason,
trade_record_monitor_type as resolve_trade_record_monitor_type, trade_record_monitor_type as resolve_trade_record_monitor_type,
trend_plan_id_from_monitor_row,
) )
from journal_chart_lib import ( from journal_chart_lib import (
JOURNAL_CHART_DEFAULT_LIMIT, JOURNAL_CHART_DEFAULT_LIMIT,
@@ -3999,6 +4001,7 @@ def reconcile_external_closes(conn, days=None):
conn, conn,
symbol=r["symbol"], symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=r["direction"], direction=r["direction"],
trigger_price=r["trigger_price"], trigger_price=r["trigger_price"],
@@ -4014,7 +4017,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=result, result=result,
miss_reason=miss_reason, miss_reason=handoff_trade_miss_reason(miss_reason, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5380,6 +5383,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5395,7 +5399,10 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=res, result=res,
miss_reason="触发价已触达,仓位已由交易所止盈/止损或其他方式平掉(本地补记)", miss_reason=handoff_trade_miss_reason(
"触发价已触达,仓位已由交易所止盈/止损或其他方式平掉(本地补记)",
r,
),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5439,6 +5446,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5454,7 +5462,7 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit), 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"]), actual_rr=calc_actual_rr(record_pnl, r["risk_amount"]),
result=record_res, result=record_res,
miss_reason=record_miss, miss_reason=handoff_trade_miss_reason(record_miss, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=record_closed, closed_at=record_closed,
) )
@@ -5518,6 +5526,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5533,6 +5542,7 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=res, result=res,
miss_reason=handoff_trade_miss_reason(None, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5585,6 +5595,7 @@ def force_close_before_reset():
conn, conn,
symbol=r["symbol"], symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5600,7 +5611,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result="强制清仓", 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, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -7304,6 +7318,7 @@ def del_order(id):
conn, conn,
symbol=row["symbol"], symbol=row["symbol"],
monitor_type=trade_record_monitor_type(conn, row), monitor_type=trade_record_monitor_type(conn, row),
trend_plan_id=trend_plan_id_from_monitor_row(row),
key_signal_type=order_row_key_signal_type(row), key_signal_type=order_row_key_signal_type(row),
direction=row["direction"], direction=row["direction"],
trigger_price=row["trigger_price"], trigger_price=row["trigger_price"],
@@ -7319,7 +7334,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, row["risk_amount"]),
result="手动平仓", result="手动平仓",
miss_reason="用户手动删除订单触发平仓", miss_reason=handoff_trade_miss_reason("用户手动删除订单触发平仓", row),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -7360,6 +7375,7 @@ def del_order(id):
conn, conn,
symbol=row["symbol"], symbol=row["symbol"],
monitor_type=trade_record_monitor_type(conn, row), monitor_type=trade_record_monitor_type(conn, row),
trend_plan_id=trend_plan_id_from_monitor_row(row),
key_signal_type=order_row_key_signal_type(row), key_signal_type=order_row_key_signal_type(row),
direction=row["direction"], direction=row["direction"],
trigger_price=row["trigger_price"], trigger_price=row["trigger_price"],
@@ -7375,7 +7391,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, row["risk_amount"]),
result=result, result=result,
miss_reason=miss_reason, miss_reason=handoff_trade_miss_reason(miss_reason, row),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
+22 -6
View File
@@ -51,7 +51,9 @@ from fib_key_monitor_lib import (
from strategy_trade_labels import ( from strategy_trade_labels import (
STRATEGY_ENTRY_REASON_OPTIONS, STRATEGY_ENTRY_REASON_OPTIONS,
entry_reason_for_monitor_type, entry_reason_for_monitor_type,
handoff_trade_miss_reason,
trade_record_monitor_type as resolve_trade_record_monitor_type, trade_record_monitor_type as resolve_trade_record_monitor_type,
trend_plan_id_from_monitor_row,
) )
from journal_chart_lib import ( from journal_chart_lib import (
JOURNAL_CHART_DEFAULT_LIMIT, JOURNAL_CHART_DEFAULT_LIMIT,
@@ -3957,6 +3959,7 @@ def reconcile_external_closes(conn, days=None):
conn, conn,
symbol=r["symbol"], symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=r["direction"], direction=r["direction"],
trigger_price=r["trigger_price"], trigger_price=r["trigger_price"],
@@ -3972,7 +3975,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=result, result=result,
miss_reason=miss_reason, miss_reason=handoff_trade_miss_reason(miss_reason, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5340,6 +5343,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5355,7 +5359,10 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=res, result=res,
miss_reason="触发价已触达,仓位已由交易所止盈/止损或其他方式平掉(本地补记)", miss_reason=handoff_trade_miss_reason(
"触发价已触达,仓位已由交易所止盈/止损或其他方式平掉(本地补记)",
r,
),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5399,6 +5406,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5414,7 +5422,7 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit), 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"]), actual_rr=calc_actual_rr(record_pnl, r["risk_amount"]),
result=record_res, result=record_res,
miss_reason=record_miss, miss_reason=handoff_trade_miss_reason(record_miss, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=record_closed, closed_at=record_closed,
) )
@@ -5468,6 +5476,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5483,6 +5492,7 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=res, result=res,
miss_reason=handoff_trade_miss_reason(None, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5535,6 +5545,7 @@ def force_close_before_reset():
conn, conn,
symbol=r["symbol"], symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5550,7 +5561,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result="强制清仓", 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, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -7458,6 +7472,7 @@ def del_order(id):
conn, conn,
symbol=row["symbol"], symbol=row["symbol"],
monitor_type=trade_record_monitor_type(conn, row), monitor_type=trade_record_monitor_type(conn, row),
trend_plan_id=trend_plan_id_from_monitor_row(row),
key_signal_type=order_row_key_signal_type(row), key_signal_type=order_row_key_signal_type(row),
direction=row["direction"], direction=row["direction"],
trigger_price=row["trigger_price"], trigger_price=row["trigger_price"],
@@ -7473,7 +7488,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, row["risk_amount"]),
result="手动平仓", result="手动平仓",
miss_reason="用户手动删除订单触发平仓", miss_reason=handoff_trade_miss_reason("用户手动删除订单触发平仓", row),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -7515,6 +7530,7 @@ def del_order(id):
conn, conn,
symbol=row["symbol"], symbol=row["symbol"],
monitor_type=trade_record_monitor_type(conn, row), monitor_type=trade_record_monitor_type(conn, row),
trend_plan_id=trend_plan_id_from_monitor_row(row),
key_signal_type=order_row_key_signal_type(row), key_signal_type=order_row_key_signal_type(row),
direction=row["direction"], direction=row["direction"],
trigger_price=row["trigger_price"], trigger_price=row["trigger_price"],
@@ -7530,7 +7546,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, row["risk_amount"]),
result=result, result=result,
miss_reason=miss_reason, miss_reason=handoff_trade_miss_reason(miss_reason, row),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
+30 -71
View File
@@ -60,7 +60,9 @@ from journal_chart_lib import (
from hub_auth import request_allowed as hub_request_allowed from hub_auth import request_allowed as hub_request_allowed
from strategy_trade_labels import ( from strategy_trade_labels import (
STRATEGY_ENTRY_REASON_OPTIONS, STRATEGY_ENTRY_REASON_OPTIONS,
handoff_trade_miss_reason,
trade_record_monitor_type as resolve_trade_record_monitor_type, trade_record_monitor_type as resolve_trade_record_monitor_type,
trend_plan_id_from_monitor_row,
) )
from history_window_lib import ( from history_window_lib import (
PRESET_CUSTOM, PRESET_CUSTOM,
@@ -4151,6 +4153,7 @@ def reconcile_external_closes(conn, days=None):
conn, conn,
symbol=r["symbol"], symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
direction=r["direction"], direction=r["direction"],
trigger_price=r["trigger_price"], trigger_price=r["trigger_price"],
stop_loss=r["stop_loss"], 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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=result, result=result,
miss_reason=miss_reason, miss_reason=handoff_trade_miss_reason(miss_reason, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_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) _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): def _trend_weighted_avg(old_avg, old_amt, fill_px, add_amt):
try: try:
oa, aa = float(old_amt), float(add_amt) oa, aa = float(old_amt), float(add_amt)
@@ -5086,6 +5029,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
stop_loss=stop_loss, 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), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=res, result=res,
miss_reason="触发价已触达,仓位已由交易所止盈/止损或其他方式平掉(本地补记)", miss_reason=handoff_trade_miss_reason(
"触发价已触达,仓位已由交易所止盈/止损或其他方式平掉(本地补记)",
r,
),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5144,6 +5091,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
stop_loss=stop_loss, 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), 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"]), actual_rr=calc_actual_rr(record_pnl, r["risk_amount"]),
result=record_res, result=record_res,
miss_reason=record_miss, miss_reason=handoff_trade_miss_reason(record_miss, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=record_closed, closed_at=record_closed,
) )
@@ -5212,6 +5160,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
stop_loss=stop_loss, 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), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=res, result=res,
miss_reason=handoff_trade_miss_reason(None, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5277,6 +5227,7 @@ def force_close_before_reset():
conn, conn,
symbol=r["symbol"], symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
stop_loss=r["stop_loss"], 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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result="强制清仓", 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, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -6660,13 +6614,17 @@ def trend_pullback_breakeven(pid):
conn.close() conn.close()
flash("未找到运行中的趋势回调计划") flash("未找到运行中的趋势回调计划")
return redirect(url_for("strategy_trading_page")) 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.commit()
conn.close() conn.close()
if ok: flash(
flash("手动保本:交易所止损已按均价+偏移更新") "已保本:趋势计划已结束,持仓已移交下单监控并挂止盈止损;平仓后将写入交易记录"
else: if ok
flash(err or "手动保本失败") else (err or "保本移交失败")
)
return redirect(url_for("strategy_trend_page")) return redirect(url_for("strategy_trend_page"))
@@ -6988,6 +6946,7 @@ def del_order(id):
conn, conn,
symbol=row["symbol"], symbol=row["symbol"],
monitor_type=trade_record_monitor_type(conn, row), monitor_type=trade_record_monitor_type(conn, row),
trend_plan_id=trend_plan_id_from_monitor_row(row),
direction=row["direction"], direction=row["direction"],
trigger_price=row["trigger_price"], trigger_price=row["trigger_price"],
stop_loss=row["stop_loss"], 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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, row["risk_amount"]),
result="手动平仓", result="手动平仓",
miss_reason="用户手动删除订单触发平仓", miss_reason=handoff_trade_miss_reason("用户手动删除订单触发平仓", row),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, row["risk_amount"]),
result=result, result=result,
miss_reason=miss_reason, miss_reason=handoff_trade_miss_reason(miss_reason, row),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -49,7 +49,7 @@
5. 再次校验:预览未过期;**当前可用余额**与预览快照相对偏差 ≤ `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT`(默认 **5%**),否则拒绝执行并要求重新预览。 5. 再次校验:预览未过期;**当前可用余额**与预览快照相对偏差 ≤ `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT`(默认 **5%**),否则拒绝执行并要求重新预览。
6. **首仓****立即市价** 开立 **总计划张数 × 50%**(不附带交易所止盈单)。 6. **首仓****立即市价** 开立 **总计划张数 × 50%**(不附带交易所止盈单)。
7. **止损**:撤销旧条件单后,挂 **仅止损** 的仓位触发单;之后每次补仓成交会 **刷新** 止损挂单。 7. **止损**:撤销旧条件单后,挂 **仅止损** 的仓位触发单;之后每次补仓成交会 **刷新** 止损挂单。
7b. **手动保本**(可选):首仓完成且交易所有持仓后,可在运行中计划卡片点击「应用保本止损」——将止损移至 **持仓均价 ± 偏移%**(默认 **+0.3%** 多 / **0.3%** 空)仅当新止损 **优于** 当前止损时生效,并同步 Gate 仓位止损单 7b. **保本移交下单监控**(可选):首仓完成且交易所有持仓后,可点击「保本移交下单监控」——将止损移至 **持仓均价 ± 偏移%**(默认 **+0.3%** 多 / **0.3%** 空)仅当新止损 **优于** 当前止损时生效;**本次趋势计划随即结束**,持仓写入 **下单监控**(备注 **趋势回调计划**),交易所在 **同一时刻挂保本止损 + 计划止盈**;后续无论中控平仓或交易所手动平仓,均经下单监控轮询 **`reconcile_external_closes` / `check_order_monitors`** 写入 **交易记录**(含 `trend_plan_id`、开仓类型「趋势回调」),供人工核对
8. **补仓**:当价格 **穿越** 下一档触发价(做多为自上向下穿越,做空为自下向上穿越)时,按该档张数 **市价加仓**;直至 `N` 档执行完毕或计划结束。 8. **补仓**:当价格 **穿越** 下一档触发价(做多为自上向下穿越,做空为自下向上穿越)时,按该档张数 **市价加仓**;直至 `N` 档执行完毕或计划结束。
9. **止盈监控**:后台线程若发现价格触及止盈,则 **市价全平** 9. **止盈监控**:后台线程若发现价格触及止盈,则 **市价全平**
10. **止损触发**:若仓位被交易所止损打光,本地检测到 **持仓为 0** 后记账为 **止损** 并结束计划。 10. **止损触发**:若仓位被交易所止损打光,本地检测到 **持仓为 0** 后记账为 **止损** 并结束计划。
@@ -88,7 +88,7 @@
| 开仓 | 单次市价 + 条件止盈+止损 | 首仓 50% 市价 + 多档补仓 + **仅止损在交易所** | | 开仓 | 单次市价 + 条件止盈+止损 | 首仓 50% 市价 + 多档补仓 + **仅止损在交易所** |
| 止盈 | 条件单 + 本地监控 | **仅本地监控市价止盈** | | 止盈 | 条件单 + 本地监控 | **仅本地监控市价止盈** |
| 仓位基数 | 以损定仓(表单/会话基数) | **可用余额快照 × 风险比例** 推导 | | 仓位基数 | 以损定仓(表单/会话基数) | **可用余额快照 × 风险比例** 推导 |
| 移动保本 | 支持(按 R 自动上移) | **手动保本**首仓后有持仓即可点;默认均价 +0.3%,可改偏移;同步 Gate 止损单**无**自动 R 保本) | | 移动保本 | 支持(按 R 自动上移) | **保本移交**结束计划→下单监控;交易所 TP+SL**无**自动 R 保本) |
--- ---
+22 -6
View File
@@ -50,7 +50,9 @@ from fib_key_monitor_lib import (
from strategy_trade_labels import ( from strategy_trade_labels import (
STRATEGY_ENTRY_REASON_OPTIONS, STRATEGY_ENTRY_REASON_OPTIONS,
entry_reason_for_monitor_type, entry_reason_for_monitor_type,
handoff_trade_miss_reason,
trade_record_monitor_type as resolve_trade_record_monitor_type, trade_record_monitor_type as resolve_trade_record_monitor_type,
trend_plan_id_from_monitor_row,
) )
from okx_orders_lib import cancel_okx_all_open_orders, fetch_okx_all_open_orders from okx_orders_lib import cancel_okx_all_open_orders, fetch_okx_all_open_orders
from journal_chart_lib import ( from journal_chart_lib import (
@@ -3392,6 +3394,7 @@ def reconcile_external_closes(conn, days=None):
conn, conn,
symbol=r["symbol"], symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=r["direction"], direction=r["direction"],
trigger_price=r["trigger_price"], trigger_price=r["trigger_price"],
@@ -3407,7 +3410,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=result, result=result,
miss_reason=miss_reason, miss_reason=handoff_trade_miss_reason(miss_reason, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5230,6 +5233,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5245,7 +5249,10 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=res, result=res,
miss_reason="触发价已触达,仓位已由交易所止盈/止损或其他方式平掉(本地补记)", miss_reason=handoff_trade_miss_reason(
"触发价已触达,仓位已由交易所止盈/止损或其他方式平掉(本地补记)",
r,
),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5288,6 +5295,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5303,7 +5311,7 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit), 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"]), actual_rr=calc_actual_rr(record_pnl, r["risk_amount"]),
result=record_res, result=record_res,
miss_reason=record_miss, miss_reason=handoff_trade_miss_reason(record_miss, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=record_closed, closed_at=record_closed,
) )
@@ -5356,6 +5364,7 @@ def check_order_monitors():
conn, conn,
symbol=sym, symbol=sym,
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5371,6 +5380,7 @@ def check_order_monitors():
planned_rr=calc_rr_ratio(direction, trigger_price, r["initial_stop_loss"] or stop_loss, take_profit), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=res, result=res,
miss_reason=handoff_trade_miss_reason(None, r),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -5421,6 +5431,7 @@ def force_close_before_reset():
conn, conn,
symbol=r["symbol"], symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r), monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r), key_signal_type=order_row_key_signal_type(r),
direction=direction, direction=direction,
trigger_price=trigger_price, trigger_price=trigger_price,
@@ -5436,7 +5447,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result="强制清仓", 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, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -7081,6 +7095,7 @@ def del_order(id):
conn, conn,
symbol=row["symbol"], symbol=row["symbol"],
monitor_type=trade_record_monitor_type(conn, row), monitor_type=trade_record_monitor_type(conn, row),
trend_plan_id=trend_plan_id_from_monitor_row(row),
key_signal_type=order_row_key_signal_type(row), key_signal_type=order_row_key_signal_type(row),
direction=row["direction"], direction=row["direction"],
trigger_price=row["trigger_price"], trigger_price=row["trigger_price"],
@@ -7096,7 +7111,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, row["risk_amount"]),
result="手动平仓", result="手动平仓",
miss_reason="用户手动删除订单触发平仓", miss_reason=handoff_trade_miss_reason("用户手动删除订单触发平仓", row),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
@@ -7135,6 +7150,7 @@ def del_order(id):
conn, conn,
symbol=row["symbol"], symbol=row["symbol"],
monitor_type=trade_record_monitor_type(conn, row), monitor_type=trade_record_monitor_type(conn, row),
trend_plan_id=trend_plan_id_from_monitor_row(row),
key_signal_type=order_row_key_signal_type(row), key_signal_type=order_row_key_signal_type(row),
direction=row["direction"], direction=row["direction"],
trigger_price=row["trigger_price"], trigger_price=row["trigger_price"],
@@ -7150,7 +7166,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"]), 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"]), actual_rr=calc_actual_rr(pnl_amount, row["risk_amount"]),
result=result, result=result,
miss_reason=miss_reason, miss_reason=handoff_trade_miss_reason(miss_reason, row),
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
) )
+1
View File
@@ -53,6 +53,7 @@ KEY_ENTRY_REASON_BY_SIGNAL = {
"收敛突破": "关键位收敛突破", "收敛突破": "关键位收敛突破",
"斐波回调0.618": "关键位斐波0.618", "斐波回调0.618": "关键位斐波0.618",
"斐波回调0.786": "关键位斐波0.786", "斐波回调0.786": "关键位斐波0.786",
"趋势回调": "趋势回调",
} }
+3
View File
@@ -147,6 +147,9 @@ def init_strategy_tables(conn) -> None:
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN outcome TEXT DEFAULT 'open'", "ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN outcome TEXT DEFAULT 'open'",
"ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN executed_plan_id INTEGER", "ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN executed_plan_id INTEGER",
"ALTER TABLE trade_records ADD COLUMN trend_plan_id INTEGER", "ALTER TABLE trade_records ADD COLUMN trend_plan_id INTEGER",
"ALTER TABLE order_monitors ADD COLUMN trend_plan_id INTEGER",
"ALTER TABLE order_monitors ADD COLUMN monitor_type TEXT",
"ALTER TABLE order_monitors ADD COLUMN key_signal_type TEXT",
): ):
try: try:
conn.execute(ddl) conn.execute(ddl)
+3 -3
View File
@@ -156,12 +156,12 @@
</div> </div>
</div> </div>
<div class="plan-card-meta" style="margin-top:8px"> <div class="plan-card-meta" style="margin-top:8px">
<form action="{{ url_for('trend_pullback_breakeven', pid=t.id) }}" method="post" class="form-row" style="margin:0;align-items:center" onsubmit="return confirm('将交易所止损移至持仓均价+偏移?仅当新止损优于当前止损时生效。');"> <form action="{{ url_for('trend_pullback_breakeven', pid=t.id) }}" method="post" class="form-row" style="margin:0;align-items:center" onsubmit="return confirm('确认保本?将结束本趋势计划,持仓移交「下单监控」(备注趋势回调计划),并在交易所同时挂保本止损与计划止盈;后续平仓会写入交易记录。');">
<label style="font-size:.78rem;color:#cfd3ef;display:flex;align-items:center;gap:6px"> <label style="font-size:.78rem;color:#cfd3ef;display:flex;align-items:center;gap:6px">
手动保本 偏移% 保本移交 偏移%
<input name="breakeven_offset_pct" type="number" min="0" step="0.01" value="{{ trend_manual_breakeven_offset_pct }}" style="width:72px;padding:4px 8px"> <input name="breakeven_offset_pct" type="number" min="0" step="0.01" value="{{ trend_manual_breakeven_offset_pct }}" style="width:72px;padding:4px 8px">
</label> </label>
<button type="submit" style="padding:6px 12px;background:#1f4a3a;color:#8fc8ff">应用保本止损</button> <button type="submit" style="padding:6px 12px;background:#1f4a3a;color:#8fc8ff">保本移交下单监控</button>
{% if t.breakeven_applied %}<span style="color:#6ab88a;font-size:.75rem">已保本 {{ (t.breakeven_applied_at or '')[:16] }}</span>{% endif %} {% if t.breakeven_applied %}<span style="color:#6ab88a;font-size:.75rem">已保本 {{ (t.breakeven_applied_at or '')[:16] }}</span>{% endif %}
</form> </form>
</div> </div>
+34
View File
@@ -1,6 +1,8 @@
"""策略交易写入 trade_records 时的类型与复盘开仓类型标注。""" """策略交易写入 trade_records 时的类型与复盘开仓类型标注。"""
from __future__ import annotations from __future__ import annotations
from typing import Optional
MONITOR_TYPE_TREND_PULLBACK = "趋势回调" MONITOR_TYPE_TREND_PULLBACK = "趋势回调"
MONITOR_TYPE_ROLL = "顺势加仓" MONITOR_TYPE_ROLL = "顺势加仓"
@@ -12,6 +14,38 @@ STRATEGY_ENTRY_REASON_OPTIONS = (
ENTRY_REASON_ROLL, ENTRY_REASON_ROLL,
) )
# 趋势回调保本移交下单监控:order_monitors.key_signal_type / 平仓备注
TREND_HANDOFF_KEY_SIGNAL = ENTRY_REASON_TREND_PULLBACK
TREND_HANDOFF_TRADE_NOTE = "趋势回调计划"
def handoff_trade_miss_reason(miss_reason, row) -> Optional[str]:
"""趋势保本移交的监控单平仓:交易记录备注带来源。"""
if trend_plan_id_from_monitor_row(row) is None:
return miss_reason
base = (miss_reason or "").strip()
if TREND_HANDOFF_TRADE_NOTE in base:
return base or TREND_HANDOFF_TRADE_NOTE
if base:
return f"{TREND_HANDOFF_TRADE_NOTE}{base}"
return TREND_HANDOFF_TRADE_NOTE
def trend_plan_id_from_monitor_row(row) -> Optional[int]:
if row is None:
return None
try:
keys = row.keys() if hasattr(row, "keys") else []
except Exception:
keys = []
if "trend_plan_id" not in keys or row["trend_plan_id"] in (None, ""):
return None
try:
tid = int(row["trend_plan_id"])
return tid if tid > 0 else None
except (TypeError, ValueError):
return None
def order_had_roll_fills(conn, order_monitor_id) -> bool: def order_had_roll_fills(conn, order_monitor_id) -> bool:
try: try:
+9
View File
@@ -74,6 +74,15 @@ def trend_market_close(cfg: dict, exchange_symbol: str, direction: str, pos_qty:
return ex.create_order(exchange_symbol, "market", side, amt, None, {"reduceOnly": True}) return ex.create_order(exchange_symbol, "market", side, amt, None, {"reduceOnly": True})
def trend_replace_tpsl(cfg: dict, order_row: dict, stop_loss: float, take_profit: float) -> None:
"""趋势保本移交:先撤条件单再挂保本止损 + 计划止盈(与下单监控一致)。"""
m = _m(cfg)
fn = getattr(m, "replace_active_monitor_tpsl_on_exchange", None)
if not callable(fn):
raise RuntimeError("当前实例未配置止盈止损同步能力")
fn(order_row, float(stop_loss), float(take_profit))
def cancel_symbol_orders(cfg: dict, exchange_symbol: str) -> None: def cancel_symbol_orders(cfg: dict, exchange_symbol: str) -> None:
m = _m(cfg) m = _m(cfg)
if hasattr(m, "cancel_all_open_orders_for_symbol"): if hasattr(m, "cancel_all_open_orders_for_symbol"):
+184 -6
View File
@@ -18,6 +18,7 @@ from strategy_trend_exchange import (
trend_market_add, trend_market_add,
trend_market_close, trend_market_close,
trend_refresh_stop_only, trend_refresh_stop_only,
trend_replace_tpsl,
) )
from strategy_trend_lib import ( from strategy_trend_lib import (
build_grid_prices, build_grid_prices,
@@ -28,6 +29,8 @@ from strategy_trend_lib import (
from strategy_trade_labels import ( from strategy_trade_labels import (
ENTRY_REASON_TREND_PULLBACK, ENTRY_REASON_TREND_PULLBACK,
MONITOR_TYPE_TREND_PULLBACK, MONITOR_TYPE_TREND_PULLBACK,
TREND_HANDOFF_KEY_SIGNAL,
TREND_HANDOFF_TRADE_NOTE,
) )
MONITOR_TYPE_TREND = MONITOR_TYPE_TREND_PULLBACK MONITOR_TYPE_TREND = MONITOR_TYPE_TREND_PULLBACK
@@ -600,7 +603,117 @@ def check_trend_pullback_plans(cfg: dict) -> None:
conn.close() 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]]: def apply_manual_breakeven(cfg: dict, conn, row, offset_pct=None) -> tuple[bool, Optional[str]]:
"""保本:结束趋势计划,持仓移交下单监控(备注趋势回调),交易所同时挂保本止损与止盈。"""
m = _m(cfg) m = _m(cfg)
if (row["status"] or "").strip() != "active": if (row["status"] or "").strip() != "active":
return False, "计划已结束" return False, "计划已结束"
@@ -610,10 +723,18 @@ def apply_manual_breakeven(cfg: dict, conn, row, offset_pct=None) -> tuple[bool,
if avg_e <= 0: if avg_e <= 0:
return False, "缺少有效持仓均价" return False, "缺少有效持仓均价"
direction = (row["direction"] or "long").lower() 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) pos = m.get_live_position_contracts(ex_sym, direction)
if pos is None or float(pos) <= 0: if pos is None or float(pos) <= 0:
return False, "交易所当前无该方向持仓" 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) be_fn = getattr(m, "calc_trend_manual_breakeven_stop", None)
if not callable(be_fn): if not callable(be_fn):
pct = float(offset_pct if offset_pct is not None else cfg["breakeven_offset_pct"]) 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: if new_sl is None:
return False, "保本价经交易所精度舍入后无效" return False, "保本价经交易所精度舍入后无效"
new_sl = float(new_sl) 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) cur_sl = float(row["stop_loss"] or 0)
if direction == "long": if direction == "long":
if new_sl <= cur_sl: if new_sl <= cur_sl:
@@ -636,15 +760,65 @@ def apply_manual_breakeven(cfg: dict, conn, row, offset_pct=None) -> tuple[bool,
else: else:
if new_sl >= cur_sl: if new_sl >= cur_sl:
return False, f"新止损 {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: try:
trend_refresh_stop_only(cfg, ex_sym, direction, new_sl) trend_replace_tpsl(cfg, handoff_row, new_sl, tp)
except Exception as e: except Exception as e:
fe = getattr(m, "friendly_exchange_error", None) fe = getattr(m, "friendly_exchange_error", None)
return False, fe(e) if callable(fe) else str(e) return False, fe(e) if callable(fe) else str(e)
conn.execute( now_s = m.app_now_str()
"UPDATE trend_pullback_plans SET stop_loss=?, breakeven_applied=1, breakeven_applied_at=? WHERE id=?", _TREND_FLAT_STREAK.pop(plan_id, None)
(new_sl, m.app_now_str(), row["id"]), 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 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) ok, err = apply_manual_breakeven(cfg, conn, row, offset_pct=offset_pct)
conn.commit() conn.commit()
conn.close() conn.close()
flash("已手动保本" if ok else (err or "手动保本失败")) flash(
"已保本:趋势计划已结束,持仓已移交下单监控并挂止盈止损;平仓后将写入交易记录"
if ok
else (err or "保本移交失败")
)
return _redirect_trend() return _redirect_trend()
@app.route("/stop_trend_pullback/<int:pid>") @app.route("/stop_trend_pullback/<int:pid>")