修改支撑阻力的企业微信推送

This commit is contained in:
dekun
2026-05-28 11:54:37 +08:00
parent a829cf50f3
commit b2b6aac094
6 changed files with 384 additions and 202 deletions
+46 -64
View File
@@ -83,8 +83,11 @@ from key_monitor_lib import (
format_auto_amp_line, format_auto_amp_line,
format_auto_confirm_line, format_auto_confirm_line,
notify_interval_elapsed, notify_interval_elapsed,
resolve_rs_break_for_alert,
rs_break_from_direction, 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 hub_auth import request_allowed as hub_request_allowed
from history_window_lib import ( from history_window_lib import (
PRESET_CUSTOM, PRESET_CUSTOM,
@@ -304,16 +307,9 @@ LIQUIDITY_RANK_CACHE = {
# 企业微信推送 # 企业微信推送
def send_wechat_msg(content): def send_wechat_msg(content):
prefix = "【加密货币】" send_wechat_webhook(
full_msg = f"{prefix}\n{content}" WECHAT_WEBHOOK, content, timeout=WECHAT_TIMEOUT_SECONDS
data = { )
"msgtype": "text",
"text": {"content": full_msg}
}
try:
requests.post(WECHAT_WEBHOOK, json=data, timeout=WECHAT_TIMEOUT_SECONDS)
except:
pass
_BREAKEVEN_EXCHANGE_WARNED_IDS = set() _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 sl_tp_mode TEXT DEFAULT 'standard'",
"ALTER TABLE key_monitors ADD COLUMN manual_take_profit REAL", "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 breakeven_enabled INTEGER DEFAULT 0",
"ALTER TABLE key_monitors ADD COLUMN last_rs_bar_ts INTEGER",
): ):
try: try:
c.execute(ddl) c.execute(ddl)
@@ -4255,39 +4252,6 @@ def _fetch_last_closed_bar(symbol):
return closed[-1] if closed else None 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): def _key_rs_gate_preview(symbol, upper, lower):
"""页面门控预览:阻力/支撑仅显示距上/下沿与是否已越线。""" """页面门控预览:阻力/支撑仅显示距上/下沿与是否已越线。"""
bar = _fetch_last_closed_bar(symbol) bar = _fetch_last_closed_bar(symbol)
@@ -4318,45 +4282,63 @@ def _process_key_rs_level_alert(conn, row):
return return
close = float(bar[4]) close = float(bar[4])
ts = bar[0] 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() 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 = tick["break_info"]
br = detect_rs_box_break(close, up, low) notify_index = int(tick["notify_index"])
if not br: max_n = int(tick["notify_max"])
return interval = int(tick["interval_min"])
else: bar_ts = tick.get("bar_ts")
if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt):
return if tick.get("need_claim_first"):
br = rs_break_from_direction(row["direction"], up, low) conn.execute(
if not br: "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 return
conn.commit()
trigger_time = ms_to_app_local_str(int(ts)) if ts else app_now_str() 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( msg = build_wechat_rs_level_message(
symbol=sym, symbol=sym,
monitor_type=typ, monitor_type=typ,
account_label=_wechat_account_label(),
trigger_time=trigger_time, trigger_time=trigger_time,
upper=up, upper_txt=format_price_for_symbol(sym, up),
lower=low, lower_txt=format_price_for_symbol(sym, low),
trigger_close=close, close_txt=format_price_for_symbol(sym, close),
break_info=br, edge_txt=format_price_for_symbol(sym, br["edge_price"]),
break_label=br["break_label"],
direction=br["direction"],
notify_index=notify_index, notify_index=notify_index,
notify_max=max_n, notify_max=max_n,
interval_min=interval,
) )
send_wechat_msg(msg) send_wechat_msg(msg)
conn.execute( conn.execute(
"UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, last_alert_message=? WHERE id=?", "UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, "
(br["direction"], notify_index, app_now_str(), msg, row["id"]), "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: if notify_index >= max_n:
hist_row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (row["id"],)).fetchone() hist_row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (row["id"],)).fetchone()
if hist_row: if hist_row:
insert_key_monitor_history(conn, hist_row, notify_index, msg, "key_level_alert_done") 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.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],))
conn.commit()
def _key_hard_lines_from_checks(checks): def _key_hard_lines_from_checks(checks):
@@ -4991,8 +4973,8 @@ def check_key_monitors():
if typ in KEY_MONITOR_RS_TYPES: if typ in KEY_MONITOR_RS_TYPES:
try: try:
_process_key_rs_level_alert(conn, r) _process_key_rs_level_alert(conn, r)
except Exception: except Exception as e:
pass print(f"[key_rs_level_alert] {sym} id={r['id']}: {e}")
continue continue
direction = (r["direction"] or "long").lower() direction = (r["direction"] or "long").lower()
+46 -64
View File
@@ -84,8 +84,11 @@ from key_monitor_lib import (
format_auto_amp_line, format_auto_amp_line,
format_auto_confirm_line, format_auto_confirm_line,
notify_interval_elapsed, notify_interval_elapsed,
resolve_rs_break_for_alert,
rs_break_from_direction, 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 hub_auth import request_allowed as hub_request_allowed
from history_window_lib import ( from history_window_lib import (
PRESET_CUSTOM, PRESET_CUSTOM,
@@ -298,16 +301,9 @@ LIQUIDITY_RANK_CACHE = {
# 企业微信推送 # 企业微信推送
def send_wechat_msg(content): def send_wechat_msg(content):
prefix = "【加密货币】" send_wechat_webhook(
full_msg = f"{prefix}\n{content}" WECHAT_WEBHOOK, content, timeout=WECHAT_TIMEOUT_SECONDS
data = { )
"msgtype": "text",
"text": {"content": full_msg}
}
try:
requests.post(WECHAT_WEBHOOK, json=data, timeout=WECHAT_TIMEOUT_SECONDS)
except:
pass
_BREAKEVEN_EXCHANGE_WARNED_IDS = set() _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 sl_tp_mode TEXT DEFAULT 'standard'",
"ALTER TABLE key_monitors ADD COLUMN manual_take_profit REAL", "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 breakeven_enabled INTEGER DEFAULT 0",
"ALTER TABLE key_monitors ADD COLUMN last_rs_bar_ts INTEGER",
): ):
try: try:
c.execute(ddl) c.execute(ddl)
@@ -4126,39 +4123,6 @@ def _fetch_last_closed_bar(symbol):
return closed[-1] if closed else None 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): def _key_rs_gate_preview(symbol, upper, lower):
"""页面门控预览:阻力/支撑仅显示距上/下沿与是否已越线。""" """页面门控预览:阻力/支撑仅显示距上/下沿与是否已越线。"""
bar = _fetch_last_closed_bar(symbol) bar = _fetch_last_closed_bar(symbol)
@@ -4189,45 +4153,63 @@ def _process_key_rs_level_alert(conn, row):
return return
close = float(bar[4]) close = float(bar[4])
ts = bar[0] 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() 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 = tick["break_info"]
br = detect_rs_box_break(close, up, low) notify_index = int(tick["notify_index"])
if not br: max_n = int(tick["notify_max"])
return interval = int(tick["interval_min"])
else: bar_ts = tick.get("bar_ts")
if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt):
return if tick.get("need_claim_first"):
br = rs_break_from_direction(row["direction"], up, low) conn.execute(
if not br: "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 return
conn.commit()
trigger_time = ms_to_app_local_str(int(ts)) if ts else app_now_str() 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( msg = build_wechat_rs_level_message(
symbol=sym, symbol=sym,
monitor_type=typ, monitor_type=typ,
account_label=_wechat_account_label(),
trigger_time=trigger_time, trigger_time=trigger_time,
upper=up, upper_txt=format_price_for_symbol(sym, up),
lower=low, lower_txt=format_price_for_symbol(sym, low),
trigger_close=close, close_txt=format_price_for_symbol(sym, close),
break_info=br, edge_txt=format_price_for_symbol(sym, br["edge_price"]),
break_label=br["break_label"],
direction=br["direction"],
notify_index=notify_index, notify_index=notify_index,
notify_max=max_n, notify_max=max_n,
interval_min=interval,
) )
send_wechat_msg(msg) send_wechat_msg(msg)
conn.execute( conn.execute(
"UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, last_alert_message=? WHERE id=?", "UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, "
(br["direction"], notify_index, app_now_str(), msg, row["id"]), "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: if notify_index >= max_n:
hist_row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (row["id"],)).fetchone() hist_row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (row["id"],)).fetchone()
if hist_row: if hist_row:
insert_key_monitor_history(conn, hist_row, notify_index, msg, "key_level_alert_done") 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.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],))
conn.commit()
def _key_hard_lines_from_checks(checks): def _key_hard_lines_from_checks(checks):
@@ -4860,8 +4842,8 @@ def check_key_monitors():
if typ in KEY_MONITOR_RS_TYPES: if typ in KEY_MONITOR_RS_TYPES:
try: try:
_process_key_rs_level_alert(conn, r) _process_key_rs_level_alert(conn, r)
except Exception: except Exception as e:
pass print(f"[key_rs_level_alert] {sym} id={r['id']}: {e}")
continue continue
direction = (r["direction"] or "long").lower() direction = (r["direction"] or "long").lower()
+5 -10
View File
@@ -275,16 +275,11 @@ LIQUIDITY_RANK_CACHE = {
# 企业微信推送 # 企业微信推送
def send_wechat_msg(content): def send_wechat_msg(content):
prefix = "【加密货币】" from wechat_notify_lib import send_wechat_webhook
full_msg = f"{prefix}\n{content}"
data = { send_wechat_webhook(
"msgtype": "text", WECHAT_WEBHOOK, content, timeout=WECHAT_TIMEOUT_SECONDS
"text": {"content": full_msg} )
}
try:
requests.post(WECHAT_WEBHOOK, json=data, timeout=WECHAT_TIMEOUT_SECONDS)
except:
pass
_BREAKEVEN_EXCHANGE_WARNED_IDS = set() _BREAKEVEN_EXCHANGE_WARNED_IDS = set()
+47 -64
View File
@@ -84,8 +84,11 @@ from key_monitor_lib import (
format_auto_amp_line, format_auto_amp_line,
format_auto_confirm_line, format_auto_confirm_line,
notify_interval_elapsed, notify_interval_elapsed,
resolve_rs_break_for_alert,
rs_break_from_direction, 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 hub_auth import request_allowed as hub_request_allowed
from history_window_lib import ( from history_window_lib import (
PRESET_CUSTOM, PRESET_CUSTOM,
@@ -299,16 +302,9 @@ LIQUIDITY_RANK_CACHE = {
# 企业微信推送 # 企业微信推送
def send_wechat_msg(content): def send_wechat_msg(content):
prefix = "【加密货币】" send_wechat_webhook(
full_msg = f"{prefix}\n{content}" WECHAT_WEBHOOK, content, timeout=WECHAT_TIMEOUT_SECONDS
data = { )
"msgtype": "text",
"text": {"content": full_msg}
}
try:
requests.post(WECHAT_WEBHOOK, json=data, timeout=WECHAT_TIMEOUT_SECONDS)
except:
pass
_BREAKEVEN_EXCHANGE_WARNED_IDS = set() _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 sl_tp_mode TEXT DEFAULT 'standard'",
"ALTER TABLE key_monitors ADD COLUMN manual_take_profit REAL", "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 breakeven_enabled INTEGER DEFAULT 0",
"ALTER TABLE key_monitors ADD COLUMN last_rs_bar_ts INTEGER",
): ):
try: try:
c.execute(ddl) c.execute(ddl)
@@ -3969,39 +3966,6 @@ def _fetch_last_closed_bar(symbol):
return closed[-1] if closed else None 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): def _key_rs_gate_preview(symbol, upper, lower):
bar = _fetch_last_closed_bar(symbol) bar = _fetch_last_closed_bar(symbol)
if not bar: if not bar:
@@ -4030,45 +3994,64 @@ def _process_key_rs_level_alert(conn, row):
return return
close = float(bar[4]) close = float(bar[4])
ts = bar[0] 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() 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 = tick["break_info"]
br = detect_rs_box_break(close, up, low) notify_index = int(tick["notify_index"])
if not br: max_n = int(tick["notify_max"])
return interval = int(tick["interval_min"])
else: bar_ts = tick.get("bar_ts")
if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt):
return if tick.get("need_claim_first"):
br = rs_break_from_direction(row["direction"], up, low) conn.execute(
if not br: "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 return
conn.commit()
trigger_time = ms_to_app_local_str(int(ts)) if ts else app_now_str() 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( msg = build_wechat_rs_level_message(
symbol=sym, symbol=sym,
monitor_type=typ, monitor_type=typ,
account_label=_wechat_account_label(),
trigger_time=trigger_time, trigger_time=trigger_time,
upper=up, upper_txt=format_price_for_symbol(sym, up),
lower=low, lower_txt=format_price_for_symbol(sym, low),
trigger_close=close, close_txt=format_price_for_symbol(sym, close),
break_info=br, edge_txt=format_price_for_symbol(sym, br["edge_price"]),
break_label=br["break_label"],
direction=br["direction"],
notify_index=notify_index, notify_index=notify_index,
notify_max=max_n, notify_max=max_n,
interval_min=interval,
extra_note="OKX 本实例为提醒模式,不自动市价开仓",
) )
send_wechat_msg(msg) send_wechat_msg(msg)
conn.execute( conn.execute(
"UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, last_alert_message=? WHERE id=?", "UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, "
(br["direction"], notify_index, app_now_str(), msg, row["id"]), "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: if notify_index >= max_n:
hist_row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (row["id"],)).fetchone() hist_row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (row["id"],)).fetchone()
if hist_row: if hist_row:
insert_key_monitor_history(conn, hist_row, notify_index, msg, "key_level_alert_done") 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.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],))
conn.commit()
def _key_hard_lines_from_checks(checks): def _key_hard_lines_from_checks(checks):
@@ -4568,8 +4551,8 @@ def check_key_monitors():
if typ in KEY_MONITOR_RS_TYPES: if typ in KEY_MONITOR_RS_TYPES:
try: try:
_process_key_rs_level_alert(conn, r) _process_key_rs_level_alert(conn, r)
except Exception: except Exception as e:
pass print(f"[key_rs_level_alert] {sym} id={r['id']}: {e}")
continue continue
if typ not in KEY_MONITOR_AUTO_TYPES: if typ not in KEY_MONITOR_AUTO_TYPES:
continue continue
+123
View File
@@ -94,6 +94,129 @@ def rs_break_from_direction(direction: str, upper: float, lower: float) -> Optio
return None 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( def notify_interval_elapsed(
last_notified_at: Optional[str], last_notified_at: Optional[str],
interval_min: int, interval_min: int,
+117
View File
@@ -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)