关键位箱体突破调整
This commit is contained in:
+228
-41
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user