diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 1ad9f36..04bb0fe 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -293,6 +293,22 @@ def send_wechat_msg(content): pass +_BREAKEVEN_EXCHANGE_WARNED_IDS = set() + + +def _send_breakeven_exchange_warn_once(order_id, message): + """移动保本同步交易所失败:同一笔监控单只推送一次,避免轮询刷屏。""" + oid = int(order_id) + if oid in _BREAKEVEN_EXCHANGE_WARNED_IDS: + return + _BREAKEVEN_EXCHANGE_WARNED_IDS.add(oid) + send_wechat_msg(message) + + +def _clear_breakeven_exchange_warn(order_id): + _BREAKEVEN_EXCHANGE_WARNED_IDS.discard(int(order_id)) + + def _wechat_account_label(): return (os.getenv("OKX_ACCOUNT_LABEL") or "okx实盘子账户").strip() @@ -308,15 +324,25 @@ def _wechat_trading_capital_text(fallback=None): except Exception: trading_capital = None if trading_capital is not None: - return f"{round(float(trading_capital), 4)}U" + return f"{round(float(trading_capital), 2)}U" if fallback is not None: try: - return f"{round(float(fallback), 4)}U" + return f"{round(float(fallback), 2)}U" except Exception: pass return "-" +def format_wechat_scalar_2dp(value): + """企业微信推送:数值统一两位小数(与交易所 tick 无关)。""" + if value in (None, ""): + return "-" + try: + return f"{float(value):.2f}" + except (TypeError, ValueError): + return str(value) + + def build_wechat_close_message( symbol, direction, @@ -332,29 +358,40 @@ def build_wechat_close_message( session_capital_fallback=None, ): hold_txt = format_hold_minutes(calc_hold_minutes(hold_seconds)) if hold_seconds is not None else "-" + ep = format_price_for_symbol(symbol, trigger_price) + cp = format_price_for_symbol(symbol, current_price) + tp = format_price_for_symbol(symbol, take_profit) + sl = format_wechat_scalar_2dp(stop_loss) + cap_txt = _wechat_trading_capital_text(session_capital_fallback) + try: + if pnl_amount is not None: + pv = float(pnl_amount) + pnl_disp = f"{'+' if pv > 0 else ''}{round(pv, 2)} U" + else: + pnl_disp = "-" + except (TypeError, ValueError): + pnl_disp = "-" + lines = [ - f"# ✅ {symbol} 实盘平仓记录", - f"**账户:{_wechat_account_label()}**", + f"📉 {symbol} 平仓完成", + f"💼 账户:{_wechat_account_label()}", "", - "---", + "🧾 平仓概要", + f"🔖 平仓单号:{close_order_id or '-'}", + f"📌 方向:{_wechat_direction_text(direction)}", + f"📌 平仓结果:{result or '-'}", + f"💰 本单盈亏:{pnl_disp}", + f"⏱ 持仓时长:{hold_txt}", + f"💵 交易账户资金:{cap_txt}", "", - "### 平仓基础信息", - f"- 方向:**{_wechat_direction_text(direction)}**", - f"- 平仓结果:**{result or '-'}**", - f"- 交易所平仓ID:`{close_order_id or '-'}`", - f"- 持仓时长:`{hold_txt}`", - f"- 本单盈亏:`{round(float(pnl_amount), 4) if pnl_amount is not None else '-'}U`", - f"- 交易账户资金:`{_wechat_trading_capital_text(session_capital_fallback)}`", - "", - "---", - "", - "### 价格信息", - f"- 入场价:`{trigger_price if trigger_price is not None else '-'}`", - f"- 平仓参考价:`{current_price if current_price is not None else '-'}`", - f"- 止盈/止损:`{take_profit if take_profit is not None else '-'}` / `{stop_loss if stop_loss is not None else '-'}`", + "🎯 价位(计划)", + f"开仓成交价:{ep}", + f"离场参考价:{cp}", + f"止盈价位:{tp}", + f"止损价位:{sl}", ] if extra_note: - lines.extend(["", f"*备注:{extra_note}*"]) + lines.extend(["", "📎 备注", extra_note]) return "\n".join(lines) @@ -371,7 +408,7 @@ def build_wechat_breakeven_message(symbol, direction, arm_txt, now_rr, locked_r, f"- 类型:**{arm_txt}**", f"- 当前RR:`{round(float(now_rr), 2)}R`", f"- 锁定RR:`{round(float(locked_r), 2)}R`", - f"- 新保护位:`{new_sl}`", + f"- 新保护位:`{format_wechat_scalar_2dp(new_sl)}`", ] ) @@ -2262,12 +2299,12 @@ def auto_transfer_once_per_day(): if from_balance is not None and from_balance < needed: conn.execute( "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", - ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"{AUTO_TRANSFER_FROM}账户USDT不足,需{needed}U,当前{round(from_balance,4)}U") + ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"{AUTO_TRANSFER_FROM}账户USDT不足,需{round(needed, 2)}U,当前{round(from_balance, 2)}U") ) conn.commit() conn.close() send_wechat_msg( - f"自动划转失败:{AUTO_TRANSFER_FROM}余额不足,需{needed}U,当前{round(from_balance,4)}U\n" + f"自动划转失败:{AUTO_TRANSFER_FROM}余额不足,需{round(needed, 2)}U,当前{round(from_balance, 2)}U\n" f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" ) return @@ -2281,13 +2318,13 @@ def auto_transfer_once_per_day(): conn.close() if ok: send_wechat_msg( - f"自动划转成功:补足到{target_amount}U,实际划转{needed}U " + f"自动划转成功:补足到{round(float(target_amount), 2)}U,实际划转{round(needed, 2)}U " f"{AUTO_TRANSFER_FROM}->{AUTO_TRANSFER_TO}\n" f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" ) else: send_wechat_msg( - f"自动划转失败:计划补足到{target_amount}U,需划转{needed}U\n原因:{msg}\n" + f"自动划转失败:计划补足到{round(float(target_amount), 2)}U,需划转{round(needed, 2)}U\n原因:{msg}\n" f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}" ) @@ -4301,7 +4338,7 @@ def _finalize_fib_key_fill(conn, row): base_amount, oid, ) - rr_txt = f"{planned_rr:.4f}" if planned_rr is not None else "-" + rr_txt = format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-" succ = ( f"# ✅ {symbol} 斐波限价成交\n" f"**账户:{_wechat_account_label()}**\n" @@ -4309,7 +4346,7 @@ def _finalize_fib_key_fill(conn, row): f"- 类型:{typ}|{_wechat_direction_text(direction)}\n" f"- 订单 ID:**{new_order_id}**\n" f"- 成交价:{format_price_for_symbol(symbol, trigger_price)}\n" - f"- 止损:{sl}|止盈:{format_price_for_symbol(symbol, tp)}\n" + f"- 止损:{format_wechat_scalar_2dp(sl)}|止盈:{format_price_for_symbol(symbol, tp)}\n" f"- 计划 RR:{rr_txt}:1\n" f"- {'已挂交易所 TP/SL' if tpsl_attached else 'TP/SL 未挂上'}\n" ) @@ -4515,7 +4552,7 @@ def check_key_monitors(): if plan_tuple: E, sl_raw, tp_raw, box_h = plan_tuple planned_rr = calc_rr_ratio(direction, E, sl_raw, tp_raw) - rr_txt = f"{planned_rr:.4f}" if planned_rr is not None else "-" + rr_txt = format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-" op_lines = [ f"录入方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'开' if be_on else '关'}", sl_tp_plan_summary_text( @@ -4523,7 +4560,7 @@ def check_key_monitors(): outside_pct=KEY_STOP_OUTSIDE_BREAKOUT_PCT, trend_outside_pct=KEY_TREND_STOP_OUTSIDE_PCT, ), - f"计划 SL:`{round(sl_raw, 8)}`|计划 TP:`{round(tp_raw, 8)}`|计划 RR(E):{rr_txt}:1", + f"计划 SL:`{format_wechat_scalar_2dp(sl_raw)}`|计划 TP:`{format_price_for_symbol(sym, tp_raw)}`|计划 RR(E):{rr_txt}:1", "说明:OKX 本实例为提醒模式,不自动市价开仓;请按方案自行下单。", ] else: @@ -4560,6 +4597,7 @@ def check_key_monitors(): "\n".join( [ f"# 🧾 {r['symbol']} 关键位监控结束", + f"**账户:{_wechat_account_label()}**", "", f"- 原因:已满 {max_n} 次提醒", "- 状态:已自动结束并记入历史", @@ -4614,6 +4652,7 @@ def check_order_monitors(): direction == "long" and new_sl > float(stop_loss) ) if should_move: + was_armed = breakeven_armed ex_sym = resolve_monitor_exchange_symbol(r) new_sl = round_price_to_exchange(ex_sym, new_sl) tp_ex = float(take_profit or 0) @@ -4623,13 +4662,15 @@ def check_order_monitors(): try: replace_active_monitor_tpsl_on_exchange(r, new_sl, tp_ex) synced_ex = True + _clear_breakeven_exchange_warn(pid) except Exception as e: print( f"[breakeven] exchange tpsl replace failed order={pid} {sym}: {e}", flush=True, ) - send_wechat_msg( - f"⚠️ {sym} 移动保本止损未同步交易所:{friendly_okx_error(e)}" + _send_breakeven_exchange_warn_once( + pid, + f"⚠️ {sym} 移动保本止损未同步交易所:{friendly_okx_error(e)}", ) elif ok_live: print( @@ -4642,18 +4683,20 @@ def check_order_monitors(): (new_sl, new_sl, pid), ) stop_loss = new_sl - arm_txt = "保本止盈" if not breakeven_armed else "移动止盈" - be_msg = build_wechat_breakeven_message( - sym, - direction, - arm_txt, - now_rr, - locked_r, - new_sl, - ) - if ok_live: - be_msg += "\n- 交易所:已先撤后挂止盈止损" - send_wechat_msg(be_msg) + breakeven_armed = 1 + if not was_armed: + arm_txt = "保本止盈" + be_msg = build_wechat_breakeven_message( + sym, + direction, + arm_txt, + now_rr, + locked_r, + new_sl, + ) + if ok_live: + be_msg += "\n- 交易所:已先撤后挂止盈止损" + send_wechat_msg(be_msg) res = None # 做多 @@ -6198,14 +6241,26 @@ def add_order(): _, trading_capital_after = get_exchange_capitals(force=True) account_base_display = ( - round(float(trading_capital_after), 4) + round(float(trading_capital_after), 2) if trading_capital_after is not None - else round(float(capital_base), 4) + else round(float(capital_base), 2) ) - account_name = (os.getenv("OKX_ACCOUNT_LABEL") or "okx实盘子账户").strip() dir_text = "多头(long)" if direction == "long" else "空头(short)" - order_state_text = "交易所止盈止损已挂单" if tpsl_attached else "未挂单(已拦截)" + order_state_text = ( + "已在交易所挂条件委托(止盈、止损各一张触发单)" + if tpsl_attached + else "条件委托未挂上(已拦截)" + ) rr_show = planned_rr if planned_rr is not None else "-" + try: + rr_show_fmt = f"{float(planned_rr):.2f}" if planned_rr is not None else None + except (TypeError, ValueError): + rr_show_fmt = None + rr_line = f"RR {rr_show_fmt} : 1" if rr_show_fmt is not None else f"RR {rr_show} : 1" + ep_wx = format_price_for_symbol(symbol, trigger_price) + sl_wx = format_wechat_scalar_2dp(stop_loss) + tp_wx = format_price_for_symbol(symbol, take_profit) + be_wx = format_price_for_symbol(symbol, breakeven_price) style_zh = "Swing 波段" if trade_style == "swing" else "Trend 趋势" wx_lines = [ f"📈 {symbol} 开仓成功", @@ -6213,22 +6268,22 @@ def add_order(): "🧾 订单基础信息", f"🔖 交易所订单 ID:{open_order_id}", f"📈 交易风格:{style_zh}", - f"⚠️ 单笔风控风险:{risk_percent}% ≈ {round(float(risk_amount_final), 4)} U", + f"⚠️ 单笔风控风险:{risk_percent}% ≈ {round(float(risk_amount_final), 2)} U", "📊 仓位配置详情", f"账户基数:{account_base_display} USDT", f"合约杠杆:{leverage} 倍", - f"名义仓位:{notional_value} USDT", + f"名义仓位:{format_wechat_scalar_2dp(notional_value)} USDT", f"仓位占比:{position_ratio}%", - f"合约张数:{amount} 张", + f"合约张数:{format_wechat_scalar_2dp(amount)} 张", f"折算标的:{base_amount} {journal_coin_from_symbol(symbol)}", "🎯 价位 & 盈亏比", - f"开仓成交价:{trigger_price}", - f"止损价位:{stop_loss}", - f"止盈价位:{take_profit}", - f"计划盈亏比:RR {rr_show} : 1", - f"移动保本位:{breakeven_rr_trigger}R → {breakeven_price}", + f"开仓成交价:{ep_wx}", + f"止损价位:{sl_wx}", + f"止盈价位:{tp_wx}", + f"计划盈亏比:{rr_line}", + f"移动保本位:{breakeven_rr_trigger}R → {be_wx}", "📌 状态统计", - f"✅ 止盈止损:{order_state_text}", + f"✅ 条件委托:{order_state_text}", f"📅 当日开仓次数:{opens_today_after} / {DAILY_OPEN_ALERT_THRESHOLD} 次(风控阈值提醒)", ] if chart_url: @@ -6236,8 +6291,8 @@ def add_order(): send_wechat_msg("\n".join(wx_lines)) flash_lines = [ - f"实盘开单成功:风格 {trade_style};风险 {risk_percent}%≈{risk_amount_final}U;基数 {margin_capital}U,杠杆 {leverage}x,名义仓位 {notional_value}U,仓位占比 {position_ratio}%,合约张数 {amount}(折算标的 {base_amount})," - f"计划RR {planned_rr if planned_rr is not None else '-'};止盈止损已挂交易所", + f"实盘开单成功:风格 {trade_style};风险 {risk_percent}%≈{round(float(risk_amount_final), 2)}U;基数 {round(float(margin_capital), 2)}U,杠杆 {leverage}x,名义仓位 {format_wechat_scalar_2dp(notional_value)}U,仓位占比 {position_ratio}%,合约张数 {format_wechat_scalar_2dp(amount)}(折算标的 {base_amount})," + f"计划RR {format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else '-'};已在交易所挂条件止盈/止损委托(非仓位绑定型)", f"本交易日累计开仓:{opens_today_after}", ] if chart_url: diff --git a/crypto_monitor_okx/更新文档.md b/crypto_monitor_okx/更新文档.md index a499da0..224c53b 100644 --- a/crypto_monitor_okx/更新文档.md +++ b/crypto_monitor_okx/更新文档.md @@ -79,6 +79,13 @@ - 手动强制同步:`GET /api/sync_exchange_pnl`(需登录)。 - 可选 `.env`:`EXCHANGE_POSITION_SYNC_FROM_BJ`(北京时间起点)、`EXCHANGE_POSITION_HISTORY_LIMIT`(默认 200)。 +## 企业微信推送(与 Gate 对齐) + +- 平仓:`📉 … 平仓完成` 模板(盈亏 ±X.XX U、价位两位/按币价精度、账户资金 2 位小数)。 +- 开仓成功:与 Gate 相同的 emoji 分段(条件委托状态文案、RR/张数/名义 2 位小数)。 +- 移动保本:仅首次触发推送;交易所同步失败同一监控单只告警一次。 +- 斐波/关键位/划转等推送数值格式与 Gate 一致(`format_wechat_scalar_2dp`)。 + ## 配置与部署 - 详见 `.env.example` 中 OKX(`OKX_*`)与通用风控项。