From 5b3448b52b320198b06e140596c2d4b625e755bc Mon Sep 17 00:00:00 2001 From: dekun Date: Mon, 29 Jun 2026 10:49:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=B3=E9=94=AE=E4=BD=8D=E5=9B=9E?= =?UTF-8?q?=E8=B0=83/=E7=AA=81=E7=A0=B4=E8=A7=A6=E4=BB=B7=E5=BC=80?= =?UTF-8?q?=E4=BB=93=E6=8B=86=E5=88=86=E4=B8=8E=E7=A9=BF=E8=B6=8A=E8=A7=A6?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- crypto_monitor_binance/app.py | 118 ++++++++++--- crypto_monitor_binance/使用说明.md | 7 +- crypto_monitor_binance/关键位自动下单说明.md | 25 ++- crypto_monitor_gate/app.py | 118 ++++++++++--- crypto_monitor_gate/使用说明.md | 3 +- crypto_monitor_gate/关键位自动下单说明.md | 25 ++- crypto_monitor_gate_bot/app.py | 118 ++++++++++--- crypto_monitor_gate_bot/使用说明.md | 3 +- crypto_monitor_okx/app.py | 118 ++++++++++--- crypto_monitor_okx/使用说明.md | 3 +- crypto_monitor_okx/关键位自动下单说明.md | 25 ++- docs/position-sizing-mode.md | 2 +- embed_templates/embed_shell.html | 2 +- fib_key_monitor_lib.py | 12 +- key_monitor_schema_lib.py | 14 ++ static/key_monitor_form.js | 2 +- strategy_templates/key_monitor_panel.html | 8 +- strategy_templates/key_monitor_rule_tips.html | 11 +- tests/test_trigger_entry_key_monitor_lib.py | 61 +++++-- trigger_entry_key_monitor_lib.py | 159 ++++++++++++++++-- 20 files changed, 662 insertions(+), 172 deletions(-) create mode 100644 key_monitor_schema_lib.py diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 240ea0e..b588b1c 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -115,21 +115,27 @@ from manual_sltp_lib import ( resolve_entrust_sltp_prices, resolve_open_sltp_prices, ) +from key_monitor_schema_lib import ensure_key_monitor_schema from trigger_entry_key_monitor_lib import ( + BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE, + CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED, TRIGGER_ENTRY_CLOSE_EXPIRED, TRIGGER_ENTRY_CLOSE_FILLED, + TRIGGER_ENTRY_CLOSE_SL_INVALIDATE, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE, TRIGGER_ENTRY_MONITOR_TYPE, + TRIGGER_ENTRY_MONITOR_TYPES, TRIGGER_ENTRY_VALIDITY_HOURS, check_trigger_entry_intent_limit, count_pending_trigger_entries, + is_breakout_trigger_entry_key_monitor_type, is_trigger_entry_expired, is_trigger_entry_key_monitor_type, trigger_entry_expires_at_text, trigger_entry_gate_preview, - trigger_entry_invalidate_by_tp, - trigger_entry_reached, + trigger_entry_invalidate, + trigger_should_fire, validate_trigger_entry_geometry, validate_trigger_entry_rr, ) @@ -1059,7 +1065,8 @@ ENTRY_REASON_OPTIONS = ( "关键位斐波0.618", "关键位斐波0.786", "关键位假突破", - "关键位触价开仓", + "关键位回调触价开仓", + "关键位突破触价开仓", ) + STRATEGY_ENTRY_REASON_OPTIONS STATS_SEGMENT_DEFS = ( @@ -1474,6 +1481,7 @@ def init_db(): except Exception: pass ensure_time_close_schema(c) + ensure_key_monitor_schema(c) try: c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL") @@ -1715,7 +1723,7 @@ def _pnl_row_matches_segment(row, segment_key): if segment_key == "key_false_breakout": return kst == FALSE_BREAKOUT_MONITOR_TYPE if segment_key == "key_trigger": - return kst == TRIGGER_ENTRY_MONITOR_TYPE + return kst in TRIGGER_ENTRY_MONITOR_TYPES return False @@ -1733,8 +1741,15 @@ def _count_opens_for_segment(conn, start_td, end_td, segment_key): "key_fib618": "斐波回调0.618", "key_fib786": "斐波回调0.786", "key_false_breakout": FALSE_BREAKOUT_MONITOR_TYPE, - "key_trigger": TRIGGER_ENTRY_MONITOR_TYPE, + "key_trigger": None, # 见 _count_opens_for_segment 多类型 } + if segment_key == "key_trigger": + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) + return conn.execute( + f"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ? " + f"AND key_signal_type IN ({placeholders})", + (start_td, end_td, *TRIGGER_ENTRY_MONITOR_TYPES), + ).fetchone()[0] kst = kst_map.get(segment_key) if kst: return conn.execute( @@ -5294,9 +5309,10 @@ def _finalize_fib_key_fill(conn, row): def _trigger_entry_exists_for_symbol(conn, symbol): + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) row = conn.execute( - "SELECT id FROM key_monitors WHERE symbol=? AND monitor_type=?", - (symbol, TRIGGER_ENTRY_MONITOR_TYPE), + f"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type IN ({placeholders})", + (symbol, *TRIGGER_ENTRY_MONITOR_TYPES), ).fetchone() return row is not None @@ -5308,15 +5324,21 @@ def _add_trigger_entry_key_monitor( entry, sl, tp, + monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, breakeven_enabled=0, time_close_enabled=0, time_close_hours=None, ): + mt = (monitor_type or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE).strip() + if mt not in TRIGGER_ENTRY_MONITOR_TYPES: + mt = CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE if _trigger_entry_exists_for_symbol(conn, symbol): return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)" ex_sym = normalize_exchange_symbol(symbol) mark = get_symbol_mark_price(symbol) - geom_err = validate_trigger_entry_geometry(direction_sel, entry, sl, tp, mark_at_add=mark) + geom_err = validate_trigger_entry_geometry( + direction_sel, entry, sl, tp, mark_at_add=mark, monitor_type=mt + ) if geom_err: return False, geom_err rr_err = validate_trigger_entry_rr( @@ -5327,7 +5349,9 @@ def _add_trigger_entry_key_monitor( entry = float(round_price_to_exchange(ex_sym, entry) or entry) sl = float(round_price_to_exchange(ex_sym, sl) or sl) tp = float(round_price_to_exchange(ex_sym, tp) or tp) - geom_err = validate_trigger_entry_geometry(direction_sel, entry, sl, tp, mark_at_add=mark) + geom_err = validate_trigger_entry_geometry( + direction_sel, entry, sl, tp, mark_at_add=mark, monitor_type=mt + ) if geom_err: return False, geom_err rr_err = validate_trigger_entry_rr( @@ -5414,7 +5438,7 @@ def _add_trigger_entry_key_monitor( "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", ( symbol, - TRIGGER_ENTRY_MONITOR_TYPE, + mt, direction_sel, float(upper_px), float(lower_px), @@ -5441,6 +5465,7 @@ def _market_open_for_trigger_entry( entry_price, stop_loss, take_profit, + monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, breakeven_enabled=0, time_close_enabled=0, time_close_hours=None, @@ -5615,7 +5640,7 @@ def _market_open_for_trigger_entry( opened_at_ms, trading_day, ORDER_MONITOR_TYPE_KEY_AUTO, - stored_key_signal_type(TRIGGER_ENTRY_MONITOR_TYPE), + stored_key_signal_type(monitor_type), tc_en, tc_h, tc_at, @@ -5667,6 +5692,7 @@ def _execute_trigger_entry_cross(conn, row): entry, sl, tp, + monitor_type=(row["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE), breakeven_enabled=be_en, time_close_enabled=tc_en, time_close_hours=tc_h, @@ -5712,42 +5738,62 @@ def _execute_trigger_entry_cross(conn, row): def check_trigger_entry_key_monitors(): conn = get_db() - rows = conn.execute("SELECT * FROM key_monitors WHERE monitor_type=?", (TRIGGER_ENTRY_MONITOR_TYPE,)).fetchall() + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) + rows = conn.execute( + f"SELECT * FROM key_monitors WHERE monitor_type IN ({placeholders})", + tuple(TRIGGER_ENTRY_MONITOR_TYPES), + ).fetchall() now_dt = app_now() for r in rows: symbol = r["symbol"] direction = (r["direction"] or "long").lower() + mt = (r["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE).strip() entry = float(_sqlite_row_val(r, "fib_entry_price") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) tp = float(_sqlite_row_val(r, "fib_take_profit") or 0) + kid = int(r["id"]) if entry <= 0 or sl <= 0 or tp <= 0: _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") continue mark = get_symbol_mark_price(symbol) if mark is None: continue + prev_mark = _sqlite_row_val(r, "last_mark_price") + prev_mark_f = float(prev_mark) if prev_mark not in (None, "") else None if is_trigger_entry_expired(r["created_at"], now_dt, hours=TRIGGER_ENTRY_VALIDITY_HOURS): exp_txt = trigger_entry_expires_at_text(r["created_at"], hours=TRIGGER_ENTRY_VALIDITY_HOURS) msg = ( f"# ⚠️ {symbol} 触价开仓已过期\n" f"**账户:{_wechat_account_label()}**\n" - f"- 类型:{TRIGGER_ENTRY_MONITOR_TYPE}|{_wechat_direction_text(direction)}\n" + f"- 类型:{mt}|{_wechat_direction_text(direction)}\n" f"- 有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h(应于 {exp_txt} 前触发)\n" ) send_wechat_msg(msg) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED) continue - if trigger_entry_invalidate_by_tp(direction, mark, tp): + inv = trigger_entry_invalidate(mt, direction, mark, sl, tp) + if inv == "tp": msg = ( f"# ⚠️ {symbol} 触价开仓失效\n" f"**账户:{_wechat_account_label()}**\n" - f"- 标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交)\n" + f"- 类型:{mt}|标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交)\n" ) send_wechat_msg(msg) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE) continue - if trigger_entry_reached(direction, mark, entry): + if inv == "sl": + msg = ( + f"# ⚠️ {symbol} 触价开仓失效\n" + f"**账户:{_wechat_account_label()}**\n" + f"- 类型:{mt}|标记价 {format_price_for_symbol(symbol, mark)} 已触达止损侧(未突破)\n" + ) + send_wechat_msg(msg) + _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_SL_INVALIDATE) + continue + if trigger_should_fire(mt, direction, mark, entry, prev_mark_f): _execute_trigger_entry_cross(conn, r) + continue + conn.execute("UPDATE key_monitors SET last_mark_price=? WHERE id=?", (float(mark), kid)) conn.commit() conn.close() @@ -7178,13 +7224,22 @@ def api_price_snapshot(): tp_v = _sqlite_row_val(r, "fib_take_profit") entry_txt = format_price_for_symbol(r["symbol"], entry) if entry else "-" tp_txt = format_price_for_symbol(r["symbol"], tp_v) if tp_v else "-" - tp_inv = trigger_entry_invalidate_by_tp(direction, price, float(tp_v)) if tp_v else False + sl_v = _sqlite_row_val(r, "fib_stop_loss") + inv = ( + trigger_entry_invalidate( + r["monitor_type"], direction, price, float(sl_v or 0), float(tp_v or 0) + ) + if tp_v + else None + ) prev = trigger_entry_gate_preview( + monitor_type=r["monitor_type"], entry_display=entry_txt, take_profit_display=tp_txt, created_at=_sqlite_row_val(r, "created_at"), now=app_now(), - tp_invalidated=tp_inv, + tp_invalidated=inv == "tp", + sl_invalidated=inv == "sl", hours=TRIGGER_ENTRY_VALIDITY_HOURS, ) gate_summary = prev.get("summary") or "-" @@ -7810,7 +7865,7 @@ def add_key(): if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt): flash( "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" - "可使用「触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" + "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" ) return redirect("/key_monitor") skip_volume_rank = is_false_breakout_key_monitor_type(mt) @@ -7847,7 +7902,7 @@ def add_key(): if direction_sel not in ("long", "short"): conn.close() conn = None - flash("触价开仓请选择做多或做空") + flash("触价请选择做多或做空") return redirect("/key_monitor") try: entry_px = float(d.get("trigger_entry") or 0) @@ -7858,11 +7913,19 @@ def add_key(): if entry_px <= 0 or sl_px <= 0 or tp_px <= 0: conn.close() conn = None - flash("触价开仓须填写有效的入场价、止损价、止盈价") + flash("触价须填写有效的入场价、止损价、止盈价") return redirect("/key_monitor") ok_te, err_te = _add_trigger_entry_key_monitor( - conn, symbol, direction_sel, entry_px, sl_px, tp_px, breakeven_enabled=be_flag, - time_close_enabled=tc_en, time_close_hours=tc_h, + conn, + symbol, + direction_sel, + entry_px, + sl_px, + tp_px, + monitor_type=mt, + breakeven_enabled=be_flag, + time_close_enabled=tc_en, + time_close_hours=tc_h, ) conn.commit() conn.close() @@ -7870,10 +7933,15 @@ def add_key(): if not ok_te: flash(err_te or "触价开仓监控添加失败") return redirect("/key_monitor") + trigger_hint = ( + "标记价穿越入场价后立即市价开仓" + if is_breakout_trigger_entry_key_monitor_type(mt) + else "标记价回调触达入场价后下一轮询市价开仓" + ) flash( - f"触价开仓已添加({symbol} 日成交量排名 {rank}/{total})" + f"{mt}已添加({symbol} 日成交量排名 {rank}/{total})" f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h" - f"|标记价触达入场价后下一轮询市价开仓" + f"|{trigger_hint}" f"|移动保本:{'开' if be_flag else '关'}" + (f"|{time_close_label(tc_h)}" if tc_en else "") ) diff --git a/crypto_monitor_binance/使用说明.md b/crypto_monitor_binance/使用说明.md index fe3995e..3692a6d 100644 --- a/crypto_monitor_binance/使用说明.md +++ b/crypto_monitor_binance/使用说明.md @@ -66,10 +66,11 @@ | **收敛突破** | 同上(自动开仓类)。 | | **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 | | **关键支撑位** | 同上(仅提醒)。 | - | **触价开仓** | **不挂交易所限价**;标记价触达计划入场价后 **下一轮询市价开仓**(RR 门槛同关键位 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h**;全仓杠杆模式可用。 | + | **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E 后 **下一轮询市价开仓**(RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** | + | **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** | -3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。 -4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**。 +3. **方向**:做多 / 做空(回调/突破触价、箱体 / 收敛 / 斐波必选;阻力/支撑不选)。 +4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价填 **入场 E / 止损 SL / 止盈 TP**。 **限制:** 活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**(默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**」;仍可添加「**关键阻力位 / 支撑位**」。 diff --git a/crypto_monitor_binance/关键位自动下单说明.md b/crypto_monitor_binance/关键位自动下单说明.md index c352af0..07743c1 100644 --- a/crypto_monitor_binance/关键位自动下单说明.md +++ b/crypto_monitor_binance/关键位自动下单说明.md @@ -16,7 +16,8 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k | **关键阻力位** | **不选**(`direction=watch`) | **否** | 5m 收盘突破上/下沿 → 微信 **3 次** → `key_level_alert_done` | | **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) | | 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) | -| **触价开仓** | **必选** 多/空 | **程序盯价 → 触 E 后市价** | 见下文 **§六** | +| **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§四** | +| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** | **添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30)**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位)。 @@ -118,25 +119,31 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k --- -## 四、触价开仓(程序触价,无交易所挂单) +## 四、回调 / 突破触价开仓(程序触价,无交易所挂单) ### 4.1 录入 -- 类型选 **触价开仓**;方向必选多/空。 -- 填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`)。 -- 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5,与箱体/斐波相同)。 -- 可选移动保本、时间平仓;**全仓杠杆模式**下可用(页面隐藏箱体/收敛/斐波/假突破)。 +- **回调触价开仓**:方向必选多/空;填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`)。 +- **突破触价开仓**:同上;添加时当前价须在突破方向一侧(做多:价低于 E;做空:价高于 E)。 +- 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)。 +- 可选移动保本、时间平仓;**全仓杠杆模式**下可用。 ### 4.2 触发与结案 -- 轮询标记价:做多 `标记价 ≤ E`、做空 `标记价 ≥ E` → **下一轮询市价开仓**,挂交易所 TP/SL,进下单监控。 -- 未成交前标记价先触 **TP 侧** → `trigger_tp_invalidate`;**24h** 未触发 → `trigger_entry_expired`。 +| 类型 | 触发条件(标记价) | +|------|-------------------| +| **回调触价** | 做多 `≤ E`;做空 `≥ E` → 下一轮询市价开仓 | +| **突破触价** | 做多**向上穿越** E;做空**向下穿越** E → **立即**市价开仓 | + +- 未成交前标记价先触 **TP 侧** → `trigger_tp_invalidate`。 +- **突破触价**另:未穿越 E 先触 **SL 侧** → `trigger_sl_invalidate`。 +- **24h** 未触发 → `trigger_entry_expired`。 - 成功 → `trigger_entry_filled`;触发后开仓失败 → `trigger_exchange_failed`。 ### 4.3 计仓与占位 - **以损定仓**:按 E、SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x、其它 5x。 -- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条。 +- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条触价监控(含回调/突破)。 共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors`。 diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 20ce98e..5c384b2 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -116,21 +116,27 @@ from manual_sltp_lib import ( resolve_entrust_sltp_prices, resolve_open_sltp_prices, ) +from key_monitor_schema_lib import ensure_key_monitor_schema from trigger_entry_key_monitor_lib import ( + BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE, + CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED, TRIGGER_ENTRY_CLOSE_EXPIRED, TRIGGER_ENTRY_CLOSE_FILLED, + TRIGGER_ENTRY_CLOSE_SL_INVALIDATE, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE, TRIGGER_ENTRY_MONITOR_TYPE, + TRIGGER_ENTRY_MONITOR_TYPES, TRIGGER_ENTRY_VALIDITY_HOURS, check_trigger_entry_intent_limit, count_pending_trigger_entries, + is_breakout_trigger_entry_key_monitor_type, is_trigger_entry_expired, is_trigger_entry_key_monitor_type, trigger_entry_expires_at_text, trigger_entry_gate_preview, - trigger_entry_invalidate_by_tp, - trigger_entry_reached, + trigger_entry_invalidate, + trigger_should_fire, validate_trigger_entry_geometry, validate_trigger_entry_rr, ) @@ -1046,7 +1052,8 @@ ENTRY_REASON_OPTIONS = ( "关键位斐波0.618", "关键位斐波0.786", "关键位假突破", - "关键位触价开仓", + "关键位回调触价开仓", + "关键位突破触价开仓", ) + STRATEGY_ENTRY_REASON_OPTIONS STATS_SEGMENT_DEFS = ( @@ -1466,6 +1473,7 @@ def init_db(): except Exception: pass ensure_time_close_schema(c) + ensure_key_monitor_schema(c) try: c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL") except Exception: @@ -1709,7 +1717,7 @@ def _pnl_row_matches_segment(row, segment_key): if segment_key == "key_false_breakout": return kst == FALSE_BREAKOUT_MONITOR_TYPE if segment_key == "key_trigger": - return kst == TRIGGER_ENTRY_MONITOR_TYPE + return kst in TRIGGER_ENTRY_MONITOR_TYPES return False @@ -1727,8 +1735,15 @@ def _count_opens_for_segment(conn, start_td, end_td, segment_key): "key_fib618": "斐波回调0.618", "key_fib786": "斐波回调0.786", "key_false_breakout": FALSE_BREAKOUT_MONITOR_TYPE, - "key_trigger": TRIGGER_ENTRY_MONITOR_TYPE, + "key_trigger": None, # 见 _count_opens_for_segment 多类型 } + if segment_key == "key_trigger": + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) + return conn.execute( + f"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ? " + f"AND key_signal_type IN ({placeholders})", + (start_td, end_td, *TRIGGER_ENTRY_MONITOR_TYPES), + ).fetchone()[0] kst = kst_map.get(segment_key) if kst: return conn.execute( @@ -5038,9 +5053,10 @@ def _finalize_fib_key_fill(conn, row): def _trigger_entry_exists_for_symbol(conn, symbol): + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) row = conn.execute( - "SELECT id FROM key_monitors WHERE symbol=? AND monitor_type=?", - (symbol, TRIGGER_ENTRY_MONITOR_TYPE), + f"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type IN ({placeholders})", + (symbol, *TRIGGER_ENTRY_MONITOR_TYPES), ).fetchone() return row is not None @@ -5052,15 +5068,21 @@ def _add_trigger_entry_key_monitor( entry, sl, tp, + monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, breakeven_enabled=0, time_close_enabled=0, time_close_hours=None, ): + mt = (monitor_type or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE).strip() + if mt not in TRIGGER_ENTRY_MONITOR_TYPES: + mt = CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE if _trigger_entry_exists_for_symbol(conn, symbol): return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)" ex_sym = normalize_exchange_symbol(symbol) mark = get_symbol_mark_price(symbol) - geom_err = validate_trigger_entry_geometry(direction_sel, entry, sl, tp, mark_at_add=mark) + geom_err = validate_trigger_entry_geometry( + direction_sel, entry, sl, tp, mark_at_add=mark, monitor_type=mt + ) if geom_err: return False, geom_err rr_err = validate_trigger_entry_rr( @@ -5071,7 +5093,9 @@ def _add_trigger_entry_key_monitor( entry = float(round_price_to_exchange(ex_sym, entry) or entry) sl = float(round_price_to_exchange(ex_sym, sl) or sl) tp = float(round_price_to_exchange(ex_sym, tp) or tp) - geom_err = validate_trigger_entry_geometry(direction_sel, entry, sl, tp, mark_at_add=mark) + geom_err = validate_trigger_entry_geometry( + direction_sel, entry, sl, tp, mark_at_add=mark, monitor_type=mt + ) if geom_err: return False, geom_err rr_err = validate_trigger_entry_rr( @@ -5158,7 +5182,7 @@ def _add_trigger_entry_key_monitor( "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", ( symbol, - TRIGGER_ENTRY_MONITOR_TYPE, + mt, direction_sel, float(upper_px), float(lower_px), @@ -5185,6 +5209,7 @@ def _market_open_for_trigger_entry( entry_price, stop_loss, take_profit, + monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, breakeven_enabled=0, time_close_enabled=0, time_close_hours=None, @@ -5359,7 +5384,7 @@ def _market_open_for_trigger_entry( opened_at_ms, trading_day, ORDER_MONITOR_TYPE_KEY_AUTO, - stored_key_signal_type(TRIGGER_ENTRY_MONITOR_TYPE), + stored_key_signal_type(monitor_type), tc_en, tc_h, tc_at, @@ -5411,6 +5436,7 @@ def _execute_trigger_entry_cross(conn, row): entry, sl, tp, + monitor_type=(row["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE), breakeven_enabled=be_en, time_close_enabled=tc_en, time_close_hours=tc_h, @@ -5456,42 +5482,62 @@ def _execute_trigger_entry_cross(conn, row): def check_trigger_entry_key_monitors(): conn = get_db() - rows = conn.execute("SELECT * FROM key_monitors WHERE monitor_type=?", (TRIGGER_ENTRY_MONITOR_TYPE,)).fetchall() + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) + rows = conn.execute( + f"SELECT * FROM key_monitors WHERE monitor_type IN ({placeholders})", + tuple(TRIGGER_ENTRY_MONITOR_TYPES), + ).fetchall() now_dt = app_now() for r in rows: symbol = r["symbol"] direction = (r["direction"] or "long").lower() + mt = (r["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE).strip() entry = float(_sqlite_row_val(r, "fib_entry_price") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) tp = float(_sqlite_row_val(r, "fib_take_profit") or 0) + kid = int(r["id"]) if entry <= 0 or sl <= 0 or tp <= 0: _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") continue mark = get_symbol_mark_price(symbol) if mark is None: continue + prev_mark = _sqlite_row_val(r, "last_mark_price") + prev_mark_f = float(prev_mark) if prev_mark not in (None, "") else None if is_trigger_entry_expired(r["created_at"], now_dt, hours=TRIGGER_ENTRY_VALIDITY_HOURS): exp_txt = trigger_entry_expires_at_text(r["created_at"], hours=TRIGGER_ENTRY_VALIDITY_HOURS) msg = ( f"# ⚠️ {symbol} 触价开仓已过期\n" f"**账户:{_wechat_account_label()}**\n" - f"- 类型:{TRIGGER_ENTRY_MONITOR_TYPE}|{_wechat_direction_text(direction)}\n" + f"- 类型:{mt}|{_wechat_direction_text(direction)}\n" f"- 有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h(应于 {exp_txt} 前触发)\n" ) send_wechat_msg(msg) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED) continue - if trigger_entry_invalidate_by_tp(direction, mark, tp): + inv = trigger_entry_invalidate(mt, direction, mark, sl, tp) + if inv == "tp": msg = ( f"# ⚠️ {symbol} 触价开仓失效\n" f"**账户:{_wechat_account_label()}**\n" - f"- 标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交)\n" + f"- 类型:{mt}|标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交)\n" ) send_wechat_msg(msg) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE) continue - if trigger_entry_reached(direction, mark, entry): + if inv == "sl": + msg = ( + f"# ⚠️ {symbol} 触价开仓失效\n" + f"**账户:{_wechat_account_label()}**\n" + f"- 类型:{mt}|标记价 {format_price_for_symbol(symbol, mark)} 已触达止损侧(未突破)\n" + ) + send_wechat_msg(msg) + _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_SL_INVALIDATE) + continue + if trigger_should_fire(mt, direction, mark, entry, prev_mark_f): _execute_trigger_entry_cross(conn, r) + continue + conn.execute("UPDATE key_monitors SET last_mark_price=? WHERE id=?", (float(mark), kid)) conn.commit() conn.close() @@ -7087,13 +7133,22 @@ def api_price_snapshot(): tp_v = _sqlite_row_val(r, "fib_take_profit") entry_txt = format_price_for_symbol(r["symbol"], entry) if entry else "-" tp_txt = format_price_for_symbol(r["symbol"], tp_v) if tp_v else "-" - tp_inv = trigger_entry_invalidate_by_tp(direction, price, float(tp_v)) if tp_v else False + sl_v = _sqlite_row_val(r, "fib_stop_loss") + inv = ( + trigger_entry_invalidate( + r["monitor_type"], direction, price, float(sl_v or 0), float(tp_v or 0) + ) + if tp_v + else None + ) prev = trigger_entry_gate_preview( + monitor_type=r["monitor_type"], entry_display=entry_txt, take_profit_display=tp_txt, created_at=_sqlite_row_val(r, "created_at"), now=app_now(), - tp_invalidated=tp_inv, + tp_invalidated=inv == "tp", + sl_invalidated=inv == "sl", hours=TRIGGER_ENTRY_VALIDITY_HOURS, ) gate_summary = prev.get("summary") or "-" @@ -7710,7 +7765,7 @@ def add_key(): if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt): flash( "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" - "可使用「触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" + "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" ) return redirect("/key_monitor") skip_volume_rank = is_false_breakout_key_monitor_type(mt) @@ -7750,7 +7805,7 @@ def add_key(): if direction_sel not in ("long", "short"): conn.close() conn = None - flash("触价开仓请选择做多或做空") + flash("触价请选择做多或做空") return redirect("/key_monitor") try: entry_px = float(d.get("trigger_entry") or 0) @@ -7761,11 +7816,19 @@ def add_key(): if entry_px <= 0 or sl_px <= 0 or tp_px <= 0: conn.close() conn = None - flash("触价开仓须填写有效的入场价、止损价、止盈价") + flash("触价须填写有效的入场价、止损价、止盈价") return redirect("/key_monitor") ok_te, err_te = _add_trigger_entry_key_monitor( - conn, symbol, direction_sel, entry_px, sl_px, tp_px, breakeven_enabled=be_flag, - time_close_enabled=tc_en, time_close_hours=tc_h, + conn, + symbol, + direction_sel, + entry_px, + sl_px, + tp_px, + monitor_type=mt, + breakeven_enabled=be_flag, + time_close_enabled=tc_en, + time_close_hours=tc_h, ) conn.commit() conn.close() @@ -7773,10 +7836,15 @@ def add_key(): if not ok_te: flash(err_te or "触价开仓监控添加失败") return redirect("/key_monitor") + trigger_hint = ( + "标记价穿越入场价后立即市价开仓" + if is_breakout_trigger_entry_key_monitor_type(mt) + else "标记价回调触达入场价后下一轮询市价开仓" + ) flash( - f"触价开仓已添加({symbol} 日成交量排名 {rank}/{total})" + f"{mt}已添加({symbol} 日成交量排名 {rank}/{total})" f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h" - f"|标记价触达入场价后下一轮询市价开仓" + f"|{trigger_hint}" f"|移动保本:{'开' if be_flag else '关'}" + (f"|{time_close_label(tc_h)}" if tc_en else "") ) diff --git a/crypto_monitor_gate/使用说明.md b/crypto_monitor_gate/使用说明.md index 714dbd3..9427d61 100644 --- a/crypto_monitor_gate/使用说明.md +++ b/crypto_monitor_gate/使用说明.md @@ -65,7 +65,8 @@ | **收敛突破** | 同上(自动开仓类)。 | | **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 | | **关键支撑位** | 同上(仅提醒)。 | - | **触价开仓** | **不挂交易所限价**;标记价触达计划入场价后 **下一轮询市价开仓**(RR 门槛同关键位 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h**;全仓杠杆模式可用。 | + | **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E 后 **下一轮询市价开仓**(RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** | + | **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** | 3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。 4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**。 diff --git a/crypto_monitor_gate/关键位自动下单说明.md b/crypto_monitor_gate/关键位自动下单说明.md index c352af0..07743c1 100644 --- a/crypto_monitor_gate/关键位自动下单说明.md +++ b/crypto_monitor_gate/关键位自动下单说明.md @@ -16,7 +16,8 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k | **关键阻力位** | **不选**(`direction=watch`) | **否** | 5m 收盘突破上/下沿 → 微信 **3 次** → `key_level_alert_done` | | **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) | | 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) | -| **触价开仓** | **必选** 多/空 | **程序盯价 → 触 E 后市价** | 见下文 **§六** | +| **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§四** | +| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** | **添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30)**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位)。 @@ -118,25 +119,31 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k --- -## 四、触价开仓(程序触价,无交易所挂单) +## 四、回调 / 突破触价开仓(程序触价,无交易所挂单) ### 4.1 录入 -- 类型选 **触价开仓**;方向必选多/空。 -- 填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`)。 -- 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5,与箱体/斐波相同)。 -- 可选移动保本、时间平仓;**全仓杠杆模式**下可用(页面隐藏箱体/收敛/斐波/假突破)。 +- **回调触价开仓**:方向必选多/空;填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`)。 +- **突破触价开仓**:同上;添加时当前价须在突破方向一侧(做多:价低于 E;做空:价高于 E)。 +- 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)。 +- 可选移动保本、时间平仓;**全仓杠杆模式**下可用。 ### 4.2 触发与结案 -- 轮询标记价:做多 `标记价 ≤ E`、做空 `标记价 ≥ E` → **下一轮询市价开仓**,挂交易所 TP/SL,进下单监控。 -- 未成交前标记价先触 **TP 侧** → `trigger_tp_invalidate`;**24h** 未触发 → `trigger_entry_expired`。 +| 类型 | 触发条件(标记价) | +|------|-------------------| +| **回调触价** | 做多 `≤ E`;做空 `≥ E` → 下一轮询市价开仓 | +| **突破触价** | 做多**向上穿越** E;做空**向下穿越** E → **立即**市价开仓 | + +- 未成交前标记价先触 **TP 侧** → `trigger_tp_invalidate`。 +- **突破触价**另:未穿越 E 先触 **SL 侧** → `trigger_sl_invalidate`。 +- **24h** 未触发 → `trigger_entry_expired`。 - 成功 → `trigger_entry_filled`;触发后开仓失败 → `trigger_exchange_failed`。 ### 4.3 计仓与占位 - **以损定仓**:按 E、SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x、其它 5x。 -- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条。 +- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条触价监控(含回调/突破)。 共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors`。 diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 0f965e7..9da1018 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -116,21 +116,27 @@ from manual_sltp_lib import ( resolve_entrust_sltp_prices, resolve_open_sltp_prices, ) +from key_monitor_schema_lib import ensure_key_monitor_schema from trigger_entry_key_monitor_lib import ( + BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE, + CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED, TRIGGER_ENTRY_CLOSE_EXPIRED, TRIGGER_ENTRY_CLOSE_FILLED, + TRIGGER_ENTRY_CLOSE_SL_INVALIDATE, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE, TRIGGER_ENTRY_MONITOR_TYPE, + TRIGGER_ENTRY_MONITOR_TYPES, TRIGGER_ENTRY_VALIDITY_HOURS, check_trigger_entry_intent_limit, count_pending_trigger_entries, + is_breakout_trigger_entry_key_monitor_type, is_trigger_entry_expired, is_trigger_entry_key_monitor_type, trigger_entry_expires_at_text, trigger_entry_gate_preview, - trigger_entry_invalidate_by_tp, - trigger_entry_reached, + trigger_entry_invalidate, + trigger_should_fire, validate_trigger_entry_geometry, validate_trigger_entry_rr, ) @@ -1046,7 +1052,8 @@ ENTRY_REASON_OPTIONS = ( "关键位斐波0.618", "关键位斐波0.786", "关键位假突破", - "关键位触价开仓", + "关键位回调触价开仓", + "关键位突破触价开仓", ) + STRATEGY_ENTRY_REASON_OPTIONS STATS_SEGMENT_DEFS = ( @@ -1466,6 +1473,7 @@ def init_db(): except Exception: pass ensure_time_close_schema(c) + ensure_key_monitor_schema(c) try: c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL") except Exception: @@ -1709,7 +1717,7 @@ def _pnl_row_matches_segment(row, segment_key): if segment_key == "key_false_breakout": return kst == FALSE_BREAKOUT_MONITOR_TYPE if segment_key == "key_trigger": - return kst == TRIGGER_ENTRY_MONITOR_TYPE + return kst in TRIGGER_ENTRY_MONITOR_TYPES return False @@ -1727,8 +1735,15 @@ def _count_opens_for_segment(conn, start_td, end_td, segment_key): "key_fib618": "斐波回调0.618", "key_fib786": "斐波回调0.786", "key_false_breakout": FALSE_BREAKOUT_MONITOR_TYPE, - "key_trigger": TRIGGER_ENTRY_MONITOR_TYPE, + "key_trigger": None, # 见 _count_opens_for_segment 多类型 } + if segment_key == "key_trigger": + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) + return conn.execute( + f"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ? " + f"AND key_signal_type IN ({placeholders})", + (start_td, end_td, *TRIGGER_ENTRY_MONITOR_TYPES), + ).fetchone()[0] kst = kst_map.get(segment_key) if kst: return conn.execute( @@ -5038,9 +5053,10 @@ def _finalize_fib_key_fill(conn, row): def _trigger_entry_exists_for_symbol(conn, symbol): + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) row = conn.execute( - "SELECT id FROM key_monitors WHERE symbol=? AND monitor_type=?", - (symbol, TRIGGER_ENTRY_MONITOR_TYPE), + f"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type IN ({placeholders})", + (symbol, *TRIGGER_ENTRY_MONITOR_TYPES), ).fetchone() return row is not None @@ -5052,15 +5068,21 @@ def _add_trigger_entry_key_monitor( entry, sl, tp, + monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, breakeven_enabled=0, time_close_enabled=0, time_close_hours=None, ): + mt = (monitor_type or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE).strip() + if mt not in TRIGGER_ENTRY_MONITOR_TYPES: + mt = CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE if _trigger_entry_exists_for_symbol(conn, symbol): return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)" ex_sym = normalize_exchange_symbol(symbol) mark = get_symbol_mark_price(symbol) - geom_err = validate_trigger_entry_geometry(direction_sel, entry, sl, tp, mark_at_add=mark) + geom_err = validate_trigger_entry_geometry( + direction_sel, entry, sl, tp, mark_at_add=mark, monitor_type=mt + ) if geom_err: return False, geom_err rr_err = validate_trigger_entry_rr( @@ -5071,7 +5093,9 @@ def _add_trigger_entry_key_monitor( entry = float(round_price_to_exchange(ex_sym, entry) or entry) sl = float(round_price_to_exchange(ex_sym, sl) or sl) tp = float(round_price_to_exchange(ex_sym, tp) or tp) - geom_err = validate_trigger_entry_geometry(direction_sel, entry, sl, tp, mark_at_add=mark) + geom_err = validate_trigger_entry_geometry( + direction_sel, entry, sl, tp, mark_at_add=mark, monitor_type=mt + ) if geom_err: return False, geom_err rr_err = validate_trigger_entry_rr( @@ -5158,7 +5182,7 @@ def _add_trigger_entry_key_monitor( "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", ( symbol, - TRIGGER_ENTRY_MONITOR_TYPE, + mt, direction_sel, float(upper_px), float(lower_px), @@ -5185,6 +5209,7 @@ def _market_open_for_trigger_entry( entry_price, stop_loss, take_profit, + monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, breakeven_enabled=0, time_close_enabled=0, time_close_hours=None, @@ -5359,7 +5384,7 @@ def _market_open_for_trigger_entry( opened_at_ms, trading_day, ORDER_MONITOR_TYPE_KEY_AUTO, - stored_key_signal_type(TRIGGER_ENTRY_MONITOR_TYPE), + stored_key_signal_type(monitor_type), tc_en, tc_h, tc_at, @@ -5411,6 +5436,7 @@ def _execute_trigger_entry_cross(conn, row): entry, sl, tp, + monitor_type=(row["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE), breakeven_enabled=be_en, time_close_enabled=tc_en, time_close_hours=tc_h, @@ -5456,42 +5482,62 @@ def _execute_trigger_entry_cross(conn, row): def check_trigger_entry_key_monitors(): conn = get_db() - rows = conn.execute("SELECT * FROM key_monitors WHERE monitor_type=?", (TRIGGER_ENTRY_MONITOR_TYPE,)).fetchall() + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) + rows = conn.execute( + f"SELECT * FROM key_monitors WHERE monitor_type IN ({placeholders})", + tuple(TRIGGER_ENTRY_MONITOR_TYPES), + ).fetchall() now_dt = app_now() for r in rows: symbol = r["symbol"] direction = (r["direction"] or "long").lower() + mt = (r["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE).strip() entry = float(_sqlite_row_val(r, "fib_entry_price") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) tp = float(_sqlite_row_val(r, "fib_take_profit") or 0) + kid = int(r["id"]) if entry <= 0 or sl <= 0 or tp <= 0: _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") continue mark = get_symbol_mark_price(symbol) if mark is None: continue + prev_mark = _sqlite_row_val(r, "last_mark_price") + prev_mark_f = float(prev_mark) if prev_mark not in (None, "") else None if is_trigger_entry_expired(r["created_at"], now_dt, hours=TRIGGER_ENTRY_VALIDITY_HOURS): exp_txt = trigger_entry_expires_at_text(r["created_at"], hours=TRIGGER_ENTRY_VALIDITY_HOURS) msg = ( f"# ⚠️ {symbol} 触价开仓已过期\n" f"**账户:{_wechat_account_label()}**\n" - f"- 类型:{TRIGGER_ENTRY_MONITOR_TYPE}|{_wechat_direction_text(direction)}\n" + f"- 类型:{mt}|{_wechat_direction_text(direction)}\n" f"- 有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h(应于 {exp_txt} 前触发)\n" ) send_wechat_msg(msg) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED) continue - if trigger_entry_invalidate_by_tp(direction, mark, tp): + inv = trigger_entry_invalidate(mt, direction, mark, sl, tp) + if inv == "tp": msg = ( f"# ⚠️ {symbol} 触价开仓失效\n" f"**账户:{_wechat_account_label()}**\n" - f"- 标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交)\n" + f"- 类型:{mt}|标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交)\n" ) send_wechat_msg(msg) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE) continue - if trigger_entry_reached(direction, mark, entry): + if inv == "sl": + msg = ( + f"# ⚠️ {symbol} 触价开仓失效\n" + f"**账户:{_wechat_account_label()}**\n" + f"- 类型:{mt}|标记价 {format_price_for_symbol(symbol, mark)} 已触达止损侧(未突破)\n" + ) + send_wechat_msg(msg) + _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_SL_INVALIDATE) + continue + if trigger_should_fire(mt, direction, mark, entry, prev_mark_f): _execute_trigger_entry_cross(conn, r) + continue + conn.execute("UPDATE key_monitors SET last_mark_price=? WHERE id=?", (float(mark), kid)) conn.commit() conn.close() @@ -7083,13 +7129,22 @@ def api_price_snapshot(): tp_v = _sqlite_row_val(r, "fib_take_profit") entry_txt = format_price_for_symbol(r["symbol"], entry) if entry else "-" tp_txt = format_price_for_symbol(r["symbol"], tp_v) if tp_v else "-" - tp_inv = trigger_entry_invalidate_by_tp(direction, price, float(tp_v)) if tp_v else False + sl_v = _sqlite_row_val(r, "fib_stop_loss") + inv = ( + trigger_entry_invalidate( + r["monitor_type"], direction, price, float(sl_v or 0), float(tp_v or 0) + ) + if tp_v + else None + ) prev = trigger_entry_gate_preview( + monitor_type=r["monitor_type"], entry_display=entry_txt, take_profit_display=tp_txt, created_at=_sqlite_row_val(r, "created_at"), now=app_now(), - tp_invalidated=tp_inv, + tp_invalidated=inv == "tp", + sl_invalidated=inv == "sl", hours=TRIGGER_ENTRY_VALIDITY_HOURS, ) gate_summary = prev.get("summary") or "-" @@ -7706,7 +7761,7 @@ def add_key(): if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt): flash( "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" - "可使用「触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" + "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" ) return redirect("/key_monitor") skip_volume_rank = is_false_breakout_key_monitor_type(mt) @@ -7746,7 +7801,7 @@ def add_key(): if direction_sel not in ("long", "short"): conn.close() conn = None - flash("触价开仓请选择做多或做空") + flash("触价请选择做多或做空") return redirect("/key_monitor") try: entry_px = float(d.get("trigger_entry") or 0) @@ -7757,11 +7812,19 @@ def add_key(): if entry_px <= 0 or sl_px <= 0 or tp_px <= 0: conn.close() conn = None - flash("触价开仓须填写有效的入场价、止损价、止盈价") + flash("触价须填写有效的入场价、止损价、止盈价") return redirect("/key_monitor") ok_te, err_te = _add_trigger_entry_key_monitor( - conn, symbol, direction_sel, entry_px, sl_px, tp_px, breakeven_enabled=be_flag, - time_close_enabled=tc_en, time_close_hours=tc_h, + conn, + symbol, + direction_sel, + entry_px, + sl_px, + tp_px, + monitor_type=mt, + breakeven_enabled=be_flag, + time_close_enabled=tc_en, + time_close_hours=tc_h, ) conn.commit() conn.close() @@ -7769,10 +7832,15 @@ def add_key(): if not ok_te: flash(err_te or "触价开仓监控添加失败") return redirect("/key_monitor") + trigger_hint = ( + "标记价穿越入场价后立即市价开仓" + if is_breakout_trigger_entry_key_monitor_type(mt) + else "标记价回调触达入场价后下一轮询市价开仓" + ) flash( - f"触价开仓已添加({symbol} 日成交量排名 {rank}/{total})" + f"{mt}已添加({symbol} 日成交量排名 {rank}/{total})" f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h" - f"|标记价触达入场价后下一轮询市价开仓" + f"|{trigger_hint}" f"|移动保本:{'开' if be_flag else '关'}" + (f"|{time_close_label(tc_h)}" if tc_en else "") ) diff --git a/crypto_monitor_gate_bot/使用说明.md b/crypto_monitor_gate_bot/使用说明.md index 714dbd3..9427d61 100644 --- a/crypto_monitor_gate_bot/使用说明.md +++ b/crypto_monitor_gate_bot/使用说明.md @@ -65,7 +65,8 @@ | **收敛突破** | 同上(自动开仓类)。 | | **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 | | **关键支撑位** | 同上(仅提醒)。 | - | **触价开仓** | **不挂交易所限价**;标记价触达计划入场价后 **下一轮询市价开仓**(RR 门槛同关键位 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h**;全仓杠杆模式可用。 | + | **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E 后 **下一轮询市价开仓**(RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** | + | **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** | 3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。 4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**。 diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 85efc25..83b3f14 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -116,21 +116,27 @@ from manual_sltp_lib import ( resolve_entrust_sltp_prices, resolve_open_sltp_prices, ) +from key_monitor_schema_lib import ensure_key_monitor_schema from trigger_entry_key_monitor_lib import ( + BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE, + CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED, TRIGGER_ENTRY_CLOSE_EXPIRED, TRIGGER_ENTRY_CLOSE_FILLED, + TRIGGER_ENTRY_CLOSE_SL_INVALIDATE, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE, TRIGGER_ENTRY_MONITOR_TYPE, + TRIGGER_ENTRY_MONITOR_TYPES, TRIGGER_ENTRY_VALIDITY_HOURS, check_trigger_entry_intent_limit, count_pending_trigger_entries, + is_breakout_trigger_entry_key_monitor_type, is_trigger_entry_expired, is_trigger_entry_key_monitor_type, trigger_entry_expires_at_text, trigger_entry_gate_preview, - trigger_entry_invalidate_by_tp, - trigger_entry_reached, + trigger_entry_invalidate, + trigger_should_fire, validate_trigger_entry_geometry, validate_trigger_entry_rr, ) @@ -1054,7 +1060,8 @@ ENTRY_REASON_OPTIONS = ( "关键位斐波0.618", "关键位斐波0.786", "关键位假突破", - "关键位触价开仓", + "关键位回调触价开仓", + "关键位突破触价开仓", ) + STRATEGY_ENTRY_REASON_OPTIONS STATS_SEGMENT_DEFS = ( @@ -1468,6 +1475,7 @@ def init_db(): except Exception: pass ensure_time_close_schema(c) + ensure_key_monitor_schema(c) try: c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL") except Exception: @@ -1693,7 +1701,7 @@ def _pnl_row_matches_segment(row, segment_key): if segment_key == "key_false_breakout": return kst == FALSE_BREAKOUT_MONITOR_TYPE if segment_key == "key_trigger": - return kst == TRIGGER_ENTRY_MONITOR_TYPE + return kst in TRIGGER_ENTRY_MONITOR_TYPES return False @@ -1711,8 +1719,15 @@ def _count_opens_for_segment(conn, start_td, end_td, segment_key): "key_fib618": "斐波回调0.618", "key_fib786": "斐波回调0.786", "key_false_breakout": FALSE_BREAKOUT_MONITOR_TYPE, - "key_trigger": TRIGGER_ENTRY_MONITOR_TYPE, + "key_trigger": None, # 见 _count_opens_for_segment 多类型 } + if segment_key == "key_trigger": + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) + return conn.execute( + f"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ? " + f"AND key_signal_type IN ({placeholders})", + (start_td, end_td, *TRIGGER_ENTRY_MONITOR_TYPES), + ).fetchone()[0] kst = kst_map.get(segment_key) if kst: return conn.execute( @@ -4558,9 +4573,10 @@ def _finalize_fib_key_fill(conn, row): def _trigger_entry_exists_for_symbol(conn, symbol): + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) row = conn.execute( - "SELECT id FROM key_monitors WHERE symbol=? AND monitor_type=?", - (symbol, TRIGGER_ENTRY_MONITOR_TYPE), + f"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type IN ({placeholders})", + (symbol, *TRIGGER_ENTRY_MONITOR_TYPES), ).fetchone() return row is not None @@ -4572,15 +4588,21 @@ def _add_trigger_entry_key_monitor( entry, sl, tp, + monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, breakeven_enabled=0, time_close_enabled=0, time_close_hours=None, ): + mt = (monitor_type or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE).strip() + if mt not in TRIGGER_ENTRY_MONITOR_TYPES: + mt = CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE if _trigger_entry_exists_for_symbol(conn, symbol): return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)" ex_sym = normalize_exchange_symbol(symbol) mark = get_symbol_mark_price(symbol) - geom_err = validate_trigger_entry_geometry(direction_sel, entry, sl, tp, mark_at_add=mark) + geom_err = validate_trigger_entry_geometry( + direction_sel, entry, sl, tp, mark_at_add=mark, monitor_type=mt + ) if geom_err: return False, geom_err rr_err = validate_trigger_entry_rr( @@ -4591,7 +4613,9 @@ def _add_trigger_entry_key_monitor( entry = float(round_price_to_exchange(ex_sym, entry) or entry) sl = float(round_price_to_exchange(ex_sym, sl) or sl) tp = float(round_price_to_exchange(ex_sym, tp) or tp) - geom_err = validate_trigger_entry_geometry(direction_sel, entry, sl, tp, mark_at_add=mark) + geom_err = validate_trigger_entry_geometry( + direction_sel, entry, sl, tp, mark_at_add=mark, monitor_type=mt + ) if geom_err: return False, geom_err rr_err = validate_trigger_entry_rr( @@ -4678,7 +4702,7 @@ def _add_trigger_entry_key_monitor( "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", ( symbol, - TRIGGER_ENTRY_MONITOR_TYPE, + mt, direction_sel, float(upper_px), float(lower_px), @@ -4705,6 +4729,7 @@ def _market_open_for_trigger_entry( entry_price, stop_loss, take_profit, + monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, breakeven_enabled=0, time_close_enabled=0, time_close_hours=None, @@ -4879,7 +4904,7 @@ def _market_open_for_trigger_entry( opened_at_ms, trading_day, ORDER_MONITOR_TYPE_KEY_AUTO, - stored_key_signal_type(TRIGGER_ENTRY_MONITOR_TYPE), + stored_key_signal_type(monitor_type), tc_en, tc_h, tc_at, @@ -4931,6 +4956,7 @@ def _execute_trigger_entry_cross(conn, row): entry, sl, tp, + monitor_type=(row["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE), breakeven_enabled=be_en, time_close_enabled=tc_en, time_close_hours=tc_h, @@ -4976,42 +5002,62 @@ def _execute_trigger_entry_cross(conn, row): def check_trigger_entry_key_monitors(): conn = get_db() - rows = conn.execute("SELECT * FROM key_monitors WHERE monitor_type=?", (TRIGGER_ENTRY_MONITOR_TYPE,)).fetchall() + placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) + rows = conn.execute( + f"SELECT * FROM key_monitors WHERE monitor_type IN ({placeholders})", + tuple(TRIGGER_ENTRY_MONITOR_TYPES), + ).fetchall() now_dt = app_now() for r in rows: symbol = r["symbol"] direction = (r["direction"] or "long").lower() + mt = (r["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE).strip() entry = float(_sqlite_row_val(r, "fib_entry_price") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) tp = float(_sqlite_row_val(r, "fib_take_profit") or 0) + kid = int(r["id"]) if entry <= 0 or sl <= 0 or tp <= 0: _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") continue mark = get_symbol_mark_price(symbol) if mark is None: continue + prev_mark = _sqlite_row_val(r, "last_mark_price") + prev_mark_f = float(prev_mark) if prev_mark not in (None, "") else None if is_trigger_entry_expired(r["created_at"], now_dt, hours=TRIGGER_ENTRY_VALIDITY_HOURS): exp_txt = trigger_entry_expires_at_text(r["created_at"], hours=TRIGGER_ENTRY_VALIDITY_HOURS) msg = ( f"# ⚠️ {symbol} 触价开仓已过期\n" f"**账户:{_wechat_account_label()}**\n" - f"- 类型:{TRIGGER_ENTRY_MONITOR_TYPE}|{_wechat_direction_text(direction)}\n" + f"- 类型:{mt}|{_wechat_direction_text(direction)}\n" f"- 有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h(应于 {exp_txt} 前触发)\n" ) send_wechat_msg(msg) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED) continue - if trigger_entry_invalidate_by_tp(direction, mark, tp): + inv = trigger_entry_invalidate(mt, direction, mark, sl, tp) + if inv == "tp": msg = ( f"# ⚠️ {symbol} 触价开仓失效\n" f"**账户:{_wechat_account_label()}**\n" - f"- 标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交)\n" + f"- 类型:{mt}|标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交)\n" ) send_wechat_msg(msg) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE) continue - if trigger_entry_reached(direction, mark, entry): + if inv == "sl": + msg = ( + f"# ⚠️ {symbol} 触价开仓失效\n" + f"**账户:{_wechat_account_label()}**\n" + f"- 类型:{mt}|标记价 {format_price_for_symbol(symbol, mark)} 已触达止损侧(未突破)\n" + ) + send_wechat_msg(msg) + _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_SL_INVALIDATE) + continue + if trigger_should_fire(mt, direction, mark, entry, prev_mark_f): _execute_trigger_entry_cross(conn, r) + continue + conn.execute("UPDATE key_monitors SET last_mark_price=? WHERE id=?", (float(mark), kid)) conn.commit() conn.close() @@ -6632,13 +6678,22 @@ def api_price_snapshot(): tp_v = _sqlite_row_val(r, "fib_take_profit") entry_txt = format_price_for_symbol(r["symbol"], entry) if entry else "-" tp_txt = format_price_for_symbol(r["symbol"], tp_v) if tp_v else "-" - tp_inv = trigger_entry_invalidate_by_tp(direction, price, float(tp_v)) if tp_v else False + sl_v = _sqlite_row_val(r, "fib_stop_loss") + inv = ( + trigger_entry_invalidate( + r["monitor_type"], direction, price, float(sl_v or 0), float(tp_v or 0) + ) + if tp_v + else None + ) prev = trigger_entry_gate_preview( + monitor_type=r["monitor_type"], entry_display=entry_txt, take_profit_display=tp_txt, created_at=_sqlite_row_val(r, "created_at"), now=app_now(), - tp_invalidated=tp_inv, + tp_invalidated=inv == "tp", + sl_invalidated=inv == "sl", hours=TRIGGER_ENTRY_VALIDITY_HOURS, ) gate_summary = prev.get("summary") or "-" @@ -7260,7 +7315,7 @@ def add_key(): if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt): flash( "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" - "可使用「触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" + "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" ) return redirect("/key_monitor") skip_volume_rank = is_false_breakout_key_monitor_type(mt) @@ -7296,7 +7351,7 @@ def add_key(): if is_trigger_entry_key_monitor_type(mt): if direction_sel not in ("long", "short"): conn.close() - flash("触价开仓请选择做多或做空") + flash("触价请选择做多或做空") return redirect("/key_monitor") try: entry_px = float(d.get("trigger_entry") or 0) @@ -7306,21 +7361,34 @@ def add_key(): entry_px = sl_px = tp_px = 0 if entry_px <= 0 or sl_px <= 0 or tp_px <= 0: conn.close() - flash("触价开仓须填写有效的入场价、止损价、止盈价") + flash("触价须填写有效的入场价、止损价、止盈价") return redirect("/key_monitor") ok_te, err_te = _add_trigger_entry_key_monitor( - conn, symbol, direction_sel, entry_px, sl_px, tp_px, breakeven_enabled=be_flag, - time_close_enabled=tc_en, time_close_hours=tc_h, + conn, + symbol, + direction_sel, + entry_px, + sl_px, + tp_px, + monitor_type=mt, + breakeven_enabled=be_flag, + time_close_enabled=tc_en, + time_close_hours=tc_h, ) conn.commit() conn.close() if not ok_te: flash(err_te or "触价开仓监控添加失败") return redirect("/key_monitor") + trigger_hint = ( + "标记价穿越入场价后立即市价开仓" + if is_breakout_trigger_entry_key_monitor_type(mt) + else "标记价回调触达入场价后下一轮询市价开仓" + ) flash( - f"触价开仓已添加({symbol} 日成交量排名 {rank}/{total})" + f"{mt}已添加({symbol} 日成交量排名 {rank}/{total})" f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h" - f"|标记价触达入场价后下一轮询市价开仓" + f"|{trigger_hint}" f"|移动保本:{'开' if be_flag else '关'}" + (f"|{time_close_label(tc_h)}" if tc_en else "") ) diff --git a/crypto_monitor_okx/使用说明.md b/crypto_monitor_okx/使用说明.md index 9a65d04..345afb6 100644 --- a/crypto_monitor_okx/使用说明.md +++ b/crypto_monitor_okx/使用说明.md @@ -65,7 +65,8 @@ | **收敛突破** | 同上(自动开仓类)。 | | **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 | | **关键支撑位** | 同上(仅提醒)。 | - | **触价开仓** | **不挂交易所限价**;标记价触达计划入场价后 **下一轮询市价开仓**(RR 门槛同关键位 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h**;全仓杠杆模式可用。 | + | **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E 后 **下一轮询市价开仓**(RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** | + | **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** | 3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。 4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**。 diff --git a/crypto_monitor_okx/关键位自动下单说明.md b/crypto_monitor_okx/关键位自动下单说明.md index c352af0..07743c1 100644 --- a/crypto_monitor_okx/关键位自动下单说明.md +++ b/crypto_monitor_okx/关键位自动下单说明.md @@ -16,7 +16,8 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k | **关键阻力位** | **不选**(`direction=watch`) | **否** | 5m 收盘突破上/下沿 → 微信 **3 次** → `key_level_alert_done` | | **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) | | 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) | -| **触价开仓** | **必选** 多/空 | **程序盯价 → 触 E 后市价** | 见下文 **§六** | +| **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§四** | +| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** | **添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30)**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位)。 @@ -118,25 +119,31 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k --- -## 四、触价开仓(程序触价,无交易所挂单) +## 四、回调 / 突破触价开仓(程序触价,无交易所挂单) ### 4.1 录入 -- 类型选 **触价开仓**;方向必选多/空。 -- 填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`)。 -- 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5,与箱体/斐波相同)。 -- 可选移动保本、时间平仓;**全仓杠杆模式**下可用(页面隐藏箱体/收敛/斐波/假突破)。 +- **回调触价开仓**:方向必选多/空;填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`)。 +- **突破触价开仓**:同上;添加时当前价须在突破方向一侧(做多:价低于 E;做空:价高于 E)。 +- 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)。 +- 可选移动保本、时间平仓;**全仓杠杆模式**下可用。 ### 4.2 触发与结案 -- 轮询标记价:做多 `标记价 ≤ E`、做空 `标记价 ≥ E` → **下一轮询市价开仓**,挂交易所 TP/SL,进下单监控。 -- 未成交前标记价先触 **TP 侧** → `trigger_tp_invalidate`;**24h** 未触发 → `trigger_entry_expired`。 +| 类型 | 触发条件(标记价) | +|------|-------------------| +| **回调触价** | 做多 `≤ E`;做空 `≥ E` → 下一轮询市价开仓 | +| **突破触价** | 做多**向上穿越** E;做空**向下穿越** E → **立即**市价开仓 | + +- 未成交前标记价先触 **TP 侧** → `trigger_tp_invalidate`。 +- **突破触价**另:未穿越 E 先触 **SL 侧** → `trigger_sl_invalidate`。 +- **24h** 未触发 → `trigger_entry_expired`。 - 成功 → `trigger_entry_filled`;触发后开仓失败 → `trigger_exchange_failed`。 ### 4.3 计仓与占位 - **以损定仓**:按 E、SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x、其它 5x。 -- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条。 +- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条触价监控(含回调/突破)。 共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors`。 diff --git a/docs/position-sizing-mode.md b/docs/position-sizing-mode.md index bbce436..819df74 100644 --- a/docs/position-sizing-mode.md +++ b/docs/position-sizing-mode.md @@ -32,7 +32,7 @@ FULL_MARGIN_BUFFER_RATIO=0.98 - 关键位:箱体突破、收敛突破、斐波、假突破(添加时拒绝;已存在则启动时撤销)。 - 趋势回调、顺势加仓(策略入口返回明确错误)。 -**允许:** 关键位 **触价开仓**(程序盯价、触达计划入场后市价成交,无交易所挂单;全仓下仅允许一条待触发)。 +**允许:** 关键位 **回调触价开仓** / **突破触价开仓**(程序盯价、触达/穿越计划入场后市价成交,无交易所挂单;全仓下仅允许一条待触发)。 ## 用脚本更新四所 `.env` diff --git a/embed_templates/embed_shell.html b/embed_templates/embed_shell.html index 6bacf10..9ea7229 100644 --- a/embed_templates/embed_shell.html +++ b/embed_templates/embed_shell.html @@ -119,7 +119,7 @@ - + {% include 'embed_boot_scripts.html' %} diff --git a/fib_key_monitor_lib.py b/fib_key_monitor_lib.py index 29e0162..ac53577 100644 --- a/fib_key_monitor_lib.py +++ b/fib_key_monitor_lib.py @@ -48,8 +48,8 @@ def stored_key_signal_type(monitor_type): mt = (monitor_type or "").strip() if mt in FIB_KEY_MONITOR_TYPES: return mt - if mt in ("假突破", "触价开仓"): - return mt + if mt in ("假突破", "回调触价开仓", "突破触价开仓", "触价开仓"): + return mt if mt != "触价开仓" else "回调触价开仓" if mt in KEY_MONITOR_AUTO_TYPES: return mt return None @@ -61,6 +61,8 @@ KEY_ENTRY_REASON_BY_SIGNAL = { "斐波回调0.618": "关键位斐波0.618", "斐波回调0.786": "关键位斐波0.786", "假突破": "关键位假突破", + "回调触价开仓": "关键位回调触价开仓", + "突破触价开仓": "关键位突破触价开仓", "触价开仓": "关键位触价开仓", "趋势回调": "趋势回调", } @@ -75,10 +77,8 @@ def key_signal_type_for_trade_record(key_signal_type, box_auto_types): kst = (key_signal_type or "").strip() if kst in FIB_KEY_MONITOR_TYPES: return kst - if kst == "假突破": - return kst - if kst == "触价开仓": - return kst + if kst in ("假突破", "回调触价开仓", "突破触价开仓", "触价开仓"): + return kst if kst != "触价开仓" else "回调触价开仓" if box_auto_types and kst in box_auto_types: return kst return None diff --git a/key_monitor_schema_lib.py b/key_monitor_schema_lib.py new file mode 100644 index 0000000..1716815 --- /dev/null +++ b/key_monitor_schema_lib.py @@ -0,0 +1,14 @@ +"""关键位监控表结构迁移(四所共用)。""" +from __future__ import annotations + +from typing import Any + + +def ensure_key_monitor_schema(conn: Any) -> None: + for sql in ( + "ALTER TABLE key_monitors ADD COLUMN last_mark_price REAL", + ): + try: + conn.execute(sql) + except Exception: + pass diff --git a/static/key_monitor_form.js b/static/key_monitor_form.js index 572d94e..4c4aea5 100644 --- a/static/key_monitor_form.js +++ b/static/key_monitor_form.js @@ -19,7 +19,7 @@ const autoTypes = new Set(["箱体突破", "收敛突破"]); const fibTypes = new Set(["斐波回调0.618", "斐波回调0.786"]); const fbTypes = new Set(["假突破"]); - const teTypes = new Set(["触价开仓"]); + const teTypes = new Set(["回调触价开仓", "突破触价开仓", "触价开仓"]); const showAuto = autoTypes.has(t); const showFb = fbTypes.has(t); const showTe = teTypes.has(t); diff --git a/strategy_templates/key_monitor_panel.html b/strategy_templates/key_monitor_panel.html index e442eca..8c55008 100644 --- a/strategy_templates/key_monitor_panel.html +++ b/strategy_templates/key_monitor_panel.html @@ -115,6 +115,7 @@ {%- elif r == 'manual' -%}手动删除 {%- elif r == 'fib_invalidate' -%}斐波失效 {%- elif r == 'trigger_tp_invalidate' -%}触价止盈失效 +{%- elif r == 'trigger_sl_invalidate' -%}触价止损失效 {%- elif r == 'trigger_entry_expired' -%}触价过期 {%- elif r == 'trigger_exchange_failed' -%}触价下单失败 {%- elif r == 'false_breakout_expired' -%}假突破过期 @@ -149,7 +150,8 @@ {% endif %} - + +