From b2b6aac094bb0686388a658c3b48411695368620 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 28 May 2026 11:54:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=94=AF=E6=92=91=E9=98=BB?= =?UTF-8?q?=E5=8A=9B=E7=9A=84=E4=BC=81=E4=B8=9A=E5=BE=AE=E4=BF=A1=E6=8E=A8?= =?UTF-8?q?=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_binance/app.py | 110 ++++++++++++----------------- crypto_monitor_gate/app.py | 110 ++++++++++++----------------- crypto_monitor_gate_bot/app.py | 15 ++-- crypto_monitor_okx/app.py | 111 +++++++++++++---------------- key_monitor_lib.py | 123 +++++++++++++++++++++++++++++++++ wechat_notify_lib.py | 117 +++++++++++++++++++++++++++++++ 6 files changed, 384 insertions(+), 202 deletions(-) create mode 100644 wechat_notify_lib.py diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 4568f3d..46f499c 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -83,8 +83,11 @@ from key_monitor_lib import ( format_auto_amp_line, format_auto_confirm_line, notify_interval_elapsed, + resolve_rs_break_for_alert, rs_break_from_direction, + run_rs_level_alert_tick, ) +from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook from hub_auth import request_allowed as hub_request_allowed from history_window_lib import ( PRESET_CUSTOM, @@ -304,16 +307,9 @@ LIQUIDITY_RANK_CACHE = { # 企业微信推送 def send_wechat_msg(content): - prefix = "【加密货币】" - full_msg = f"{prefix}\n{content}" - data = { - "msgtype": "text", - "text": {"content": full_msg} - } - try: - requests.post(WECHAT_WEBHOOK, json=data, timeout=WECHAT_TIMEOUT_SECONDS) - except: - pass + send_wechat_webhook( + WECHAT_WEBHOOK, content, timeout=WECHAT_TIMEOUT_SECONDS + ) _BREAKEVEN_EXCHANGE_WARNED_IDS = set() @@ -1386,6 +1382,7 @@ def init_db(): "ALTER TABLE key_monitors ADD COLUMN sl_tp_mode TEXT DEFAULT 'standard'", "ALTER TABLE key_monitors ADD COLUMN manual_take_profit REAL", "ALTER TABLE key_monitors ADD COLUMN breakeven_enabled INTEGER DEFAULT 0", + "ALTER TABLE key_monitors ADD COLUMN last_rs_bar_ts INTEGER", ): try: c.execute(ddl) @@ -4255,39 +4252,6 @@ def _fetch_last_closed_bar(symbol): return closed[-1] if closed else None -def build_wechat_rs_level_message( - symbol, - monitor_type, - trigger_time, - upper, - lower, - trigger_close, - break_info, - notify_index, - notify_max, -): - lines = [ - f"# 📌 {symbol} 关键位突破提醒({notify_index}/{notify_max})", - f"**账户:{_wechat_account_label()}**", - "", - "---", - "", - "### 突破判定(5m 收盘)", - f"- 类型:**{monitor_type}**", - f"- 触发时间:`{trigger_time}`", - f"- 上沿:`{upper}`|下沿:`{lower}`", - f"- 触发收盘:`{format_price_for_symbol(symbol, trigger_close)}`", - f"- **{break_info['break_label']}**(程序推断:**{_wechat_direction_text(break_info['direction'])}**)", - f"- 突破价位:`{format_price_for_symbol(symbol, break_info['edge_price'])}`", - "", - "### 说明", - "- 本条为**人工盯盘**用途:录入时**不选多空**,由上/下沿突破方向自动判定。", - f"- 共推送 **{notify_max}** 次(间隔约 {KEY_ALERT_INTERVAL_MINUTES} 分钟),推送完毕后本条监控结案。", - "- **不参与**自动开仓、量能/二确/盈亏比门控。", - ] - return "\n".join(lines) - - def _key_rs_gate_preview(symbol, upper, lower): """页面门控预览:阻力/支撑仅显示距上/下沿与是否已越线。""" bar = _fetch_last_closed_bar(symbol) @@ -4318,45 +4282,63 @@ def _process_key_rs_level_alert(conn, row): return close = float(bar[4]) ts = bar[0] - count = int(row["notification_count"] or 0) - max_n = max(1, int(row["max_notify"] or KEY_ALERT_MAX_TIMES)) - interval = max(1, int(row["notify_interval_min"] or KEY_ALERT_INTERVAL_MINUTES)) now_dt = app_now() + tick = run_rs_level_alert_tick( + row, + close, + ts, + now_dt, + default_max_notify=KEY_ALERT_MAX_TIMES, + default_interval_min=KEY_ALERT_INTERVAL_MINUTES, + ) + if not tick: + return - if count == 0: - br = detect_rs_box_break(close, up, low) - if not br: - return - else: - if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt): - return - br = rs_break_from_direction(row["direction"], up, low) - if not br: + br = tick["break_info"] + notify_index = int(tick["notify_index"]) + max_n = int(tick["notify_max"]) + interval = int(tick["interval_min"]) + bar_ts = tick.get("bar_ts") + + if tick.get("need_claim_first"): + conn.execute( + "UPDATE key_monitors SET notification_count=1, direction=?, last_notified_at=?, last_rs_bar_ts=? " + "WHERE id=? AND COALESCE(notification_count,0)=0", + (br["direction"], app_now_str(), bar_ts, row["id"]), + ) + if conn.total_changes == 0: return + conn.commit() trigger_time = ms_to_app_local_str(int(ts)) if ts else app_now_str() - notify_index = count + 1 msg = build_wechat_rs_level_message( symbol=sym, monitor_type=typ, + account_label=_wechat_account_label(), trigger_time=trigger_time, - upper=up, - lower=low, - trigger_close=close, - break_info=br, + upper_txt=format_price_for_symbol(sym, up), + lower_txt=format_price_for_symbol(sym, low), + close_txt=format_price_for_symbol(sym, close), + edge_txt=format_price_for_symbol(sym, br["edge_price"]), + break_label=br["break_label"], + direction=br["direction"], notify_index=notify_index, notify_max=max_n, + interval_min=interval, ) send_wechat_msg(msg) conn.execute( - "UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, last_alert_message=? WHERE id=?", - (br["direction"], notify_index, app_now_str(), msg, row["id"]), + "UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, " + "last_alert_message=?, last_rs_bar_ts=? WHERE id=?", + (br["direction"], notify_index, app_now_str(), msg, bar_ts, row["id"]), ) + conn.commit() if notify_index >= max_n: hist_row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (row["id"],)).fetchone() if hist_row: insert_key_monitor_history(conn, hist_row, notify_index, msg, "key_level_alert_done") conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],)) + conn.commit() def _key_hard_lines_from_checks(checks): @@ -4991,8 +4973,8 @@ def check_key_monitors(): if typ in KEY_MONITOR_RS_TYPES: try: _process_key_rs_level_alert(conn, r) - except Exception: - pass + except Exception as e: + print(f"[key_rs_level_alert] {sym} id={r['id']}: {e}") continue direction = (r["direction"] or "long").lower() diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index f82c7d8..66c40fd 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -84,8 +84,11 @@ from key_monitor_lib import ( format_auto_amp_line, format_auto_confirm_line, notify_interval_elapsed, + resolve_rs_break_for_alert, rs_break_from_direction, + run_rs_level_alert_tick, ) +from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook from hub_auth import request_allowed as hub_request_allowed from history_window_lib import ( PRESET_CUSTOM, @@ -298,16 +301,9 @@ LIQUIDITY_RANK_CACHE = { # 企业微信推送 def send_wechat_msg(content): - prefix = "【加密货币】" - full_msg = f"{prefix}\n{content}" - data = { - "msgtype": "text", - "text": {"content": full_msg} - } - try: - requests.post(WECHAT_WEBHOOK, json=data, timeout=WECHAT_TIMEOUT_SECONDS) - except: - pass + send_wechat_webhook( + WECHAT_WEBHOOK, content, timeout=WECHAT_TIMEOUT_SECONDS + ) _BREAKEVEN_EXCHANGE_WARNED_IDS = set() @@ -1385,6 +1381,7 @@ def init_db(): "ALTER TABLE key_monitors ADD COLUMN sl_tp_mode TEXT DEFAULT 'standard'", "ALTER TABLE key_monitors ADD COLUMN manual_take_profit REAL", "ALTER TABLE key_monitors ADD COLUMN breakeven_enabled INTEGER DEFAULT 0", + "ALTER TABLE key_monitors ADD COLUMN last_rs_bar_ts INTEGER", ): try: c.execute(ddl) @@ -4126,39 +4123,6 @@ def _fetch_last_closed_bar(symbol): return closed[-1] if closed else None -def build_wechat_rs_level_message( - symbol, - monitor_type, - trigger_time, - upper, - lower, - trigger_close, - break_info, - notify_index, - notify_max, -): - lines = [ - f"# 📌 {symbol} 关键位突破提醒({notify_index}/{notify_max})", - f"**账户:{_wechat_account_label()}**", - "", - "---", - "", - "### 突破判定(5m 收盘)", - f"- 类型:**{monitor_type}**", - f"- 触发时间:`{trigger_time}`", - f"- 上沿:`{upper}`|下沿:`{lower}`", - f"- 触发收盘:`{format_price_for_symbol(symbol, trigger_close)}`", - f"- **{break_info['break_label']}**(程序推断:**{_wechat_direction_text(break_info['direction'])}**)", - f"- 突破价位:`{format_price_for_symbol(symbol, break_info['edge_price'])}`", - "", - "### 说明", - "- 本条为**人工盯盘**用途:录入时**不选多空**,由上/下沿突破方向自动判定。", - f"- 共推送 **{notify_max}** 次(间隔约 {KEY_ALERT_INTERVAL_MINUTES} 分钟),推送完毕后本条监控结案。", - "- **不参与**自动开仓、量能/二确/盈亏比门控。", - ] - return "\n".join(lines) - - def _key_rs_gate_preview(symbol, upper, lower): """页面门控预览:阻力/支撑仅显示距上/下沿与是否已越线。""" bar = _fetch_last_closed_bar(symbol) @@ -4189,45 +4153,63 @@ def _process_key_rs_level_alert(conn, row): return close = float(bar[4]) ts = bar[0] - count = int(row["notification_count"] or 0) - max_n = max(1, int(row["max_notify"] or KEY_ALERT_MAX_TIMES)) - interval = max(1, int(row["notify_interval_min"] or KEY_ALERT_INTERVAL_MINUTES)) now_dt = app_now() + tick = run_rs_level_alert_tick( + row, + close, + ts, + now_dt, + default_max_notify=KEY_ALERT_MAX_TIMES, + default_interval_min=KEY_ALERT_INTERVAL_MINUTES, + ) + if not tick: + return - if count == 0: - br = detect_rs_box_break(close, up, low) - if not br: - return - else: - if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt): - return - br = rs_break_from_direction(row["direction"], up, low) - if not br: + br = tick["break_info"] + notify_index = int(tick["notify_index"]) + max_n = int(tick["notify_max"]) + interval = int(tick["interval_min"]) + bar_ts = tick.get("bar_ts") + + if tick.get("need_claim_first"): + conn.execute( + "UPDATE key_monitors SET notification_count=1, direction=?, last_notified_at=?, last_rs_bar_ts=? " + "WHERE id=? AND COALESCE(notification_count,0)=0", + (br["direction"], app_now_str(), bar_ts, row["id"]), + ) + if conn.total_changes == 0: return + conn.commit() trigger_time = ms_to_app_local_str(int(ts)) if ts else app_now_str() - notify_index = count + 1 msg = build_wechat_rs_level_message( symbol=sym, monitor_type=typ, + account_label=_wechat_account_label(), trigger_time=trigger_time, - upper=up, - lower=low, - trigger_close=close, - break_info=br, + upper_txt=format_price_for_symbol(sym, up), + lower_txt=format_price_for_symbol(sym, low), + close_txt=format_price_for_symbol(sym, close), + edge_txt=format_price_for_symbol(sym, br["edge_price"]), + break_label=br["break_label"], + direction=br["direction"], notify_index=notify_index, notify_max=max_n, + interval_min=interval, ) send_wechat_msg(msg) conn.execute( - "UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, last_alert_message=? WHERE id=?", - (br["direction"], notify_index, app_now_str(), msg, row["id"]), + "UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, " + "last_alert_message=?, last_rs_bar_ts=? WHERE id=?", + (br["direction"], notify_index, app_now_str(), msg, bar_ts, row["id"]), ) + conn.commit() if notify_index >= max_n: hist_row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (row["id"],)).fetchone() if hist_row: insert_key_monitor_history(conn, hist_row, notify_index, msg, "key_level_alert_done") conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],)) + conn.commit() def _key_hard_lines_from_checks(checks): @@ -4860,8 +4842,8 @@ def check_key_monitors(): if typ in KEY_MONITOR_RS_TYPES: try: _process_key_rs_level_alert(conn, r) - except Exception: - pass + except Exception as e: + print(f"[key_rs_level_alert] {sym} id={r['id']}: {e}") continue direction = (r["direction"] or "long").lower() diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index ea7ef5e..b37824a 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -275,16 +275,11 @@ LIQUIDITY_RANK_CACHE = { # 企业微信推送 def send_wechat_msg(content): - prefix = "【加密货币】" - full_msg = f"{prefix}\n{content}" - data = { - "msgtype": "text", - "text": {"content": full_msg} - } - try: - requests.post(WECHAT_WEBHOOK, json=data, timeout=WECHAT_TIMEOUT_SECONDS) - except: - pass + from wechat_notify_lib import send_wechat_webhook + + send_wechat_webhook( + WECHAT_WEBHOOK, content, timeout=WECHAT_TIMEOUT_SECONDS + ) _BREAKEVEN_EXCHANGE_WARNED_IDS = set() diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 0ebad00..ec21929 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -84,8 +84,11 @@ from key_monitor_lib import ( format_auto_amp_line, format_auto_confirm_line, notify_interval_elapsed, + resolve_rs_break_for_alert, rs_break_from_direction, + run_rs_level_alert_tick, ) +from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook from hub_auth import request_allowed as hub_request_allowed from history_window_lib import ( PRESET_CUSTOM, @@ -299,16 +302,9 @@ LIQUIDITY_RANK_CACHE = { # 企业微信推送 def send_wechat_msg(content): - prefix = "【加密货币】" - full_msg = f"{prefix}\n{content}" - data = { - "msgtype": "text", - "text": {"content": full_msg} - } - try: - requests.post(WECHAT_WEBHOOK, json=data, timeout=WECHAT_TIMEOUT_SECONDS) - except: - pass + send_wechat_webhook( + WECHAT_WEBHOOK, content, timeout=WECHAT_TIMEOUT_SECONDS + ) _BREAKEVEN_EXCHANGE_WARNED_IDS = set() @@ -1349,6 +1345,7 @@ def init_db(): "ALTER TABLE key_monitors ADD COLUMN sl_tp_mode TEXT DEFAULT 'standard'", "ALTER TABLE key_monitors ADD COLUMN manual_take_profit REAL", "ALTER TABLE key_monitors ADD COLUMN breakeven_enabled INTEGER DEFAULT 0", + "ALTER TABLE key_monitors ADD COLUMN last_rs_bar_ts INTEGER", ): try: c.execute(ddl) @@ -3969,39 +3966,6 @@ def _fetch_last_closed_bar(symbol): return closed[-1] if closed else None -def build_wechat_rs_level_message( - symbol, - monitor_type, - trigger_time, - upper, - lower, - trigger_close, - break_info, - notify_index, - notify_max, -): - lines = [ - f"# 📌 {symbol} 关键位突破提醒({notify_index}/{notify_max})", - f"**账户:{_wechat_account_label()}**", - "", - "---", - "", - "### 突破判定(5m 收盘)", - f"- 类型:**{monitor_type}**", - f"- 触发时间:`{trigger_time}`", - f"- 上沿:`{upper}`|下沿:`{lower}`", - f"- 触发收盘:`{format_price_for_symbol(symbol, trigger_close)}`", - f"- **{break_info['break_label']}**(程序推断:**{_wechat_direction_text(break_info['direction'])}**)", - f"- 突破价位:`{format_price_for_symbol(symbol, break_info['edge_price'])}`", - "", - "### 说明", - "- 本条为**人工盯盘**用途:录入时**不选多空**,由上/下沿突破方向自动判定。", - f"- 共推送 **{notify_max}** 次(间隔约 {KEY_ALERT_INTERVAL_MINUTES} 分钟),推送完毕后本条监控结案。", - "- OKX 本实例为提醒模式,不自动开仓。", - ] - return "\n".join(lines) - - def _key_rs_gate_preview(symbol, upper, lower): bar = _fetch_last_closed_bar(symbol) if not bar: @@ -4030,45 +3994,64 @@ def _process_key_rs_level_alert(conn, row): return close = float(bar[4]) ts = bar[0] - count = int(row["notification_count"] or 0) - max_n = max(1, int(row["max_notify"] or KEY_ALERT_MAX_TIMES)) - interval = max(1, int(row["notify_interval_min"] or KEY_ALERT_INTERVAL_MINUTES)) now_dt = app_now() + tick = run_rs_level_alert_tick( + row, + close, + ts, + now_dt, + default_max_notify=KEY_ALERT_MAX_TIMES, + default_interval_min=KEY_ALERT_INTERVAL_MINUTES, + ) + if not tick: + return - if count == 0: - br = detect_rs_box_break(close, up, low) - if not br: - return - else: - if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt): - return - br = rs_break_from_direction(row["direction"], up, low) - if not br: + br = tick["break_info"] + notify_index = int(tick["notify_index"]) + max_n = int(tick["notify_max"]) + interval = int(tick["interval_min"]) + bar_ts = tick.get("bar_ts") + + if tick.get("need_claim_first"): + conn.execute( + "UPDATE key_monitors SET notification_count=1, direction=?, last_notified_at=?, last_rs_bar_ts=? " + "WHERE id=? AND COALESCE(notification_count,0)=0", + (br["direction"], app_now_str(), bar_ts, row["id"]), + ) + if conn.total_changes == 0: return + conn.commit() trigger_time = ms_to_app_local_str(int(ts)) if ts else app_now_str() - notify_index = count + 1 msg = build_wechat_rs_level_message( symbol=sym, monitor_type=typ, + account_label=_wechat_account_label(), trigger_time=trigger_time, - upper=up, - lower=low, - trigger_close=close, - break_info=br, + upper_txt=format_price_for_symbol(sym, up), + lower_txt=format_price_for_symbol(sym, low), + close_txt=format_price_for_symbol(sym, close), + edge_txt=format_price_for_symbol(sym, br["edge_price"]), + break_label=br["break_label"], + direction=br["direction"], notify_index=notify_index, notify_max=max_n, + interval_min=interval, + extra_note="OKX 本实例为提醒模式,不自动市价开仓", ) send_wechat_msg(msg) conn.execute( - "UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, last_alert_message=? WHERE id=?", - (br["direction"], notify_index, app_now_str(), msg, row["id"]), + "UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, " + "last_alert_message=?, last_rs_bar_ts=? WHERE id=?", + (br["direction"], notify_index, app_now_str(), msg, bar_ts, row["id"]), ) + conn.commit() if notify_index >= max_n: hist_row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (row["id"],)).fetchone() if hist_row: insert_key_monitor_history(conn, hist_row, notify_index, msg, "key_level_alert_done") conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],)) + conn.commit() def _key_hard_lines_from_checks(checks): @@ -4568,8 +4551,8 @@ def check_key_monitors(): if typ in KEY_MONITOR_RS_TYPES: try: _process_key_rs_level_alert(conn, r) - except Exception: - pass + except Exception as e: + print(f"[key_rs_level_alert] {sym} id={r['id']}: {e}") continue if typ not in KEY_MONITOR_AUTO_TYPES: continue diff --git a/key_monitor_lib.py b/key_monitor_lib.py index 6df5969..600edee 100644 --- a/key_monitor_lib.py +++ b/key_monitor_lib.py @@ -94,6 +94,129 @@ def rs_break_from_direction(direction: str, upper: float, lower: float) -> Optio return None +def rs_break_infer_from_close(close: float, upper: float, lower: float) -> dict[str, Any]: + """ + 续发提醒时价格已回到箱体内:按收盘价相对箱体中线推断首次突破边, + 保证第 2/3 次企业微信提醒仍能发出。 + """ + mid = (float(upper) + float(lower)) / 2.0 + if float(close) >= mid: + br = rs_break_from_direction("long", upper, lower) + else: + br = rs_break_from_direction("short", upper, lower) + if br: + return br + return { + "break_side": "upper", + "direction": "long", + "edge_price": float(upper), + "key_price": float(upper), + "break_label": "向上突破上沿", + } + + +def parse_last_rs_bar_ts(row: Any) -> Optional[int]: + if row is None: + return None + try: + keys = row.keys() if hasattr(row, "keys") else [] + except Exception: + keys = [] + raw = row["last_rs_bar_ts"] if "last_rs_bar_ts" in keys else None + if raw is None: + return None + try: + return int(raw) + except (TypeError, ValueError): + return None + + +def run_rs_level_alert_tick( + row: Any, + close: float, + bar_ts: Optional[int], + now_dt: datetime, + *, + default_max_notify: int, + default_interval_min: int, +) -> Optional[dict[str, Any]]: + """ + 判定本轮回合是否应推送阻力/支撑提醒。 + 首条:仅在新 5m 闭合 K 越线时触发,并 need_claim_first 防 3 秒轮询刷屏。 + """ + up, lo = float(row["upper"]), float(row["lower"]) + if up <= lo: + return None + count = int(row["notification_count"] or 0) + max_n = max(1, int(row["max_notify"] or default_max_notify)) + interval = max(1, int(row["notify_interval_min"] or default_interval_min)) + if count >= max_n: + return None + + bar_ts_i: Optional[int] = None + if bar_ts is not None: + try: + bar_ts_i = int(bar_ts) + except (TypeError, ValueError): + bar_ts_i = None + last_bar_i = parse_last_rs_bar_ts(row) + + if count == 0: + br = detect_rs_box_break(close, up, lo) + if not br: + return None + if bar_ts_i is not None and last_bar_i is not None and bar_ts_i == last_bar_i: + return None + return { + "break_info": br, + "notify_index": 1, + "notify_max": max_n, + "interval_min": interval, + "bar_ts": bar_ts_i, + "need_claim_first": True, + } + + if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt): + return None + br = resolve_rs_break_for_alert(count, row["direction"], close, up, lo) + if not br: + return None + return { + "break_info": br, + "notify_index": count + 1, + "notify_max": max_n, + "interval_min": interval, + "bar_ts": bar_ts_i, + "need_claim_first": False, + } + + +def resolve_rs_break_for_alert( + notification_count: int, + direction: Optional[str], + close: float, + upper: float, + lower: float, +) -> Optional[dict[str, Any]]: + """ + 阻力/支撑提醒:首次用 5m 收盘越线判定;后续用已存方向,兼容 direction=watch。 + """ + count = int(notification_count or 0) + up, lo, c = float(upper), float(lower), float(close) + if count <= 0: + return detect_rs_box_break(c, up, lo) + br = rs_break_from_direction(direction, up, lo) + if br: + return br + d = (direction or "").strip().lower() + if d not in ("", KEY_DIRECTION_WATCH): + return None + br = detect_rs_box_break(c, up, lo) + if br: + return br + return rs_break_infer_from_close(c, up, lo) + + def notify_interval_elapsed( last_notified_at: Optional[str], interval_min: int, diff --git a/wechat_notify_lib.py b/wechat_notify_lib.py new file mode 100644 index 0000000..9a20a79 --- /dev/null +++ b/wechat_notify_lib.py @@ -0,0 +1,117 @@ +"""企业微信机器人 Webhook 推送(多实例共用)。""" +from __future__ import annotations + +import re +from typing import Optional + +import requests + + +def strip_markdown_for_text(content: str) -> str: + s = str(content or "") + s = re.sub(r"\*\*([^*]+)\*\*", r"\1", s) + s = re.sub(r"`([^`]+)`", r"\1", s) + s = re.sub(r"^#+\s*", "", s, flags=re.MULTILINE) + s = re.sub(r"^---\s*$", "", s, flags=re.MULTILINE) + return s.strip() + + +def looks_like_wechat_markdown(content: str) -> bool: + if not content: + return False + if re.search(r"^#+\s", content, re.MULTILINE): + return True + return "**" in content or "`" in content + + +def send_wechat_webhook( + webhook_url: str, + content: str, + *, + timeout: int = 10, + prefix: str = "【加密货币】", +) -> bool: + url = (webhook_url or "").strip() + if not url or "replace-me" in url: + return False + body = str(content or "").strip() + if prefix: + full = f"{prefix}\n{body}" if body else prefix + else: + full = body + if not full.strip(): + return False + + payloads = [] + if looks_like_wechat_markdown(full): + payloads.append({"msgtype": "markdown", "markdown": {"content": full}}) + plain = strip_markdown_for_text(full) if looks_like_wechat_markdown(full) else full + payloads.append({"msgtype": "text", "text": {"content": plain}}) + + seen = set() + for payload in payloads: + key = payload["msgtype"] + if key in seen: + continue + seen.add(key) + try: + resp = requests.post(url, json=payload, timeout=timeout) + if resp.status_code != 200: + continue + data = resp.json() + if int(data.get("errcode", -1)) == 0: + return True + except Exception: + continue + return False + + +def wechat_direction_label(direction: str) -> str: + d = (direction or "").strip().lower() + if d == "long": + return "多头(long)" + if d == "short": + return "空头(short)" + return "双向(watch)" + + +def build_wechat_rs_level_message( + *, + symbol: str, + monitor_type: str, + account_label: str, + trigger_time: str, + upper_txt: str, + lower_txt: str, + close_txt: str, + edge_txt: str, + break_label: str, + direction: str, + notify_index: int, + notify_max: int, + interval_min: int, + extra_note: Optional[str] = None, +) -> str: + """阻力/支撑突破提醒(与开平仓推送一致的 emoji 纯文本风格)。""" + head = "📈" if (direction or "").strip().lower() == "long" else "📉" + dir_txt = wechat_direction_label(direction) + lines = [ + f"{head} {symbol} 关键位突破提醒({notify_index}/{notify_max})", + f"💼 账户:{account_label}", + "", + "🧾 突破概要", + f"📌 类型:{monitor_type}", + f"⏱ 触发时间:{trigger_time}", + f"📊 上沿:{upper_txt}|下沿:{lower_txt}", + f"💹 触发收盘:{close_txt}", + f"🎯 {break_label}({dir_txt})", + f"📍 突破价位:{edge_txt}", + "", + "📎 说明", + f"· 人工盯盘,共推送 {notify_max} 次(间隔约 {interval_min} 分钟)", + "· 推送完毕后本条监控自动结案", + "· 不参与自动开仓", + ] + if extra_note: + lines.append(f"· {extra_note}") + return "\n".join(lines)