From 1b0bd41e3b81dcbe6aac94f037736857f4f32972 Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 23 May 2026 18:06:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B3=E9=94=AE=E4=BD=8D=E7=AE=B1=E4=BD=93?= =?UTF-8?q?=E7=AA=81=E7=A0=B4=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_binance/.env.example | 8 +- crypto_monitor_binance/app.py | 264 ++++++++++++++---- crypto_monitor_binance/templates/index.html | 14 +- crypto_monitor_binance/关键位自动下单说明.md | 163 ++++++----- crypto_monitor_gate/.env.example | 4 + crypto_monitor_gate/app.py | 265 ++++++++++++++---- crypto_monitor_gate/templates/index.html | 155 +++++------ crypto_monitor_gate/关键位自动下单说明.md | 240 ++++++++++------- crypto_monitor_okx/.env.example | 4 + crypto_monitor_okx/app.py | 269 ++++++++++++++++--- crypto_monitor_okx/templates/index.html | 14 +- crypto_monitor_okx/关键位自动下单说明.md | 163 ++++++----- key_monitor_lib.py | 125 +++++++++ 13 files changed, 1221 insertions(+), 467 deletions(-) create mode 100644 key_monitor_lib.py diff --git a/crypto_monitor_binance/.env.example b/crypto_monitor_binance/.env.example index d5487a4..2698024 100644 --- a/crypto_monitor_binance/.env.example +++ b/crypto_monitor_binance/.env.example @@ -93,9 +93,13 @@ KEY_CONFIRM_BAR=-1 # 【量能】突破棒成交量 > 前 N 根均量 × 倍数(默认 N=20,倍数=1.3 即放大 30%) KEY_VOLUME_MA_BARS=20 KEY_VOLUME_RATIO_MIN=1.3 -# 【突破K实体幅度】占开盘价百分比区间(须同时满足有效突破) +# 【箱体/收敛】突破K收盘越过关键位(占该侧价格%)的下限;无上限(过猛由计划RR过滤) KEY_BREAKOUT_AMP_MIN_PCT=0.03 +# 已不参与门控,可保留配置项兼容旧环境 KEY_BREAKOUT_AMP_MAX_PCT=0.5 +# 【阻力/支撑】突破后微信提醒次数与间隔(分钟) +KEY_ALERT_MAX_TIMES=3 +KEY_ALERT_INTERVAL_MINUTES=5 # 【日成交量排名】品种须在该排名前 N 名(添加关键位与运行时门控均校验) KEY_DAILY_VOLUME_RANK_MAX=30 # 【关键位自动开仓盈亏比】按确认K收盘 E 计算,严格大于该值才市价开仓(如 1.5 表示须 >1.5:1) @@ -104,8 +108,6 @@ KEY_AUTO_MIN_PLANNED_RR=1.5 KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5 # 趋势单方案:止损在突破 K 极值外侧的百分比(默认 1 即 1%) KEY_TREND_STOP_OUTSIDE_PCT=1 -KEY_ALERT_MAX_TIMES=3 -KEY_ALERT_INTERVAL_MINUTES=5 # ============================================================================= # 交易执行 / 人工风控(页面「实盘下单」) diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 2b7254c..fc940e0 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -54,6 +54,19 @@ from key_sl_tp_lib import ( sl_tp_mode_label, sl_tp_plan_summary_text, ) +from key_monitor_lib import ( + KEY_DIRECTION_WATCH, + KEY_MONITOR_ALERT_ONLY_TYPES, + KEY_MONITOR_AUTO_TYPES, + KEY_MONITOR_RS_TYPES, + auto_amp_ok, + auto_confirm_ok, + detect_rs_box_break, + format_auto_amp_line, + format_auto_confirm_line, + notify_interval_elapsed, + rs_break_from_direction, +) from hub_auth import request_allowed as hub_request_allowed from history_window_lib import ( PRESET_CUSTOM, @@ -175,7 +188,7 @@ KEY_CONFIRM_BAR = int(os.getenv("KEY_CONFIRM_BAR", "-1")) KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT = os.getenv("KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT", "true").lower() == "true" ORDER_MONITOR_TYPE_MANUAL = "下单监控" ORDER_MONITOR_TYPE_KEY_AUTO = "关键位监控" -KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"}) +# KEY_MONITOR_AUTO_TYPES / KEY_MONITOR_ALERT_ONLY_TYPES:见 key_monitor_lib # 与币安 App「仓位历史-实现盈亏」对齐:默认仅 REALIZED_PNL(手续费另计;避免与 COMMISSION 重复扣) BINANCE_APP_PNL_INCOME_TYPES = frozenset({"REALIZED_PNL"}) BINANCE_APP_PNL_INCOME_WITH_FEE = frozenset({"REALIZED_PNL", "COMMISSION"}) @@ -187,7 +200,6 @@ BINANCE_PNL_INCLUDE_FUNDING = os.getenv("BINANCE_PNL_INCLUDE_FUNDING", "false"). "true", "yes", ) -KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"}) AUTO_TRANSFER_ENABLED = os.getenv("AUTO_TRANSFER_ENABLED", "false").lower() == "true" AUTO_TRANSFER_AMOUNT = float(os.getenv("AUTO_TRANSFER_AMOUNT", "30")) AUTO_TRANSFER_FROM = os.getenv("AUTO_TRANSFER_FROM", "funding") @@ -4145,19 +4157,17 @@ def _key_hard_checks(symbol, direction, upper, lower, monitor_type): avg20 = sum(float(x[5]) for x in prev_vol) / max(len(prev_vol), 1) vol_break = float(breakout[5]) vol_ok = vol_break > avg20 * KEY_VOLUME_RATIO_MIN if avg20 > 0 else False - open_b = float(breakout[1]) close_b = float(breakout[4]) high_b = float(breakout[2]) low_b = float(breakout[3]) - amp_pct = abs(close_b - open_b) / open_b * 100 if open_b > 0 else 0 - amp_ok = (amp_pct > KEY_BREAKOUT_AMP_MIN_PCT) and (amp_pct < KEY_BREAKOUT_AMP_MAX_PCT) cfm_close = float(confirm[4]) - # 区间极值点严格以前端录入 upper/lower 为准:做多看上沿,做空看下沿 edge = float(upper) if direction == "long" else float(lower) breakout_ok = (close_b > float(upper)) if direction == "long" else (close_b < float(lower)) - confirm_ok_raw = (cfm_close > edge) if direction == "long" else (cfm_close < edge) - # 口径收紧:未发生有效突破时,不标记幅度/二确通过,避免出现“还没到位却显示Y” + amp_ok, amp_pct = auto_amp_ok( + direction, close_b, float(upper), float(lower), KEY_BREAKOUT_AMP_MIN_PCT + ) amp_ok = amp_ok and breakout_ok + confirm_ok_raw = auto_confirm_ok(direction, cfm_close, float(upper), float(lower)) confirm_ok = confirm_ok_raw and breakout_ok rank, total = _daily_volume_rank(symbol) rank_ok = (rank is not None) and (rank <= KEY_DAILY_VOLUME_RANK_MAX) @@ -4219,13 +4229,130 @@ def _finalize_key_monitor_one_shot(conn, row, last_msg, close_reason): conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],)) +def _fetch_last_closed_bar(symbol): + """最近一根闭合 K:[ts, o, h, l, c, v] 或 None。""" + ex_sym = normalize_exchange_symbol(symbol) + bars = exchange.fetch_ohlcv(ex_sym, timeframe=KLINE_TIMEFRAME, limit=5) or [] + if len(bars) < 2: + return None + closed = bars[:-1] + 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) + if not bar: + return {"summary": "5m数据不足", "metrics": ""} + close = float(bar[4]) + br = detect_rs_box_break(close, upper, lower) + if br: + return { + "summary": f"已越线:{br['break_label']}", + "metrics": f"收盘:{format_price_for_symbol(symbol, close)}", + } + return { + "summary": "待突破", + "metrics": f"收盘:{format_price_for_symbol(symbol, close)}", + } + + +def _process_key_rs_level_alert(conn, row): + """关键阻力位/支撑位:5m 收盘越上沿或下沿后,按间隔推送最多 KEY_ALERT_MAX_TIMES 次。""" + sym = row["symbol"] + typ = (row["monitor_type"] or "").strip() + up, low = float(row["upper"]), float(row["lower"]) + if up <= low: + return + bar = _fetch_last_closed_bar(sym) + if not bar: + 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() + + 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: + return + + 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, + trigger_time=trigger_time, + upper=up, + lower=low, + trigger_close=close, + break_info=br, + notify_index=notify_index, + notify_max=max_n, + ) + 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"]), + ) + 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"],)) + + def _key_hard_lines_from_checks(checks): + direction = (checks.get("direction") or "long").lower() return [ f"量能:{'通过' if checks['vol_ok'] else '不通过'}(突破K量 {round(checks['vol_break'], 4)} / 前20均量 {round(checks['avg20'], 4)},阈值1.3x)", f"突破价位:{'通过' if checks['breakout_ok'] else '不通过'}(突破K收盘 {round(float(checks['breakout_close']), 8)},关键位 {checks['edge_price']})", - f"突破K幅度:{'通过' if checks['amp_ok'] else '不通过'}({round(checks['amp_pct'], 4)}%,要求0.03%~0.5%)", - f"第二根确认:{'通过' if checks['confirm_ok'] else '不通过'}(确认收盘 {checks['confirm_close']},关键位 {checks['edge_price']})", - f"日成交量排名:{'通过' if checks['rank_ok'] else '不通过'}({checks['rank']}/{checks['rank_total']},要求前30)", + format_auto_amp_line(checks["amp_ok"], checks["amp_pct"], KEY_BREAKOUT_AMP_MIN_PCT), + format_auto_confirm_line( + checks["confirm_ok"], checks["confirm_close"], checks["edge_price"], direction + ), + f"日成交量排名:{'通过' if checks['rank_ok'] else '不通过'}({checks['rank']}/{checks['rank_total']},要求前{KEY_DAILY_VOLUME_RANK_MAX})", ] @@ -4836,7 +4963,7 @@ def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px, br return True, None -# 关键位监控(箱体/收敛可自动开仓;阻力/支撑位仅单次提醒结案) +# 关键位监控(箱体/收敛可自动开仓;阻力/支撑为双向 5m 收盘突破 + 三次提醒) def check_key_monitors(): conn = get_db() rows = conn.execute("SELECT * FROM key_monitors").fetchall() @@ -4845,7 +4972,16 @@ def check_key_monitors(): typ = (typ_raw or "").strip() if is_fib_key_monitor_type(typ): continue + if typ in KEY_MONITOR_RS_TYPES: + try: + _process_key_rs_level_alert(conn, r) + except Exception: + pass + continue + direction = (r["direction"] or "long").lower() + if direction == KEY_DIRECTION_WATCH: + continue try: checks = _key_hard_checks(sym, direction, up, low, typ) except Exception: @@ -4863,31 +4999,7 @@ def check_key_monitors(): hard_lines = _key_hard_lines_from_checks(checks) trigger_time = ms_to_app_local_str(int(checks["confirm_ts"])) if checks.get("confirm_ts") else app_now_str() - alert_only = typ in KEY_MONITOR_ALERT_ONLY_TYPES or ( - typ not in KEY_MONITOR_AUTO_TYPES and typ not in KEY_MONITOR_ALERT_ONLY_TYPES - ) - - if alert_only: - op_lines = [ - "- 本条为关键阻力/支撑或非标类型:**仅单次推送**,不进行自动开仓。", - "- 本条关键位将在推送后记入历史并从监控列表移除。", - ] - msg = build_wechat_key_monitor_message( - symbol=sym, - direction=direction, - monitor_type=typ, - trigger_time=trigger_time, - key_price=key_price, - confirm_close=checks["confirm_close"], - hard_lines=hard_lines, - btc8h_status=btc8h_status, - coin4h_status=coin4h_status, - swing4h_pct=checks.get("swing4h_pct") or 0.0, - op_lines=op_lines, - risk_tip=risk_tip, - ) - send_wechat_msg(msg) - _finalize_key_monitor_one_shot(conn, r, msg, "key_level_alert_only") + if typ not in KEY_MONITOR_AUTO_TYPES: continue plan_tuple, sl_tp_mode = _key_plan_sl_tp_for_row(r, direction, up, low, checks) @@ -5665,11 +5777,10 @@ def render_main_page(page="trade"): active_count = len(order_list) can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS key_gate_rule_text = ( - f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}、确认棒偏移 {KEY_CONFIRM_BAR}|" - f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|" - f"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}|" - f"箱体/收敛可选 SL/TP 方案(标准 / 箱体1R·止盈1.5H / 趋势单+自填止盈)|移动保本默认关|" - f"斐波:限价 @ E(SL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%" + f"【箱体/收敛】{KLINE_TIMEFRAME} 两根闭合K|突破越过关键位 > {KEY_BREAKOUT_AMP_MIN_PCT}%|" + f"确认K收于箱外|量能>前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|" + f"RR>{KEY_AUTO_MIN_PLANNED_RR}|日成交前{KEY_DAILY_VOLUME_RANK_MAX}|" + f"【阻力/支撑】填上/下沿,5m 收盘突破任一侧即提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不选方向、不自动开仓" ) strategy_extra = {} if page in ("strategy", "strategy_trend", "strategy_roll"): @@ -5856,9 +5967,22 @@ def api_price_snapshot(): gate_summary = f"斐波 挂E={entry_txt} {'标记价将失效' if inval else '等待成交'}" if _sqlite_row_val(r, "fib_limit_order_id"): gate_metrics = f"限价单:{_sqlite_row_val(r, 'fib_limit_order_id')}" + elif (r["monitor_type"] or "").strip() in KEY_MONITOR_RS_TYPES: + try: + prev = _key_rs_gate_preview(r["symbol"], r["upper"], r["lower"]) + gate_summary = prev.get("summary") or "-" + gate_metrics = prev.get("metrics") or "" + except Exception: + gate_summary = "-" else: try: - gate = _key_hard_checks(r["symbol"], (r["direction"] or "long").lower(), r["upper"], r["lower"], r["monitor_type"]) + gate = _key_hard_checks( + r["symbol"], + (r["direction"] or "long").lower(), + r["upper"], + r["lower"], + r["monitor_type"], + ) except Exception: gate = None if gate: @@ -6330,11 +6454,13 @@ def add_key(): if not symbol: flash("symbol 不能为空") return redirect("/key_monitor") - direction_sel = (d.get("direction") or "").strip().lower() - if direction_sel not in ("long", "short"): - flash("请选择做多或做空") - return redirect("/key_monitor") mt = (d.get("type") or "").strip() + direction_sel = (d.get("direction") or "").strip().lower() + if mt in KEY_MONITOR_RS_TYPES: + direction_sel = KEY_DIRECTION_WATCH + elif direction_sel not in ("long", "short"): + flash("箱体/收敛突破请选择做多或做空") + return redirect("/key_monitor") allowed_types = ( tuple(KEY_MONITOR_AUTO_TYPES) + tuple(KEY_MONITOR_ALERT_ONLY_TYPES) @@ -6369,6 +6495,10 @@ def add_key(): lw = round_price_to_exchange(ex_sym_key, float(d["lower"])) upper_px = float(uh) if uh is not None else float(d["upper"]) lower_px = float(lw) if lw is not None else float(d["lower"]) + if upper_px <= lower_px: + conn.close() + flash("上沿必须大于下沿") + return redirect("/key_monitor") be_flag = parse_breakeven_enabled_form(d.get("breakeven_enabled")) if is_fib_key_monitor_type(mt): ok_fib, err_fib = _add_fib_key_monitor( @@ -6408,12 +6538,32 @@ def add_key(): mtpx = round_price_to_exchange(ex_sym_key, manual_tp) if mtpx is not None: manual_tp = float(mtpx) - conn.execute( - "INSERT INTO key_monitors " - "(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled) " - "VALUES (?,?,?,?,?,?,?,?)", - (symbol, mt, direction_sel, upper_px, lower_px, sl_tp_mode, manual_tp, be_flag), - ) + if mt in KEY_MONITOR_RS_TYPES: + conn.execute( + "INSERT INTO key_monitors " + "(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled," + "max_notify,notify_interval_min) " + "VALUES (?,?,?,?,?,?,?,?,?,?)", + ( + symbol, + mt, + direction_sel, + upper_px, + lower_px, + sl_tp_mode, + manual_tp, + be_flag, + KEY_ALERT_MAX_TIMES, + KEY_ALERT_INTERVAL_MINUTES, + ), + ) + else: + conn.execute( + "INSERT INTO key_monitors " + "(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled) " + "VALUES (?,?,?,?,?,?,?,?)", + (symbol, mt, direction_sel, upper_px, lower_px, sl_tp_mode, manual_tp, be_flag), + ) conn.commit() conn.close() ctr = False @@ -6427,7 +6577,13 @@ def add_key(): extra = "" if mt in KEY_MONITOR_AUTO_TYPES: extra = f"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'开' if be_flag else '关'}" - flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}){extra}") + if mt in KEY_MONITOR_RS_TYPES: + flash( + f"添加成功({symbol} 日成交量排名 {rank}/{total})|阻力/支撑:双向监控上/下沿," + f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)" + ) + else: + flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}){extra}") if ctr: flash( "⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。" diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 9ffe01c..237fb59 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -281,7 +281,7 @@ - @@ -304,7 +304,11 @@
{{ k.symbol }} + {% if k.direction == 'watch' %} + 双向 + {% else %} {{ '做多' if k.direction == 'long' else '做空' }} + {% endif %} {{ k.monitor_type }}
@@ -1406,6 +1410,7 @@ if(journalForm){ function syncKeyMonitorFormFields(){ const typeEl = document.querySelector('#key-form [name="type"]'); + const dirEl = document.getElementById("key-direction"); const modeEl = document.getElementById("key-sl-tp-mode"); const manualTp = document.getElementById("key-manual-tp"); const beWrap = document.getElementById("key-breakeven-wrap"); @@ -1413,8 +1418,15 @@ function syncKeyMonitorFormFields(){ const t = (typeEl.value || "").trim(); const autoTypes = new Set(["箱体突破","收敛突破"]); const fibTypes = new Set(["斐波回调0.618","斐波回调0.786"]); + const rsTypes = new Set(["关键阻力位","关键支撑位"]); const showAuto = autoTypes.has(t); const showBe = showAuto || fibTypes.has(t); + const showDir = !rsTypes.has(t); + if(dirEl){ + dirEl.style.display = showDir ? "" : "none"; + dirEl.required = showDir; + if(!showDir) dirEl.value = ""; + } if(modeEl) modeEl.style.display = showAuto ? "" : "none"; if(manualTp){ const trend = showAuto && modeEl && modeEl.value === "trend_manual"; diff --git a/crypto_monitor_binance/关键位自动下单说明.md b/crypto_monitor_binance/关键位自动下单说明.md index 230d87e..87d98ff 100644 --- a/crypto_monitor_binance/关键位自动下单说明.md +++ b/crypto_monitor_binance/关键位自动下单说明.md @@ -1,101 +1,142 @@ -# 关键位自动下单说明 +# 关键位监控说明(自动开仓 + 人工盯盘) -**适用仓库:`crypto_monitor_binance`|交易所:Binance U 本位永续**(Gate 版见同名的 `crypto_monitor_gate` 目录。) +**适用:`crypto_monitor_binance`(Binance U 本位永续)** +Gate / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `key_monitor_lib.py`。 -本文档与 `.env`、`app.check_key_monitors`、`app.add_key`、`_market_open_for_key_monitor` 的实现一致。 +本文档与 `.env`、`check_key_monitors`、`add_key`、`_key_hard_checks`、`_process_key_rs_level_alert` 一致。 --- -## 结构与是否自动开仓 +## 一、监控类型总览 -| `key_monitors.monitor_type`(录入类型) | 自动下单 | 触发后处置 | -|---------------------------------------|----------|------------| -| **箱体突破** | 是(满足全部条件) | **一次性结案**:写 `key_monitor_history` → 从 `key_monitors` **删除** | -| **收敛突破** | 是(同上) | 同上 | -| **关键阻力位** | 否 | 企业微信 **1 次** → `close_reason=key_level_alert_only` → **失效** | -| **关键支撑位** | 否 | 同上 | +| 录入类型 | 录入时选方向 | 自动市价开仓 | 触发与结案 | +|----------|--------------|--------------|------------| +| **箱体突破** | **必选** 多/空 | **是**(门控 + RR) | 条件满足 → 开仓或 `rr_insufficient` / `exchange_failed` → **一次性删除** | +| **收敛突破** | **必选** 多/空 | **是**(同上) | 同上 | +| **关键阻力位** | **不选**(`direction=watch`) | **否** | 5m 收盘突破上/下沿 → 微信 **3 次** → `key_level_alert_done` | +| **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) | +| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) | -触发条件:**5m 收线硬门控** `_key_hard_checks`(量能、突破幅度、第二根收盘确认、日成交量前 30 等)。 +**添加时(所有类型):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30)**;上沿 **>** 下沿。 --- -## 录入限制(`/add_key`) +## 二、关键阻力位 / 关键支撑位(人工盯盘) -- 存在 **`order_monitors.status='active'`** 时:**禁止添加** 「箱体突破」「收敛突破」。 -- **关键阻力位 / 关键支撑位**:不受上条限制;触发后 **仅单次微信提醒**,然后结案。 -- **4h EMA55 与所选方向逆势**:**不拦截**;添加成功后 **Flash** 提示。 -- 上下沿入库前经 **`round_price_to_exchange`** 按合约 **价格精度** 取整。 +### 2.1 录入 ---- +- 填写 **上沿 `upper`** 与 **下沿 `lower`**(程序同时监控两侧,**无法预先判定**做多还是做空)。 +- 页面 **不显示、不要求** 方向;库中 `direction` 初始为 `watch`,**首次突破后** 写入 `long`(向上突破上沿)或 `short`(向下突破下沿)。 -## 环境与参数(`.env`) +### 2.2 触发(极简) -| 变量 | 含义 | 默认 | +- 周期:**`KLINE_TIMEFRAME`(默认 5m)最近一根已闭合 K** 的 **收盘价**(非影线)。 +- **向上突破上沿:** `收盘 > upper` → 推断方向 **多 / 向上**,本次监控任务开始按节奏提醒。 +- **向下突破下沿:** `收盘 < lower` → 推断方向 **空 / 向下**,本次任务同样开始提醒。 +- **任一侧突破即结束本条监控周期**(不会在突破后再等待另一侧;上沿、下沿谁先满足用谁,同根 K 仅可能满足一侧)。 + +**不参与:** 量能、二确 K、越过幅度下限、日成交排名(运行时)、计划 RR、自动开仓。 + +### 2.3 微信提醒次数 + +| 配置 | 默认 | 含义 | |------|------|------| -| `KEY_AUTO_MIN_PLANNED_RR` | 计划 RR 阈值:**仅当严格大于该值** 才自动开仓(按下方 `E` 计算) | `1.5` | -| `KEY_STOP_OUTSIDE_BREAKOUT_PCT` | 止损:突破 K 极值向外 **百分比**(多:`低×(1−p/100)`;空:`高×(1+p/100)`) | `0.5` | +| `KEY_ALERT_MAX_TIMES` | `3` | 突破后最多推送 3 次 | +| `KEY_ALERT_INTERVAL_MINUTES` | `5` | 相邻两次推送至少间隔 5 分钟 | -**其余与本仓库手动实盘一致:** `KLINE_TIMEFRAME`、`RISK_PERCENT`、`LIVE_TRADING_ENABLED`、`BREAKEVEN_*`、`DAILY_OPEN_ALERT_THRESHOLD`,以及 **`BINANCE_*`**(密钥、`BINANCE_MARGIN_MODE`、`BINANCE_POSITION_MODE`、`BINANCE_TRIGGER_WORKING_TYPE` 等)。资金字段舍入端口径与 **`FUNDS_DECIMALS`** 一致。 +- 第 1 次:首次检测到突破的当次轮询(若已闭合 5m 满足条件)。 +- 第 2、3 次:仅按间隔推送(**不要求**价格仍在箱外)。 +- 第 3 次推送后:写入 `key_monitor_history`,`close_reason=**key_level_alert_done**`,从 `key_monitors` **删除**。 + +### 2.4 与箱体/收敛的区别 + +| 项目 | 阻力/支撑 | 箱体/收敛 | +|------|-----------|-----------| +| 方向 | 程序推断 | 人工选择 | +| K 线根数 | 1 根闭合 5m | 2 根(突破 K + 确认 K) | +| 提醒次数 | 3 次后结案 | 自动单:触发后 1 次业务推送并结案 | --- -## 计价与下单口径 +## 三、箱体突破 / 收敛突破(自动开仓) -| 用途 | 价格 | -|------|------| -| 企业微信展示、**与 RR 门槛比较的计划 RR** | 确认 K(第二根闭合 5m)收盘 **`E`** | -| **实际开仓** | **市价**(`place_exchange_order`,与 `/add_order` 一致);成交价可能与 `E` **滑点** | -| **以损定仓** | `calc_risk_fraction(direction, 当前市价, 止损)` + `RISK_PERCENT`(保证金等 **`FUNDS_DECIMALS`** 舍入,与 `/add_order` 一致) | +### 3.1 K 线结构(默认索引) -- 开仓成功后:`order_monitors.monitor_type` 为 **关键位监控**;持仓卡片「来源」显示之。手动开仓为 **下单监控**。 -- 持仓列表中的 **盈亏比**:按 **实际成交价** 相对 SL/TP 重算,可与「按 `E` 算的计划 RR」略有偏差。 -- **本仓库止盈止损挂单**:开仓后由 **`_binance_place_tp_sl_orders`** 挂载(与手动一致:U 本位条件/Algo 类触发单;具体类型以 ccxt / 交易所为准)。 +| 角色 | 环境变量 | 默认 | 含义 | +|------|----------|------|------| +| 突破 K | `KEY_CONFIRM_BREAKOUT_BAR` | `-2` | 倒数第 2 根闭合 K | +| 确认 K | `KEY_CONFIRM_BAR` | `-1` | 倒数第 1 根闭合 K | ---- +### 3.2 硬门控(须全部通过) -## 自动单止盈 / 止损(仅箱体突破、收敛突破) +1. **有效突破(收盘越界)** + - 多:`突破 K 收盘 > upper` + - 空:`突破 K 收盘 < lower` -添加关键位时在页面选择 **止盈止损方案**(写入 `key_monitors.sl_tp_mode`)。确认 K 收盘 **E**,箱体高 **H = |upper − lower|`**。 +2. **突破越过幅度(仅下限)** + - 多:`(突破 K 收盘 − upper) / upper × 100 > KEY_BREAKOUT_AMP_MIN_PCT`(默认 **0.03%**) + - 空:`(lower − 突破 K 收盘) / lower × 100 >` 同上 + - **无上限**;突破过猛由 **计划 RR** 过滤。 + - **不再**使用 K 线实体占开盘价比例;`KEY_BREAKOUT_AMP_MAX_PCT` **已不参与门控**。 + +3. **确认 K 不进箱体** + - 多:确认 K 收盘 **`> upper`**(不得在 `[lower, upper]` 内) + - 空:确认 K 收盘 **`< lower`** + +4. **量能:** 突破 K 成交量 > 前 `KEY_VOLUME_MA_BARS`(默认 20)根均量 × `KEY_VOLUME_RATIO_MIN`(默认 1.3) + +5. **日成交量排名:** 运行时仍须前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30) + +6. **计划 RR(最后经济门控):** 按确认 K 收盘 **E** 计算 SL/TP 后,`RR` **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)才市价开仓 + +### 3.3 止损 / 止盈(确认 K 收盘为 E) + +箱体高 **H = |upper − lower|**。止损锚在 **突破 K 极值** 外侧: + +| 方向 | 止损(标准/趋势方案) | +|------|------------------------| +| 多 | 突破 K **最低价** × (1 − `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) | +| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) | + +止盈方案见下表(与改版前一致): | 方案 | `sl_tp_mode` | 多:SL / TP | 空:SL / TP | |------|--------------|-------------|-------------| -| 标准突破(默认) | `standard` | 突破 K 低 × (1−`KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) / **E+H** | 突破 K 高 × (1+外侧%) / **E−H** | -| 箱体1R·止盈1.5H | `box_1p5` | **E−H** / **E+1.5×H**(RR≈1.5) | **E+H** / **E−1.5×H** | -| 趋势单·自填止盈 | `trend_manual` | 突破 K 低 × (1−`KEY_TREND_STOP_OUTSIDE_PCT`%) / **录入止盈** | 突破 K 高 × (1+外侧%) / **录入止盈** | +| 标准突破 | `standard` | 突破 K 低外侧% / **E+H** | 突破 K 高外侧% / **E−H** | +| 箱体 1R·止盈 1.5H | `box_1p5` | **E−H** / **E+1.5×H** | **E+H** / **E−1.5×H** | +| 趋势单·自填止盈 | `trend_manual` | 突破 K 低 × (1−`KEY_TREND_STOP_OUTSIDE_PCT`%) / **录入止盈** | 突破 K 高外侧% / **录入止盈** | -计划 **`RR = calc_rr_ratio(direction, E, SL, TP)`**。若为 `None` 或 **RR ≤ `KEY_AUTO_MIN_PLANNED_RR`** → **不下单**,走 `rr_insufficient` 结案。 - -**移动保本:** 添加时可勾选(默认关);开仓写入 `order_monitors.breakeven_enabled` 与勾选一致。详见仓库根目录 `关键位止盈止损与移动保本更新说明.md`。 - ---- - -## 一次性结案(`close_reason`) - -以下任一发生:**按需发微信** → **`key_monitor_history`** → **从 `key_monitors` 删除**;**不会对同一条关键位重复轮询重试开仓**。 +### 3.4 一次性结案(`close_reason`) | `close_reason` | 含义 | |----------------|------| -| `rr_insufficient` | 门控通过,但计划 RR 未达标或 SL/TP / RR **几何无效** | -| `exchange_failed` | 计划 RR 达标,但未开实盘、`LIVE_TRADING_ENABLED=false`、风控、保证金或 **交易所报错** 等导致 **开仓失败** | -| `auto_opened` | 计划 RR 达标且 **市价开仓成功**(已写 `order_monitors`,并已挂止盈止损) | -| `key_level_alert_only` | 阻力/支撑位 **仅推送**结案 | +| `rr_insufficient` | 门控通过但 RR 不达标或 SL/TP 几何无效 | +| `exchange_failed` | RR 达标但实盘/交易所等原因未开仓 | +| `auto_opened` | RR 达标且市价开仓成功 | +| `key_level_alert_done` | 阻力/支撑 **3 次提醒** 完成 | --- -## 与企业微信推送 +## 四、环境与参数(`.env` 摘要) -每种结案路径 **至多一条**主业务推送(RR 不足 / 下单失败 / 开仓成功 / 阻力支撑仅提醒)。 - -旧版「满 `KEY_ALERT_MAX_TIMES` 次再归档」对已触发结案的路径 **不再适用**;表中 `notification_count`、`max_notify` 等字段仍可能存在,以 **导出、兼容** 为主。 +| 变量 | 箱体/收敛 | 阻力/支撑 | +|------|-----------|-----------| +| `KEY_BREAKOUT_AMP_MIN_PCT` | 突破越过下限(默认 0.03) | 不用 | +| `KEY_BREAKOUT_AMP_MAX_PCT` | **已废弃门控** | 不用 | +| `KEY_VOLUME_*` / `KEY_CONFIRM_*` | 用 | 不用 | +| `KEY_AUTO_MIN_PLANNED_RR` | 用 | 不用 | +| `KEY_ALERT_MAX_TIMES` / `KEY_ALERT_INTERVAL_MINUTES` | 不用 | 用(默认 3 次 / 5 分钟) | +| `KEY_DAILY_VOLUME_RANK_MAX` | 添加时 + 运行时 | **仅添加时** | --- -## 相关代码位置(通用) +## 五、相关代码 -| 说明 | 符号 | +| 说明 | 位置 | |------|------| -| 门控与主循环 | `check_key_monitors` | -| 录入、有仓拦截、4h Flash | `add_key` | -| 市价开仓 + 写 `order_monitors` | `_market_open_for_key_monitor` | -| 计划 RR | `calc_rr_ratio(direction, E, SL, TP)` | -| 价格精度 | `round_price_to_exchange` | +| 共享判定 | `key_monitor_lib.py` | +| 主循环 | `check_key_monitors` | +| 自动门控 | `_key_hard_checks` | +| 阻力/支撑提醒 | `_process_key_rs_level_alert` | +| 录入 | `add_key` | +| 开仓 | `_market_open_for_key_monitor` | diff --git a/crypto_monitor_gate/.env.example b/crypto_monitor_gate/.env.example index fd4f819..cf59823 100644 --- a/crypto_monitor_gate/.env.example +++ b/crypto_monitor_gate/.env.example @@ -94,8 +94,12 @@ KEY_CONFIRM_BAR=-1 KEY_VOLUME_MA_BARS=20 KEY_VOLUME_RATIO_MIN=1.3 # 【突破K实体幅度】占开盘价百分比区间 +# 【箱体/收敛】突破K收盘越过关键位下限%;无上限(过猛由计划RR过滤) KEY_BREAKOUT_AMP_MIN_PCT=0.03 KEY_BREAKOUT_AMP_MAX_PCT=0.5 +# 【阻力/支撑】突破后微信提醒 +KEY_ALERT_MAX_TIMES=3 +KEY_ALERT_INTERVAL_MINUTES=5 # 【日成交量排名】品种须在该排名前 N 名 KEY_DAILY_VOLUME_RANK_MAX=30 # 【关键位自动开仓盈亏比】严格大于该值才市价开仓 diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 96cf117..614e997 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -55,6 +55,19 @@ from key_sl_tp_lib import ( sl_tp_mode_label, sl_tp_plan_summary_text, ) +from key_monitor_lib import ( + KEY_DIRECTION_WATCH, + KEY_MONITOR_ALERT_ONLY_TYPES, + KEY_MONITOR_AUTO_TYPES, + KEY_MONITOR_RS_TYPES, + auto_amp_ok, + auto_confirm_ok, + detect_rs_box_break, + format_auto_amp_line, + format_auto_confirm_line, + notify_interval_elapsed, + rs_break_from_direction, +) from hub_auth import request_allowed as hub_request_allowed from history_window_lib import ( PRESET_CUSTOM, @@ -181,8 +194,7 @@ EXCHANGE_POSITION_SYNC_FROM_BJ = (os.getenv("EXCHANGE_POSITION_SYNC_FROM_BJ") or EXCHANGE_POSITION_HISTORY_LIMIT = max(50, min(1000, int(os.getenv("EXCHANGE_POSITION_HISTORY_LIMIT", "200")))) _LAST_EXCHANGE_PNL_SYNC_AT = 0.0 -KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"}) -KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"}) +# KEY_MONITOR_AUTO_TYPES / KEY_MONITOR_ALERT_ONLY_TYPES:见 key_monitor_lib AUTO_TRANSFER_ENABLED = os.getenv("AUTO_TRANSFER_ENABLED", "false").lower() == "true" AUTO_TRANSFER_AMOUNT = float(os.getenv("AUTO_TRANSFER_AMOUNT", "30")) AUTO_TRANSFER_FROM = os.getenv("AUTO_TRANSFER_FROM", "funding") @@ -4016,19 +4028,17 @@ def _key_hard_checks(symbol, direction, upper, lower, monitor_type): avg20 = sum(float(x[5]) for x in prev_vol) / max(len(prev_vol), 1) vol_break = float(breakout[5]) vol_ok = vol_break > avg20 * KEY_VOLUME_RATIO_MIN if avg20 > 0 else False - open_b = float(breakout[1]) close_b = float(breakout[4]) high_b = float(breakout[2]) low_b = float(breakout[3]) - amp_pct = abs(close_b - open_b) / open_b * 100 if open_b > 0 else 0 - amp_ok = (amp_pct > KEY_BREAKOUT_AMP_MIN_PCT) and (amp_pct < KEY_BREAKOUT_AMP_MAX_PCT) cfm_close = float(confirm[4]) - # 区间极值点严格以前端录入 upper/lower 为准:做多看上沿,做空看下沿 edge = float(upper) if direction == "long" else float(lower) breakout_ok = (close_b > float(upper)) if direction == "long" else (close_b < float(lower)) - confirm_ok_raw = (cfm_close > edge) if direction == "long" else (cfm_close < edge) - # 口径收紧:未发生有效突破时,不标记幅度/二确通过,避免出现“还没到位却显示Y” + amp_ok, amp_pct = auto_amp_ok( + direction, close_b, float(upper), float(lower), KEY_BREAKOUT_AMP_MIN_PCT + ) amp_ok = amp_ok and breakout_ok + confirm_ok_raw = auto_confirm_ok(direction, cfm_close, float(upper), float(lower)) confirm_ok = confirm_ok_raw and breakout_ok rank, total = _daily_volume_rank(symbol) rank_ok = (rank is not None) and (rank <= KEY_DAILY_VOLUME_RANK_MAX) @@ -4090,13 +4100,130 @@ def _finalize_key_monitor_one_shot(conn, row, last_msg, close_reason): conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],)) +def _fetch_last_closed_bar(symbol): + """最近一根闭合 K:[ts, o, h, l, c, v] 或 None。""" + ex_sym = normalize_exchange_symbol(symbol) + bars = exchange.fetch_ohlcv(ex_sym, timeframe=KLINE_TIMEFRAME, limit=5) or [] + if len(bars) < 2: + return None + closed = bars[:-1] + 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) + if not bar: + return {"summary": "5m数据不足", "metrics": ""} + close = float(bar[4]) + br = detect_rs_box_break(close, upper, lower) + if br: + return { + "summary": f"已越线:{br['break_label']}", + "metrics": f"收盘:{format_price_for_symbol(symbol, close)}", + } + return { + "summary": "待突破", + "metrics": f"收盘:{format_price_for_symbol(symbol, close)}", + } + + +def _process_key_rs_level_alert(conn, row): + """关键阻力位/支撑位:5m 收盘越上沿或下沿后,按间隔推送最多 KEY_ALERT_MAX_TIMES 次。""" + sym = row["symbol"] + typ = (row["monitor_type"] or "").strip() + up, low = float(row["upper"]), float(row["lower"]) + if up <= low: + return + bar = _fetch_last_closed_bar(sym) + if not bar: + 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() + + 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: + return + + 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, + trigger_time=trigger_time, + upper=up, + lower=low, + trigger_close=close, + break_info=br, + notify_index=notify_index, + notify_max=max_n, + ) + 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"]), + ) + 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"],)) + + def _key_hard_lines_from_checks(checks): + direction = (checks.get("direction") or "long").lower() return [ f"量能:{'通过' if checks['vol_ok'] else '不通过'}(突破K量 {round(checks['vol_break'], 4)} / 前20均量 {round(checks['avg20'], 4)},阈值1.3x)", f"突破价位:{'通过' if checks['breakout_ok'] else '不通过'}(突破K收盘 {round(float(checks['breakout_close']), 8)},关键位 {checks['edge_price']})", - f"突破K幅度:{'通过' if checks['amp_ok'] else '不通过'}({round(checks['amp_pct'], 4)}%,要求0.03%~0.5%)", - f"第二根确认:{'通过' if checks['confirm_ok'] else '不通过'}(确认收盘 {checks['confirm_close']},关键位 {checks['edge_price']})", - f"日成交量排名:{'通过' if checks['rank_ok'] else '不通过'}({checks['rank']}/{checks['rank_total']},要求前30)", + format_auto_amp_line(checks["amp_ok"], checks["amp_pct"], KEY_BREAKOUT_AMP_MIN_PCT), + format_auto_confirm_line( + checks["confirm_ok"], checks["confirm_close"], checks["edge_price"], direction + ), + f"日成交量排名:{'通过' if checks['rank_ok'] else '不通过'}({checks['rank']}/{checks['rank_total']},要求前{KEY_DAILY_VOLUME_RANK_MAX})", ] @@ -4705,7 +4832,7 @@ def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px, br return True, None -# 关键位监控(箱体/收敛可自动开仓;阻力/支撑位仅单次提醒结案) +# 关键位监控(箱体/收敛可自动开仓;阻力/支撑为双向 5m 收盘突破 + 三次提醒) def check_key_monitors(): conn = get_db() rows = conn.execute("SELECT * FROM key_monitors").fetchall() @@ -4714,7 +4841,16 @@ def check_key_monitors(): typ = (typ_raw or "").strip() if is_fib_key_monitor_type(typ): continue + if typ in KEY_MONITOR_RS_TYPES: + try: + _process_key_rs_level_alert(conn, r) + except Exception: + pass + continue + direction = (r["direction"] or "long").lower() + if direction == KEY_DIRECTION_WATCH: + continue try: checks = _key_hard_checks(sym, direction, up, low, typ) except Exception: @@ -4732,31 +4868,7 @@ def check_key_monitors(): hard_lines = _key_hard_lines_from_checks(checks) trigger_time = ms_to_app_local_str(int(checks["confirm_ts"])) if checks.get("confirm_ts") else app_now_str() - alert_only = typ in KEY_MONITOR_ALERT_ONLY_TYPES or ( - typ not in KEY_MONITOR_AUTO_TYPES and typ not in KEY_MONITOR_ALERT_ONLY_TYPES - ) - - if alert_only: - op_lines = [ - "- 本条为关键阻力/支撑或非标类型:**仅单次推送**,不进行自动开仓。", - "- 本条关键位将在推送后记入历史并从监控列表移除。", - ] - msg = build_wechat_key_monitor_message( - symbol=sym, - direction=direction, - monitor_type=typ, - trigger_time=trigger_time, - key_price=key_price, - confirm_close=checks["confirm_close"], - hard_lines=hard_lines, - btc8h_status=btc8h_status, - coin4h_status=coin4h_status, - swing4h_pct=checks.get("swing4h_pct") or 0.0, - op_lines=op_lines, - risk_tip=risk_tip, - ) - send_wechat_msg(msg) - _finalize_key_monitor_one_shot(conn, r, msg, "key_level_alert_only") + if typ not in KEY_MONITOR_AUTO_TYPES: continue plan_tuple, sl_tp_mode = _key_plan_sl_tp_for_row(r, direction, up, low, checks) @@ -5670,11 +5782,10 @@ def render_main_page(page="trade"): active_count = len(order_list) can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS key_gate_rule_text = ( - f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}、确认棒偏移 {KEY_CONFIRM_BAR}|" - f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|" - f"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}|" - f"箱体/收敛可选 SL/TP 方案(标准 / 箱体1R·止盈1.5H / 趋势单+自填止盈)|移动保本默认关|" - f"斐波:限价 @ E(SL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%" + f"【箱体/收敛】{KLINE_TIMEFRAME} 两根闭合K|突破越过关键位 > {KEY_BREAKOUT_AMP_MIN_PCT}%|" + f"确认K收于箱外|量能>前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|" + f"RR>{KEY_AUTO_MIN_PLANNED_RR}|日成交前{KEY_DAILY_VOLUME_RANK_MAX}|" + f"【阻力/支撑】填上/下沿,5m 收盘突破任一侧即提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不选方向、不自动开仓" ) strategy_extra = {} if page in ("strategy", "strategy_trend", "strategy_roll"): @@ -5885,9 +5996,22 @@ def api_price_snapshot(): gate_summary = f"斐波 挂E={entry_txt} {'标记价将失效' if inval else '等待成交'}" if _sqlite_row_val(r, "fib_limit_order_id"): gate_metrics = f"限价单:{_sqlite_row_val(r, 'fib_limit_order_id')}" + elif (r["monitor_type"] or "").strip() in KEY_MONITOR_RS_TYPES: + try: + prev = _key_rs_gate_preview(r["symbol"], r["upper"], r["lower"]) + gate_summary = prev.get("summary") or "-" + gate_metrics = prev.get("metrics") or "" + except Exception: + gate_summary = "-" else: try: - gate = _key_hard_checks(r["symbol"], (r["direction"] or "long").lower(), r["upper"], r["lower"], r["monitor_type"]) + gate = _key_hard_checks( + r["symbol"], + (r["direction"] or "long").lower(), + r["upper"], + r["lower"], + r["monitor_type"], + ) except Exception: gate = None if gate: @@ -6380,11 +6504,13 @@ def add_key(): if not symbol: flash("symbol 不能为空") return redirect("/key_monitor") - direction_sel = (d.get("direction") or "").strip().lower() - if direction_sel not in ("long", "short"): - flash("请选择做多或做空") - return redirect("/key_monitor") mt = (d.get("type") or "").strip() + direction_sel = (d.get("direction") or "").strip().lower() + if mt in KEY_MONITOR_RS_TYPES: + direction_sel = KEY_DIRECTION_WATCH + elif direction_sel not in ("long", "short"): + flash("箱体/收敛突破请选择做多或做空") + return redirect("/key_monitor") allowed_types = ( tuple(KEY_MONITOR_AUTO_TYPES) + tuple(KEY_MONITOR_ALERT_ONLY_TYPES) @@ -6428,6 +6554,11 @@ def add_key(): return redirect("/key_monitor") upper_px = round_price_to_exchange(ex_sym_key, upper_raw) lower_px = round_price_to_exchange(ex_sym_key, lower_raw) + if float(upper_px) <= float(lower_px): + conn.close() + conn = None + flash("上沿必须大于下沿") + return redirect("/key_monitor") be_flag = parse_breakeven_enabled_form(d.get("breakeven_enabled")) if is_fib_key_monitor_type(mt): ok_fib, err_fib = _add_fib_key_monitor( @@ -6471,12 +6602,32 @@ def add_key(): mtpx = round_price_to_exchange(ex_sym_key, manual_tp) if mtpx is not None: manual_tp = float(mtpx) - conn.execute( - "INSERT INTO key_monitors " - "(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled) " - "VALUES (?,?,?,?,?,?,?,?)", - (symbol, mt, direction_sel, upper_px, lower_px, sl_tp_mode, manual_tp, be_flag), - ) + if mt in KEY_MONITOR_RS_TYPES: + conn.execute( + "INSERT INTO key_monitors " + "(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled," + "max_notify,notify_interval_min) " + "VALUES (?,?,?,?,?,?,?,?,?,?)", + ( + symbol, + mt, + direction_sel, + upper_px, + lower_px, + sl_tp_mode, + manual_tp, + be_flag, + KEY_ALERT_MAX_TIMES, + KEY_ALERT_INTERVAL_MINUTES, + ), + ) + else: + conn.execute( + "INSERT INTO key_monitors " + "(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled) " + "VALUES (?,?,?,?,?,?,?,?)", + (symbol, mt, direction_sel, upper_px, lower_px, sl_tp_mode, manual_tp, be_flag), + ) conn.commit() conn.close() conn = None @@ -6491,7 +6642,13 @@ def add_key(): extra = "" if mt in KEY_MONITOR_AUTO_TYPES: extra = f"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'开' if be_flag else '关'}" - flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}){extra}") + if mt in KEY_MONITOR_RS_TYPES: + flash( + f"添加成功({symbol} 日成交量排名 {rank}/{total})|阻力/支撑:双向监控上/下沿," + f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)" + ) + else: + flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}){extra}") if ctr: flash( "⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。" diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index bce99a2..237fb59 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -150,11 +150,11 @@ .pos-side-badge{padding:3px 8px;border-radius:6px;font-size:.72rem;font-weight:500;line-height:1.2} .pos-side-long{background:#253a6e;color:#6eb5ff} .pos-side-short{background:#4a2230;color:#ff8a8a} - .pos-close-btn{padding:6px 14px;background:#c45454;color:#fff;border-radius:8px;text-decoration:none;font-size:.82rem;font-weight:500;flex-shrink:0;white-space:nowrap} - .pos-close-btn:hover{background:#d66565;color:#fff} .pos-head-actions{display:flex;align-items:center;gap:6px;flex-shrink:0} .pos-entrust-btn{padding:6px 12px;background:#2a4a7a;color:#8fc8ff;border:none;border-radius:8px;font-size:.82rem;font-weight:500;cursor:pointer;white-space:nowrap} .pos-entrust-btn:hover{background:#355d96} + .pos-close-btn{padding:6px 14px;background:#c45454;color:#fff;border-radius:8px;text-decoration:none;font-size:.82rem;font-weight:500;flex-shrink:0;white-space:nowrap;border:none;cursor:pointer;display:inline-block} + .pos-close-btn:hover{background:#d66565;color:#fff} .pos-ex-orders{margin-top:10px;padding-top:10px;border-top:1px dashed #2a3348} .pos-ex-orders-title{font-size:.74rem;color:#7d8799;margin-bottom:6px} .pos-ex-order-row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:.78rem;color:#c5cce0;margin-top:5px} @@ -199,14 +199,14 @@
开单次数
{{ s.opens_count }}
平仓笔数
{{ s.closed_count }}
胜率
{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}
-
净盈亏(U)
{{ signed_usdt_fmt(s.net_pnl_u) }}
-
亏损额合计(U)
{{ usdt_fmt(s.loss_sum_u) }}
-
单笔最大亏损(U)
{% if s.max_single_loss is not none %}{{ signed_usdt_fmt(s.max_single_loss) }}{% else %}-{% endif %}
-
单笔最大盈利(U)
{% if s.max_single_profit is not none %}{{ usdt_fmt(s.max_single_profit) }}{% else %}-{% endif %}
-
最大回撤(U)
{{ usdt_fmt(s.max_drawdown_u) }}
+
净盈亏(U)
{{ funds_fmt(s.net_pnl_u) }}
+
亏损额合计(U)
{{ funds_fmt(s.loss_sum_u) }}
+
单笔最大亏损(U)
{% if s.max_single_loss is not none %}{{ funds_fmt(s.max_single_loss) }}{% else %}-{% endif %}
+
单笔最大盈利(U)
{% if s.max_single_profit is not none %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}
+
最大回撤(U)
{{ funds_fmt(s.max_drawdown_u) }}
当前连续亏损笔数
{{ s.consecutive_losses }}
最长连续亏损(交易日)
{{ s.max_loss_streak_days }} 天
-
期内最大亏损日
{% if s.worst_day %}{{ s.worst_day }}({{ signed_usdt_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}
+
期内最大亏损日
{% if s.worst_day %}{{ s.worst_day }}({{ funds_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}
{% endmacro %} @@ -253,9 +253,9 @@
总交易
{{ total }}
错过次数
{{ miss_count }}
胜率
{{ rate }}%
-
资金账户(USDT)
{% if funding_usdt is not none %}{{ usdt_fmt(funding_usdt) }}U{% else %}—{% endif %}
+
资金账户(USDT)
{% if funding_usdt is not none %}{{ funds_fmt(funding_usdt) }}U{% else %}—{% endif %}
交易日
{{ trading_day }}
-
当日资金(交易账户)
{{ usdt_fmt(current_capital) }}U
+
当日资金(交易账户)
{{ funds_fmt(current_capital) }}U
实时价格更新时间:--(北京时间 UTC+8)
@@ -281,7 +281,7 @@ - @@ -304,7 +304,11 @@
{{ k.symbol }} + {% if k.direction == 'watch' %} + 双向 + {% else %} {{ '做多' if k.direction == 'long' else '做空' }} + {% endif %} {{ k.monitor_type }}
@@ -444,13 +448,13 @@
- 平仓 + 平仓
来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %} 风格: {{ o.trade_style or 'trend' }} - 风险: {{ o.risk_percent or '-' }}%≈{% if o.risk_amount is not none %}{{ usdt_fmt(o.risk_amount) }}{% else %}-{% endif %}U + 风险: {{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %} @@ -491,7 +495,7 @@
@@ -548,16 +552,6 @@ {% if page == 'records' %}

交易记录 & 错过机会

-
- 盈亏U:=交易所平仓历史, - =本地估算。 - {% if exchange_pnl_sync %} - {% if exchange_pnl_sync.skipped %}(25秒内已同步,可点右侧按钮强制){% else %} - 本轮:平仓历史 {{ exchange_pnl_sync.hist_count or 0 }} 条,对齐 {{ exchange_pnl_sync.matched or 0 }} 笔{% if exchange_pnl_sync.reason %} — {{ exchange_pnl_sync.reason }}{% endif %} - {% endif %} - {% endif %} - -