feat(trend): 趋势回调保本移交下单监控并统一写交易记录
保本后结束趋势计划,持仓转入下单监控(备注趋势回调),交易所同时挂保本止损与计划止盈;中控或交易所平仓均经下单监控写入交易记录(trend_plan_id、开仓类型),四所共用 strategy_trend_register。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -50,7 +50,9 @@ from fib_key_monitor_lib import (
|
||||
from strategy_trade_labels import (
|
||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||
entry_reason_for_monitor_type,
|
||||
handoff_trade_miss_reason,
|
||||
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
||||
trend_plan_id_from_monitor_row,
|
||||
)
|
||||
from journal_chart_lib import (
|
||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||
@@ -3999,6 +4001,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=r["direction"],
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
@@ -5380,6 +5383,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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),
|
||||
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,
|
||||
)
|
||||
@@ -5439,6 +5446,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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),
|
||||
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,
|
||||
)
|
||||
@@ -5518,6 +5526,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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),
|
||||
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,
|
||||
)
|
||||
@@ -5585,6 +5595,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
@@ -7304,6 +7318,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),
|
||||
key_signal_type=order_row_key_signal_type(row),
|
||||
direction=row["direction"],
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
@@ -7359,8 +7374,9 @@ def del_order(id):
|
||||
insert_trade_record(
|
||||
conn,
|
||||
symbol=row["symbol"],
|
||||
monitor_type=trade_record_monitor_type(conn, row),
|
||||
key_signal_type=order_row_key_signal_type(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),
|
||||
direction=row["direction"],
|
||||
trigger_price=row["trigger_price"],
|
||||
stop_loss=row["stop_loss"],
|
||||
@@ -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"]),
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -51,7 +51,9 @@ from fib_key_monitor_lib import (
|
||||
from strategy_trade_labels import (
|
||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||
entry_reason_for_monitor_type,
|
||||
handoff_trade_miss_reason,
|
||||
trade_record_monitor_type as resolve_trade_record_monitor_type,
|
||||
trend_plan_id_from_monitor_row,
|
||||
)
|
||||
from journal_chart_lib import (
|
||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||
@@ -3957,6 +3959,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=r["direction"],
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
@@ -5340,6 +5343,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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),
|
||||
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,
|
||||
)
|
||||
@@ -5399,6 +5406,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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),
|
||||
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,
|
||||
)
|
||||
@@ -5468,6 +5476,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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),
|
||||
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,
|
||||
)
|
||||
@@ -5535,6 +5545,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
@@ -7458,6 +7472,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),
|
||||
key_signal_type=order_row_key_signal_type(row),
|
||||
direction=row["direction"],
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
@@ -7515,6 +7530,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),
|
||||
key_signal_type=order_row_key_signal_type(row),
|
||||
direction=row["direction"],
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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 保本) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ from fib_key_monitor_lib import (
|
||||
from strategy_trade_labels import (
|
||||
STRATEGY_ENTRY_REASON_OPTIONS,
|
||||
entry_reason_for_monitor_type,
|
||||
handoff_trade_miss_reason,
|
||||
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 journal_chart_lib import (
|
||||
@@ -3392,6 +3394,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=r["direction"],
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
@@ -5230,6 +5233,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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),
|
||||
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,
|
||||
)
|
||||
@@ -5288,6 +5295,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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),
|
||||
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,
|
||||
)
|
||||
@@ -5356,6 +5364,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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),
|
||||
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,
|
||||
)
|
||||
@@ -5421,6 +5431,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),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=direction,
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
@@ -7081,6 +7095,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),
|
||||
key_signal_type=order_row_key_signal_type(row),
|
||||
direction=row["direction"],
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
@@ -7135,6 +7150,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),
|
||||
key_signal_type=order_row_key_signal_type(row),
|
||||
direction=row["direction"],
|
||||
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"]),
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@ KEY_ENTRY_REASON_BY_SIGNAL = {
|
||||
"收敛突破": "关键位收敛突破",
|
||||
"斐波回调0.618": "关键位斐波0.618",
|
||||
"斐波回调0.786": "关键位斐波0.786",
|
||||
"趋势回调": "趋势回调",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 executed_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:
|
||||
conn.execute(ddl)
|
||||
|
||||
@@ -156,12 +156,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
手动保本 偏移%
|
||||
保本移交 偏移%
|
||||
<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>
|
||||
<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 %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""策略交易写入 trade_records 时的类型与复盘开仓类型标注。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
MONITOR_TYPE_TREND_PULLBACK = "趋势回调"
|
||||
MONITOR_TYPE_ROLL = "顺势加仓"
|
||||
|
||||
@@ -12,6 +14,38 @@ STRATEGY_ENTRY_REASON_OPTIONS = (
|
||||
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:
|
||||
try:
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
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:
|
||||
m = _m(cfg)
|
||||
if hasattr(m, "cancel_all_open_orders_for_symbol"):
|
||||
|
||||
+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