关键位箱体突破调整

This commit is contained in:
dekun
2026-05-23 18:06:06 +08:00
parent a4c13fd8cd
commit 1b0bd41e3b
13 changed files with 1221 additions and 467 deletions
+228 -41
View File
@@ -54,6 +54,19 @@ from key_sl_tp_lib import (
sl_tp_mode_label,
sl_tp_plan_summary_text,
)
from key_monitor_lib import (
KEY_DIRECTION_WATCH,
KEY_MONITOR_ALERT_ONLY_TYPES,
KEY_MONITOR_AUTO_TYPES,
KEY_MONITOR_RS_TYPES,
auto_amp_ok,
auto_confirm_ok,
detect_rs_box_break,
format_auto_amp_line,
format_auto_confirm_line,
notify_interval_elapsed,
rs_break_from_direction,
)
from hub_auth import request_allowed as hub_request_allowed
from history_window_lib import (
PRESET_CUSTOM,
@@ -182,8 +195,7 @@ BREAKEVEN_OFFSET_PCT = float(os.getenv("BREAKEVEN_OFFSET_PCT", "0.02"))
BREAKEVEN_STEP_R = float(os.getenv("BREAKEVEN_STEP_R", "1.0"))
ORDER_MONITOR_TYPE_MANUAL = "下单监控"
ORDER_MONITOR_TYPE_KEY_AUTO = "关键位监控"
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
# KEY_MONITOR_AUTO_TYPES / KEY_MONITOR_ALERT_ONLY_TYPES:见 key_monitor_lib
KEY_AUTO_MIN_PLANNED_RR = float(os.getenv("KEY_AUTO_MIN_PLANNED_RR", "1.5"))
KEY_STOP_OUTSIDE_BREAKOUT_PCT = float(os.getenv("KEY_STOP_OUTSIDE_BREAKOUT_PCT", "0.5"))
KEY_TREND_STOP_OUTSIDE_PCT = float(os.getenv("KEY_TREND_STOP_OUTSIDE_PCT", "1"))
@@ -3230,31 +3242,34 @@ def _key_hard_checks(symbol, direction, upper, lower, monitor_type):
out["reason"] = "5m K线数量不足"
return out
closed = bars[:-1] if len(bars) >= 3 else bars
if len(closed) < 23:
out["reason"] = "闭合K线不足"
min_closed = KEY_VOLUME_MA_BARS + 3
if len(closed) < min_closed:
out["reason"] = f"{KLINE_TIMEFRAME} 闭合K线不足"
return out
breakout = closed[-2]
confirm = closed[-1]
prev20 = closed[-22:-2]
avg20 = sum(float(x[5]) for x in prev20) / max(len(prev20), 1)
try:
breakout = closed[KEY_CONFIRM_BREAKOUT_BAR]
confirm = closed[KEY_CONFIRM_BAR]
except IndexError:
out["reason"] = "确认K索引超出范围,请检查 KEY_CONFIRM_* 配置"
return out
prev_vol = closed[KEY_CONFIRM_BREAKOUT_BAR - KEY_VOLUME_MA_BARS : KEY_CONFIRM_BREAKOUT_BAR]
avg20 = sum(float(x[5]) for x in prev_vol) / max(len(prev_vol), 1)
vol_break = float(breakout[5])
vol_ok = vol_break > avg20 * 1.3 if avg20 > 0 else False
open_b = float(breakout[1])
vol_ok = vol_break > avg20 * KEY_VOLUME_RATIO_MIN if avg20 > 0 else False
close_b = float(breakout[4])
high_b = float(breakout[2])
low_b = float(breakout[3])
amp_pct = abs(close_b - open_b) / open_b * 100 if open_b > 0 else 0
amp_ok = (amp_pct > 0.03) and (amp_pct < 0.5)
cfm_close = float(confirm[4])
# 区间极值点严格以前端录入 upper/lower 为准:做多看上沿,做空看下沿
edge = float(upper) if direction == "long" else float(lower)
breakout_ok = (close_b > float(upper)) if direction == "long" else (close_b < float(lower))
confirm_ok_raw = (cfm_close > edge) if direction == "long" else (cfm_close < edge)
# 口径收紧:未发生有效突破时,不标记幅度/二确通过,避免出现“还没到位却显示Y”
amp_ok, amp_pct = auto_amp_ok(
direction, close_b, float(upper), float(lower), KEY_BREAKOUT_AMP_MIN_PCT
)
amp_ok = amp_ok and breakout_ok
confirm_ok_raw = auto_confirm_ok(direction, cfm_close, float(upper), float(lower))
confirm_ok = confirm_ok_raw and breakout_ok
rank, total = _daily_volume_rank(symbol)
rank_ok = (rank is not None) and (rank <= 30)
rank_ok = (rank is not None) and (rank <= KEY_DAILY_VOLUME_RANK_MAX)
swing4h_pct = 0.0
try:
seg48 = closed[-48:] if len(closed) >= 48 else closed
@@ -3389,6 +3404,130 @@ def _finalize_key_monitor_one_shot(conn, row, last_msg, close_reason):
conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],))
def _fetch_last_closed_bar(symbol):
ex_sym = normalize_okx_symbol(symbol)
bars = exchange.fetch_ohlcv(ex_sym, timeframe=KLINE_TIMEFRAME, limit=5) or []
if len(bars) < 2:
return None
closed = bars[:-1]
return closed[-1] if closed else None
def build_wechat_rs_level_message(
symbol,
monitor_type,
trigger_time,
upper,
lower,
trigger_close,
break_info,
notify_index,
notify_max,
):
lines = [
f"# 📌 {symbol} 关键位突破提醒({notify_index}/{notify_max}",
f"**账户:{_wechat_account_label()}**",
"",
"---",
"",
"### 突破判定(5m 收盘)",
f"- 类型:**{monitor_type}**",
f"- 触发时间:`{trigger_time}`",
f"- 上沿:`{upper}`|下沿:`{lower}`",
f"- 触发收盘:`{format_price_for_symbol(symbol, trigger_close)}`",
f"- **{break_info['break_label']}**(程序推断:**{_wechat_direction_text(break_info['direction'])}**",
f"- 突破价位:`{format_price_for_symbol(symbol, break_info['edge_price'])}`",
"",
"### 说明",
"- 本条为**人工盯盘**用途:录入时**不选多空**,由上/下沿突破方向自动判定。",
f"- 共推送 **{notify_max}** 次(间隔约 {KEY_ALERT_INTERVAL_MINUTES} 分钟),推送完毕后本条监控结案。",
"- OKX 本实例为提醒模式,不自动开仓。",
]
return "\n".join(lines)
def _key_rs_gate_preview(symbol, upper, lower):
bar = _fetch_last_closed_bar(symbol)
if not bar:
return {"summary": "5m数据不足", "metrics": ""}
close = float(bar[4])
br = detect_rs_box_break(close, upper, lower)
if br:
return {
"summary": f"已越线:{br['break_label']}",
"metrics": f"收盘:{format_price_for_symbol(symbol, close)}",
}
return {
"summary": "待突破",
"metrics": f"收盘:{format_price_for_symbol(symbol, close)}",
}
def _process_key_rs_level_alert(conn, row):
sym = row["symbol"]
typ = (row["monitor_type"] or "").strip()
up, low = float(row["upper"]), float(row["lower"])
if up <= low:
return
bar = _fetch_last_closed_bar(sym)
if not bar:
return
close = float(bar[4])
ts = bar[0]
count = int(row["notification_count"] or 0)
max_n = max(1, int(row["max_notify"] or KEY_ALERT_MAX_TIMES))
interval = max(1, int(row["notify_interval_min"] or KEY_ALERT_INTERVAL_MINUTES))
now_dt = app_now()
if count == 0:
br = detect_rs_box_break(close, up, low)
if not br:
return
else:
if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt):
return
br = rs_break_from_direction(row["direction"], up, low)
if not br:
return
trigger_time = ms_to_app_local_str(int(ts)) if ts else app_now_str()
notify_index = count + 1
msg = build_wechat_rs_level_message(
symbol=sym,
monitor_type=typ,
trigger_time=trigger_time,
upper=up,
lower=low,
trigger_close=close,
break_info=br,
notify_index=notify_index,
notify_max=max_n,
)
send_wechat_msg(msg)
conn.execute(
"UPDATE key_monitors SET direction=?, notification_count=?, last_notified_at=?, last_alert_message=? WHERE id=?",
(br["direction"], notify_index, app_now_str(), msg, row["id"]),
)
if notify_index >= max_n:
hist_row = conn.execute("SELECT * FROM key_monitors WHERE id=?", (row["id"],)).fetchone()
if hist_row:
insert_key_monitor_history(conn, hist_row, notify_index, msg, "key_level_alert_done")
conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],))
def _key_hard_lines_from_checks(checks):
direction = (checks.get("direction") or "long").lower()
return [
f"量能:{'通过' if checks['vol_ok'] else '不通过'}(突破K量 {round(checks['vol_break'], 4)} / 前20均量 {round(checks['avg20'], 4)},阈值1.3x",
f"突破价位:{'通过' if checks['breakout_ok'] else '不通过'}(突破K收盘 {round(float(checks['breakout_close']), 8)},关键位 {checks['edge_price']}",
format_auto_amp_line(checks["amp_ok"], checks["amp_pct"], KEY_BREAKOUT_AMP_MIN_PCT),
format_auto_confirm_line(
checks["confirm_ok"], checks["confirm_close"], checks["edge_price"], direction
),
f"日成交量排名:{'通过' if checks['rank_ok'] else '不通过'}{checks['rank']}/{checks['rank_total']},要求前{KEY_DAILY_VOLUME_RANK_MAX})",
]
def get_symbol_mark_price(symbol):
"""斐波失效判定用标记价。"""
ex_sym = normalize_okx_symbol(symbol)
@@ -3831,7 +3970,7 @@ def breakout_too_far(p, edge_price, limit_pct):
return False
# 关键位监控
# 关键位监控(箱体/收敛:硬门控后提醒;阻力/支撑:5m 双向突破 + 三次提醒)
def check_key_monitors():
conn = get_db()
rows = conn.execute("SELECT * FROM key_monitors").fetchall()
@@ -3840,7 +3979,17 @@ def check_key_monitors():
typ = (typ_raw or "").strip()
if is_fib_key_monitor_type(typ):
continue
if typ in KEY_MONITOR_RS_TYPES:
try:
_process_key_rs_level_alert(conn, r)
except Exception:
pass
continue
if typ not in KEY_MONITOR_AUTO_TYPES:
continue
direction = (r["direction"] or "long").lower()
if direction == KEY_DIRECTION_WATCH:
continue
now_dt = app_now()
if not can_notify_key_monitor(r, now_dt):
continue
@@ -3859,13 +4008,7 @@ def check_key_monitors():
sl_tp_mode = sl_tp_mode_from_row(r, "standard")
be_on = breakeven_enabled_from_row(r, 0)
plan_tuple, _mode = _key_plan_sl_tp_for_row(r, direction, up, low, checks)
hard_lines = [
f"量能:{'通过' if checks['vol_ok'] else '不通过'}(突破K量 {round(checks['vol_break'], 4)} / 前20均量 {round(checks['avg20'], 4)},阈值1.3x",
f"突破价位:{'通过' if checks['breakout_ok'] else '不通过'}(突破K收盘 {round(float(checks['breakout_close']), 8)},关键位 {checks['edge_price']}",
f"突破K幅度:{'通过' if checks['amp_ok'] else '不通过'}{round(checks['amp_pct'], 4)}%,要求0.03%~0.5%",
f"第二根确认:{'通过' if checks['confirm_ok'] else '不通过'}(确认收盘 {checks['confirm_close']},关键位 {checks['edge_price']}",
f"日成交量排名:{'通过' if checks['rank_ok'] else '不通过'}{checks['rank']}/{checks['rank_total']},要求前30",
]
hard_lines = _key_hard_lines_from_checks(checks)
if plan_tuple:
E, sl_raw, tp_raw, box_h = plan_tuple
planned_rr = calc_rr_ratio(direction, E, sl_raw, tp_raw)
@@ -4413,11 +4556,10 @@ def render_main_page(page="trade"):
active_count = len(order_list)
can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
key_gate_rule_text = (
f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}、确认棒偏移 {KEY_CONFIRM_BAR}"
f"量能:突破量 > {KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}"
f"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}"
f"箱体/收敛可选 SL/TP 方案(标准 / 箱体1R·止盈1.5H / 趋势单+自填止盈)|移动保本默认关|"
f"斐波:限价 @ E(SL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%"
f"【箱体/收敛】{KLINE_TIMEFRAME} 两根闭合K|突破越过关键位 > {KEY_BREAKOUT_AMP_MIN_PCT}%"
f"确认K收于箱外|量能>{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}"
f"计划RR参考>{KEY_AUTO_MIN_PLANNED_RR}|日成交{KEY_DAILY_VOLUME_RANK_MAX}OKX 提醒模式不自动开仓|"
f"【阻力/支撑】填上/下沿,5m 收盘突破任一侧提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不选方向"
)
strategy_extra = {}
if page in ("strategy", "strategy_trend", "strategy_roll"):
@@ -4613,9 +4755,22 @@ def api_price_snapshot():
gate_summary = f"斐波 挂E={entry_txt} {'标记价将失效' if inval else '等待成交'}"
if _sqlite_row_val(r, "fib_limit_order_id"):
gate_metrics = f"限价单:{_sqlite_row_val(r, 'fib_limit_order_id')}"
elif (r["monitor_type"] or "").strip() in KEY_MONITOR_RS_TYPES:
try:
prev = _key_rs_gate_preview(r["symbol"], r["upper"], r["lower"])
gate_summary = prev.get("summary") or "-"
gate_metrics = prev.get("metrics") or ""
except Exception:
gate_summary = "-"
else:
try:
gate = _key_hard_checks(r["symbol"], (r["direction"] or "long").lower(), r["upper"], r["lower"], r["monitor_type"])
gate = _key_hard_checks(
r["symbol"],
(r["direction"] or "long").lower(),
r["upper"],
r["lower"],
r["monitor_type"],
)
except Exception:
gate = None
if gate:
@@ -5089,11 +5244,13 @@ def add_key():
if not symbol:
flash("symbol 不能为空")
return redirect("/key_monitor")
direction_sel = (d.get("direction") or "").strip().lower()
if direction_sel not in ("long", "short"):
flash("请选择做多或做空")
return redirect("/key_monitor")
mt = (d.get("type") or "").strip()
direction_sel = (d.get("direction") or "").strip().lower()
if mt in KEY_MONITOR_RS_TYPES:
direction_sel = KEY_DIRECTION_WATCH
elif direction_sel not in ("long", "short"):
flash("箱体/收敛突破请选择做多或做空")
return redirect("/key_monitor")
allowed_types = tuple(KEY_MONITOR_AUTO_TYPES) + tuple(KEY_MONITOR_ALERT_ONLY_TYPES) + tuple(FIB_KEY_MONITOR_TYPES)
if mt not in allowed_types:
flash("监控类型无效")
@@ -5124,6 +5281,10 @@ def add_key():
lw = round_price_to_exchange(ex_sym_key, float(d["lower"]))
upper_px = float(uh) if uh is not None else float(d["upper"])
lower_px = float(lw) if lw is not None else float(d["lower"])
if upper_px <= lower_px:
conn.close()
flash("上沿必须大于下沿")
return redirect("/")
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(
@@ -5163,18 +5324,44 @@ def add_key():
mtpx = round_price_to_exchange(ex_sym_key, manual_tp)
if mtpx is not None:
manual_tp = float(mtpx)
conn.execute(
"INSERT INTO key_monitors "
"(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled) "
"VALUES (?,?,?,?,?,?,?,?)",
(symbol, mt, direction_sel, upper_px, lower_px, sl_tp_mode, manual_tp, be_flag),
)
if mt in KEY_MONITOR_RS_TYPES:
conn.execute(
"INSERT INTO key_monitors "
"(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled,"
"max_notify,notify_interval_min) "
"VALUES (?,?,?,?,?,?,?,?,?,?)",
(
symbol,
mt,
direction_sel,
upper_px,
lower_px,
sl_tp_mode,
manual_tp,
be_flag,
KEY_ALERT_MAX_TIMES,
KEY_ALERT_INTERVAL_MINUTES,
),
)
else:
conn.execute(
"INSERT INTO key_monitors "
"(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled) "
"VALUES (?,?,?,?,?,?,?,?)",
(symbol, mt, direction_sel, upper_px, lower_px, sl_tp_mode, manual_tp, be_flag),
)
conn.commit()
conn.close()
extra = ""
if mt in KEY_MONITOR_AUTO_TYPES:
extra = f"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'' if be_flag else ''}"
flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}{extra}")
if mt in KEY_MONITOR_RS_TYPES:
flash(
f"添加成功({symbol} 日成交量排名 {rank}/{total})|阻力/支撑:双向监控上/下沿,"
f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)"
)
else:
flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}{extra}")
return redirect("/key_monitor")
@app.route("/add_order", methods=["POST"])