修改支撑阻力的企业微信推送
This commit is contained in:
@@ -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()
|
||||
|
||||
+46
-64
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
+47
-64
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user