关键位监控
This commit is contained in:
@@ -98,6 +98,8 @@ KEY_DAILY_VOLUME_RANK_MAX=30
|
|||||||
KEY_AUTO_MIN_PLANNED_RR=1.5
|
KEY_AUTO_MIN_PLANNED_RR=1.5
|
||||||
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
|
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
|
||||||
KEY_STOP_OUTSIDE_BREAKOUT_PCT=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_MAX_TIMES=3
|
||||||
KEY_ALERT_INTERVAL_MINUTES=5
|
KEY_ALERT_INTERVAL_MINUTES=5
|
||||||
|
|
||||||
|
|||||||
+128
-38
@@ -44,6 +44,15 @@ from fib_key_monitor_lib import (
|
|||||||
key_signal_type_for_trade_record,
|
key_signal_type_for_trade_record,
|
||||||
stored_key_signal_type,
|
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 (
|
from history_window_lib import (
|
||||||
PRESET_CUSTOM,
|
PRESET_CUSTOM,
|
||||||
PRESET_UTC_LAST24H,
|
PRESET_UTC_LAST24H,
|
||||||
@@ -147,6 +156,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_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_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_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"))
|
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")))
|
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")))
|
KEY_VOLUME_MA_BARS = max(1, int(os.getenv("KEY_VOLUME_MA_BARS", "20")))
|
||||||
@@ -1344,6 +1354,9 @@ def init_db():
|
|||||||
"ALTER TABLE key_monitors ADD COLUMN fib_order_amount REAL",
|
"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_margin_capital REAL",
|
||||||
"ALTER TABLE key_monitors ADD COLUMN fib_leverage INTEGER",
|
"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:
|
try:
|
||||||
c.execute(ddl)
|
c.execute(ddl)
|
||||||
@@ -4161,26 +4174,33 @@ def _key_hard_lines_from_checks(checks):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _key_plan_auto_sl_tp(direction, upper, lower, checks, outside_pct):
|
def _key_plan_sl_tp_for_row(row, direction, upper, lower, checks):
|
||||||
"""
|
"""按 key_monitors 录入的方案计算计划 SL/TP。"""
|
||||||
计划 SL/TP:止损在突破 K 极值外侧 outside_pct%,止盈为确认收盘 ± 箱体高。
|
mode = sl_tp_mode_from_row(row, "standard")
|
||||||
返回 (E, raw_sl, raw_tp, box_h)。
|
manual_tp = _sqlite_row_val(row, "manual_take_profit")
|
||||||
"""
|
planned = plan_key_sl_tp(
|
||||||
E = float(checks["confirm_close"])
|
mode,
|
||||||
H = abs(float(upper) - float(lower))
|
direction,
|
||||||
br_hi = float(checks["breakout_high"])
|
upper,
|
||||||
br_lo = float(checks["breakout_low"])
|
lower,
|
||||||
m = float(outside_pct) / 100.0
|
checks,
|
||||||
if direction == "long":
|
outside_pct=KEY_STOP_OUTSIDE_BREAKOUT_PCT,
|
||||||
sl_raw = br_lo * (1.0 - m) if br_lo > 0 else 0.0
|
trend_outside_pct=KEY_TREND_STOP_OUTSIDE_PCT,
|
||||||
tp_raw = E + H
|
manual_take_profit=manual_tp,
|
||||||
else:
|
)
|
||||||
sl_raw = br_hi * (1.0 + m) if br_hi > 0 else 0.0
|
return planned, mode
|
||||||
tp_raw = E - H
|
|
||||||
return E, sl_raw, tp_raw, H
|
|
||||||
|
|
||||||
|
|
||||||
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 写入(Binance U 本位)。
|
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入(Binance U 本位)。
|
||||||
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
|
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
|
||||||
@@ -4290,7 +4310,7 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
|
|||||||
breakeven_price = round(float(trigger_price) * (1 - breakeven_offset_pct / 100.0), 8)
|
breakeven_price = round(float(trigger_price) * (1 - breakeven_offset_pct / 100.0), 8)
|
||||||
else:
|
else:
|
||||||
breakeven_price = round(float(trigger_price) * (1 + breakeven_offset_pct / 100.0), 8)
|
breakeven_price = round(float(trigger_price) * (1 + breakeven_offset_pct / 100.0), 8)
|
||||||
breakeven_enabled = 1
|
be_enabled = 1 if int(breakeven_enabled or 0) != 0 else 0
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO order_monitors "
|
"INSERT INTO order_monitors "
|
||||||
@@ -4317,7 +4337,7 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
|
|||||||
breakeven_step_r,
|
breakeven_step_r,
|
||||||
0,
|
0,
|
||||||
breakeven_price,
|
breakeven_price,
|
||||||
breakeven_enabled,
|
be_enabled,
|
||||||
notional_value,
|
notional_value,
|
||||||
position_ratio,
|
position_ratio,
|
||||||
base_amount,
|
base_amount,
|
||||||
@@ -4506,6 +4526,7 @@ def _insert_order_monitor_from_fib_fill(
|
|||||||
else:
|
else:
|
||||||
breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
|
breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
|
||||||
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
|
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
|
||||||
|
be_enabled = 1 if breakeven_enabled_from_row(row, 0) else 0
|
||||||
opened_at_bj = app_now_str()
|
opened_at_bj = app_now_str()
|
||||||
opened_at_ms = _to_ms_with_fallback(None, opened_at_bj)
|
opened_at_ms = _to_ms_with_fallback(None, opened_at_bj)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -4533,7 +4554,7 @@ def _insert_order_monitor_from_fib_fill(
|
|||||||
breakeven_step_r,
|
breakeven_step_r,
|
||||||
0,
|
0,
|
||||||
breakeven_price,
|
breakeven_price,
|
||||||
1,
|
be_enabled,
|
||||||
notional_value,
|
notional_value,
|
||||||
position_ratio,
|
position_ratio,
|
||||||
base_amount,
|
base_amount,
|
||||||
@@ -4685,7 +4706,7 @@ def check_fib_key_monitors():
|
|||||||
conn.close()
|
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):
|
if _fib_key_exists_for_symbol(conn, symbol):
|
||||||
return False, f"{symbol} 已有斐波监控(同币仅允许一条 0.618/0.786)"
|
return False, f"{symbol} 已有斐波监控(同币仅允许一条 0.618/0.786)"
|
||||||
ratio = fib_ratio_from_type(mt)
|
ratio = fib_ratio_from_type(mt)
|
||||||
@@ -4745,15 +4766,16 @@ def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px):
|
|||||||
return False, "交易所未返回限价单 ID"
|
return False, "交易所未返回限价单 ID"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, friendly_exchange_error(e, available_usdt=available_usdt)
|
return False, friendly_exchange_error(e, available_usdt=available_usdt)
|
||||||
|
be_flag = 1 if int(breakeven_enabled or 0) != 0 else 0
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO key_monitors "
|
"INSERT INTO key_monitors "
|
||||||
"(symbol, monitor_type, direction, upper, lower, "
|
"(symbol, monitor_type, direction, upper, lower, "
|
||||||
"fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
|
"fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
|
||||||
"fib_order_amount, fib_margin_capital, fib_leverage) "
|
"fib_order_amount, fib_margin_capital, fib_leverage, breakeven_enabled) "
|
||||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
(
|
(
|
||||||
symbol, mt, direction_sel, upper_px, lower_px,
|
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
|
return True, None
|
||||||
@@ -4813,9 +4835,27 @@ def check_key_monitors():
|
|||||||
_finalize_key_monitor_one_shot(conn, r, msg, "key_level_alert_only")
|
_finalize_key_monitor_one_shot(conn, r, msg, "key_level_alert_only")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
E, sl_raw, tp_raw, box_h = _key_plan_auto_sl_tp(
|
plan_tuple, sl_tp_mode = _key_plan_sl_tp_for_row(r, direction, up, low, checks)
|
||||||
direction, up, low, checks, KEY_STOP_OUTSIDE_BREAKOUT_PCT,
|
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)
|
exchange_symbol = normalize_exchange_symbol(sym)
|
||||||
try:
|
try:
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
@@ -4833,16 +4873,21 @@ def check_key_monitors():
|
|||||||
|
|
||||||
if not rr_ok:
|
if not rr_ok:
|
||||||
fmt_rr = f"{planned_rr:.4f}" if planned_rr is not None else "无法计算(止损/止盈与确认价几何关系无效)"
|
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 = (
|
rr_msg = (
|
||||||
f"# ⚠️ {sym} 关键位自动单:计划 RR 未达标\n"
|
f"# ⚠️ {sym} 关键位自动单:计划 RR 未达标\n"
|
||||||
f"**账户:{_wechat_account_label()}**\n"
|
f"**账户:{_wechat_account_label()}**\n"
|
||||||
f"- 类型:{typ}\n"
|
f"- 类型:{typ}|{plan_line}\n"
|
||||||
f"- 方向:**{_wechat_direction_text(direction)}**\n"
|
f"- 方向:**{_wechat_direction_text(direction)}**\n"
|
||||||
f"- 触发时间:`{trigger_time}`\n"
|
f"- 触发时间:`{trigger_time}`\n"
|
||||||
f"- 确认K收盘(E):`{format_price_for_symbol(sym, E)}`\n"
|
f"- 确认K收盘(E):`{format_price_for_symbol(sym, E)}`\n"
|
||||||
f"- 箱体高 H:`{format_price_for_symbol(sym, box_h)}`\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"- 计划止损:`{format_wechat_scalar_2dp(sl_raw)}`\n"
|
||||||
f"- 计划止盈(E±1×H):`{format_price_for_symbol(sym, tp_raw)}`\n"
|
f"- 计划止盈:`{format_price_for_symbol(sym, tp_raw)}`\n"
|
||||||
f"- **计划 RR(按确认收盘 E):{fmt_rr} : 1**(要求 **>{KEY_AUTO_MIN_PLANNED_RR}:1**,未开仓)\n"
|
f"- **计划 RR(按确认收盘 E):{fmt_rr} : 1**(要求 **>{KEY_AUTO_MIN_PLANNED_RR}:1**,未开仓)\n"
|
||||||
"---\n"
|
"---\n"
|
||||||
"### 硬条件\n"
|
"### 硬条件\n"
|
||||||
@@ -4855,8 +4900,16 @@ def check_key_monitors():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
key_sig = typ if typ in KEY_MONITOR_AUTO_TYPES else None
|
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(
|
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 = (
|
planned_rr_txt = (
|
||||||
format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
|
format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
|
||||||
@@ -4897,7 +4950,8 @@ def check_key_monitors():
|
|||||||
f"- **来源:**{ORDER_MONITOR_TYPE_KEY_AUTO}(市价)",
|
f"- **来源:**{ORDER_MONITOR_TYPE_KEY_AUTO}(市价)",
|
||||||
f"- 页面订单 ID:**{det['new_order_id']}**",
|
f"- 页面订单 ID:**{det['new_order_id']}**",
|
||||||
f"- 交易所订单 ID:`{det.get('open_order_id') or '-'}`",
|
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"- 触发时间:`{trigger_time}`",
|
||||||
f"- 确认K收盘(E):{format_price_for_symbol(sym, E)}(RR 阈值按此计价)",
|
f"- 确认K收盘(E):{format_price_for_symbol(sym, E)}(RR 阈值按此计价)",
|
||||||
f"- **计划 RR(E):{planned_rr_txt}:1**",
|
f"- **计划 RR(E):{planned_rr_txt}:1**",
|
||||||
@@ -5556,7 +5610,8 @@ def render_main_page(page="trade"):
|
|||||||
f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}、确认棒偏移 {KEY_CONFIRM_BAR}|"
|
f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}、确认棒偏移 {KEY_CONFIRM_BAR}|"
|
||||||
f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|"
|
f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|"
|
||||||
f"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}|"
|
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()
|
conn.close()
|
||||||
return render_template(
|
return render_template(
|
||||||
@@ -6244,18 +6299,50 @@ def add_key():
|
|||||||
lw = round_price_to_exchange(ex_sym_key, float(d["lower"]))
|
lw = round_price_to_exchange(ex_sym_key, float(d["lower"]))
|
||||||
upper_px = float(uh) if uh is not None else float(d["upper"])
|
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"])
|
lower_px = float(lw) if lw is not None else float(d["lower"])
|
||||||
|
be_flag = parse_breakeven_enabled_form(d.get("breakeven_enabled"))
|
||||||
if is_fib_key_monitor_type(mt):
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not ok_fib:
|
if not ok_fib:
|
||||||
flash(err_fib or "斐波监控添加失败")
|
flash(err_fib or "斐波监控添加失败")
|
||||||
return redirect("/key_monitor")
|
return redirect("/key_monitor")
|
||||||
flash(f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})")
|
flash(
|
||||||
|
f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})"
|
||||||
|
f"|移动保本:{'开' if be_flag else '关'}"
|
||||||
|
)
|
||||||
return redirect("/key_monitor")
|
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(
|
conn.execute(
|
||||||
"INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
|
"INSERT INTO key_monitors "
|
||||||
(symbol, mt, direction_sel, upper_px, lower_px),
|
"(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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -6267,7 +6354,10 @@ def add_key():
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
if ctr:
|
||||||
flash(
|
flash(
|
||||||
"⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。"
|
"⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。"
|
||||||
|
|||||||
@@ -270,6 +270,15 @@
|
|||||||
</select>
|
</select>
|
||||||
<input name="upper" step="0.0001" placeholder="上沿/阻力" required>
|
<input name="upper" step="0.0001" placeholder="上沿/阻力" required>
|
||||||
<input name="lower" 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>
|
<button type="submit">添加</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="rule-tip">{{ key_gate_rule_text }}</div>
|
<div class="rule-tip">{{ key_gate_rule_text }}</div>
|
||||||
@@ -289,6 +298,10 @@
|
|||||||
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
|
<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 %}
|
{% 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>
|
<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>
|
||||||
<div class="pos-grid">
|
<div class="pos-grid">
|
||||||
<div class="pos-cell"><span class="pos-label">现价</span><span class="pos-value" id="key-price-{{ k.id }}">-</span></div>
|
<div class="pos-cell"><span class="pos-label">现价</span><span class="pos-value" id="key-price-{{ k.id }}">-</span></div>
|
||||||
@@ -1352,6 +1365,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");
|
const keyForm = document.getElementById("key-form");
|
||||||
if(keyForm){
|
if(keyForm){
|
||||||
keyForm.addEventListener("submit", (e)=>{
|
keyForm.addEventListener("submit", (e)=>{
|
||||||
|
|||||||
@@ -55,15 +55,18 @@
|
|||||||
|
|
||||||
## 自动单止盈 / 止损(仅箱体突破、收敛突破)
|
## 自动单止盈 / 止损(仅箱体突破、收敛突破)
|
||||||
|
|
||||||
设箱体高度 **`H = |upper − lower|`**(录入上下沿)。
|
添加关键位时在页面选择 **止盈止损方案**(写入 `key_monitors.sl_tp_mode`)。确认 K 收盘 **E**,箱体高 **H = |upper − lower|`**。
|
||||||
|
|
||||||
| 方向 | 止损 SL | 止盈 TP |
|
| 方案 | `sl_tp_mode` | 多:SL / TP | 空:SL / TP |
|
||||||
|------|---------|---------|
|
|------|--------------|-------------|-------------|
|
||||||
| 多 | 突破 K **最低价** × (1 − `KEY_STOP_OUTSIDE_BREAKOUT_PCT`/100) | **`E + 1×H`** |
|
| 标准突破(默认) | `standard` | 突破 K 低 × (1−`KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) / **E+H** | 突破 K 高 × (1+外侧%) / **E−H** |
|
||||||
| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`/100) | **`E − 1×H`** |
|
| 箱体1R·止盈1.5H | `box_1p5` | **E−H** / **E+1.5×H**(RR≈1.5) | **E+H** / **E−1.5×H** |
|
||||||
|
| 趋势单·自填止盈 | `trend_manual` | 突破 K 低 × (1−`KEY_TREND_STOP_OUTSIDE_PCT`%) / **录入止盈** | 突破 K 高 × (1+外侧%) / **录入止盈** |
|
||||||
|
|
||||||
计划 **`RR = calc_rr_ratio(direction, E, SL, TP)`**。若为 `None` 或 **RR ≤ `KEY_AUTO_MIN_PLANNED_RR`** → **不下单**,走 `rr_insufficient` 结案。
|
计划 **`RR = calc_rr_ratio(direction, E, SL, TP)`**。若为 `None` 或 **RR ≤ `KEY_AUTO_MIN_PLANNED_RR`** → **不下单**,走 `rr_insufficient` 结案。
|
||||||
|
|
||||||
|
**移动保本:** 添加时可勾选(默认关);开仓写入 `order_monitors.breakeven_enabled` 与勾选一致。详见仓库根目录 `关键位止盈止损与移动保本更新说明.md`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 一次性结案(`close_reason`)
|
## 一次性结案(`close_reason`)
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ KEY_DAILY_VOLUME_RANK_MAX=30
|
|||||||
KEY_AUTO_MIN_PLANNED_RR=1.5
|
KEY_AUTO_MIN_PLANNED_RR=1.5
|
||||||
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
|
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
|
||||||
KEY_STOP_OUTSIDE_BREAKOUT_PCT=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_MAX_TIMES=3
|
||||||
KEY_ALERT_INTERVAL_MINUTES=5
|
KEY_ALERT_INTERVAL_MINUTES=5
|
||||||
|
|
||||||
|
|||||||
+127
-38
@@ -45,6 +45,15 @@ from fib_key_monitor_lib import (
|
|||||||
key_signal_type_for_trade_record,
|
key_signal_type_for_trade_record,
|
||||||
stored_key_signal_type,
|
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 (
|
from history_window_lib import (
|
||||||
PRESET_CUSTOM,
|
PRESET_CUSTOM,
|
||||||
PRESET_UTC_LAST24H,
|
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_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_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_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"))
|
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")))
|
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")))
|
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_order_amount REAL",
|
||||||
"ALTER TABLE key_monitors ADD COLUMN fib_margin_capital 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 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:
|
try:
|
||||||
c.execute(ddl)
|
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):
|
def _key_plan_sl_tp_for_row(row, direction, upper, lower, checks):
|
||||||
"""
|
"""按 key_monitors 录入的方案计算计划 SL/TP。"""
|
||||||
计划 SL/TP:止损在突破 K 极值外侧 outside_pct%,止盈为确认收盘 ± 箱体高。
|
mode = sl_tp_mode_from_row(row, "standard")
|
||||||
返回 (E, raw_sl, raw_tp, box_h)。
|
manual_tp = _sqlite_row_val(row, "manual_take_profit")
|
||||||
"""
|
planned = plan_key_sl_tp(
|
||||||
E = float(checks["confirm_close"])
|
mode,
|
||||||
H = abs(float(upper) - float(lower))
|
direction,
|
||||||
br_hi = float(checks["breakout_high"])
|
upper,
|
||||||
br_lo = float(checks["breakout_low"])
|
lower,
|
||||||
m = float(outside_pct) / 100.0
|
checks,
|
||||||
if direction == "long":
|
outside_pct=KEY_STOP_OUTSIDE_BREAKOUT_PCT,
|
||||||
sl_raw = br_lo * (1.0 - m) if br_lo > 0 else 0.0
|
trend_outside_pct=KEY_TREND_STOP_OUTSIDE_PCT,
|
||||||
tp_raw = E + H
|
manual_take_profit=manual_tp,
|
||||||
else:
|
)
|
||||||
sl_raw = br_hi * (1.0 + m) if br_hi > 0 else 0.0
|
return planned, mode
|
||||||
tp_raw = E - H
|
|
||||||
return E, sl_raw, tp_raw, H
|
|
||||||
|
|
||||||
|
|
||||||
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 写入。
|
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入。
|
||||||
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
|
返回 (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:
|
else:
|
||||||
breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
|
breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
|
||||||
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
|
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(
|
conn.execute(
|
||||||
"INSERT INTO order_monitors "
|
"INSERT INTO order_monitors "
|
||||||
@@ -4164,7 +4184,7 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
|
|||||||
breakeven_step_r,
|
breakeven_step_r,
|
||||||
0,
|
0,
|
||||||
breakeven_price,
|
breakeven_price,
|
||||||
breakeven_enabled,
|
be_enabled,
|
||||||
notional_value,
|
notional_value,
|
||||||
position_ratio,
|
position_ratio,
|
||||||
base_amount,
|
base_amount,
|
||||||
@@ -4376,7 +4396,7 @@ def _insert_order_monitor_from_fib_fill(
|
|||||||
breakeven_step_r,
|
breakeven_step_r,
|
||||||
0,
|
0,
|
||||||
breakeven_price,
|
breakeven_price,
|
||||||
1,
|
1 if breakeven_enabled_from_row(row, 0) else 0,
|
||||||
notional_value,
|
notional_value,
|
||||||
position_ratio,
|
position_ratio,
|
||||||
base_amount,
|
base_amount,
|
||||||
@@ -4529,7 +4549,7 @@ def check_fib_key_monitors():
|
|||||||
conn.close()
|
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):
|
if _fib_key_exists_for_symbol(conn, symbol):
|
||||||
return False, f"{symbol} 已有斐波监控(同币仅允许一条 0.618/0.786)"
|
return False, f"{symbol} 已有斐波监控(同币仅允许一条 0.618/0.786)"
|
||||||
ratio = fib_ratio_from_type(mt)
|
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"
|
return False, "交易所未返回限价单 ID"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, friendly_exchange_error(e, available_usdt=available_usdt)
|
return False, friendly_exchange_error(e, available_usdt=available_usdt)
|
||||||
|
be_flag = 1 if int(breakeven_enabled or 0) != 0 else 0
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO key_monitors "
|
"INSERT INTO key_monitors "
|
||||||
"(symbol, monitor_type, direction, upper, lower, "
|
"(symbol, monitor_type, direction, upper, lower, "
|
||||||
"fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
|
"fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
|
||||||
"fib_order_amount, fib_margin_capital, fib_leverage) "
|
"fib_order_amount, fib_margin_capital, fib_leverage, breakeven_enabled) "
|
||||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
(
|
(
|
||||||
symbol, mt, direction_sel, upper_px, lower_px,
|
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
|
return True, None
|
||||||
@@ -4657,9 +4678,27 @@ def check_key_monitors():
|
|||||||
_finalize_key_monitor_one_shot(conn, r, msg, "key_level_alert_only")
|
_finalize_key_monitor_one_shot(conn, r, msg, "key_level_alert_only")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
E, sl_raw, tp_raw, box_h = _key_plan_auto_sl_tp(
|
plan_tuple, sl_tp_mode = _key_plan_sl_tp_for_row(r, direction, up, low, checks)
|
||||||
direction, up, low, checks, KEY_STOP_OUTSIDE_BREAKOUT_PCT,
|
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)
|
exchange_symbol = normalize_exchange_symbol(sym)
|
||||||
try:
|
try:
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
@@ -4677,16 +4716,21 @@ def check_key_monitors():
|
|||||||
|
|
||||||
if not rr_ok:
|
if not rr_ok:
|
||||||
fmt_rr = f"{planned_rr:.4f}" if planned_rr is not None else "无法计算(止损/止盈与确认价几何关系无效)"
|
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 = (
|
rr_msg = (
|
||||||
f"# ⚠️ {sym} 关键位自动单:计划 RR 未达标\n"
|
f"# ⚠️ {sym} 关键位自动单:计划 RR 未达标\n"
|
||||||
f"**账户:{_wechat_account_label()}**\n"
|
f"**账户:{_wechat_account_label()}**\n"
|
||||||
f"- 类型:{typ}\n"
|
f"- 类型:{typ}|{plan_line}\n"
|
||||||
f"- 方向:**{_wechat_direction_text(direction)}**\n"
|
f"- 方向:**{_wechat_direction_text(direction)}**\n"
|
||||||
f"- 触发时间:`{trigger_time}`\n"
|
f"- 触发时间:`{trigger_time}`\n"
|
||||||
f"- 确认K收盘(E):`{format_price_for_symbol(sym, E)}`\n"
|
f"- 确认K收盘(E):`{format_price_for_symbol(sym, E)}`\n"
|
||||||
f"- 箱体高 H:`{format_price_for_symbol(sym, box_h)}`\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"- 计划止损:`{format_wechat_scalar_2dp(sl_raw)}`\n"
|
||||||
f"- 计划止盈(E±1×H):`{format_price_for_symbol(sym, tp_raw)}`\n"
|
f"- 计划止盈:`{format_price_for_symbol(sym, tp_raw)}`\n"
|
||||||
f"- **计划 RR(按确认收盘 E):{fmt_rr} : 1**(要求 **>{KEY_AUTO_MIN_PLANNED_RR}:1**,未开仓)\n"
|
f"- **计划 RR(按确认收盘 E):{fmt_rr} : 1**(要求 **>{KEY_AUTO_MIN_PLANNED_RR}:1**,未开仓)\n"
|
||||||
"---\n"
|
"---\n"
|
||||||
"### 硬条件\n"
|
"### 硬条件\n"
|
||||||
@@ -4699,8 +4743,16 @@ def check_key_monitors():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
key_sig = typ if typ in KEY_MONITOR_AUTO_TYPES else None
|
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(
|
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 = (
|
planned_rr_txt = (
|
||||||
format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
|
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"- **来源:**{ORDER_MONITOR_TYPE_KEY_AUTO}(市价)",
|
||||||
f"- 页面订单 ID:**{det['new_order_id']}**",
|
f"- 页面订单 ID:**{det['new_order_id']}**",
|
||||||
f"- 交易所订单 ID:`{det.get('open_order_id') or '-'}`",
|
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"- 触发时间:`{trigger_time}`",
|
||||||
f"- 确认K收盘(E):{format_price_for_symbol(sym, E)}(RR 阈值按此计价)",
|
f"- 确认K收盘(E):{format_price_for_symbol(sym, E)}(RR 阈值按此计价)",
|
||||||
f"- **计划 RR(E):{planned_rr_txt}:1**",
|
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"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}、确认棒偏移 {KEY_CONFIRM_BAR}|"
|
||||||
f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|"
|
f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|"
|
||||||
f"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}|"
|
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()
|
conn.close()
|
||||||
return render_template(
|
return render_template(
|
||||||
@@ -6265,18 +6319,50 @@ def add_key():
|
|||||||
pass
|
pass
|
||||||
upper_px = round_price_to_exchange(ex_sym_key, float(d["upper"]))
|
upper_px = round_price_to_exchange(ex_sym_key, float(d["upper"]))
|
||||||
lower_px = round_price_to_exchange(ex_sym_key, float(d["lower"]))
|
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):
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not ok_fib:
|
if not ok_fib:
|
||||||
flash(err_fib or "斐波监控添加失败")
|
flash(err_fib or "斐波监控添加失败")
|
||||||
return redirect("/key_monitor")
|
return redirect("/key_monitor")
|
||||||
flash(f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})")
|
flash(
|
||||||
|
f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})"
|
||||||
|
f"|移动保本:{'开' if be_flag else '关'}"
|
||||||
|
)
|
||||||
return redirect("/key_monitor")
|
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(
|
conn.execute(
|
||||||
"INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
|
"INSERT INTO key_monitors "
|
||||||
(symbol, mt, direction_sel, upper_px, lower_px),
|
"(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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -6288,7 +6374,10 @@ def add_key():
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
if ctr:
|
||||||
flash(
|
flash(
|
||||||
"⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。"
|
"⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。"
|
||||||
|
|||||||
@@ -270,6 +270,15 @@
|
|||||||
</select>
|
</select>
|
||||||
<input name="upper" step="0.0001" placeholder="上沿/阻力" required>
|
<input name="upper" step="0.0001" placeholder="上沿/阻力" required>
|
||||||
<input name="lower" 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>
|
<button type="submit">添加</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="rule-tip">{{ key_gate_rule_text }}</div>
|
<div class="rule-tip">{{ key_gate_rule_text }}</div>
|
||||||
@@ -289,6 +298,10 @@
|
|||||||
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
|
<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 %}
|
{% 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>
|
<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>
|
||||||
<div class="pos-grid">
|
<div class="pos-grid">
|
||||||
<div class="pos-cell"><span class="pos-label">现价</span><span class="pos-value" id="key-price-{{ k.id }}">-</span></div>
|
<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");
|
const keyForm = document.getElementById("key-form");
|
||||||
if(keyForm){
|
if(keyForm){
|
||||||
keyForm.addEventListener("submit", (e)=>{
|
keyForm.addEventListener("submit", (e)=>{
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
|
|||||||
# ORDER_CHART_LIMIT=100
|
# ORDER_CHART_LIMIT=100
|
||||||
# ORDER_CHART_DIR=static/images/order_charts
|
# ORDER_CHART_DIR=static/images/order_charts
|
||||||
# DAILY_OPEN_ALERT_THRESHOLD=5
|
# DAILY_OPEN_ALERT_THRESHOLD=5
|
||||||
|
# 关键位:标准方案止损外侧%、趋势单方案止损外侧%(默认 0.5 / 1)
|
||||||
|
# KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
|
||||||
|
# KEY_TREND_STOP_OUTSIDE_PCT=1
|
||||||
# 以损定仓(按交易账户资金的百分比)
|
# 以损定仓(按交易账户资金的百分比)
|
||||||
# RISK_PERCENT=2
|
# RISK_PERCENT=2
|
||||||
# 移动保本触发(达到多少R触发)与偏移(百分比)
|
# 移动保本触发(达到多少R触发)与偏移(百分比)
|
||||||
|
|||||||
+95
-25
@@ -44,6 +44,15 @@ from fib_key_monitor_lib import (
|
|||||||
key_signal_type_for_trade_record,
|
key_signal_type_for_trade_record,
|
||||||
stored_key_signal_type,
|
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 (
|
from history_window_lib import (
|
||||||
PRESET_CUSTOM,
|
PRESET_CUSTOM,
|
||||||
PRESET_UTC_LAST24H,
|
PRESET_UTC_LAST24H,
|
||||||
@@ -166,6 +175,8 @@ ORDER_MONITOR_TYPE_KEY_AUTO = "关键位监控"
|
|||||||
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
|
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
|
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
|
||||||
KEY_AUTO_MIN_PLANNED_RR = float(os.getenv("KEY_AUTO_MIN_PLANNED_RR", "1.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"))
|
||||||
KEY_DAILY_VOLUME_RANK_MAX = int(os.getenv("KEY_DAILY_VOLUME_RANK_MAX", "30"))
|
KEY_DAILY_VOLUME_RANK_MAX = int(os.getenv("KEY_DAILY_VOLUME_RANK_MAX", "30"))
|
||||||
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT = os.getenv("KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT", "true").lower() in (
|
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT = os.getenv("KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT", "true").lower() in (
|
||||||
"1",
|
"1",
|
||||||
@@ -1223,6 +1234,9 @@ def init_db():
|
|||||||
"ALTER TABLE key_monitors ADD COLUMN fib_order_amount REAL",
|
"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_margin_capital REAL",
|
||||||
"ALTER TABLE key_monitors ADD COLUMN fib_leverage INTEGER",
|
"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:
|
try:
|
||||||
c.execute(ddl)
|
c.execute(ddl)
|
||||||
@@ -2985,6 +2999,21 @@ def _key_hard_checks(symbol, direction, upper, lower, monitor_type):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _key_plan_sl_tp_for_row(row, direction, upper, lower, checks):
|
||||||
|
mode = sl_tp_mode_from_row(row, "standard")
|
||||||
|
manual_tp = _sqlite_row_val(row, "manual_take_profit")
|
||||||
|
return 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,
|
||||||
|
), mode
|
||||||
|
|
||||||
|
|
||||||
def calc_price_diff_pct(current_price, target_price):
|
def calc_price_diff_pct(current_price, target_price):
|
||||||
try:
|
try:
|
||||||
if target_price is None:
|
if target_price is None:
|
||||||
@@ -3238,7 +3267,7 @@ def _insert_order_monitor_from_fib_fill(
|
|||||||
breakeven_step_r,
|
breakeven_step_r,
|
||||||
0,
|
0,
|
||||||
breakeven_price,
|
breakeven_price,
|
||||||
1,
|
1 if breakeven_enabled_from_row(row, 0) else 0,
|
||||||
notional_value,
|
notional_value,
|
||||||
position_ratio,
|
position_ratio,
|
||||||
base_amount,
|
base_amount,
|
||||||
@@ -3399,7 +3428,7 @@ def check_fib_key_monitors():
|
|||||||
conn.close()
|
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):
|
if _fib_key_exists_for_symbol(conn, symbol):
|
||||||
return False, f"{symbol} 已有斐波监控(同币仅允许一条 0.618/0.786)"
|
return False, f"{symbol} 已有斐波监控(同币仅允许一条 0.618/0.786)"
|
||||||
ratio = fib_ratio_from_type(mt)
|
ratio = fib_ratio_from_type(mt)
|
||||||
@@ -3459,12 +3488,13 @@ def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px):
|
|||||||
return False, "交易所未返回限价单 ID"
|
return False, "交易所未返回限价单 ID"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, friendly_okx_error(e, available_usdt=available_usdt)
|
return False, friendly_okx_error(e, available_usdt=available_usdt)
|
||||||
|
be_flag = 1 if int(breakeven_enabled or 0) != 0 else 0
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO key_monitors "
|
"INSERT INTO key_monitors "
|
||||||
"(symbol, monitor_type, direction, upper, lower, "
|
"(symbol, monitor_type, direction, upper, lower, "
|
||||||
"fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
|
"fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
|
||||||
"fib_order_amount, fib_margin_capital, fib_leverage) "
|
"fib_order_amount, fib_margin_capital, fib_leverage, breakeven_enabled) "
|
||||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
(
|
(
|
||||||
symbol,
|
symbol,
|
||||||
mt,
|
mt,
|
||||||
@@ -3478,6 +3508,7 @@ def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px):
|
|||||||
float(amount),
|
float(amount),
|
||||||
margin_capital,
|
margin_capital,
|
||||||
leverage,
|
leverage,
|
||||||
|
be_flag,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return True, None
|
return True, None
|
||||||
@@ -3532,21 +3563,10 @@ def check_key_monitors():
|
|||||||
risk_tip = None
|
risk_tip = None
|
||||||
if (direction == "long" and coin4h_status == "空头") or (direction == "short" and coin4h_status == "多头"):
|
if (direction == "long" and coin4h_status == "空头") or (direction == "short" and coin4h_status == "多头"):
|
||||||
risk_tip = "当前信号与本币4h(EMA55)主趋势逆势,建议降低仓位并严格执行止损。"
|
risk_tip = "当前信号与本币4h(EMA55)主趋势逆势,建议降低仓位并严格执行止损。"
|
||||||
box_h = abs(float(up) - float(low)) if up is not None and low is not None else 0.0
|
|
||||||
c_close = float(checks.get("confirm_close") or 0)
|
|
||||||
b_high = float(checks.get("breakout_high") or 0)
|
|
||||||
b_low = float(checks.get("breakout_low") or 0)
|
|
||||||
key_price = float(low) if direction == "long" else float(up)
|
key_price = float(low) if direction == "long" else float(up)
|
||||||
if direction == "long":
|
sl_tp_mode = sl_tp_mode_from_row(r, "standard")
|
||||||
tp1 = c_close + box_h
|
be_on = breakeven_enabled_from_row(r, 0)
|
||||||
tp2 = c_close + box_h * 1.5
|
plan_tuple, _mode = _key_plan_sl_tp_for_row(r, direction, up, low, checks)
|
||||||
sl1 = b_low * (1 - 0.002) if b_low > 0 else None
|
|
||||||
sl2 = key_price * (1 - 0.002) if key_price > 0 else None
|
|
||||||
else:
|
|
||||||
tp1 = c_close - box_h
|
|
||||||
tp2 = c_close - box_h * 1.5
|
|
||||||
sl1 = b_high * (1 + 0.002) if b_high > 0 else None
|
|
||||||
sl2 = key_price * (1 + 0.002) if key_price > 0 else None
|
|
||||||
hard_lines = [
|
hard_lines = [
|
||||||
f"量能:{'通过' if checks['vol_ok'] else '不通过'}(突破K量 {round(checks['vol_break'], 4)} / 前20均量 {round(checks['avg20'], 4)},阈值1.3x)",
|
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"突破价位:{'通过' if checks['breakout_ok'] else '不通过'}(突破K收盘 {round(float(checks['breakout_close']), 8)},关键位 {checks['edge_price']})",
|
||||||
@@ -3554,9 +3574,24 @@ def check_key_monitors():
|
|||||||
f"第二根确认:{'通过' if checks['confirm_ok'] else '不通过'}(确认收盘 {checks['confirm_close']},关键位 {checks['edge_price']})",
|
f"第二根确认:{'通过' if checks['confirm_ok'] else '不通过'}(确认收盘 {checks['confirm_close']},关键位 {checks['edge_price']})",
|
||||||
f"日成交量排名:{'通过' if checks['rank_ok'] else '不通过'}({checks['rank']}/{checks['rank_total']},要求前30)",
|
f"日成交量排名:{'通过' if checks['rank_ok'] else '不通过'}({checks['rank']}/{checks['rank_total']},要求前30)",
|
||||||
]
|
]
|
||||||
|
if plan_tuple:
|
||||||
|
E, sl_raw, tp_raw, box_h = plan_tuple
|
||||||
|
planned_rr = calc_rr_ratio(direction, E, sl_raw, tp_raw)
|
||||||
|
rr_txt = f"{planned_rr:.4f}" if planned_rr is not None else "-"
|
||||||
op_lines = [
|
op_lines = [
|
||||||
f"方案A:止盈=箱体1.0倍({round(tp1, 8) if tp1 else '-' }),止损=突破K极值外0.2%({round(sl1, 8) if sl1 else '-' })",
|
f"录入方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'开' if be_on else '关'}",
|
||||||
f"方案B:止盈=箱体1.5倍({round(tp2, 8) if tp2 else '-' }),止损=箱体关键位外0.2%({round(sl2, 8) if sl2 else '-' })",
|
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,
|
||||||
|
),
|
||||||
|
f"计划 SL:`{round(sl_raw, 8)}`|计划 TP:`{round(tp_raw, 8)}`|计划 RR(E):{rr_txt}:1",
|
||||||
|
"说明:OKX 本实例为提醒模式,不自动市价开仓;请按方案自行下单。",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
op_lines = [
|
||||||
|
f"录入方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'开' if be_on else '关'}",
|
||||||
|
"计划 SL/TP 几何无效,请检查上下沿或趋势单止盈价。",
|
||||||
]
|
]
|
||||||
trigger_time = ms_to_app_local_str(int(checks["confirm_ts"])) if checks.get("confirm_ts") else app_now_str()
|
trigger_time = ms_to_app_local_str(int(checks["confirm_ts"])) if checks.get("confirm_ts") else app_now_str()
|
||||||
msg = build_wechat_key_monitor_message(
|
msg = build_wechat_key_monitor_message(
|
||||||
@@ -4601,22 +4636,57 @@ def add_key():
|
|||||||
lw = round_price_to_exchange(ex_sym_key, float(d["lower"]))
|
lw = round_price_to_exchange(ex_sym_key, float(d["lower"]))
|
||||||
upper_px = float(uh) if uh is not None else float(d["upper"])
|
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"])
|
lower_px = float(lw) if lw is not None else float(d["lower"])
|
||||||
|
be_flag = parse_breakeven_enabled_form(d.get("breakeven_enabled"))
|
||||||
if is_fib_key_monitor_type(mt):
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not ok_fib:
|
if not ok_fib:
|
||||||
flash(err_fib or "斐波监控添加失败")
|
flash(err_fib or "斐波监控添加失败")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
flash(f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})")
|
flash(
|
||||||
|
f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total})"
|
||||||
|
f"|移动保本:{'开' if be_flag else '关'}"
|
||||||
|
)
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
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("/")
|
||||||
|
if direction_sel == "long" and manual_tp <= upper_px:
|
||||||
|
conn.close()
|
||||||
|
flash("做多趋势单:止盈价应高于上沿(阻力)")
|
||||||
|
return redirect("/")
|
||||||
|
if direction_sel == "short" and manual_tp >= lower_px:
|
||||||
|
conn.close()
|
||||||
|
flash("做空趋势单:止盈价应低于下沿(支撑)")
|
||||||
|
return redirect("/")
|
||||||
|
mtpx = round_price_to_exchange(ex_sym_key, manual_tp)
|
||||||
|
if mtpx is not None:
|
||||||
|
manual_tp = float(mtpx)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
|
"INSERT INTO key_monitors "
|
||||||
(symbol, mt, direction_sel, upper_px, lower_px),
|
"(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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
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}")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
@app.route("/add_order", methods=["POST"])
|
@app.route("/add_order", methods=["POST"])
|
||||||
|
|||||||
@@ -208,6 +208,15 @@
|
|||||||
</select>
|
</select>
|
||||||
<input name="upper" step="0.0001" placeholder="上沿/阻力" required>
|
<input name="upper" step="0.0001" placeholder="上沿/阻力" required>
|
||||||
<input name="lower" 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>
|
<button type="submit">添加</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="rule-tip">{{ key_gate_rule_text }}</div>
|
<div class="rule-tip">{{ key_gate_rule_text }}</div>
|
||||||
@@ -219,6 +228,8 @@
|
|||||||
上:{{ k.upper }} 下:{{ k.lower }}
|
上:{{ k.upper }} 下:{{ k.lower }}
|
||||||
{% if k.fib_entry_price %}| 挂E:{{ k.fib_entry_price }}{% endif %}
|
{% if k.fib_entry_price %}| 挂E:{{ k.fib_entry_price }}{% endif %}
|
||||||
| 已提醒:{{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}
|
| 已提醒:{{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}
|
||||||
|
{% if k.monitor_type in ['箱体突破','收敛突破'] %}| 方案:{{ '标准' if (k.sl_tp_mode or 'standard') == 'standard' else ('1.5H' if k.sl_tp_mode == 'box_1p5' else '趋势') }}{% endif %}
|
||||||
|
| 保本:{{ '开' if k.breakeven_enabled else '关' }}
|
||||||
| 现价:<span id="key-price-{{ k.id }}">-</span>
|
| 现价:<span id="key-price-{{ k.id }}">-</span>
|
||||||
| 距上沿:<span id="key-up-diff-{{ k.id }}">-</span>
|
| 距上沿:<span id="key-up-diff-{{ k.id }}">-</span>
|
||||||
| 距下沿:<span id="key-low-diff-{{ k.id }}">-</span>
|
| 距下沿:<span id="key-low-diff-{{ k.id }}">-</span>
|
||||||
@@ -1168,6 +1179,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");
|
const keyForm = document.getElementById("key-form");
|
||||||
if(keyForm){
|
if(keyForm){
|
||||||
keyForm.addEventListener("submit", (e)=>{
|
keyForm.addEventListener("submit", (e)=>{
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""关键位箱体/收敛:止盈止损方案(Binance / Gate / OKX 共用)。"""
|
||||||
|
|
||||||
|
KEY_SL_TP_MODES = frozenset({"standard", "box_1p5", "trend_manual"})
|
||||||
|
|
||||||
|
KEY_SL_TP_MODE_LABELS = {
|
||||||
|
"standard": "标准突破",
|
||||||
|
"box_1p5": "箱体1R·止盈1.5H",
|
||||||
|
"trend_manual": "趋势单·自填止盈",
|
||||||
|
}
|
||||||
|
|
||||||
|
KEY_MONITOR_AUTO_TYPES_FOR_FORM = frozenset({"箱体突破", "收敛突破"})
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_sl_tp_mode(raw):
|
||||||
|
m = (raw or "standard").strip().lower()
|
||||||
|
if m in ("box_1p5", "box15", "box-1.5", "box_1.5"):
|
||||||
|
return "box_1p5"
|
||||||
|
if m in ("trend_manual", "trend", "manual"):
|
||||||
|
return "trend_manual"
|
||||||
|
if m in KEY_SL_TP_MODES:
|
||||||
|
return m
|
||||||
|
return "standard"
|
||||||
|
|
||||||
|
|
||||||
|
def sl_tp_mode_label(mode):
|
||||||
|
return KEY_SL_TP_MODE_LABELS.get(normalize_sl_tp_mode(mode), normalize_sl_tp_mode(mode))
|
||||||
|
|
||||||
|
|
||||||
|
def sl_tp_mode_from_row(row, default="standard"):
|
||||||
|
try:
|
||||||
|
if hasattr(row, "keys") and "sl_tp_mode" in row.keys():
|
||||||
|
raw = row["sl_tp_mode"]
|
||||||
|
else:
|
||||||
|
raw = row.get("sl_tp_mode") if isinstance(row, dict) else None
|
||||||
|
except Exception:
|
||||||
|
raw = None
|
||||||
|
return normalize_sl_tp_mode(raw if raw not in (None, "") else default)
|
||||||
|
|
||||||
|
|
||||||
|
def breakeven_enabled_from_row(row, default=0):
|
||||||
|
try:
|
||||||
|
if hasattr(row, "keys") and "breakeven_enabled" in row.keys():
|
||||||
|
v = row["breakeven_enabled"]
|
||||||
|
else:
|
||||||
|
v = row.get("breakeven_enabled") if isinstance(row, dict) else None
|
||||||
|
except Exception:
|
||||||
|
v = None
|
||||||
|
if v is None:
|
||||||
|
return int(default) != 0
|
||||||
|
return int(v) != 0
|
||||||
|
|
||||||
|
|
||||||
|
def parse_breakeven_enabled_form(form_value):
|
||||||
|
return 1 if (form_value or "").strip().lower() in ("1", "true", "on", "yes") else 0
|
||||||
|
|
||||||
|
|
||||||
|
def plan_key_sl_tp(
|
||||||
|
mode,
|
||||||
|
direction,
|
||||||
|
upper,
|
||||||
|
lower,
|
||||||
|
checks,
|
||||||
|
*,
|
||||||
|
outside_pct,
|
||||||
|
trend_outside_pct,
|
||||||
|
manual_take_profit=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
以确认 K 收盘 E 为「当前价」计算计划 SL/TP。
|
||||||
|
返回 (E, sl_raw, tp_raw, box_h) 或 None(几何无效 / 模式3缺止盈)。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
E = float(checks["confirm_close"])
|
||||||
|
H = abs(float(upper) - float(lower))
|
||||||
|
except (TypeError, ValueError, KeyError):
|
||||||
|
return None
|
||||||
|
if H <= 0:
|
||||||
|
return None
|
||||||
|
direction = (direction or "long").strip().lower()
|
||||||
|
mode = normalize_sl_tp_mode(mode)
|
||||||
|
|
||||||
|
if mode == "box_1p5":
|
||||||
|
if direction == "long":
|
||||||
|
sl_raw = E - H
|
||||||
|
tp_raw = E + 1.5 * H
|
||||||
|
else:
|
||||||
|
sl_raw = E + H
|
||||||
|
tp_raw = E - 1.5 * H
|
||||||
|
return E, sl_raw, tp_raw, H
|
||||||
|
|
||||||
|
if mode == "trend_manual":
|
||||||
|
try:
|
||||||
|
br_hi = float(checks["breakout_high"])
|
||||||
|
br_lo = float(checks["breakout_low"])
|
||||||
|
tp_raw = float(manual_take_profit)
|
||||||
|
except (TypeError, ValueError, KeyError):
|
||||||
|
return None
|
||||||
|
m = float(trend_outside_pct) / 100.0
|
||||||
|
if direction == "long":
|
||||||
|
sl_raw = br_lo * (1.0 - m) if br_lo > 0 else 0.0
|
||||||
|
if tp_raw <= E or sl_raw <= 0:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
sl_raw = br_hi * (1.0 + m) if br_hi > 0 else 0.0
|
||||||
|
if tp_raw >= E or sl_raw <= 0:
|
||||||
|
return None
|
||||||
|
return E, sl_raw, tp_raw, H
|
||||||
|
|
||||||
|
# standard:突破 K 极值外侧 + 止盈 E±1×H
|
||||||
|
try:
|
||||||
|
br_hi = float(checks["breakout_high"])
|
||||||
|
br_lo = float(checks["breakout_low"])
|
||||||
|
except (TypeError, ValueError, KeyError):
|
||||||
|
return None
|
||||||
|
om = float(outside_pct) / 100.0
|
||||||
|
if direction == "long":
|
||||||
|
sl_raw = br_lo * (1.0 - om) if br_lo > 0 else 0.0
|
||||||
|
tp_raw = E + H
|
||||||
|
else:
|
||||||
|
sl_raw = br_hi * (1.0 + om) if br_hi > 0 else 0.0
|
||||||
|
tp_raw = E - H
|
||||||
|
return E, sl_raw, tp_raw, H
|
||||||
|
|
||||||
|
|
||||||
|
def sl_tp_plan_summary_text(mode, direction, E, sl_raw, tp_raw, box_h, *, outside_pct, trend_outside_pct):
|
||||||
|
"""微信/页面用一行计划 SL/TP 说明。"""
|
||||||
|
mode = normalize_sl_tp_mode(mode)
|
||||||
|
direction = (direction or "long").strip().lower()
|
||||||
|
if mode == "box_1p5":
|
||||||
|
return (
|
||||||
|
f"方案:{sl_tp_mode_label(mode)}|E={E}|SL=E∓1×H({box_h})|TP=E∓1.5×H"
|
||||||
|
)
|
||||||
|
if mode == "trend_manual":
|
||||||
|
return (
|
||||||
|
f"方案:{sl_tp_mode_label(mode)}|E={E}|SL=突破K极值外{trend_outside_pct}%|TP={tp_raw}(录入)"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"方案:{sl_tp_mode_label(mode)}|E={E}|SL=突破K外{outside_pct}%|TP=E±1×H({box_h})"
|
||||||
|
)
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# 关键位止盈止损方案 & 移动保本开关 — 更新说明
|
||||||
|
|
||||||
|
**版本日期:** 2026-05-21
|
||||||
|
**影响实例:** `crypto_monitor_binance`(币安)、`crypto_monitor_gate`(Gate)、`crypto_monitor_okx`(OKX)
|
||||||
|
**共用库:** 仓库根目录 `key_sl_tp_lib.py`(计算逻辑)、`fib_key_monitor_lib.py`(斐波 SL/TP 不变)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 变更摘要
|
||||||
|
|
||||||
|
| 项 | 变更前 | 变更后 |
|
||||||
|
|----|--------|--------|
|
||||||
|
| 箱体/收敛 SL/TP | 仅一种(标准突破) | 添加时可三选一 |
|
||||||
|
| 移动保本(关键位) | 自动单写死开启 | 添加时可勾选,**默认关闭** |
|
||||||
|
| 斐波 SL/TP | H/L 限价方案 | **不变**;仅增加移动保本开关 |
|
||||||
|
| 历史 `key_monitors` | 无新字段 | 缺字段视为 **标准方案 + 保本关** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 数据库(`key_monitors` 新增列)
|
||||||
|
|
||||||
|
启动时自动 `ALTER TABLE`(已有库兼容):
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `sl_tp_mode` | TEXT | `standard` | `standard` / `box_1p5` / `trend_manual` |
|
||||||
|
| `manual_take_profit` | REAL | NULL | 仅 `trend_manual` 使用 |
|
||||||
|
| `breakeven_enabled` | INTEGER | `0` | 0=关,1=开 |
|
||||||
|
|
||||||
|
旧记录无上述列时,读取逻辑按 **`standard` + 保本关** 处理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 三种止盈止损方案(仅箱体突破 / 收敛突破)
|
||||||
|
|
||||||
|
计划价 **E** = 确认 K(倒数第 1 根已闭合 5m)收盘价;**H** = \|上沿 − 下沿\|。
|
||||||
|
自动开仓(币安/Gate)前仍须:**计划 RR(按 E)> `KEY_AUTO_MIN_PLANNED_RR`**(默认 1.5)。
|
||||||
|
|
||||||
|
### 3.1 标准突破 `standard`(原逻辑)
|
||||||
|
|
||||||
|
| 方向 | 止损 SL | 止盈 TP |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 多 | 突破 K 最低价 × (1 − `KEY_STOP_OUTSIDE_BREAKOUT_PCT`/100) | E + 1×H |
|
||||||
|
| 空 | 突破 K 最高价 × (1 + 外侧%) | E − 1×H |
|
||||||
|
|
||||||
|
默认外侧:**0.5%**(`KEY_STOP_OUTSIDE_BREAKOUT_PCT`)。
|
||||||
|
|
||||||
|
### 3.2 箱体 1R / 止盈 1.5H `box_1p5`
|
||||||
|
|
||||||
|
以 **E 为当前价**,风险距离 = 1×H,止盈距离 = 1.5×H,**计划 RR 固定约 1.5:1**。
|
||||||
|
|
||||||
|
| 方向 | 止损 SL | 止盈 TP |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 多 | E − H | E + 1.5×H |
|
||||||
|
| 空 | E + H | E − 1.5×H |
|
||||||
|
|
||||||
|
### 3.3 趋势单 + 自填止盈 `trend_manual`
|
||||||
|
|
||||||
|
| 方向 | 止损 SL | 止盈 TP |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 多 | 突破 K 最低价 × (1 − `KEY_TREND_STOP_OUTSIDE_PCT`/100) | 添加时录入的 `manual_take_profit` |
|
||||||
|
| 空 | 突破 K 最高价 × (1 + 外侧%) | 同上 |
|
||||||
|
|
||||||
|
- 环境变量 **`KEY_TREND_STOP_OUTSIDE_PCT`**,默认 **1**(即 1%)。
|
||||||
|
- 添加时校验:做多止盈 > 上沿;做空止盈 < 下沿。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 斐波回调 0.618 / 0.786
|
||||||
|
|
||||||
|
- **SL/TP**:仍为 `calc_fib_plan`(多:SL=L、TP=H;空:SL=H、TP=L),**无**三方案下拉。
|
||||||
|
- **移动保本**:添加时可勾选;成交写入 `order_monitors` 时带入该勾选状态(默认关)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 移动保本
|
||||||
|
|
||||||
|
| 场景 | 行为 |
|
||||||
|
|------|------|
|
||||||
|
| 关键位添加 | 复选框「移动保本」,**默认不勾选** |
|
||||||
|
| 箱体/收敛自动开仓成功 | `order_monitors.breakeven_enabled` = 添加时的选择 |
|
||||||
|
| 斐波限价成交后 | 同上 |
|
||||||
|
| 人工「实盘下单」 | **不变**:仍为表单勾选,默认仍可按原页面逻辑 |
|
||||||
|
|
||||||
|
触发参数仍用全局 `.env`:`BREAKEVEN_RR_TRIGGER`、`BREAKEVEN_STEP_R`、`BREAKEVEN_OFFSET_PCT`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 前端(关键位添加表单)
|
||||||
|
|
||||||
|
在「上沿 / 下沿」后增加:
|
||||||
|
|
||||||
|
1. **止盈止损方案**(仅类型为箱体突破、收敛突破时显示)
|
||||||
|
2. **趋势单止盈价**(仅选「趋势单·自填止盈」时显示且必填)
|
||||||
|
3. **移动保本**(箱体/收敛/斐波显示;默认不勾)
|
||||||
|
|
||||||
|
活跃列表卡片展示:**方案**、**保本:开/关**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 环境变量
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 标准方案:突破 K 极值外侧 %
|
||||||
|
KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
|
||||||
|
|
||||||
|
# 趋势单方案:突破 K 极值外侧 %
|
||||||
|
KEY_TREND_STOP_OUTSIDE_PCT=1
|
||||||
|
```
|
||||||
|
|
||||||
|
已写入各实例 `.env.example`(Binance / Gate / OKX)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 交易所差异
|
||||||
|
|
||||||
|
| 实例 | 箱体/收敛触发后 |
|
||||||
|
|------|----------------|
|
||||||
|
| **Binance / Gate** | 门控通过 → 按方案算 SL/TP → 市价开仓 → 挂交易所 TP/SL → 写入下单监控 |
|
||||||
|
| **OKX** | 门控通过 → **企业微信提醒**(推送中含录入方案的计划 SL/TP/RR),**不自动市价开仓** |
|
||||||
|
|
||||||
|
OKX 用户按推送中的计划价自行下单;斐波仍为限价 + 成交后挂 TP/SL(与原先一致)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 涉及文件清单
|
||||||
|
|
||||||
|
| 路径 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `key_sl_tp_lib.py` | **新建**,三方案计算与文案 |
|
||||||
|
| `crypto_monitor_binance/app.py` | 门控触发、开仓、斐波、add_key |
|
||||||
|
| `crypto_monitor_binance/templates/index.html` | 表单 + JS + 列表展示 |
|
||||||
|
| `crypto_monitor_binance/.env.example` | `KEY_TREND_STOP_OUTSIDE_PCT` |
|
||||||
|
| `crypto_monitor_gate/app.py` | 同 Binance |
|
||||||
|
| `crypto_monitor_gate/templates/index.html` | 同 Binance |
|
||||||
|
| `crypto_monitor_gate/.env.example` | 同上 |
|
||||||
|
| `crypto_monitor_okx/app.py` | add_key、提醒文案、斐波保本 |
|
||||||
|
| `crypto_monitor_okx/templates/index.html` | 表单 + JS |
|
||||||
|
| `crypto_monitor_okx/.env.example` | 注释项 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 部署与验证建议
|
||||||
|
|
||||||
|
1. `git pull` 后重启三个实例的 Flask 进程(会自动迁移 `key_monitors` 列)。
|
||||||
|
2. 在 `.env` 中按需设置 `KEY_TREND_STOP_OUTSIDE_PCT`(不配则用默认 1)。
|
||||||
|
3. **验证 Binance/Gate**
|
||||||
|
- 添加箱体突破,选「箱体1R·止盈1.5H」,不勾保本 → 触发后微信应显示方案名、保本关、SL/TP 符合 E±H / E±1.5H。
|
||||||
|
- 添加趋势单,填止盈,勾保本 → 成交后持仓卡片「移动保本:开」。
|
||||||
|
4. **验证 OKX**:门控通过时微信应含「录入方案」与计划 SL/TP,并注明提醒模式不自动开仓。
|
||||||
|
5. 旧关键位条目:列表应显示「方案:标准突破」「保本:关」(除非库中已有新字段值)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 代码入口(便于二次开发)
|
||||||
|
|
||||||
|
| 功能 | 符号 |
|
||||||
|
|------|------|
|
||||||
|
| 计划 SL/TP | `plan_key_sl_tp()` in `key_sl_tp_lib.py` |
|
||||||
|
| 按监控行计算 | `_key_plan_sl_tp_for_row()` in各 `app.py` |
|
||||||
|
| 添加关键位 | `add_key()` |
|
||||||
|
| 箱体/收敛轮询 | `check_key_monitors()`(OKX 仅提醒) |
|
||||||
|
| 斐波添加 | `_add_fib_key_monitor(..., breakeven_enabled=)` |
|
||||||
|
| 自动开仓写监控 | `_market_open_for_key_monitor(..., breakeven_enabled=)` |
|
||||||
Reference in New Issue
Block a user