feat: add false breakout key monitor for BTC/ETH on three exchanges

Place limit orders outside key levels with fixed SL and 1.5 RR, 24h expiry, separate stats, and full-margin mode guard.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-09 10:13:53 +08:00
parent 2786acf884
commit 7cb55f6557
10 changed files with 903 additions and 88 deletions
+195 -22
View File
@@ -51,6 +51,18 @@ from fib_key_monitor_lib import (
key_signal_type_for_trade_record,
stored_key_signal_type,
)
from false_breakout_key_monitor_lib import (
FALSE_BREAKOUT_MONITOR_TYPE,
FALSE_BREAKOUT_VALIDITY_HOURS,
calc_false_breakout_plan,
expires_at_text,
is_false_breakout_expired,
is_false_breakout_key_monitor_type,
is_limit_key_monitor_type,
key_price_from_row,
normalize_false_breakout_symbol,
storage_bounds_from_key_price,
)
from strategy_trade_labels import (
STRATEGY_ENTRY_REASON_OPTIONS,
apply_order_monitor_source_labels,
@@ -992,6 +1004,7 @@ ENTRY_REASON_OPTIONS = (
"关键位收敛突破",
"关键位斐波0.618",
"关键位斐波0.786",
"关键位假突破",
) + STRATEGY_ENTRY_REASON_OPTIONS
STATS_SEGMENT_DEFS = (
@@ -1001,6 +1014,7 @@ STATS_SEGMENT_DEFS = (
("key_conv", "关键位收敛结构", {"segment": "key_conv"}),
("key_fib618", "关键位斐波0.618", {"segment": "key_fib618"}),
("key_fib786", "关键位斐波0.786", {"segment": "key_fib786"}),
("key_false_breakout", "关键位假突破", {"segment": "key_false_breakout"}),
)
# 复盘表单「其他」选项的 value(非入库值;自定义文本走 entry_reason_custom
ENTRY_REASON_OTHER = "__OTHER__"
@@ -1587,6 +1601,8 @@ def _pnl_row_matches_segment(row, segment_key):
return kst == "斐波回调0.618"
if segment_key == "key_fib786":
return kst == "斐波回调0.786"
if segment_key == "key_false_breakout":
return kst == FALSE_BREAKOUT_MONITOR_TYPE
return False
@@ -1603,6 +1619,7 @@ def _count_opens_for_segment(conn, start_td, end_td, segment_key):
"key_conv": "收敛突破",
"key_fib618": "斐波回调0.618",
"key_fib786": "斐波回调0.786",
"key_false_breakout": FALSE_BREAKOUT_MONITOR_TYPE,
}
kst = kst_map.get(segment_key)
if kst:
@@ -2610,7 +2627,7 @@ def order_row_key_signal_type(row):
if "key_signal_type" not in keys:
return None
kst = (row["key_signal_type"] or "").strip()
if kst in KEY_MONITOR_AUTO_TYPES or is_fib_key_monitor_type(kst):
if kst in KEY_MONITOR_AUTO_TYPES or is_fib_key_monitor_type(kst) or is_false_breakout_key_monitor_type(kst):
return kst
return None
@@ -4787,6 +4804,19 @@ def _fib_plan_for_row(row):
return calc_fib_plan(row["direction"], row["upper"], row["lower"], ratio)
def _limit_key_plan_for_row(row):
typ = (row["monitor_type"] or "").strip()
if is_fib_key_monitor_type(typ):
return _fib_plan_for_row(row)
if is_false_breakout_key_monitor_type(typ):
direction = (row["direction"] or "long").lower()
key_px = key_price_from_row(direction, row["upper"], row["lower"])
if key_px is None:
return None
return calc_false_breakout_plan(direction, key_px)
return None
def _cancel_fib_monitor_limit(row):
ex_sym = normalize_exchange_symbol(row["symbol"])
oid = _sqlite_row_val(row, "fib_limit_order_id")
@@ -4873,10 +4903,11 @@ def _finalize_fib_key_fill(conn, row):
symbol = row["symbol"]
direction = (row["direction"] or "long").lower()
typ = (row["monitor_type"] or "").strip()
kind = "假突破" if is_false_breakout_key_monitor_type(typ) else "斐波"
ex_sym = normalize_exchange_symbol(symbol)
plan = _fib_plan_for_row(row)
plan = _limit_key_plan_for_row(row)
if not plan:
_finalize_key_monitor_one_shot(conn, row, "斐波计划无效", "fib_plan_invalid")
_finalize_key_monitor_one_shot(conn, row, f"{kind}计划无效", "fib_plan_invalid")
return
entry_plan, sl_plan, tp_plan = plan
sl = float(_sqlite_row_val(row, "fib_stop_loss", sl_plan) or sl_plan)
@@ -4907,7 +4938,7 @@ def _finalize_fib_key_fill(conn, row):
amount = float(live_amt or 0)
if amount <= 0:
send_wechat_msg(
f"# ❌ {symbol} 斐波成交后处理失败\n"
f"# ❌ {symbol} {kind}成交后处理失败\n"
f"**账户:{_wechat_account_label()}**\n"
f"- 无法取得持仓/下单数量,未挂 TP/SL\n"
)
@@ -4915,7 +4946,7 @@ def _finalize_fib_key_fill(conn, row):
ok, reason = precheck_risk(conn, symbol, direction)
if not ok:
send_wechat_msg(
f"# ❌ {symbol} 斐波成交后风控拒绝\n"
f"# ❌ {symbol} {kind}成交后风控拒绝\n"
f"**账户:{_wechat_account_label()}**\n"
f"- 类型:{typ}\n"
f"- 原因:{reason}\n"
@@ -4928,7 +4959,7 @@ def _finalize_fib_key_fill(conn, row):
tpsl_attached = True
except Exception as e:
send_wechat_msg(
f"# ❌ {symbol} 斐波成交后挂 TP/SL 失败\n"
f"# ❌ {symbol} {kind}成交后挂 TP/SL 失败\n"
f"**账户:{_wechat_account_label()}**\n"
f"- 错误:{friendly_exchange_error(e)}\n"
f"- 请手动补挂止盈止损\n"
@@ -4946,8 +4977,9 @@ def _finalize_fib_key_fill(conn, row):
notional_value, position_ratio, base_amount, oid, tpsl_attached,
)
rr_txt = format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
close_reason = "false_breakout_filled" if is_false_breakout_key_monitor_type(typ) else "fib_filled"
succ = (
f"# ✅ {symbol} 斐波限价成交\n"
f"# ✅ {symbol} {kind}限价成交\n"
f"**账户:{_wechat_account_label()}**\n"
f"- 来源:{ORDER_MONITOR_TYPE_KEY_AUTO}(限价 @ E\n"
f"- 类型:{typ}{_wechat_direction_text(direction)}\n"
@@ -4958,7 +4990,7 @@ def _finalize_fib_key_fill(conn, row):
f"- {'已挂交易所 TP/SL' if tpsl_attached else 'TP/SL 未挂上'}\n"
)
send_wechat_msg(succ)
_finalize_key_monitor_one_shot(conn, row, succ, "fib_filled")
_finalize_key_monitor_one_shot(conn, row, succ, close_reason)
def check_fib_key_monitors():
@@ -4966,13 +4998,28 @@ def check_fib_key_monitors():
rows = conn.execute("SELECT * FROM key_monitors").fetchall()
for r in rows:
typ = (r["monitor_type"] or "").strip()
if not is_fib_key_monitor_type(typ):
if not is_limit_key_monitor_type(typ):
continue
symbol = r["symbol"]
direction = (r["direction"] or "long").lower()
ex_sym = normalize_exchange_symbol(symbol)
up, low = float(r["upper"]), float(r["lower"])
oid = _sqlite_row_val(r, "fib_limit_order_id")
if is_false_breakout_key_monitor_type(typ):
now_dt = app_now()
if is_false_breakout_expired(r["created_at"], now_dt):
_cancel_fib_monitor_limit(r)
exp_txt = expires_at_text(r["created_at"])
msg = (
f"# ⚠️ {symbol} 假突破监控已过期\n"
f"**账户:{_wechat_account_label()}**\n"
f"- 类型:{typ}{_wechat_direction_text(direction)}\n"
f"- 有效期 {FALSE_BREAKOUT_VALIDITY_HOURS}h(应于 {exp_txt} 前成交)\n"
f"- 已撤销限价单\n"
)
send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, "false_breakout_expired")
continue
mark = get_symbol_mark_price(symbol)
if mark is None:
continue
@@ -4980,7 +5027,7 @@ def check_fib_key_monitors():
if status == "filled" or (status != "open" and _fib_has_live_position(ex_sym, direction)):
_finalize_fib_key_fill(conn, r)
continue
if status == "open":
if is_fib_key_monitor_type(typ) and status == "open":
if fib_invalidate_by_mark(direction, mark, up, low):
_cancel_fib_monitor_limit(r)
msg = (
@@ -4992,7 +5039,7 @@ def check_fib_key_monitors():
send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, "fib_invalidate")
continue
if status in ("canceled", "missing", "unknown") and fib_invalidate_by_mark(direction, mark, up, low):
if is_fib_key_monitor_type(typ) and status in ("canceled", "missing", "unknown") and fib_invalidate_by_mark(direction, mark, up, low):
msg = (
f"# ⚠️ {symbol} 斐波监控失效(限价已不在挂单)\n"
f"**账户:{_wechat_account_label()}**\n"
@@ -5079,6 +5126,86 @@ def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px, br
return True, None
def _false_breakout_exists_for_symbol(conn, symbol):
row = conn.execute(
"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type=?",
(symbol, FALSE_BREAKOUT_MONITOR_TYPE),
).fetchone()
return row is not None
def _add_false_breakout_key_monitor(
conn, symbol, direction_sel, upper_px, lower_px, key_px, breakeven_enabled=0,
):
if _false_breakout_exists_for_symbol(conn, symbol):
return False, f"{symbol} 已有假突破监控(同币仅允许一条)"
plan = calc_false_breakout_plan(direction_sel, key_px)
if not plan:
return False, "假突破价位无效,请核对方向与关键价位"
entry, sl, tp = plan
ex_sym = normalize_exchange_symbol(symbol)
entry = round_price_to_exchange(ex_sym, entry)
sl = round_price_to_exchange(ex_sym, sl)
tp = round_price_to_exchange(ex_sym, tp)
if entry is None or sl is None or tp is None:
return False, "假突破价位经交易所精度舍入后无效"
entry, sl, tp = float(entry), float(sl), float(tp)
ok, reason = precheck_risk(conn, symbol, direction_sel)
if not ok:
return False, reason
ok_live, reason_live = ensure_exchange_live_ready()
if not ok_live:
return False, reason_live
now = app_now()
trading_day = get_trading_day(now)
session_row = ensure_session(conn, trading_day)
_, trading_capital_live = get_exchange_capitals(force=True)
live_capital = float(trading_capital_live) if trading_capital_live is not None else float(session_row["current_capital"])
capital_base = resolve_capital_base_for_key_open(conn, trading_day, live_capital)
default_leverage = get_synced_leverage(ex_sym, direction_sel) or infer_leverage(symbol)
leverage = int(default_leverage) if default_leverage else 5
if leverage <= 0:
leverage = 5
available_usdt = get_available_trading_usdt()
risk_fraction = calc_risk_fraction(direction_sel, entry, sl)
if risk_fraction is None:
return False, "止损方向不合法(相对挂单价);请核对方向与关键价位"
risk_percent = max(0.01, float(RISK_PERCENT))
risk_amount = round(capital_base * risk_percent / 100.0, 4)
notional_value = round(risk_amount / risk_fraction, 4)
margin_capital = round(notional_value / leverage, 4)
if capital_base and margin_capital > capital_base:
return False, "以损定仓后保证金超过当前交易资金"
if available_usdt is not None:
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
if margin_capital > max_margin:
return (
False,
f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {round(max_margin, 2)}U",
)
try:
amount, _ = prepare_order_amount(ex_sym, margin_capital, leverage, entry)
order_resp = place_fib_limit_order(ex_sym, direction_sel, amount, leverage, entry)
oid = str(order_resp.get("id") or "")
if not oid:
return False, "交易所未返回限价单 ID"
except Exception as e:
return False, friendly_exchange_error(e, available_usdt=available_usdt)
be_flag = 1 if int(breakeven_enabled or 0) != 0 else 0
conn.execute(
"INSERT INTO key_monitors "
"(symbol, monitor_type, direction, upper, lower, "
"fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
"fib_order_amount, fib_margin_capital, fib_leverage, breakeven_enabled) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol, FALSE_BREAKOUT_MONITOR_TYPE, direction_sel, upper_px, lower_px,
oid, entry, sl, tp, float(amount), margin_capital, leverage, be_flag,
),
)
return True, None
# 关键位监控(箱体/收敛可自动开仓;阻力/支撑为双向 5m 收盘突破 + 三次提醒)
def check_key_monitors():
conn = get_db()
@@ -5086,7 +5213,7 @@ def check_key_monitors():
for r in rows:
sym, typ_raw, up, low = r["symbol"], r["monitor_type"], r["upper"], r["lower"]
typ = (typ_raw or "").strip()
if is_fib_key_monitor_type(typ):
if is_limit_key_monitor_type(typ):
continue
if typ in KEY_MONITOR_RS_TYPES:
try:
@@ -5969,6 +6096,7 @@ def render_main_page(page="trade"):
f"【箱体/收敛】{KLINE_TIMEFRAME} 两根闭合K|突破越过关键位 > {KEY_BREAKOUT_AMP_MIN_PCT}%"
f"确认K收于箱外|量能>前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}"
f"RR>{KEY_AUTO_MIN_PLANNED_RR}|日成交前{KEY_DAILY_VOLUME_RANK_MAX}"
f"【假突破·BTC/ETH】做空填高点/做多填低点,外侧 0.1% 挂限价,止损 0.5%,RR 1.5,有效期 {FALSE_BREAKOUT_VALIDITY_HOURS}h"
f"【阻力/支撑】填上/下沿,5m 收盘突破任一侧即提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不选方向、不自动开仓"
)
strategy_extra = {}
@@ -6715,6 +6843,7 @@ def add_key():
tuple(KEY_MONITOR_AUTO_TYPES)
+ tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
+ tuple(FIB_KEY_MONITOR_TYPES)
+ (FALSE_BREAKOUT_MONITOR_TYPE,)
)
if mt not in allowed_types:
flash("监控类型无效")
@@ -6725,13 +6854,16 @@ def add_key():
"请改用阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。"
)
return redirect("/key_monitor")
rank, total = _daily_volume_rank(symbol)
if rank is None:
flash("日成交量排名读取失败,请稍后重试")
return redirect("/key_monitor")
if rank > KEY_DAILY_VOLUME_RANK_MAX:
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前{KEY_DAILY_VOLUME_RANK_MAX},已拒绝添加关键位")
return redirect("/key_monitor")
skip_volume_rank = is_false_breakout_key_monitor_type(mt)
rank, total = None, None
if not skip_volume_rank:
rank, total = _daily_volume_rank(symbol)
if rank is None:
flash("日成交量排名读取失败,请稍后重试")
return redirect("/key_monitor")
if rank > KEY_DAILY_VOLUME_RANK_MAX:
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前{KEY_DAILY_VOLUME_RANK_MAX},已拒绝添加关键位")
return redirect("/key_monitor")
conn = get_db()
if mt in KEY_MONITOR_AUTO_TYPES:
occupied = get_active_position_count(conn)
@@ -6747,6 +6879,48 @@ def add_key():
ensure_markets_loaded()
except Exception:
pass
be_flag = parse_breakeven_enabled_form(d.get("breakeven_enabled"))
if is_false_breakout_key_monitor_type(mt):
fb_sym = normalize_false_breakout_symbol(symbol)
if not fb_sym:
conn.close()
flash("假突破仅支持 BTC / ETH")
return redirect("/key_monitor")
symbol = fb_sym
if direction_sel not in ("long", "short"):
conn.close()
flash("假突破请选择做多或做空")
return redirect("/key_monitor")
try:
key_px = float(d.get("key_price") or 0)
except (TypeError, ValueError):
key_px = 0
if key_px <= 0:
conn.close()
flash("请填写关键价位(做空填高点,做多填低点)")
return redirect("/key_monitor")
ex_sym_key = normalize_exchange_symbol(symbol)
key_adj = round_price_to_exchange(ex_sym_key, key_px)
key_px = float(key_adj) if key_adj is not None else float(key_px)
try:
upper_px, lower_px = storage_bounds_from_key_price(direction_sel, key_px)
except ValueError as e:
conn.close()
flash(str(e))
return redirect("/key_monitor")
ok_fb, err_fb = _add_false_breakout_key_monitor(
conn, symbol, direction_sel, upper_px, lower_px, key_px, breakeven_enabled=be_flag,
)
conn.commit()
conn.close()
if not ok_fb:
flash(err_fb or "假突破监控添加失败")
return redirect("/key_monitor")
flash(
f"假突破监控已添加,限价单已挂出({symbol}"
f"|有效期 {FALSE_BREAKOUT_VALIDITY_HOURS}h|移动保本:{'' if be_flag else ''}"
)
return redirect("/key_monitor")
uh = round_price_to_exchange(ex_sym_key, float(d["upper"]))
lw = round_price_to_exchange(ex_sym_key, float(d["lower"]))
upper_px = float(uh) if uh is not None else float(d["upper"])
@@ -6755,7 +6929,6 @@ def add_key():
conn.close()
flash("上沿必须大于下沿")
return redirect("/key_monitor")
be_flag = parse_breakeven_enabled_form(d.get("breakeven_enabled"))
if is_fib_key_monitor_type(mt):
ok_fib, err_fib = _add_fib_key_monitor(
conn, symbol, direction_sel, mt, upper_px, lower_px, breakeven_enabled=be_flag,
@@ -7187,7 +7360,7 @@ def delete_key_monitor(kid):
if not row:
conn.close()
return jsonify({"ok": False, "error": "not_found"})
if is_fib_key_monitor_type(row["monitor_type"]):
if is_limit_key_monitor_type(row["monitor_type"]):
_cancel_fib_monitor_limit(row)
insert_key_monitor_history(conn, row, int(row["notification_count"] or 0), None, "manual")
cur = conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,))
@@ -7212,7 +7385,7 @@ def del_key(id):
conn = get_db()
row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (id,)).fetchone()
if row:
if is_fib_key_monitor_type(row["monitor_type"]):
if is_limit_key_monitor_type(row["monitor_type"]):
_cancel_fib_monitor_limit(row)
insert_key_monitor_history(conn, row, int(row["notification_count"] or 0), None, "manual")
conn.execute("DELETE FROM key_monitors WHERE id=?", (id,))