feat: 关键位回调/突破触价开仓拆分与穿越触发

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-29 10:49:43 +08:00
parent e51d7824a7
commit 5b3448b52b
20 changed files with 662 additions and 172 deletions
+93 -25
View File
@@ -115,21 +115,27 @@ from manual_sltp_lib import (
resolve_entrust_sltp_prices, resolve_entrust_sltp_prices,
resolve_open_sltp_prices, resolve_open_sltp_prices,
) )
from key_monitor_schema_lib import ensure_key_monitor_schema
from trigger_entry_key_monitor_lib import ( 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_EXCHANGE_FAILED,
TRIGGER_ENTRY_CLOSE_EXPIRED, TRIGGER_ENTRY_CLOSE_EXPIRED,
TRIGGER_ENTRY_CLOSE_FILLED, TRIGGER_ENTRY_CLOSE_FILLED,
TRIGGER_ENTRY_CLOSE_SL_INVALIDATE,
TRIGGER_ENTRY_CLOSE_TP_INVALIDATE, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE,
TRIGGER_ENTRY_MONITOR_TYPE, TRIGGER_ENTRY_MONITOR_TYPE,
TRIGGER_ENTRY_MONITOR_TYPES,
TRIGGER_ENTRY_VALIDITY_HOURS, TRIGGER_ENTRY_VALIDITY_HOURS,
check_trigger_entry_intent_limit, check_trigger_entry_intent_limit,
count_pending_trigger_entries, count_pending_trigger_entries,
is_breakout_trigger_entry_key_monitor_type,
is_trigger_entry_expired, is_trigger_entry_expired,
is_trigger_entry_key_monitor_type, is_trigger_entry_key_monitor_type,
trigger_entry_expires_at_text, trigger_entry_expires_at_text,
trigger_entry_gate_preview, trigger_entry_gate_preview,
trigger_entry_invalidate_by_tp, trigger_entry_invalidate,
trigger_entry_reached, trigger_should_fire,
validate_trigger_entry_geometry, validate_trigger_entry_geometry,
validate_trigger_entry_rr, validate_trigger_entry_rr,
) )
@@ -1059,7 +1065,8 @@ ENTRY_REASON_OPTIONS = (
"关键位斐波0.618", "关键位斐波0.618",
"关键位斐波0.786", "关键位斐波0.786",
"关键位假突破", "关键位假突破",
"关键位触价开仓", "关键位回调触价开仓",
"关键位突破触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS ) + STRATEGY_ENTRY_REASON_OPTIONS
STATS_SEGMENT_DEFS = ( STATS_SEGMENT_DEFS = (
@@ -1474,6 +1481,7 @@ def init_db():
except Exception: except Exception:
pass pass
ensure_time_close_schema(c) ensure_time_close_schema(c)
ensure_key_monitor_schema(c)
try: try:
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL") 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": if segment_key == "key_false_breakout":
return kst == FALSE_BREAKOUT_MONITOR_TYPE return kst == FALSE_BREAKOUT_MONITOR_TYPE
if segment_key == "key_trigger": if segment_key == "key_trigger":
return kst == TRIGGER_ENTRY_MONITOR_TYPE return kst in TRIGGER_ENTRY_MONITOR_TYPES
return False return False
@@ -1733,8 +1741,15 @@ def _count_opens_for_segment(conn, start_td, end_td, segment_key):
"key_fib618": "斐波回调0.618", "key_fib618": "斐波回调0.618",
"key_fib786": "斐波回调0.786", "key_fib786": "斐波回调0.786",
"key_false_breakout": FALSE_BREAKOUT_MONITOR_TYPE, "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) kst = kst_map.get(segment_key)
if kst: if kst:
return conn.execute( return conn.execute(
@@ -5294,9 +5309,10 @@ def _finalize_fib_key_fill(conn, row):
def _trigger_entry_exists_for_symbol(conn, symbol): def _trigger_entry_exists_for_symbol(conn, symbol):
placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES))
row = conn.execute( row = conn.execute(
"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type=?", f"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type IN ({placeholders})",
(symbol, TRIGGER_ENTRY_MONITOR_TYPE), (symbol, *TRIGGER_ENTRY_MONITOR_TYPES),
).fetchone() ).fetchone()
return row is not None return row is not None
@@ -5308,15 +5324,21 @@ def _add_trigger_entry_key_monitor(
entry, entry,
sl, sl,
tp, tp,
monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
breakeven_enabled=0, breakeven_enabled=0,
time_close_enabled=0, time_close_enabled=0,
time_close_hours=None, 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): if _trigger_entry_exists_for_symbol(conn, symbol):
return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)" return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)"
ex_sym = normalize_exchange_symbol(symbol) ex_sym = normalize_exchange_symbol(symbol)
mark = get_symbol_mark_price(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: if geom_err:
return False, geom_err return False, geom_err
rr_err = validate_trigger_entry_rr( 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) entry = float(round_price_to_exchange(ex_sym, entry) or entry)
sl = float(round_price_to_exchange(ex_sym, sl) or sl) sl = float(round_price_to_exchange(ex_sym, sl) or sl)
tp = float(round_price_to_exchange(ex_sym, tp) or tp) 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: if geom_err:
return False, geom_err return False, geom_err
rr_err = validate_trigger_entry_rr( rr_err = validate_trigger_entry_rr(
@@ -5414,7 +5438,7 @@ def _add_trigger_entry_key_monitor(
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
( (
symbol, symbol,
TRIGGER_ENTRY_MONITOR_TYPE, mt,
direction_sel, direction_sel,
float(upper_px), float(upper_px),
float(lower_px), float(lower_px),
@@ -5441,6 +5465,7 @@ def _market_open_for_trigger_entry(
entry_price, entry_price,
stop_loss, stop_loss,
take_profit, take_profit,
monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
breakeven_enabled=0, breakeven_enabled=0,
time_close_enabled=0, time_close_enabled=0,
time_close_hours=None, time_close_hours=None,
@@ -5615,7 +5640,7 @@ def _market_open_for_trigger_entry(
opened_at_ms, opened_at_ms,
trading_day, trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO, ORDER_MONITOR_TYPE_KEY_AUTO,
stored_key_signal_type(TRIGGER_ENTRY_MONITOR_TYPE), stored_key_signal_type(monitor_type),
tc_en, tc_en,
tc_h, tc_h,
tc_at, tc_at,
@@ -5667,6 +5692,7 @@ def _execute_trigger_entry_cross(conn, row):
entry, entry,
sl, sl,
tp, tp,
monitor_type=(row["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE),
breakeven_enabled=be_en, breakeven_enabled=be_en,
time_close_enabled=tc_en, time_close_enabled=tc_en,
time_close_hours=tc_h, time_close_hours=tc_h,
@@ -5712,42 +5738,62 @@ def _execute_trigger_entry_cross(conn, row):
def check_trigger_entry_key_monitors(): def check_trigger_entry_key_monitors():
conn = get_db() 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() now_dt = app_now()
for r in rows: for r in rows:
symbol = r["symbol"] symbol = r["symbol"]
direction = (r["direction"] or "long").lower() 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) entry = float(_sqlite_row_val(r, "fib_entry_price") or 0)
sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0)
tp = float(_sqlite_row_val(r, "fib_take_profit") 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: if entry <= 0 or sl <= 0 or tp <= 0:
_finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid")
continue continue
mark = get_symbol_mark_price(symbol) mark = get_symbol_mark_price(symbol)
if mark is None: if mark is None:
continue 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): 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) exp_txt = trigger_entry_expires_at_text(r["created_at"], hours=TRIGGER_ENTRY_VALIDITY_HOURS)
msg = ( msg = (
f"# ⚠️ {symbol} 触价开仓已过期\n" f"# ⚠️ {symbol} 触价开仓已过期\n"
f"**账户:{_wechat_account_label()}**\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" f"- 有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h(应于 {exp_txt} 前触发)\n"
) )
send_wechat_msg(msg) send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED)
continue continue
if trigger_entry_invalidate_by_tp(direction, mark, tp): inv = trigger_entry_invalidate(mt, direction, mark, sl, tp)
if inv == "tp":
msg = ( msg = (
f"# ⚠️ {symbol} 触价开仓失效\n" f"# ⚠️ {symbol} 触价开仓失效\n"
f"**账户:{_wechat_account_label()}**\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) send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE)
continue 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) _execute_trigger_entry_cross(conn, r)
continue
conn.execute("UPDATE key_monitors SET last_mark_price=? WHERE id=?", (float(mark), kid))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -7178,13 +7224,22 @@ def api_price_snapshot():
tp_v = _sqlite_row_val(r, "fib_take_profit") tp_v = _sqlite_row_val(r, "fib_take_profit")
entry_txt = format_price_for_symbol(r["symbol"], entry) if entry else "-" 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_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( prev = trigger_entry_gate_preview(
monitor_type=r["monitor_type"],
entry_display=entry_txt, entry_display=entry_txt,
take_profit_display=tp_txt, take_profit_display=tp_txt,
created_at=_sqlite_row_val(r, "created_at"), created_at=_sqlite_row_val(r, "created_at"),
now=app_now(), now=app_now(),
tp_invalidated=tp_inv, tp_invalidated=inv == "tp",
sl_invalidated=inv == "sl",
hours=TRIGGER_ENTRY_VALIDITY_HOURS, hours=TRIGGER_ENTRY_VALIDITY_HOURS,
) )
gate_summary = prev.get("summary") or "-" 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): if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt):
flash( flash(
"全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;"
"可使用「触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。"
) )
return redirect("/key_monitor") return redirect("/key_monitor")
skip_volume_rank = is_false_breakout_key_monitor_type(mt) skip_volume_rank = is_false_breakout_key_monitor_type(mt)
@@ -7847,7 +7902,7 @@ def add_key():
if direction_sel not in ("long", "short"): if direction_sel not in ("long", "short"):
conn.close() conn.close()
conn = None conn = None
flash("触价开仓请选择做多或做空") flash("触价请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
try: try:
entry_px = float(d.get("trigger_entry") or 0) 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: if entry_px <= 0 or sl_px <= 0 or tp_px <= 0:
conn.close() conn.close()
conn = None conn = None
flash("触价开仓须填写有效的入场价、止损价、止盈价") flash("触价须填写有效的入场价、止损价、止盈价")
return redirect("/key_monitor") return redirect("/key_monitor")
ok_te, err_te = _add_trigger_entry_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, conn,
time_close_enabled=tc_en, time_close_hours=tc_h, 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.commit()
conn.close() conn.close()
@@ -7870,10 +7933,15 @@ def add_key():
if not ok_te: if not ok_te:
flash(err_te or "触价开仓监控添加失败") flash(err_te or "触价开仓监控添加失败")
return redirect("/key_monitor") return redirect("/key_monitor")
trigger_hint = (
"标记价穿越入场价后立即市价开仓"
if is_breakout_trigger_entry_key_monitor_type(mt)
else "标记价回调触达入场价后下一轮询市价开仓"
)
flash( flash(
f"触价开仓已添加({symbol} 日成交量排名 {rank}/{total}" f"{mt}已添加({symbol} 日成交量排名 {rank}/{total}"
f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h" f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h"
f"标记价触达入场价后下一轮询市价开仓" f"{trigger_hint}"
f"|移动保本:{'' if be_flag else ''}" f"|移动保本:{'' if be_flag else ''}"
+ (f"{time_close_label(tc_h)}" if tc_en else "") + (f"{time_close_label(tc_h)}" if tc_en else "")
) )
+4 -3
View File
@@ -66,10 +66,11 @@
| **收敛突破** | 同上(自动开仓类)。 | | **收敛突破** | 同上(自动开仓类)。 |
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 | | **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 |
| **关键支撑位** | 同上(仅提醒)。 | | **关键支撑位** | 同上(仅提醒)。 |
| **触价开仓** | **不挂交易所限价**;标记价触达计划入场价**下一轮询市价开仓**RR 门槛同关键位 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h**;全仓杠杆模式可用。 | | **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E **下一轮询市价开仓**RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** |
3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。 3. **方向**:做多 / 做空(回调/突破触价、箱体 / 收敛 / 斐波必选;阻力/支撑不选)。
4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓**入场 E / 止损 SL / 止盈 TP** 4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价填 **入场 E / 止损 SL / 止盈 TP**
**限制:** **限制:**
活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**(默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**」;仍可添加「**关键阻力位 / 支撑位**」。 活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**(默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**」;仍可添加「**关键阻力位 / 支撑位**」。
@@ -16,7 +16,8 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k
| **关键阻力位** | **不选**`direction=watch` | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` | | **关键阻力位** | **不选**`direction=watch` | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` |
| **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) | | **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) |
| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) | | 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) |
| **触价开仓** | **必选** 多/空 | **程序盯价 → 触 E 后市价** | 见下文 **§** | | **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§** |
| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** |
**添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位)。 **添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位)。
@@ -118,25 +119,31 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k
--- ---
## 四、触价开仓(程序触价,无交易所挂单) ## 四、回调 / 突破触价开仓(程序触价,无交易所挂单)
### 4.1 录入 ### 4.1 录入
- 类型选 **触价开仓**;方向必选多/空 - **回调触价开仓**:方向必选多/空;填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`
- 填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`)。 - **突破触价开仓**:同上;添加时当前价须在突破方向一侧(做多:价低于 E;做空:价高于 E)。
- 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5,与箱体/斐波相同)。 - 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)。
- 可选移动保本、时间平仓;**全仓杠杆模式**下可用(页面隐藏箱体/收敛/斐波/假突破) - 可选移动保本、时间平仓;**全仓杠杆模式**下可用。
### 4.2 触发与结案 ### 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` - 成功 → `trigger_entry_filled`;触发后开仓失败 → `trigger_exchange_failed`
### 4.3 计仓与占位 ### 4.3 计仓与占位
- **以损定仓**:按 E、SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x、其它 5x。 - **以损定仓**:按 E、SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x、其它 5x。
- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条。 - **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条触价监控(含回调/突破)
共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors` 共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors`
+93 -25
View File
@@ -116,21 +116,27 @@ from manual_sltp_lib import (
resolve_entrust_sltp_prices, resolve_entrust_sltp_prices,
resolve_open_sltp_prices, resolve_open_sltp_prices,
) )
from key_monitor_schema_lib import ensure_key_monitor_schema
from trigger_entry_key_monitor_lib import ( 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_EXCHANGE_FAILED,
TRIGGER_ENTRY_CLOSE_EXPIRED, TRIGGER_ENTRY_CLOSE_EXPIRED,
TRIGGER_ENTRY_CLOSE_FILLED, TRIGGER_ENTRY_CLOSE_FILLED,
TRIGGER_ENTRY_CLOSE_SL_INVALIDATE,
TRIGGER_ENTRY_CLOSE_TP_INVALIDATE, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE,
TRIGGER_ENTRY_MONITOR_TYPE, TRIGGER_ENTRY_MONITOR_TYPE,
TRIGGER_ENTRY_MONITOR_TYPES,
TRIGGER_ENTRY_VALIDITY_HOURS, TRIGGER_ENTRY_VALIDITY_HOURS,
check_trigger_entry_intent_limit, check_trigger_entry_intent_limit,
count_pending_trigger_entries, count_pending_trigger_entries,
is_breakout_trigger_entry_key_monitor_type,
is_trigger_entry_expired, is_trigger_entry_expired,
is_trigger_entry_key_monitor_type, is_trigger_entry_key_monitor_type,
trigger_entry_expires_at_text, trigger_entry_expires_at_text,
trigger_entry_gate_preview, trigger_entry_gate_preview,
trigger_entry_invalidate_by_tp, trigger_entry_invalidate,
trigger_entry_reached, trigger_should_fire,
validate_trigger_entry_geometry, validate_trigger_entry_geometry,
validate_trigger_entry_rr, validate_trigger_entry_rr,
) )
@@ -1046,7 +1052,8 @@ ENTRY_REASON_OPTIONS = (
"关键位斐波0.618", "关键位斐波0.618",
"关键位斐波0.786", "关键位斐波0.786",
"关键位假突破", "关键位假突破",
"关键位触价开仓", "关键位回调触价开仓",
"关键位突破触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS ) + STRATEGY_ENTRY_REASON_OPTIONS
STATS_SEGMENT_DEFS = ( STATS_SEGMENT_DEFS = (
@@ -1466,6 +1473,7 @@ def init_db():
except Exception: except Exception:
pass pass
ensure_time_close_schema(c) ensure_time_close_schema(c)
ensure_key_monitor_schema(c)
try: try:
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL") c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
except Exception: except Exception:
@@ -1709,7 +1717,7 @@ def _pnl_row_matches_segment(row, segment_key):
if segment_key == "key_false_breakout": if segment_key == "key_false_breakout":
return kst == FALSE_BREAKOUT_MONITOR_TYPE return kst == FALSE_BREAKOUT_MONITOR_TYPE
if segment_key == "key_trigger": if segment_key == "key_trigger":
return kst == TRIGGER_ENTRY_MONITOR_TYPE return kst in TRIGGER_ENTRY_MONITOR_TYPES
return False return False
@@ -1727,8 +1735,15 @@ def _count_opens_for_segment(conn, start_td, end_td, segment_key):
"key_fib618": "斐波回调0.618", "key_fib618": "斐波回调0.618",
"key_fib786": "斐波回调0.786", "key_fib786": "斐波回调0.786",
"key_false_breakout": FALSE_BREAKOUT_MONITOR_TYPE, "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) kst = kst_map.get(segment_key)
if kst: if kst:
return conn.execute( return conn.execute(
@@ -5038,9 +5053,10 @@ def _finalize_fib_key_fill(conn, row):
def _trigger_entry_exists_for_symbol(conn, symbol): def _trigger_entry_exists_for_symbol(conn, symbol):
placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES))
row = conn.execute( row = conn.execute(
"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type=?", f"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type IN ({placeholders})",
(symbol, TRIGGER_ENTRY_MONITOR_TYPE), (symbol, *TRIGGER_ENTRY_MONITOR_TYPES),
).fetchone() ).fetchone()
return row is not None return row is not None
@@ -5052,15 +5068,21 @@ def _add_trigger_entry_key_monitor(
entry, entry,
sl, sl,
tp, tp,
monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
breakeven_enabled=0, breakeven_enabled=0,
time_close_enabled=0, time_close_enabled=0,
time_close_hours=None, 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): if _trigger_entry_exists_for_symbol(conn, symbol):
return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)" return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)"
ex_sym = normalize_exchange_symbol(symbol) ex_sym = normalize_exchange_symbol(symbol)
mark = get_symbol_mark_price(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: if geom_err:
return False, geom_err return False, geom_err
rr_err = validate_trigger_entry_rr( 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) entry = float(round_price_to_exchange(ex_sym, entry) or entry)
sl = float(round_price_to_exchange(ex_sym, sl) or sl) sl = float(round_price_to_exchange(ex_sym, sl) or sl)
tp = float(round_price_to_exchange(ex_sym, tp) or tp) 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: if geom_err:
return False, geom_err return False, geom_err
rr_err = validate_trigger_entry_rr( rr_err = validate_trigger_entry_rr(
@@ -5158,7 +5182,7 @@ def _add_trigger_entry_key_monitor(
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
( (
symbol, symbol,
TRIGGER_ENTRY_MONITOR_TYPE, mt,
direction_sel, direction_sel,
float(upper_px), float(upper_px),
float(lower_px), float(lower_px),
@@ -5185,6 +5209,7 @@ def _market_open_for_trigger_entry(
entry_price, entry_price,
stop_loss, stop_loss,
take_profit, take_profit,
monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
breakeven_enabled=0, breakeven_enabled=0,
time_close_enabled=0, time_close_enabled=0,
time_close_hours=None, time_close_hours=None,
@@ -5359,7 +5384,7 @@ def _market_open_for_trigger_entry(
opened_at_ms, opened_at_ms,
trading_day, trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO, ORDER_MONITOR_TYPE_KEY_AUTO,
stored_key_signal_type(TRIGGER_ENTRY_MONITOR_TYPE), stored_key_signal_type(monitor_type),
tc_en, tc_en,
tc_h, tc_h,
tc_at, tc_at,
@@ -5411,6 +5436,7 @@ def _execute_trigger_entry_cross(conn, row):
entry, entry,
sl, sl,
tp, tp,
monitor_type=(row["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE),
breakeven_enabled=be_en, breakeven_enabled=be_en,
time_close_enabled=tc_en, time_close_enabled=tc_en,
time_close_hours=tc_h, time_close_hours=tc_h,
@@ -5456,42 +5482,62 @@ def _execute_trigger_entry_cross(conn, row):
def check_trigger_entry_key_monitors(): def check_trigger_entry_key_monitors():
conn = get_db() 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() now_dt = app_now()
for r in rows: for r in rows:
symbol = r["symbol"] symbol = r["symbol"]
direction = (r["direction"] or "long").lower() 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) entry = float(_sqlite_row_val(r, "fib_entry_price") or 0)
sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0)
tp = float(_sqlite_row_val(r, "fib_take_profit") 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: if entry <= 0 or sl <= 0 or tp <= 0:
_finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid")
continue continue
mark = get_symbol_mark_price(symbol) mark = get_symbol_mark_price(symbol)
if mark is None: if mark is None:
continue 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): 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) exp_txt = trigger_entry_expires_at_text(r["created_at"], hours=TRIGGER_ENTRY_VALIDITY_HOURS)
msg = ( msg = (
f"# ⚠️ {symbol} 触价开仓已过期\n" f"# ⚠️ {symbol} 触价开仓已过期\n"
f"**账户:{_wechat_account_label()}**\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" f"- 有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h(应于 {exp_txt} 前触发)\n"
) )
send_wechat_msg(msg) send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED)
continue continue
if trigger_entry_invalidate_by_tp(direction, mark, tp): inv = trigger_entry_invalidate(mt, direction, mark, sl, tp)
if inv == "tp":
msg = ( msg = (
f"# ⚠️ {symbol} 触价开仓失效\n" f"# ⚠️ {symbol} 触价开仓失效\n"
f"**账户:{_wechat_account_label()}**\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) send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE)
continue 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) _execute_trigger_entry_cross(conn, r)
continue
conn.execute("UPDATE key_monitors SET last_mark_price=? WHERE id=?", (float(mark), kid))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -7087,13 +7133,22 @@ def api_price_snapshot():
tp_v = _sqlite_row_val(r, "fib_take_profit") tp_v = _sqlite_row_val(r, "fib_take_profit")
entry_txt = format_price_for_symbol(r["symbol"], entry) if entry else "-" 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_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( prev = trigger_entry_gate_preview(
monitor_type=r["monitor_type"],
entry_display=entry_txt, entry_display=entry_txt,
take_profit_display=tp_txt, take_profit_display=tp_txt,
created_at=_sqlite_row_val(r, "created_at"), created_at=_sqlite_row_val(r, "created_at"),
now=app_now(), now=app_now(),
tp_invalidated=tp_inv, tp_invalidated=inv == "tp",
sl_invalidated=inv == "sl",
hours=TRIGGER_ENTRY_VALIDITY_HOURS, hours=TRIGGER_ENTRY_VALIDITY_HOURS,
) )
gate_summary = prev.get("summary") or "-" 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): if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt):
flash( flash(
"全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;"
"可使用「触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。"
) )
return redirect("/key_monitor") return redirect("/key_monitor")
skip_volume_rank = is_false_breakout_key_monitor_type(mt) skip_volume_rank = is_false_breakout_key_monitor_type(mt)
@@ -7750,7 +7805,7 @@ def add_key():
if direction_sel not in ("long", "short"): if direction_sel not in ("long", "short"):
conn.close() conn.close()
conn = None conn = None
flash("触价开仓请选择做多或做空") flash("触价请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
try: try:
entry_px = float(d.get("trigger_entry") or 0) 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: if entry_px <= 0 or sl_px <= 0 or tp_px <= 0:
conn.close() conn.close()
conn = None conn = None
flash("触价开仓须填写有效的入场价、止损价、止盈价") flash("触价须填写有效的入场价、止损价、止盈价")
return redirect("/key_monitor") return redirect("/key_monitor")
ok_te, err_te = _add_trigger_entry_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, conn,
time_close_enabled=tc_en, time_close_hours=tc_h, 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.commit()
conn.close() conn.close()
@@ -7773,10 +7836,15 @@ def add_key():
if not ok_te: if not ok_te:
flash(err_te or "触价开仓监控添加失败") flash(err_te or "触价开仓监控添加失败")
return redirect("/key_monitor") return redirect("/key_monitor")
trigger_hint = (
"标记价穿越入场价后立即市价开仓"
if is_breakout_trigger_entry_key_monitor_type(mt)
else "标记价回调触达入场价后下一轮询市价开仓"
)
flash( flash(
f"触价开仓已添加({symbol} 日成交量排名 {rank}/{total}" f"{mt}已添加({symbol} 日成交量排名 {rank}/{total}"
f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h" f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h"
f"标记价触达入场价后下一轮询市价开仓" f"{trigger_hint}"
f"|移动保本:{'' if be_flag else ''}" f"|移动保本:{'' if be_flag else ''}"
+ (f"{time_close_label(tc_h)}" if tc_en else "") + (f"{time_close_label(tc_h)}" if tc_en else "")
) )
+2 -1
View File
@@ -65,7 +65,8 @@
| **收敛突破** | 同上(自动开仓类)。 | | **收敛突破** | 同上(自动开仓类)。 |
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 | | **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 |
| **关键支撑位** | 同上(仅提醒)。 | | **关键支撑位** | 同上(仅提醒)。 |
| **触价开仓** | **不挂交易所限价**;标记价触达计划入场价**下一轮询市价开仓**RR 门槛同关键位 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h**;全仓杠杆模式可用。 | | **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E **下一轮询市价开仓**RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** |
3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。 3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。
4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP** 4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**
@@ -16,7 +16,8 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k
| **关键阻力位** | **不选**`direction=watch` | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` | | **关键阻力位** | **不选**`direction=watch` | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` |
| **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) | | **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) |
| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) | | 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) |
| **触价开仓** | **必选** 多/空 | **程序盯价 → 触 E 后市价** | 见下文 **§** | | **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§** |
| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** |
**添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位)。 **添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位)。
@@ -118,25 +119,31 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k
--- ---
## 四、触价开仓(程序触价,无交易所挂单) ## 四、回调 / 突破触价开仓(程序触价,无交易所挂单)
### 4.1 录入 ### 4.1 录入
- 类型选 **触价开仓**;方向必选多/空 - **回调触价开仓**:方向必选多/空;填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`
- 填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`)。 - **突破触价开仓**:同上;添加时当前价须在突破方向一侧(做多:价低于 E;做空:价高于 E)。
- 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5,与箱体/斐波相同)。 - 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)。
- 可选移动保本、时间平仓;**全仓杠杆模式**下可用(页面隐藏箱体/收敛/斐波/假突破) - 可选移动保本、时间平仓;**全仓杠杆模式**下可用。
### 4.2 触发与结案 ### 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` - 成功 → `trigger_entry_filled`;触发后开仓失败 → `trigger_exchange_failed`
### 4.3 计仓与占位 ### 4.3 计仓与占位
- **以损定仓**:按 E、SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x、其它 5x。 - **以损定仓**:按 E、SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x、其它 5x。
- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条。 - **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条触价监控(含回调/突破)
共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors` 共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors`
+93 -25
View File
@@ -116,21 +116,27 @@ from manual_sltp_lib import (
resolve_entrust_sltp_prices, resolve_entrust_sltp_prices,
resolve_open_sltp_prices, resolve_open_sltp_prices,
) )
from key_monitor_schema_lib import ensure_key_monitor_schema
from trigger_entry_key_monitor_lib import ( 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_EXCHANGE_FAILED,
TRIGGER_ENTRY_CLOSE_EXPIRED, TRIGGER_ENTRY_CLOSE_EXPIRED,
TRIGGER_ENTRY_CLOSE_FILLED, TRIGGER_ENTRY_CLOSE_FILLED,
TRIGGER_ENTRY_CLOSE_SL_INVALIDATE,
TRIGGER_ENTRY_CLOSE_TP_INVALIDATE, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE,
TRIGGER_ENTRY_MONITOR_TYPE, TRIGGER_ENTRY_MONITOR_TYPE,
TRIGGER_ENTRY_MONITOR_TYPES,
TRIGGER_ENTRY_VALIDITY_HOURS, TRIGGER_ENTRY_VALIDITY_HOURS,
check_trigger_entry_intent_limit, check_trigger_entry_intent_limit,
count_pending_trigger_entries, count_pending_trigger_entries,
is_breakout_trigger_entry_key_monitor_type,
is_trigger_entry_expired, is_trigger_entry_expired,
is_trigger_entry_key_monitor_type, is_trigger_entry_key_monitor_type,
trigger_entry_expires_at_text, trigger_entry_expires_at_text,
trigger_entry_gate_preview, trigger_entry_gate_preview,
trigger_entry_invalidate_by_tp, trigger_entry_invalidate,
trigger_entry_reached, trigger_should_fire,
validate_trigger_entry_geometry, validate_trigger_entry_geometry,
validate_trigger_entry_rr, validate_trigger_entry_rr,
) )
@@ -1046,7 +1052,8 @@ ENTRY_REASON_OPTIONS = (
"关键位斐波0.618", "关键位斐波0.618",
"关键位斐波0.786", "关键位斐波0.786",
"关键位假突破", "关键位假突破",
"关键位触价开仓", "关键位回调触价开仓",
"关键位突破触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS ) + STRATEGY_ENTRY_REASON_OPTIONS
STATS_SEGMENT_DEFS = ( STATS_SEGMENT_DEFS = (
@@ -1466,6 +1473,7 @@ def init_db():
except Exception: except Exception:
pass pass
ensure_time_close_schema(c) ensure_time_close_schema(c)
ensure_key_monitor_schema(c)
try: try:
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL") c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
except Exception: except Exception:
@@ -1709,7 +1717,7 @@ def _pnl_row_matches_segment(row, segment_key):
if segment_key == "key_false_breakout": if segment_key == "key_false_breakout":
return kst == FALSE_BREAKOUT_MONITOR_TYPE return kst == FALSE_BREAKOUT_MONITOR_TYPE
if segment_key == "key_trigger": if segment_key == "key_trigger":
return kst == TRIGGER_ENTRY_MONITOR_TYPE return kst in TRIGGER_ENTRY_MONITOR_TYPES
return False return False
@@ -1727,8 +1735,15 @@ def _count_opens_for_segment(conn, start_td, end_td, segment_key):
"key_fib618": "斐波回调0.618", "key_fib618": "斐波回调0.618",
"key_fib786": "斐波回调0.786", "key_fib786": "斐波回调0.786",
"key_false_breakout": FALSE_BREAKOUT_MONITOR_TYPE, "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) kst = kst_map.get(segment_key)
if kst: if kst:
return conn.execute( return conn.execute(
@@ -5038,9 +5053,10 @@ def _finalize_fib_key_fill(conn, row):
def _trigger_entry_exists_for_symbol(conn, symbol): def _trigger_entry_exists_for_symbol(conn, symbol):
placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES))
row = conn.execute( row = conn.execute(
"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type=?", f"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type IN ({placeholders})",
(symbol, TRIGGER_ENTRY_MONITOR_TYPE), (symbol, *TRIGGER_ENTRY_MONITOR_TYPES),
).fetchone() ).fetchone()
return row is not None return row is not None
@@ -5052,15 +5068,21 @@ def _add_trigger_entry_key_monitor(
entry, entry,
sl, sl,
tp, tp,
monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
breakeven_enabled=0, breakeven_enabled=0,
time_close_enabled=0, time_close_enabled=0,
time_close_hours=None, 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): if _trigger_entry_exists_for_symbol(conn, symbol):
return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)" return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)"
ex_sym = normalize_exchange_symbol(symbol) ex_sym = normalize_exchange_symbol(symbol)
mark = get_symbol_mark_price(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: if geom_err:
return False, geom_err return False, geom_err
rr_err = validate_trigger_entry_rr( 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) entry = float(round_price_to_exchange(ex_sym, entry) or entry)
sl = float(round_price_to_exchange(ex_sym, sl) or sl) sl = float(round_price_to_exchange(ex_sym, sl) or sl)
tp = float(round_price_to_exchange(ex_sym, tp) or tp) 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: if geom_err:
return False, geom_err return False, geom_err
rr_err = validate_trigger_entry_rr( rr_err = validate_trigger_entry_rr(
@@ -5158,7 +5182,7 @@ def _add_trigger_entry_key_monitor(
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
( (
symbol, symbol,
TRIGGER_ENTRY_MONITOR_TYPE, mt,
direction_sel, direction_sel,
float(upper_px), float(upper_px),
float(lower_px), float(lower_px),
@@ -5185,6 +5209,7 @@ def _market_open_for_trigger_entry(
entry_price, entry_price,
stop_loss, stop_loss,
take_profit, take_profit,
monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
breakeven_enabled=0, breakeven_enabled=0,
time_close_enabled=0, time_close_enabled=0,
time_close_hours=None, time_close_hours=None,
@@ -5359,7 +5384,7 @@ def _market_open_for_trigger_entry(
opened_at_ms, opened_at_ms,
trading_day, trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO, ORDER_MONITOR_TYPE_KEY_AUTO,
stored_key_signal_type(TRIGGER_ENTRY_MONITOR_TYPE), stored_key_signal_type(monitor_type),
tc_en, tc_en,
tc_h, tc_h,
tc_at, tc_at,
@@ -5411,6 +5436,7 @@ def _execute_trigger_entry_cross(conn, row):
entry, entry,
sl, sl,
tp, tp,
monitor_type=(row["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE),
breakeven_enabled=be_en, breakeven_enabled=be_en,
time_close_enabled=tc_en, time_close_enabled=tc_en,
time_close_hours=tc_h, time_close_hours=tc_h,
@@ -5456,42 +5482,62 @@ def _execute_trigger_entry_cross(conn, row):
def check_trigger_entry_key_monitors(): def check_trigger_entry_key_monitors():
conn = get_db() 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() now_dt = app_now()
for r in rows: for r in rows:
symbol = r["symbol"] symbol = r["symbol"]
direction = (r["direction"] or "long").lower() 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) entry = float(_sqlite_row_val(r, "fib_entry_price") or 0)
sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0)
tp = float(_sqlite_row_val(r, "fib_take_profit") 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: if entry <= 0 or sl <= 0 or tp <= 0:
_finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid")
continue continue
mark = get_symbol_mark_price(symbol) mark = get_symbol_mark_price(symbol)
if mark is None: if mark is None:
continue 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): 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) exp_txt = trigger_entry_expires_at_text(r["created_at"], hours=TRIGGER_ENTRY_VALIDITY_HOURS)
msg = ( msg = (
f"# ⚠️ {symbol} 触价开仓已过期\n" f"# ⚠️ {symbol} 触价开仓已过期\n"
f"**账户:{_wechat_account_label()}**\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" f"- 有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h(应于 {exp_txt} 前触发)\n"
) )
send_wechat_msg(msg) send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED)
continue continue
if trigger_entry_invalidate_by_tp(direction, mark, tp): inv = trigger_entry_invalidate(mt, direction, mark, sl, tp)
if inv == "tp":
msg = ( msg = (
f"# ⚠️ {symbol} 触价开仓失效\n" f"# ⚠️ {symbol} 触价开仓失效\n"
f"**账户:{_wechat_account_label()}**\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) send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE)
continue 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) _execute_trigger_entry_cross(conn, r)
continue
conn.execute("UPDATE key_monitors SET last_mark_price=? WHERE id=?", (float(mark), kid))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -7083,13 +7129,22 @@ def api_price_snapshot():
tp_v = _sqlite_row_val(r, "fib_take_profit") tp_v = _sqlite_row_val(r, "fib_take_profit")
entry_txt = format_price_for_symbol(r["symbol"], entry) if entry else "-" 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_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( prev = trigger_entry_gate_preview(
monitor_type=r["monitor_type"],
entry_display=entry_txt, entry_display=entry_txt,
take_profit_display=tp_txt, take_profit_display=tp_txt,
created_at=_sqlite_row_val(r, "created_at"), created_at=_sqlite_row_val(r, "created_at"),
now=app_now(), now=app_now(),
tp_invalidated=tp_inv, tp_invalidated=inv == "tp",
sl_invalidated=inv == "sl",
hours=TRIGGER_ENTRY_VALIDITY_HOURS, hours=TRIGGER_ENTRY_VALIDITY_HOURS,
) )
gate_summary = prev.get("summary") or "-" 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): if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt):
flash( flash(
"全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;"
"可使用「触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。"
) )
return redirect("/key_monitor") return redirect("/key_monitor")
skip_volume_rank = is_false_breakout_key_monitor_type(mt) skip_volume_rank = is_false_breakout_key_monitor_type(mt)
@@ -7746,7 +7801,7 @@ def add_key():
if direction_sel not in ("long", "short"): if direction_sel not in ("long", "short"):
conn.close() conn.close()
conn = None conn = None
flash("触价开仓请选择做多或做空") flash("触价请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
try: try:
entry_px = float(d.get("trigger_entry") or 0) 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: if entry_px <= 0 or sl_px <= 0 or tp_px <= 0:
conn.close() conn.close()
conn = None conn = None
flash("触价开仓须填写有效的入场价、止损价、止盈价") flash("触价须填写有效的入场价、止损价、止盈价")
return redirect("/key_monitor") return redirect("/key_monitor")
ok_te, err_te = _add_trigger_entry_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, conn,
time_close_enabled=tc_en, time_close_hours=tc_h, 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.commit()
conn.close() conn.close()
@@ -7769,10 +7832,15 @@ def add_key():
if not ok_te: if not ok_te:
flash(err_te or "触价开仓监控添加失败") flash(err_te or "触价开仓监控添加失败")
return redirect("/key_monitor") return redirect("/key_monitor")
trigger_hint = (
"标记价穿越入场价后立即市价开仓"
if is_breakout_trigger_entry_key_monitor_type(mt)
else "标记价回调触达入场价后下一轮询市价开仓"
)
flash( flash(
f"触价开仓已添加({symbol} 日成交量排名 {rank}/{total}" f"{mt}已添加({symbol} 日成交量排名 {rank}/{total}"
f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h" f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h"
f"标记价触达入场价后下一轮询市价开仓" f"{trigger_hint}"
f"|移动保本:{'' if be_flag else ''}" f"|移动保本:{'' if be_flag else ''}"
+ (f"{time_close_label(tc_h)}" if tc_en else "") + (f"{time_close_label(tc_h)}" if tc_en else "")
) )
+2 -1
View File
@@ -65,7 +65,8 @@
| **收敛突破** | 同上(自动开仓类)。 | | **收敛突破** | 同上(自动开仓类)。 |
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 | | **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 |
| **关键支撑位** | 同上(仅提醒)。 | | **关键支撑位** | 同上(仅提醒)。 |
| **触价开仓** | **不挂交易所限价**;标记价触达计划入场价**下一轮询市价开仓**RR 门槛同关键位 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h**;全仓杠杆模式可用。 | | **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E **下一轮询市价开仓**RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** |
3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。 3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。
4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP** 4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**
+93 -25
View File
@@ -116,21 +116,27 @@ from manual_sltp_lib import (
resolve_entrust_sltp_prices, resolve_entrust_sltp_prices,
resolve_open_sltp_prices, resolve_open_sltp_prices,
) )
from key_monitor_schema_lib import ensure_key_monitor_schema
from trigger_entry_key_monitor_lib import ( 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_EXCHANGE_FAILED,
TRIGGER_ENTRY_CLOSE_EXPIRED, TRIGGER_ENTRY_CLOSE_EXPIRED,
TRIGGER_ENTRY_CLOSE_FILLED, TRIGGER_ENTRY_CLOSE_FILLED,
TRIGGER_ENTRY_CLOSE_SL_INVALIDATE,
TRIGGER_ENTRY_CLOSE_TP_INVALIDATE, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE,
TRIGGER_ENTRY_MONITOR_TYPE, TRIGGER_ENTRY_MONITOR_TYPE,
TRIGGER_ENTRY_MONITOR_TYPES,
TRIGGER_ENTRY_VALIDITY_HOURS, TRIGGER_ENTRY_VALIDITY_HOURS,
check_trigger_entry_intent_limit, check_trigger_entry_intent_limit,
count_pending_trigger_entries, count_pending_trigger_entries,
is_breakout_trigger_entry_key_monitor_type,
is_trigger_entry_expired, is_trigger_entry_expired,
is_trigger_entry_key_monitor_type, is_trigger_entry_key_monitor_type,
trigger_entry_expires_at_text, trigger_entry_expires_at_text,
trigger_entry_gate_preview, trigger_entry_gate_preview,
trigger_entry_invalidate_by_tp, trigger_entry_invalidate,
trigger_entry_reached, trigger_should_fire,
validate_trigger_entry_geometry, validate_trigger_entry_geometry,
validate_trigger_entry_rr, validate_trigger_entry_rr,
) )
@@ -1054,7 +1060,8 @@ ENTRY_REASON_OPTIONS = (
"关键位斐波0.618", "关键位斐波0.618",
"关键位斐波0.786", "关键位斐波0.786",
"关键位假突破", "关键位假突破",
"关键位触价开仓", "关键位回调触价开仓",
"关键位突破触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS ) + STRATEGY_ENTRY_REASON_OPTIONS
STATS_SEGMENT_DEFS = ( STATS_SEGMENT_DEFS = (
@@ -1468,6 +1475,7 @@ def init_db():
except Exception: except Exception:
pass pass
ensure_time_close_schema(c) ensure_time_close_schema(c)
ensure_key_monitor_schema(c)
try: try:
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL") c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
except Exception: except Exception:
@@ -1693,7 +1701,7 @@ def _pnl_row_matches_segment(row, segment_key):
if segment_key == "key_false_breakout": if segment_key == "key_false_breakout":
return kst == FALSE_BREAKOUT_MONITOR_TYPE return kst == FALSE_BREAKOUT_MONITOR_TYPE
if segment_key == "key_trigger": if segment_key == "key_trigger":
return kst == TRIGGER_ENTRY_MONITOR_TYPE return kst in TRIGGER_ENTRY_MONITOR_TYPES
return False return False
@@ -1711,8 +1719,15 @@ def _count_opens_for_segment(conn, start_td, end_td, segment_key):
"key_fib618": "斐波回调0.618", "key_fib618": "斐波回调0.618",
"key_fib786": "斐波回调0.786", "key_fib786": "斐波回调0.786",
"key_false_breakout": FALSE_BREAKOUT_MONITOR_TYPE, "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) kst = kst_map.get(segment_key)
if kst: if kst:
return conn.execute( return conn.execute(
@@ -4558,9 +4573,10 @@ def _finalize_fib_key_fill(conn, row):
def _trigger_entry_exists_for_symbol(conn, symbol): def _trigger_entry_exists_for_symbol(conn, symbol):
placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES))
row = conn.execute( row = conn.execute(
"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type=?", f"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type IN ({placeholders})",
(symbol, TRIGGER_ENTRY_MONITOR_TYPE), (symbol, *TRIGGER_ENTRY_MONITOR_TYPES),
).fetchone() ).fetchone()
return row is not None return row is not None
@@ -4572,15 +4588,21 @@ def _add_trigger_entry_key_monitor(
entry, entry,
sl, sl,
tp, tp,
monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
breakeven_enabled=0, breakeven_enabled=0,
time_close_enabled=0, time_close_enabled=0,
time_close_hours=None, 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): if _trigger_entry_exists_for_symbol(conn, symbol):
return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)" return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)"
ex_sym = normalize_exchange_symbol(symbol) ex_sym = normalize_exchange_symbol(symbol)
mark = get_symbol_mark_price(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: if geom_err:
return False, geom_err return False, geom_err
rr_err = validate_trigger_entry_rr( 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) entry = float(round_price_to_exchange(ex_sym, entry) or entry)
sl = float(round_price_to_exchange(ex_sym, sl) or sl) sl = float(round_price_to_exchange(ex_sym, sl) or sl)
tp = float(round_price_to_exchange(ex_sym, tp) or tp) 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: if geom_err:
return False, geom_err return False, geom_err
rr_err = validate_trigger_entry_rr( rr_err = validate_trigger_entry_rr(
@@ -4678,7 +4702,7 @@ def _add_trigger_entry_key_monitor(
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
( (
symbol, symbol,
TRIGGER_ENTRY_MONITOR_TYPE, mt,
direction_sel, direction_sel,
float(upper_px), float(upper_px),
float(lower_px), float(lower_px),
@@ -4705,6 +4729,7 @@ def _market_open_for_trigger_entry(
entry_price, entry_price,
stop_loss, stop_loss,
take_profit, take_profit,
monitor_type=CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
breakeven_enabled=0, breakeven_enabled=0,
time_close_enabled=0, time_close_enabled=0,
time_close_hours=None, time_close_hours=None,
@@ -4879,7 +4904,7 @@ def _market_open_for_trigger_entry(
opened_at_ms, opened_at_ms,
trading_day, trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO, ORDER_MONITOR_TYPE_KEY_AUTO,
stored_key_signal_type(TRIGGER_ENTRY_MONITOR_TYPE), stored_key_signal_type(monitor_type),
tc_en, tc_en,
tc_h, tc_h,
tc_at, tc_at,
@@ -4931,6 +4956,7 @@ def _execute_trigger_entry_cross(conn, row):
entry, entry,
sl, sl,
tp, tp,
monitor_type=(row["monitor_type"] or CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE),
breakeven_enabled=be_en, breakeven_enabled=be_en,
time_close_enabled=tc_en, time_close_enabled=tc_en,
time_close_hours=tc_h, time_close_hours=tc_h,
@@ -4976,42 +5002,62 @@ def _execute_trigger_entry_cross(conn, row):
def check_trigger_entry_key_monitors(): def check_trigger_entry_key_monitors():
conn = get_db() 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() now_dt = app_now()
for r in rows: for r in rows:
symbol = r["symbol"] symbol = r["symbol"]
direction = (r["direction"] or "long").lower() 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) entry = float(_sqlite_row_val(r, "fib_entry_price") or 0)
sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0)
tp = float(_sqlite_row_val(r, "fib_take_profit") 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: if entry <= 0 or sl <= 0 or tp <= 0:
_finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid")
continue continue
mark = get_symbol_mark_price(symbol) mark = get_symbol_mark_price(symbol)
if mark is None: if mark is None:
continue 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): 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) exp_txt = trigger_entry_expires_at_text(r["created_at"], hours=TRIGGER_ENTRY_VALIDITY_HOURS)
msg = ( msg = (
f"# ⚠️ {symbol} 触价开仓已过期\n" f"# ⚠️ {symbol} 触价开仓已过期\n"
f"**账户:{_wechat_account_label()}**\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" f"- 有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h(应于 {exp_txt} 前触发)\n"
) )
send_wechat_msg(msg) send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED)
continue continue
if trigger_entry_invalidate_by_tp(direction, mark, tp): inv = trigger_entry_invalidate(mt, direction, mark, sl, tp)
if inv == "tp":
msg = ( msg = (
f"# ⚠️ {symbol} 触价开仓失效\n" f"# ⚠️ {symbol} 触价开仓失效\n"
f"**账户:{_wechat_account_label()}**\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) send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE) _finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE)
continue 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) _execute_trigger_entry_cross(conn, r)
continue
conn.execute("UPDATE key_monitors SET last_mark_price=? WHERE id=?", (float(mark), kid))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -6632,13 +6678,22 @@ def api_price_snapshot():
tp_v = _sqlite_row_val(r, "fib_take_profit") tp_v = _sqlite_row_val(r, "fib_take_profit")
entry_txt = format_price_for_symbol(r["symbol"], entry) if entry else "-" 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_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( prev = trigger_entry_gate_preview(
monitor_type=r["monitor_type"],
entry_display=entry_txt, entry_display=entry_txt,
take_profit_display=tp_txt, take_profit_display=tp_txt,
created_at=_sqlite_row_val(r, "created_at"), created_at=_sqlite_row_val(r, "created_at"),
now=app_now(), now=app_now(),
tp_invalidated=tp_inv, tp_invalidated=inv == "tp",
sl_invalidated=inv == "sl",
hours=TRIGGER_ENTRY_VALIDITY_HOURS, hours=TRIGGER_ENTRY_VALIDITY_HOURS,
) )
gate_summary = prev.get("summary") or "-" 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): if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt):
flash( flash(
"全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;"
"可使用「触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。"
) )
return redirect("/key_monitor") return redirect("/key_monitor")
skip_volume_rank = is_false_breakout_key_monitor_type(mt) 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 is_trigger_entry_key_monitor_type(mt):
if direction_sel not in ("long", "short"): if direction_sel not in ("long", "short"):
conn.close() conn.close()
flash("触价开仓请选择做多或做空") flash("触价请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
try: try:
entry_px = float(d.get("trigger_entry") or 0) entry_px = float(d.get("trigger_entry") or 0)
@@ -7306,21 +7361,34 @@ def add_key():
entry_px = sl_px = tp_px = 0 entry_px = sl_px = tp_px = 0
if entry_px <= 0 or sl_px <= 0 or tp_px <= 0: if entry_px <= 0 or sl_px <= 0 or tp_px <= 0:
conn.close() conn.close()
flash("触价开仓须填写有效的入场价、止损价、止盈价") flash("触价须填写有效的入场价、止损价、止盈价")
return redirect("/key_monitor") return redirect("/key_monitor")
ok_te, err_te = _add_trigger_entry_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, conn,
time_close_enabled=tc_en, time_close_hours=tc_h, 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.commit()
conn.close() conn.close()
if not ok_te: if not ok_te:
flash(err_te or "触价开仓监控添加失败") flash(err_te or "触价开仓监控添加失败")
return redirect("/key_monitor") return redirect("/key_monitor")
trigger_hint = (
"标记价穿越入场价后立即市价开仓"
if is_breakout_trigger_entry_key_monitor_type(mt)
else "标记价回调触达入场价后下一轮询市价开仓"
)
flash( flash(
f"触价开仓已添加({symbol} 日成交量排名 {rank}/{total}" f"{mt}已添加({symbol} 日成交量排名 {rank}/{total}"
f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h" f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h"
f"标记价触达入场价后下一轮询市价开仓" f"{trigger_hint}"
f"|移动保本:{'' if be_flag else ''}" f"|移动保本:{'' if be_flag else ''}"
+ (f"{time_close_label(tc_h)}" if tc_en else "") + (f"{time_close_label(tc_h)}" if tc_en else "")
) )
+2 -1
View File
@@ -65,7 +65,8 @@
| **收敛突破** | 同上(自动开仓类)。 | | **收敛突破** | 同上(自动开仓类)。 |
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 | | **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 |
| **关键支撑位** | 同上(仅提醒)。 | | **关键支撑位** | 同上(仅提醒)。 |
| **触价开仓** | **不挂交易所限价**;标记价触达计划入场价**下一轮询市价开仓**RR 门槛同关键位 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h**;全仓杠杆模式可用。 | | **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E **下一轮询市价开仓**RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** |
3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。 3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。
4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP** 4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**
@@ -16,7 +16,8 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k
| **关键阻力位** | **不选**`direction=watch` | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` | | **关键阻力位** | **不选**`direction=watch` | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` |
| **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) | | **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) |
| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) | | 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) |
| **触价开仓** | **必选** 多/空 | **程序盯价 → 触 E 后市价** | 见下文 **§** | | **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§** |
| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** |
**添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位)。 **添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位)。
@@ -118,25 +119,31 @@ Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `k
--- ---
## 四、触价开仓(程序触价,无交易所挂单) ## 四、回调 / 突破触价开仓(程序触价,无交易所挂单)
### 4.1 录入 ### 4.1 录入
- 类型选 **触价开仓**;方向必选多/空 - **回调触价开仓**:方向必选多/空;填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`
- 填写 **计划入场价 E**、**止损 SL**、**止盈 TP**(做多须 `SL < E < TP`)。 - **突破触价开仓**:同上;添加时当前价须在突破方向一侧(做多:价低于 E;做空:价高于 E)。
- 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5,与箱体/斐波相同)。 - 计划 RR 以 **E** 为基准,须 **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)。
- 可选移动保本、时间平仓;**全仓杠杆模式**下可用(页面隐藏箱体/收敛/斐波/假突破) - 可选移动保本、时间平仓;**全仓杠杆模式**下可用。
### 4.2 触发与结案 ### 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` - 成功 → `trigger_entry_filled`;触发后开仓失败 → `trigger_exchange_failed`
### 4.3 计仓与占位 ### 4.3 计仓与占位
- **以损定仓**:按 E、SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x、其它 5x。 - **以损定仓**:按 E、SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x、其它 5x。
- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条。 - **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条触价监控(含回调/突破)
共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors` 共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors`
+1 -1
View File
@@ -32,7 +32,7 @@ FULL_MARGIN_BUFFER_RATIO=0.98
- 关键位:箱体突破、收敛突破、斐波、假突破(添加时拒绝;已存在则启动时撤销)。 - 关键位:箱体突破、收敛突破、斐波、假突破(添加时拒绝;已存在则启动时撤销)。
- 趋势回调、顺势加仓(策略入口返回明确错误)。 - 趋势回调、顺势加仓(策略入口返回明确错误)。
**允许:** 关键位 **触价开仓**(程序盯价、触达计划入场后市价成交,无交易所挂单;全仓下仅允许一条待触发)。 **允许:** 关键位 **回调触价开仓** / **突破触价开仓**(程序盯价、触达/穿越计划入场后市价成交,无交易所挂单;全仓下仅允许一条待触发)。
## 用脚本更新四所 `.env` ## 用脚本更新四所 `.env`
+1 -1
View File
@@ -119,7 +119,7 @@
<script src="/static/form_submit_guard.js?v=2"></script> <script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script> <script src="/static/strategy_roll.js?v=5"></script>
<script src="/static/key_monitor_form.js?v=1"></script> <script src="/static/key_monitor_form.js?v=2"></script>
{% include 'embed_boot_scripts.html' %} {% include 'embed_boot_scripts.html' %}
<script src="/static/instance_embed.js?v=4"></script> <script src="/static/instance_embed.js?v=4"></script>
</body> </body>
+6 -6
View File
@@ -48,8 +48,8 @@ def stored_key_signal_type(monitor_type):
mt = (monitor_type or "").strip() mt = (monitor_type or "").strip()
if mt in FIB_KEY_MONITOR_TYPES: if mt in FIB_KEY_MONITOR_TYPES:
return mt return mt
if mt in ("假突破", "触价开仓"): if mt in ("假突破", "回调触价开仓", "突破触价开仓", "触价开仓"):
return mt return mt if mt != "触价开仓" else "回调触价开仓"
if mt in KEY_MONITOR_AUTO_TYPES: if mt in KEY_MONITOR_AUTO_TYPES:
return mt return mt
return None return None
@@ -61,6 +61,8 @@ KEY_ENTRY_REASON_BY_SIGNAL = {
"斐波回调0.618": "关键位斐波0.618", "斐波回调0.618": "关键位斐波0.618",
"斐波回调0.786": "关键位斐波0.786", "斐波回调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() kst = (key_signal_type or "").strip()
if kst in FIB_KEY_MONITOR_TYPES: if kst in FIB_KEY_MONITOR_TYPES:
return kst return kst
if kst == "假突破": if kst in ("假突破", "回调触价开仓", "突破触价开仓", "触价开仓"):
return kst return kst if kst != "触价开仓" else "回调触价开仓"
if kst == "触价开仓":
return kst
if box_auto_types and kst in box_auto_types: if box_auto_types and kst in box_auto_types:
return kst return kst
return None return None
+14
View File
@@ -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
+1 -1
View File
@@ -19,7 +19,7 @@
const autoTypes = new Set(["箱体突破", "收敛突破"]); const autoTypes = new Set(["箱体突破", "收敛突破"]);
const fibTypes = new Set(["斐波回调0.618", "斐波回调0.786"]); const fibTypes = new Set(["斐波回调0.618", "斐波回调0.786"]);
const fbTypes = new Set(["假突破"]); const fbTypes = new Set(["假突破"]);
const teTypes = new Set(["触价开仓"]); const teTypes = new Set(["回调触价开仓", "突破触价开仓", "触价开仓"]);
const showAuto = autoTypes.has(t); const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t); const showFb = fbTypes.has(t);
const showTe = teTypes.has(t); const showTe = teTypes.has(t);
+5 -3
View File
@@ -115,6 +115,7 @@
{%- elif r == 'manual' -%}手动删除 {%- elif r == 'manual' -%}手动删除
{%- elif r == 'fib_invalidate' -%}斐波失效 {%- elif r == 'fib_invalidate' -%}斐波失效
{%- elif r == 'trigger_tp_invalidate' -%}触价止盈失效 {%- elif r == 'trigger_tp_invalidate' -%}触价止盈失效
{%- elif r == 'trigger_sl_invalidate' -%}触价止损失效
{%- elif r == 'trigger_entry_expired' -%}触价过期 {%- elif r == 'trigger_entry_expired' -%}触价过期
{%- elif r == 'trigger_exchange_failed' -%}触价下单失败 {%- elif r == 'trigger_exchange_failed' -%}触价下单失败
{%- elif r == 'false_breakout_expired' -%}假突破过期 {%- elif r == 'false_breakout_expired' -%}假突破过期
@@ -149,7 +150,8 @@
<option value="斐波回调0.786">斐波回调0.786</option> <option value="斐波回调0.786">斐波回调0.786</option>
<option value="假突破">假突破(BTC/ETH</option> <option value="假突破">假突破(BTC/ETH</option>
{% endif %} {% endif %}
<option value="触价开仓">触价开仓</option> <option value="回调触价开仓">回调触价开仓</option>
<option value="突破触价开仓">突破触价开仓</option>
<option value="关键支撑阻力">关键支撑阻力</option> <option value="关键支撑阻力">关键支撑阻力</option>
</select> </select>
<select name="direction" id="key-direction" required> <select name="direction" id="key-direction" required>
@@ -216,7 +218,7 @@
<div class="pos-meta"> <div class="pos-meta">
<span class="pos-meta-item">上沿: {{ k.upper }}</span> <span class="pos-meta-item">上沿: {{ k.upper }}</span>
<span class="pos-meta-item">下沿: {{ k.lower }}</span> <span class="pos-meta-item">下沿: {{ k.lower }}</span>
{% if k.fib_entry_price and k.monitor_type == '触价开仓' %}<span class="pos-meta-item">E: {{ k.fib_entry_price }} / SL: {{ k.fib_stop_loss }} / TP: {{ k.fib_take_profit }}</span>{% elif k.fib_entry_price %}<span class="pos-meta-item">挂E: {{ k.fib_entry_price }}</span>{% endif %} {% if k.fib_entry_price and k.monitor_type in ['回调触价开仓','突破触价开仓','触价开仓'] %}<span class="pos-meta-item">E: {{ k.fib_entry_price }} / SL: {{ k.fib_stop_loss }} / TP: {{ k.fib_take_profit }}</span>{% elif k.fib_entry_price %}<span class="pos-meta-item">挂E: {{ k.fib_entry_price }}</span>{% endif %}
{% if k.monitor_type == '假突破' and k.fib_stop_loss %}<span class="pos-meta-item">SL: {{ k.fib_stop_loss }} / TP: {{ k.fib_take_profit }}</span>{% endif %} {% if k.monitor_type == '假突破' and k.fib_stop_loss %}<span class="pos-meta-item">SL: {{ k.fib_stop_loss }} / TP: {{ k.fib_take_profit }}</span>{% endif %}
<span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span> <span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span>
{% if k.monitor_type in ['箱体突破','收敛突破'] %} {% if k.monitor_type in ['箱体突破','收敛突破'] %}
@@ -321,4 +323,4 @@ document.querySelectorAll(".key-row-collapse").forEach((row)=>{
}); });
}); });
</script> </script>
<script src="/static/key_monitor_form.js?v=1"></script> <script src="/static/key_monitor_form.js?v=2"></script>
@@ -33,9 +33,16 @@
<td class="key-rule-cell">即挂限价<br>成交/过期→历史</td> <td class="key-rule-cell">即挂限价<br>成交/过期→历史</td>
</tr> </tr>
<tr> <tr>
<td class="key-rule-type">触价开仓</td> <td class="key-rule-type">回调触价开仓</td>
<td class="key-rule-cell">方向 + 入场 E / 止损 SL / 止盈 TP<br>可勾移动保本、时间平仓</td> <td class="key-rule-cell">方向 + 入场 E / 止损 SL / 止盈 TP<br>可勾移动保本、时间平仓</td>
<td class="key-rule-cell">RR &gt;{{ r.min_rr }};做多 SL&lt;E&lt;TP<br>标记价触 E 后下一轮询市价开<br>先触 TP 侧失效;有效 {{ r.trigger_entry_validity_hours }}h</td> <td class="key-rule-cell">RR &gt;{{ r.min_rr }};做多 SL&lt;E&lt;TP<br>标记价回调触 E(多≤E / 空≥E后下一轮询市价开<br>先触 TP 侧失效;有效 {{ r.trigger_entry_validity_hours }}h</td>
<td class="key-rule-cell">程序盯价,无交易所挂单<br>成交后挂所 TP/SL → 下单监控</td>
<td class="key-rule-cell">占当日开仓意图<br>全仓模式可用</td>
</tr>
<tr>
<td class="key-rule-type">突破触价开仓</td>
<td class="key-rule-cell">方向 + 突破价 E / 止损 SL / 止盈 TP<br>可勾移动保本、时间平仓</td>
<td class="key-rule-cell">RR &gt;{{ r.min_rr }};做多 SL&lt;E&lt;TP<br>标记价<strong>穿越</strong> E 立即市价开(多向上 / 空向下)<br>先触 TP 或 SL 侧失效;有效 {{ r.trigger_entry_validity_hours }}h</td>
<td class="key-rule-cell">程序盯价,无交易所挂单<br>成交后挂所 TP/SL → 下单监控</td> <td class="key-rule-cell">程序盯价,无交易所挂单<br>成交后挂所 TP/SL → 下单监控</td>
<td class="key-rule-cell">占当日开仓意图<br>全仓模式可用</td> <td class="key-rule-cell">占当日开仓意图<br>全仓模式可用</td>
</tr> </tr>
+50 -11
View File
@@ -1,11 +1,17 @@
"""触价开仓关键位监控单元测试。""" """触价开仓(回调/突破)关键位监控单元测试。"""
from trigger_entry_key_monitor_lib import ( from trigger_entry_key_monitor_lib import (
TRIGGER_ENTRY_MONITOR_TYPE, BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
LEGACY_TRIGGER_ENTRY_MONITOR_TYPE,
TRIGGER_ENTRY_MONITOR_TYPES,
TRIGGER_ENTRY_VALIDITY_HOURS, TRIGGER_ENTRY_VALIDITY_HOURS,
breakout_trigger_entry_crossed,
check_trigger_entry_intent_limit, check_trigger_entry_intent_limit,
is_breakout_trigger_entry_key_monitor_type,
is_trigger_entry_key_monitor_type, is_trigger_entry_key_monitor_type,
trigger_entry_invalidate_by_tp, trigger_entry_invalidate,
trigger_entry_reached, trigger_entry_reached,
trigger_should_fire,
validate_trigger_entry_geometry, validate_trigger_entry_geometry,
) )
@@ -14,7 +20,7 @@ class _FakeConn:
def execute(self, sql, params=()): def execute(self, sql, params=()):
class R: class R:
def fetchone(self_inner): def fetchone(self_inner):
return (params[1] == "2026-06-07" and 2,) # 2 pending return (2,)
return R() return R()
@@ -24,14 +30,43 @@ def test_trigger_entry_reached_long():
assert trigger_entry_reached("long", 2051.0, 2050.0) is False assert trigger_entry_reached("long", 2051.0, 2050.0) is False
def test_trigger_entry_invalidate_long(): def test_breakout_cross_long_up():
assert trigger_entry_invalidate_by_tp("long", 2100.0, 2100.0) is True assert breakout_trigger_entry_crossed("long", 99.0, 100.5, 100.0) is True
assert trigger_entry_invalidate_by_tp("long", 2099.0, 2100.0) is False assert breakout_trigger_entry_crossed("long", None, 101.0, 100.0) is True
assert breakout_trigger_entry_crossed("long", 100.0, 100.0, 100.0) is False
def test_validate_geometry_long(): def test_breakout_cross_short_down():
assert breakout_trigger_entry_crossed("short", 101.0, 99.5, 100.0) is True
assert breakout_trigger_entry_crossed("short", None, 99.0, 100.0) is True
def test_trigger_should_fire_modes():
assert trigger_should_fire(CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, "long", 2049.0, 2050.0) is True
assert trigger_should_fire(BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE, "long", 100.5, 100.0, 99.0) is True
def test_validate_geometry_callback_long():
assert validate_trigger_entry_geometry("long", 2050, 2000, 2100, 2090) is None assert validate_trigger_entry_geometry("long", 2050, 2000, 2100, 2090) is None
assert "止损" in (validate_trigger_entry_geometry("long", 2050, 2100, 2000) or "")
def test_validate_geometry_breakout_short_requires_mark_above_entry():
assert (
validate_trigger_entry_geometry(
"short", 551, 568, 540, 560, monitor_type=BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE
)
is None
)
err = validate_trigger_entry_geometry(
"short", 551, 568, 540, 550, monitor_type=BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE
)
assert err is not None
assert "高于入场价" in err
def test_invalidate_breakout_sl_side():
assert trigger_entry_invalidate(BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE, "long", 96, 97, 110) == "sl"
assert trigger_entry_invalidate(CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, "long", 96, 97, 110) is None
def test_intent_limit(): def test_intent_limit():
@@ -40,6 +75,10 @@ def test_intent_limit():
assert "意图" in msg assert "意图" in msg
def test_type_name(): def test_type_names():
assert is_trigger_entry_key_monitor_type(TRIGGER_ENTRY_MONITOR_TYPE) assert is_trigger_entry_key_monitor_type(CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE)
assert is_trigger_entry_key_monitor_type(BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE)
assert is_trigger_entry_key_monitor_type(LEGACY_TRIGGER_ENTRY_MONITOR_TYPE)
assert is_breakout_trigger_entry_key_monitor_type(BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE)
assert CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE in TRIGGER_ENTRY_MONITOR_TYPES
assert TRIGGER_ENTRY_VALIDITY_HOURS == 24 assert TRIGGER_ENTRY_VALIDITY_HOURS == 24
+145 -14
View File
@@ -1,7 +1,7 @@
"""触价开仓关键位监控:程序盯价、触达计划入场后市价成交(四所共用逻辑)。""" """回调/突破触价开仓关键位监控:程序盯价、触达计划入场后市价成交(四所共用逻辑)。"""
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime
from typing import Any, Callable, Optional from typing import Any, Callable, Optional
from false_breakout_key_monitor_lib import ( from false_breakout_key_monitor_lib import (
@@ -11,24 +11,101 @@ from false_breakout_key_monitor_lib import (
) )
from strategy_trend_lib import trend_dca_level_reached from strategy_trend_lib import trend_dca_level_reached
TRIGGER_ENTRY_MONITOR_TYPE = "触价开仓" # 回调触价(原「触价开仓」)
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE = "回调触价开仓"
LEGACY_TRIGGER_ENTRY_MONITOR_TYPE = "触价开仓"
# 突破触价:标记价穿越 E 后立即市价开仓
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE = "突破触价开仓"
TRIGGER_ENTRY_MONITOR_TYPES = frozenset(
{
CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE,
BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE,
LEGACY_TRIGGER_ENTRY_MONITOR_TYPE,
}
)
TRIGGER_ENTRY_VALIDITY_HOURS = 24 TRIGGER_ENTRY_VALIDITY_HOURS = 24
TRIGGER_ENTRY_CLOSE_FILLED = "trigger_entry_filled" TRIGGER_ENTRY_CLOSE_FILLED = "trigger_entry_filled"
TRIGGER_ENTRY_CLOSE_TP_INVALIDATE = "trigger_tp_invalidate" TRIGGER_ENTRY_CLOSE_TP_INVALIDATE = "trigger_tp_invalidate"
TRIGGER_ENTRY_CLOSE_SL_INVALIDATE = "trigger_sl_invalidate"
TRIGGER_ENTRY_CLOSE_EXPIRED = "trigger_entry_expired" TRIGGER_ENTRY_CLOSE_EXPIRED = "trigger_entry_expired"
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED = "trigger_exchange_failed" TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED = "trigger_exchange_failed"
KEY_ENTRY_REASON_TRIGGER = "关键位触价开仓" KEY_ENTRY_REASON_CALLBACK = "关键位回调触价开仓"
KEY_ENTRY_REASON_BREAKOUT = "关键位突破触价开仓"
KEY_ENTRY_REASON_TRIGGER_LEGACY = "关键位触价开仓"
def normalize_trigger_entry_monitor_type(monitor_type: Optional[str]) -> str:
mt = (monitor_type or "").strip()
if mt == LEGACY_TRIGGER_ENTRY_MONITOR_TYPE:
return CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE
return mt
def is_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool: def is_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool:
return (monitor_type or "").strip() == TRIGGER_ENTRY_MONITOR_TYPE return (monitor_type or "").strip() in TRIGGER_ENTRY_MONITOR_TYPES
def is_callback_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool:
mt = normalize_trigger_entry_monitor_type(monitor_type)
return mt == CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE
def is_breakout_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool:
return (monitor_type or "").strip() == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE
def key_entry_reason_for_monitor_type(monitor_type: Optional[str]) -> str:
if is_breakout_trigger_entry_key_monitor_type(monitor_type):
return KEY_ENTRY_REASON_BREAKOUT
if is_trigger_entry_key_monitor_type(monitor_type):
return KEY_ENTRY_REASON_CALLBACK
return KEY_ENTRY_REASON_TRIGGER_LEGACY
def trigger_entry_reached(direction: str, mark_price: float, entry: float) -> bool: def trigger_entry_reached(direction: str, mark_price: float, entry: float) -> bool:
"""回调触价:多=价跌至 E;空=价涨至 E。"""
return trend_dca_level_reached(direction, mark_price, entry) return trend_dca_level_reached(direction, mark_price, entry)
def breakout_trigger_entry_crossed(
direction: str,
prev_mark: Optional[float],
mark: float,
entry: float,
) -> bool:
"""突破触价:多=向上穿越 E;空=向下穿越 E。"""
try:
m = float(mark)
e = float(entry)
pm = float(prev_mark) if prev_mark is not None else None
except (TypeError, ValueError):
return False
direction = (direction or "long").strip().lower()
if direction == "long":
if pm is None:
return m > e
return pm <= e and m > e
if pm is None:
return m < e
return pm >= e and m < e
def trigger_should_fire(
monitor_type: Optional[str],
direction: str,
mark: float,
entry: float,
prev_mark: Optional[float] = None,
) -> bool:
if is_breakout_trigger_entry_key_monitor_type(monitor_type):
return breakout_trigger_entry_crossed(direction, prev_mark, mark, entry)
return trigger_entry_reached(direction, mark, entry)
def trigger_entry_invalidate_by_tp(direction: str, mark_price: float, take_profit: float) -> bool: def trigger_entry_invalidate_by_tp(direction: str, mark_price: float, take_profit: float) -> bool:
"""未开仓前标记价先触达止盈侧则失效。""" """未开仓前标记价先触达止盈侧则失效。"""
try: try:
@@ -42,12 +119,42 @@ def trigger_entry_invalidate_by_tp(direction: str, mark_price: float, take_profi
return m >= tp return m >= tp
def trigger_entry_invalidate_by_sl(direction: str, mark_price: float, stop_loss: float) -> bool:
"""突破触价:未到 E 先触达止损侧则失效。"""
try:
m = float(mark_price)
sl = float(stop_loss)
except (TypeError, ValueError):
return False
d = (direction or "long").strip().lower()
if d == "long":
return m <= sl
return m >= sl
def trigger_entry_invalidate(
monitor_type: Optional[str],
direction: str,
mark: float,
stop_loss: float,
take_profit: float,
) -> Optional[str]:
if trigger_entry_invalidate_by_tp(direction, mark, take_profit):
return "tp"
if is_breakout_trigger_entry_key_monitor_type(monitor_type):
if trigger_entry_invalidate_by_sl(direction, mark, stop_loss):
return "sl"
return None
def validate_trigger_entry_geometry( def validate_trigger_entry_geometry(
direction: str, direction: str,
entry: float, entry: float,
stop_loss: float, stop_loss: float,
take_profit: float, take_profit: float,
mark_at_add: Optional[float] = None, mark_at_add: Optional[float] = None,
*,
monitor_type: Optional[str] = None,
) -> Optional[str]: ) -> Optional[str]:
"""返回错误文案;合法则 None。""" """返回错误文案;合法则 None。"""
try: try:
@@ -59,16 +166,26 @@ def validate_trigger_entry_geometry(
if e <= 0 or sl <= 0 or tp <= 0: if e <= 0 or sl <= 0 or tp <= 0:
return "入场价、止损、止盈须大于 0" return "入场价、止损、止盈须大于 0"
d = (direction or "long").strip().lower() d = (direction or "long").strip().lower()
mt = normalize_trigger_entry_monitor_type(monitor_type)
label = "突破触价开仓" if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE else "回调触价开仓"
if d == "long": if d == "long":
if not (sl < e < tp): if not (sl < e < tp):
return "做多:须满足 止损 < 入场价 < 止盈" return "做多:须满足 止损 < 入场价 < 止盈"
if mark_at_add is not None and float(mark_at_add) >= tp: if mark_at_add is not None:
return "做多:当前价已不低于止盈,无法添加触价开仓" m = float(mark_at_add)
if m >= tp:
return f"做多:当前价已不低于止盈,无法添加{label}"
if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE and m >= e:
return "做多:当前价须低于入场价(等待向上突破)"
elif d == "short": elif d == "short":
if not (tp < e < sl): if not (tp < e < sl):
return "做空:须满足 止盈 < 入场价 < 止损" return "做空:须满足 止盈 < 入场价 < 止损"
if mark_at_add is not None and float(mark_at_add) <= tp: if mark_at_add is not None:
return "做空:当前价已不高于止盈,无法添加触价开仓" m = float(mark_at_add)
if m <= tp:
return f"做空:当前价已不高于止盈,无法添加{label}"
if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE and m <= e:
return "做空:当前价须高于入场价(等待向下跌破)"
else: else:
return "方向须为 long 或 short" return "方向须为 long 或 short"
return None return None
@@ -110,9 +227,10 @@ def count_pending_trigger_entries(conn: Any, trading_day: str) -> int:
td = (trading_day or "").strip() td = (trading_day or "").strip()
if not td: if not td:
return 0 return 0
placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES))
row = conn.execute( row = conn.execute(
"SELECT COUNT(*) FROM key_monitors WHERE monitor_type=? AND session_date=?", f"SELECT COUNT(*) FROM key_monitors WHERE monitor_type IN ({placeholders}) AND session_date=?",
(TRIGGER_ENTRY_MONITOR_TYPE, td), (*TRIGGER_ENTRY_MONITOR_TYPES, td),
).fetchone() ).fetchone()
return int(row[0] if row else 0) return int(row[0] if row else 0)
@@ -138,28 +256,41 @@ def check_trigger_entry_intent_limit(
def trigger_entry_gate_preview( def trigger_entry_gate_preview(
*, *,
monitor_type: Optional[str] = None,
entry_display: str, entry_display: str,
take_profit_display: str, take_profit_display: str,
created_at: Any = None, created_at: Any = None,
now: Optional[datetime] = None, now: Optional[datetime] = None,
expired: bool = False, expired: bool = False,
tp_invalidated: bool = False, tp_invalidated: bool = False,
sl_invalidated: bool = False,
hours: int = TRIGGER_ENTRY_VALIDITY_HOURS, hours: int = TRIGGER_ENTRY_VALIDITY_HOURS,
) -> dict[str, Any]: ) -> dict[str, Any]:
now_dt = now or datetime.now() now_dt = now or datetime.now()
is_exp = expired or is_trigger_entry_expired(created_at, now_dt, hours=hours) is_exp = expired or is_trigger_entry_expired(created_at, now_dt, hours=hours)
exp_txt = trigger_entry_expires_at_text(created_at, hours=hours) exp_txt = trigger_entry_expires_at_text(created_at, hours=hours)
mt = normalize_trigger_entry_monitor_type(monitor_type)
if tp_invalidated: if tp_invalidated:
status = "止盈侧失效" status = "止盈侧失效"
elif sl_invalidated:
status = "止损侧失效"
elif is_exp: elif is_exp:
status = "已过期" status = "已过期"
elif mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE:
status = "突破待触发"
else: else:
status = "触价待触发" status = "回调待触发"
mode = "突破" if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE else "回调"
metrics_parts: list[str] = [f"TP:{take_profit_display}"] metrics_parts: list[str] = [f"TP:{take_profit_display}"]
if exp_txt != "": if exp_txt != "":
metrics_parts.append(f"截至:{exp_txt}") metrics_parts.append(f"截至:{exp_txt}")
return { return {
"summary": f"触价 E={entry_display} {status}", "summary": f"{mode}触价 E={entry_display} {status}",
"metrics": " ".join(metrics_parts), "metrics": " ".join(metrics_parts),
"gate_ok": not is_exp and not tp_invalidated, "gate_ok": not is_exp and not tp_invalidated and not sl_invalidated,
} }
# 兼容旧 import
TRIGGER_ENTRY_MONITOR_TYPE = CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE
KEY_ENTRY_REASON_TRIGGER = KEY_ENTRY_REASON_CALLBACK