feat: 关键位回调/突破触价开仓拆分与穿越触发
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+93
-25
@@ -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 "")
|
||||
)
|
||||
|
||||
@@ -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**。
|
||||
|
||||
@@ -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`。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user