关键位监控
This commit is contained in:
@@ -98,6 +98,8 @@ KEY_DAILY_VOLUME_RANK_MAX=30
|
||||
KEY_AUTO_MIN_PLANNED_RR=1.5
|
||||
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
|
||||
KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
|
||||
# 趋势单方案:止损在突破 K 极值外侧的百分比(默认 1 即 1%)
|
||||
KEY_TREND_STOP_OUTSIDE_PCT=1
|
||||
KEY_ALERT_MAX_TIMES=3
|
||||
KEY_ALERT_INTERVAL_MINUTES=5
|
||||
|
||||
|
||||
+128
-39
@@ -45,6 +45,15 @@ from fib_key_monitor_lib import (
|
||||
key_signal_type_for_trade_record,
|
||||
stored_key_signal_type,
|
||||
)
|
||||
from key_sl_tp_lib import (
|
||||
breakeven_enabled_from_row,
|
||||
normalize_sl_tp_mode,
|
||||
parse_breakeven_enabled_form,
|
||||
plan_key_sl_tp,
|
||||
sl_tp_mode_from_row,
|
||||
sl_tp_mode_label,
|
||||
sl_tp_plan_summary_text,
|
||||
)
|
||||
from history_window_lib import (
|
||||
PRESET_CUSTOM,
|
||||
PRESET_UTC_LAST24H,
|
||||
@@ -149,6 +158,7 @@ KEY_ALERT_MAX_TIMES = int(os.getenv("KEY_ALERT_MAX_TIMES", "3"))
|
||||
KEY_ALERT_INTERVAL_MINUTES = int(os.getenv("KEY_ALERT_INTERVAL_MINUTES", "5"))
|
||||
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"))
|
||||
MANUAL_MIN_PLANNED_RR = float(os.getenv("MANUAL_MIN_PLANNED_RR", "1.4"))
|
||||
MAX_ACTIVE_POSITIONS = max(1, int(os.getenv("MAX_ACTIVE_POSITIONS", "1")))
|
||||
KEY_VOLUME_MA_BARS = max(1, int(os.getenv("KEY_VOLUME_MA_BARS", "20")))
|
||||
@@ -1343,6 +1353,9 @@ def init_db():
|
||||
"ALTER TABLE key_monitors ADD COLUMN fib_order_amount REAL",
|
||||
"ALTER TABLE key_monitors ADD COLUMN fib_margin_capital REAL",
|
||||
"ALTER TABLE key_monitors ADD COLUMN fib_leverage INTEGER",
|
||||
"ALTER TABLE key_monitors ADD COLUMN sl_tp_mode TEXT DEFAULT 'standard'",
|
||||
"ALTER TABLE key_monitors ADD COLUMN manual_take_profit REAL",
|
||||
"ALTER TABLE key_monitors ADD COLUMN breakeven_enabled INTEGER DEFAULT 0",
|
||||
):
|
||||
try:
|
||||
c.execute(ddl)
|
||||
@@ -4006,26 +4019,33 @@ def _key_hard_lines_from_checks(checks):
|
||||
]
|
||||
|
||||
|
||||
def _key_plan_auto_sl_tp(direction, upper, lower, checks, outside_pct):
|
||||
"""
|
||||
计划 SL/TP:止损在突破 K 极值外侧 outside_pct%,止盈为确认收盘 ± 箱体高。
|
||||
返回 (E, raw_sl, raw_tp, box_h)。
|
||||
"""
|
||||
E = float(checks["confirm_close"])
|
||||
H = abs(float(upper) - float(lower))
|
||||
br_hi = float(checks["breakout_high"])
|
||||
br_lo = float(checks["breakout_low"])
|
||||
m = float(outside_pct) / 100.0
|
||||
if direction == "long":
|
||||
sl_raw = br_lo * (1.0 - m) if br_lo > 0 else 0.0
|
||||
tp_raw = E + H
|
||||
else:
|
||||
sl_raw = br_hi * (1.0 + m) if br_hi > 0 else 0.0
|
||||
tp_raw = E - H
|
||||
return E, sl_raw, tp_raw, H
|
||||
def _key_plan_sl_tp_for_row(row, direction, upper, lower, checks):
|
||||
"""按 key_monitors 录入的方案计算计划 SL/TP。"""
|
||||
mode = sl_tp_mode_from_row(row, "standard")
|
||||
manual_tp = _sqlite_row_val(row, "manual_take_profit")
|
||||
planned = plan_key_sl_tp(
|
||||
mode,
|
||||
direction,
|
||||
upper,
|
||||
lower,
|
||||
checks,
|
||||
outside_pct=KEY_STOP_OUTSIDE_BREAKOUT_PCT,
|
||||
trend_outside_pct=KEY_TREND_STOP_OUTSIDE_PCT,
|
||||
manual_take_profit=manual_tp,
|
||||
)
|
||||
return planned, mode
|
||||
|
||||
|
||||
def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_loss, take_profit, key_signal_type=None):
|
||||
def _market_open_for_key_monitor(
|
||||
conn,
|
||||
symbol,
|
||||
direction,
|
||||
exchange_symbol,
|
||||
stop_loss,
|
||||
take_profit,
|
||||
key_signal_type=None,
|
||||
breakeven_enabled=0,
|
||||
):
|
||||
"""
|
||||
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入。
|
||||
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
|
||||
@@ -4137,7 +4157,7 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
|
||||
else:
|
||||
breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
|
||||
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
|
||||
breakeven_enabled = 1
|
||||
be_enabled = 1 if int(breakeven_enabled or 0) != 0 else 0
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO order_monitors "
|
||||
@@ -4164,7 +4184,7 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
|
||||
breakeven_step_r,
|
||||
0,
|
||||
breakeven_price,
|
||||
breakeven_enabled,
|
||||
be_enabled,
|
||||
notional_value,
|
||||
position_ratio,
|
||||
base_amount,
|
||||
@@ -4376,7 +4396,7 @@ def _insert_order_monitor_from_fib_fill(
|
||||
breakeven_step_r,
|
||||
0,
|
||||
breakeven_price,
|
||||
1,
|
||||
1 if breakeven_enabled_from_row(row, 0) else 0,
|
||||
notional_value,
|
||||
position_ratio,
|
||||
base_amount,
|
||||
@@ -4529,7 +4549,7 @@ def check_fib_key_monitors():
|
||||
conn.close()
|
||||
|
||||
|
||||
def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px):
|
||||
def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px, breakeven_enabled=0):
|
||||
if _fib_key_exists_for_symbol(conn, symbol):
|
||||
return False, f"{symbol} 已有斐波监控(同币仅允许一条 0.618/0.786)"
|
||||
ratio = fib_ratio_from_type(mt)
|
||||
@@ -4589,15 +4609,16 @@ def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px):
|
||||
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) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
"fib_order_amount, fib_margin_capital, fib_leverage, breakeven_enabled) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
symbol, mt, direction_sel, upper_px, lower_px,
|
||||
oid, entry, sl, tp, float(amount), margin_capital, leverage,
|
||||
oid, entry, sl, tp, float(amount), margin_capital, leverage, be_flag,
|
||||
),
|
||||
)
|
||||
return True, None
|
||||
@@ -4657,9 +4678,27 @@ def check_key_monitors():
|
||||
_finalize_key_monitor_one_shot(conn, r, msg, "key_level_alert_only")
|
||||
continue
|
||||
|
||||
E, sl_raw, tp_raw, box_h = _key_plan_auto_sl_tp(
|
||||
direction, up, low, checks, KEY_STOP_OUTSIDE_BREAKOUT_PCT,
|
||||
)
|
||||
plan_tuple, sl_tp_mode = _key_plan_sl_tp_for_row(r, direction, up, low, checks)
|
||||
if not plan_tuple:
|
||||
fmt_rr = "无法计算(止损/止盈与确认价几何关系无效)"
|
||||
rr_msg = (
|
||||
f"# ⚠️ {sym} 关键位自动单:计划无效\n"
|
||||
f"**账户:{_wechat_account_label()}**\n"
|
||||
f"- 类型:{typ}|方案:{sl_tp_mode_label(sl_tp_mode)}\n"
|
||||
f"- 方向:**{_wechat_direction_text(direction)}**\n"
|
||||
f"- 触发时间:`{trigger_time}`\n"
|
||||
f"- 确认K收盘(E):`{format_price_for_symbol(sym, checks.get('confirm_close'))}`\n"
|
||||
f"- **{fmt_rr}**(未开仓)\n"
|
||||
"---\n"
|
||||
"### 硬条件\n"
|
||||
+ "\n".join(f"- {x}" for x in hard_lines)
|
||||
)
|
||||
if risk_tip:
|
||||
rr_msg += f"\n---\n### 逆势风险提示\n- {risk_tip}"
|
||||
send_wechat_msg(rr_msg)
|
||||
_finalize_key_monitor_one_shot(conn, r, rr_msg, "rr_insufficient")
|
||||
continue
|
||||
E, sl_raw, tp_raw, box_h = plan_tuple
|
||||
exchange_symbol = normalize_exchange_symbol(sym)
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
@@ -4677,16 +4716,21 @@ def check_key_monitors():
|
||||
|
||||
if not rr_ok:
|
||||
fmt_rr = f"{planned_rr:.4f}" if planned_rr is not None else "无法计算(止损/止盈与确认价几何关系无效)"
|
||||
plan_line = sl_tp_plan_summary_text(
|
||||
sl_tp_mode, direction, E, sl_raw, tp_raw, box_h,
|
||||
outside_pct=KEY_STOP_OUTSIDE_BREAKOUT_PCT,
|
||||
trend_outside_pct=KEY_TREND_STOP_OUTSIDE_PCT,
|
||||
)
|
||||
rr_msg = (
|
||||
f"# ⚠️ {sym} 关键位自动单:计划 RR 未达标\n"
|
||||
f"**账户:{_wechat_account_label()}**\n"
|
||||
f"- 类型:{typ}\n"
|
||||
f"- 类型:{typ}|{plan_line}\n"
|
||||
f"- 方向:**{_wechat_direction_text(direction)}**\n"
|
||||
f"- 触发时间:`{trigger_time}`\n"
|
||||
f"- 确认K收盘(E):`{format_price_for_symbol(sym, E)}`\n"
|
||||
f"- 箱体高 H:`{format_price_for_symbol(sym, box_h)}`\n"
|
||||
f"- 计划止损(突破K外侧 {KEY_STOP_OUTSIDE_BREAKOUT_PCT}%):`{format_wechat_scalar_2dp(sl_raw)}`\n"
|
||||
f"- 计划止盈(E±1×H):`{format_price_for_symbol(sym, tp_raw)}`\n"
|
||||
f"- 计划止损:`{format_wechat_scalar_2dp(sl_raw)}`\n"
|
||||
f"- 计划止盈:`{format_price_for_symbol(sym, tp_raw)}`\n"
|
||||
f"- **计划 RR(按确认收盘 E):{fmt_rr} : 1**(要求 **>{KEY_AUTO_MIN_PLANNED_RR}:1**,未开仓)\n"
|
||||
"---\n"
|
||||
"### 硬条件\n"
|
||||
@@ -4699,8 +4743,16 @@ def check_key_monitors():
|
||||
continue
|
||||
|
||||
key_sig = typ if typ in KEY_MONITOR_AUTO_TYPES else None
|
||||
be_on = breakeven_enabled_from_row(r, 0)
|
||||
ok_trade, trade_err, det = _market_open_for_key_monitor(
|
||||
conn, sym, direction, exchange_symbol, sl_raw, tp_raw, key_signal_type=key_sig,
|
||||
conn,
|
||||
sym,
|
||||
direction,
|
||||
exchange_symbol,
|
||||
sl_raw,
|
||||
tp_raw,
|
||||
key_signal_type=key_sig,
|
||||
breakeven_enabled=1 if be_on else 0,
|
||||
)
|
||||
planned_rr_txt = (
|
||||
format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
|
||||
@@ -4741,7 +4793,8 @@ def check_key_monitors():
|
||||
f"- **来源:**{ORDER_MONITOR_TYPE_KEY_AUTO}(市价)",
|
||||
f"- 页面订单 ID:**{det['new_order_id']}**",
|
||||
f"- 交易所订单 ID:`{det.get('open_order_id') or '-'}`",
|
||||
f"- 类型:{typ}|{_wechat_direction_text(direction)}",
|
||||
f"- 类型:{typ}|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'开' if be_on else '关'}",
|
||||
f"- 方向:**{_wechat_direction_text(direction)}**",
|
||||
f"- 触发时间:`{trigger_time}`",
|
||||
f"- 确认K收盘(E):{format_price_for_symbol(sym, E)}(RR 阈值按此计价)",
|
||||
f"- **计划 RR(E):{planned_rr_txt}:1**",
|
||||
@@ -5536,7 +5589,8 @@ def render_main_page(page="trade"):
|
||||
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"斐波:添加后立即挂限价 @ E,失效按标记价触达 H/L(未成交撤单)"
|
||||
f"箱体/收敛可选 SL/TP 方案(标准 / 箱体1R·止盈1.5H / 趋势单+自填止盈)|移动保本默认关|"
|
||||
f"斐波:限价 @ E(SL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%"
|
||||
)
|
||||
conn.close()
|
||||
return render_template(
|
||||
@@ -6265,18 +6319,50 @@ def add_key():
|
||||
pass
|
||||
upper_px = round_price_to_exchange(ex_sym_key, float(d["upper"]))
|
||||
lower_px = round_price_to_exchange(ex_sym_key, float(d["lower"]))
|
||||
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)
|
||||
ok_fib, err_fib = _add_fib_key_monitor(
|
||||
conn, symbol, direction_sel, mt, upper_px, lower_px, breakeven_enabled=be_flag,
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if not ok_fib:
|
||||
flash(err_fib or "斐波监控添加失败")
|
||||
return redirect("/key_monitor")
|
||||
flash(f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})")
|
||||
flash(
|
||||
f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})"
|
||||
f"|移动保本:{'开' if be_flag else '关'}"
|
||||
)
|
||||
return redirect("/key_monitor")
|
||||
sl_tp_mode = "standard"
|
||||
manual_tp = None
|
||||
if mt in KEY_MONITOR_AUTO_TYPES:
|
||||
sl_tp_mode = normalize_sl_tp_mode(d.get("sl_tp_mode"))
|
||||
if sl_tp_mode == "trend_manual":
|
||||
try:
|
||||
manual_tp = float(d.get("manual_take_profit") or 0)
|
||||
except (TypeError, ValueError):
|
||||
manual_tp = 0
|
||||
if manual_tp <= 0:
|
||||
conn.close()
|
||||
flash("趋势单方案须填写有效止盈价")
|
||||
return redirect("/key_monitor")
|
||||
if direction_sel == "long" and manual_tp <= upper_px:
|
||||
conn.close()
|
||||
flash("做多趋势单:止盈价应高于上沿(阻力)")
|
||||
return redirect("/key_monitor")
|
||||
if direction_sel == "short" and manual_tp >= lower_px:
|
||||
conn.close()
|
||||
flash("做空趋势单:止盈价应低于下沿(支撑)")
|
||||
return redirect("/key_monitor")
|
||||
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) VALUES (?,?,?,?,?)",
|
||||
(symbol, mt, direction_sel, upper_px, lower_px),
|
||||
"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()
|
||||
@@ -6288,7 +6374,10 @@ def add_key():
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
flash(f"添加成功({symbol} 日成交量排名 {rank}/{total})")
|
||||
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 ctr:
|
||||
flash(
|
||||
"⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。"
|
||||
|
||||
@@ -270,6 +270,15 @@
|
||||
</select>
|
||||
<input name="upper" step="0.0001" placeholder="上沿/阻力" required>
|
||||
<input name="lower" step="0.0001" placeholder="下沿/支撑" required>
|
||||
<select name="sl_tp_mode" id="key-sl-tp-mode" title="止盈止损方案">
|
||||
<option value="standard">标准突破</option>
|
||||
<option value="box_1p5">箱体1R·止盈1.5H</option>
|
||||
<option value="trend_manual">趋势单·自填止盈</option>
|
||||
</select>
|
||||
<input name="manual_take_profit" id="key-manual-tp" step="0.0001" placeholder="趋势单止盈价" style="display:none">
|
||||
<label id="key-breakeven-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
|
||||
<input type="checkbox" name="breakeven_enabled" value="1" id="key-breakeven-cb"> 移动保本
|
||||
</label>
|
||||
<button type="submit">添加</button>
|
||||
</form>
|
||||
<div class="rule-tip">{{ key_gate_rule_text }}</div>
|
||||
@@ -289,6 +298,10 @@
|
||||
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
|
||||
{% if k.fib_entry_price %}<span class="pos-meta-item">挂E: {{ k.fib_entry_price }}</span>{% endif %}
|
||||
<span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span>
|
||||
{% if k.monitor_type in ['箱体突破','收敛突破'] %}
|
||||
<span class="pos-meta-item">方案: {{ '标准突破' if (k.sl_tp_mode or 'standard') == 'standard' else ('箱体1R·止盈1.5H' if k.sl_tp_mode == 'box_1p5' else '趋势单') }}</span>
|
||||
{% endif %}
|
||||
<span class="pos-meta-item">保本: {{ '开' if k.breakeven_enabled else '关' }}</span>
|
||||
</div>
|
||||
<div class="pos-grid">
|
||||
<div class="pos-cell"><span class="pos-label">现价</span><span class="pos-value" id="key-price-{{ k.id }}">-</span></div>
|
||||
@@ -1362,6 +1375,31 @@ if(journalForm){
|
||||
}
|
||||
}
|
||||
|
||||
function syncKeyMonitorFormFields(){
|
||||
const typeEl = document.querySelector('#key-form [name="type"]');
|
||||
const modeEl = document.getElementById("key-sl-tp-mode");
|
||||
const manualTp = document.getElementById("key-manual-tp");
|
||||
const beWrap = document.getElementById("key-breakeven-wrap");
|
||||
if(!typeEl) return;
|
||||
const t = (typeEl.value || "").trim();
|
||||
const autoTypes = new Set(["箱体突破","收敛突破"]);
|
||||
const fibTypes = new Set(["斐波回调0.618","斐波回调0.786"]);
|
||||
const showAuto = autoTypes.has(t);
|
||||
const showBe = showAuto || fibTypes.has(t);
|
||||
if(modeEl) modeEl.style.display = showAuto ? "" : "none";
|
||||
if(manualTp){
|
||||
const trend = showAuto && modeEl && modeEl.value === "trend_manual";
|
||||
manualTp.style.display = trend ? "" : "none";
|
||||
manualTp.required = !!trend;
|
||||
}
|
||||
if(beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
|
||||
}
|
||||
const keyTypeSel = document.querySelector('#key-form [name="type"]');
|
||||
const keyModeSel = document.getElementById("key-sl-tp-mode");
|
||||
if(keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
|
||||
if(keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
|
||||
syncKeyMonitorFormFields();
|
||||
|
||||
const keyForm = document.getElementById("key-form");
|
||||
if(keyForm){
|
||||
keyForm.addEventListener("submit", (e)=>{
|
||||
|
||||
Reference in New Issue
Block a user