diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index c03af31..2d58c45 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -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, ) diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 32895d2..3e19af0 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -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, ) diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 7fb1d04..8462534 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -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, ) diff --git a/crypto_monitor_gate_bot/趋势回调策略说明.md b/crypto_monitor_gate_bot/趋势回调策略说明.md index 932dbd5..9f4f07b 100644 --- a/crypto_monitor_gate_bot/趋势回调策略说明.md +++ b/crypto_monitor_gate_bot/趋势回调策略说明.md @@ -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 保本) | --- diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 3a8737b..1473186 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -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, ) diff --git a/fib_key_monitor_lib.py b/fib_key_monitor_lib.py index 9909cec..26df2b4 100644 --- a/fib_key_monitor_lib.py +++ b/fib_key_monitor_lib.py @@ -53,6 +53,7 @@ KEY_ENTRY_REASON_BY_SIGNAL = { "收敛突破": "关键位收敛突破", "斐波回调0.618": "关键位斐波0.618", "斐波回调0.786": "关键位斐波0.786", + "趋势回调": "趋势回调", } diff --git a/strategy_db.py b/strategy_db.py index eabde2f..bcb9884 100644 --- a/strategy_db.py +++ b/strategy_db.py @@ -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) diff --git a/strategy_templates/strategy_trend_panel.html b/strategy_templates/strategy_trend_panel.html index 24a32b9..6a8d7c0 100644 --- a/strategy_templates/strategy_trend_panel.html +++ b/strategy_templates/strategy_trend_panel.html @@ -156,12 +156,12 @@
-
+ - + {% if t.breakeven_applied %}已保本 {{ (t.breakeven_applied_at or '')[:16] }}{% endif %}
diff --git a/strategy_trade_labels.py b/strategy_trade_labels.py index be68f14..be3c7a9 100644 --- a/strategy_trade_labels.py +++ b/strategy_trade_labels.py @@ -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: diff --git a/strategy_trend_exchange.py b/strategy_trend_exchange.py index 4eac0f4..20de0d4 100644 --- a/strategy_trend_exchange.py +++ b/strategy_trend_exchange.py @@ -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"): diff --git a/strategy_trend_register.py b/strategy_trend_register.py index 4a2cfe0..b0327da 100644 --- a/strategy_trend_register.py +++ b/strategy_trend_register.py @@ -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/")