feat(key-monitor): add program trigger entry across four exchanges

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-14 00:42:21 +08:00
parent c95ca6ac35
commit edf4bb835d
26 changed files with 3002 additions and 76 deletions
+544 -5
View File
@@ -116,8 +116,27 @@ from manual_sltp_lib import (
resolve_entrust_sltp_prices,
resolve_open_sltp_prices,
)
from trigger_entry_key_monitor_lib import (
TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED,
TRIGGER_ENTRY_CLOSE_EXPIRED,
TRIGGER_ENTRY_CLOSE_FILLED,
TRIGGER_ENTRY_CLOSE_TP_INVALIDATE,
TRIGGER_ENTRY_MONITOR_TYPE,
TRIGGER_ENTRY_VALIDITY_HOURS,
check_trigger_entry_intent_limit,
count_pending_trigger_entries,
is_trigger_entry_expired,
is_trigger_entry_key_monitor_type,
trigger_entry_expires_at_text,
trigger_entry_gate_preview,
trigger_entry_invalidate_by_tp,
trigger_entry_reached,
validate_trigger_entry_geometry,
validate_trigger_entry_rr,
)
from position_sizing_lib import (
OPEN_SOURCE_KEY_AUTO,
OPEN_SOURCE_KEY_TRIGGER,
OPEN_SOURCE_MANUAL,
assert_open_source_allowed,
compute_full_margin_sizing,
@@ -1022,6 +1041,7 @@ ENTRY_REASON_OPTIONS = (
"关键位斐波0.618",
"关键位斐波0.786",
"关键位假突破",
"关键位触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS
STATS_SEGMENT_DEFS = (
@@ -1032,6 +1052,7 @@ STATS_SEGMENT_DEFS = (
("key_fib618", "关键位斐波0.618", {"segment": "key_fib618"}),
("key_fib786", "关键位斐波0.786", {"segment": "key_fib786"}),
("key_false_breakout", "关键位假突破", {"segment": "key_false_breakout"}),
("key_trigger", "关键位触价开仓", {"segment": "key_trigger"}),
)
# 复盘表单「其他」选项的 value(非入库值;自定义文本走 entry_reason_custom
ENTRY_REASON_OTHER = "__OTHER__"
@@ -1433,6 +1454,7 @@ def init_db():
"ALTER TABLE key_monitors ADD COLUMN manual_take_profit REAL",
"ALTER TABLE key_monitors ADD COLUMN breakeven_enabled INTEGER DEFAULT 0",
"ALTER TABLE key_monitors ADD COLUMN last_rs_bar_ts INTEGER",
"ALTER TABLE key_monitors ADD COLUMN session_date TEXT",
):
try:
c.execute(ddl)
@@ -1630,6 +1652,8 @@ def _pnl_row_matches_segment(row, segment_key):
return kst == "斐波回调0.786"
if segment_key == "key_false_breakout":
return kst == FALSE_BREAKOUT_MONITOR_TYPE
if segment_key == "key_trigger":
return kst == TRIGGER_ENTRY_MONITOR_TYPE
return False
@@ -1647,6 +1671,7 @@ def _count_opens_for_segment(conn, start_td, end_td, segment_key):
"key_fib618": "斐波回调0.618",
"key_fib786": "斐波回调0.786",
"key_false_breakout": FALSE_BREAKOUT_MONITOR_TYPE,
"key_trigger": TRIGGER_ENTRY_MONITOR_TYPE,
}
kst = kst_map.get(segment_key)
if kst:
@@ -5001,7 +5026,464 @@ def _finalize_fib_key_fill(conn, row):
_finalize_key_monitor_one_shot(conn, row, succ, close_reason)
def check_fib_key_monitors():
def _trigger_entry_exists_for_symbol(conn, symbol):
row = conn.execute(
"SELECT id FROM key_monitors WHERE symbol=? AND monitor_type=?",
(symbol, TRIGGER_ENTRY_MONITOR_TYPE),
).fetchone()
return row is not None
def _add_trigger_entry_key_monitor(
conn,
symbol,
direction_sel,
entry,
sl,
tp,
breakeven_enabled=0,
time_close_enabled=0,
time_close_hours=None,
):
if _trigger_entry_exists_for_symbol(conn, symbol):
return False, f"{symbol} 已有触价开仓监控(同币仅允许一条)"
ex_sym = normalize_exchange_symbol(symbol)
mark = get_symbol_mark_price(symbol)
geom_err = validate_trigger_entry_geometry(direction_sel, entry, sl, tp, mark_at_add=mark)
if geom_err:
return False, geom_err
rr_err = validate_trigger_entry_rr(
direction_sel, entry, sl, tp, KEY_AUTO_MIN_PLANNED_RR, calc_rr_ratio
)
if rr_err:
return False, rr_err
entry = float(round_price_to_exchange(ex_sym, entry) or entry)
sl = float(round_price_to_exchange(ex_sym, sl) or sl)
tp = float(round_price_to_exchange(ex_sym, tp) or tp)
geom_err = validate_trigger_entry_geometry(direction_sel, entry, sl, tp, mark_at_add=mark)
if geom_err:
return False, geom_err
rr_err = validate_trigger_entry_rr(
direction_sel, entry, sl, tp, KEY_AUTO_MIN_PLANNED_RR, calc_rr_ratio
)
if rr_err:
return False, rr_err
ok_live, reason_live = ensure_exchange_live_ready()
if not ok_live:
return False, reason_live
now = app_now()
trading_day = get_trading_day(now)
opens_today = count_opens_for_trading_day(conn, trading_day)
ok_intent, intent_msg = check_trigger_entry_intent_limit(
conn, trading_day, opens_today, DAILY_OPEN_HARD_LIMIT
)
if not ok_intent:
return False, intent_msg
if is_full_margin_mode(POSITION_SIZING_MODE):
ok_flat, flat_msg = full_margin_requires_flat_position(get_active_position_count(conn))
if not ok_flat:
return False, flat_msg
if count_pending_trigger_entries(conn, trading_day) > 0:
return False, "全仓杠杆模式下仅允许一条待触发触价监控"
session_row = ensure_session(conn, trading_day)
_, trading_capital_live = get_exchange_capitals(force=True)
live_capital = float(trading_capital_live) if trading_capital_live is not None else float(session_row["current_capital"])
capital_base = resolve_capital_base_for_key_open(conn, trading_day, live_capital)
available_usdt = get_available_trading_usdt()
if is_full_margin_mode(POSITION_SIZING_MODE):
leverage = leverage_for_full_margin(symbol, BTC_LEVERAGE, ALT_LEVERAGE)
sizing, sizing_err = compute_full_margin_sizing(
symbol=symbol,
available_usdt=available_usdt if available_usdt is not None else 0.0,
capital_base=capital_base,
buffer_ratio=FULL_MARGIN_BUFFER_RATIO,
btc_leverage=BTC_LEVERAGE,
alt_leverage=ALT_LEVERAGE,
funds_decimals=2,
)
if sizing_err:
return False, sizing_err
margin_capital = float(sizing["margin_capital"])
amount_plan = None
else:
default_leverage = get_synced_leverage(ex_sym, direction_sel) or infer_leverage(symbol)
leverage = int(default_leverage) if default_leverage else 5
if leverage <= 0:
leverage = 5
risk_fraction = calc_risk_fraction(direction_sel, entry, sl)
if risk_fraction is None:
return False, "止损方向不合法(相对计划入场价)"
risk_percent = max(0.01, float(RISK_PERCENT))
risk_amount = round(capital_base * risk_percent / 100.0, 4)
notional_value = round(risk_amount / risk_fraction, 4)
margin_capital = round(notional_value / leverage, 4)
if capital_base and margin_capital > capital_base:
return False, "以损定仓后保证金超过当前交易资金"
if available_usdt is not None:
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
if margin_capital > max_margin:
return (
False,
f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {round(max_margin, 2)}U",
)
try:
amount_plan, _ = prepare_order_amount(ex_sym, margin_capital, leverage, entry)
except Exception as e:
return False, friendly_exchange_error(e, available_usdt=available_usdt)
upper_px = round_price_to_exchange(ex_sym, max(entry, tp))
lower_px = round_price_to_exchange(ex_sym, min(entry, sl))
if upper_px is None or lower_px is None or float(upper_px) <= float(lower_px):
upper_px, lower_px = float(max(entry, tp, sl)), float(min(entry, tp, sl))
if upper_px <= lower_px:
lower_px = upper_px * 0.9999
be_flag = 1 if int(breakeven_enabled or 0) != 0 else 0
tc_en, tc_h, _ = time_close_insert_values(time_close_enabled, time_close_hours, None)
conn.execute(
"INSERT INTO key_monitors "
"(symbol, monitor_type, direction, upper, lower, "
"fib_entry_price, fib_stop_loss, fib_take_profit, "
"fib_order_amount, fib_margin_capital, fib_leverage, breakeven_enabled, "
"time_close_enabled, time_close_hours, session_date) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol,
TRIGGER_ENTRY_MONITOR_TYPE,
direction_sel,
float(upper_px),
float(lower_px),
entry,
sl,
tp,
float(amount_plan) if amount_plan is not None else None,
margin_capital,
leverage,
be_flag,
tc_en,
tc_h,
trading_day,
),
)
return True, None
def _market_open_for_trigger_entry(
conn,
symbol,
direction,
exchange_symbol,
entry_price,
stop_loss,
take_profit,
breakeven_enabled=0,
time_close_enabled=0,
time_close_hours=None,
):
"""触价触发后市价开仓,计仓规则与实盘下单/关键位 RR 门槛一致。"""
ok_src, src_msg = assert_open_source_allowed(POSITION_SIZING_MODE, OPEN_SOURCE_KEY_TRIGGER)
if not ok_src:
return False, src_msg, None
now = app_now()
ok, reason = precheck_risk(conn, symbol, direction)
if not ok:
return False, f"风控拒绝下单:{reason}", None
ok_live, reason_live = ensure_exchange_live_ready()
if not ok_live:
return False, reason_live, None
trading_day = get_trading_day(now)
opens_today_before = count_opens_for_trading_day(conn, trading_day)
session_row = ensure_session(conn, trading_day)
_, trading_capital_live = get_exchange_capitals(force=True)
live_capital = float(trading_capital_live) if trading_capital_live is not None else float(session_row["current_capital"])
capital_base = resolve_capital_base_for_key_open(conn, trading_day, live_capital)
trade_style = (DEFAULT_TRADE_STYLE or "trend").strip().lower()
if trade_style not in ("trend", "swing"):
trade_style = "trend"
available_usdt = get_available_trading_usdt()
live_price = get_symbol_mark_price(symbol) or get_price(symbol)
if live_price is None:
return False, "获取标记价/实时价失败", None
try:
ensure_markets_loaded()
except Exception:
pass
lp_r = round_price_to_exchange(exchange_symbol, live_price)
if lp_r is not None:
live_price = float(lp_r)
entry_price = float(entry_price)
sl_adj = round_price_to_exchange(exchange_symbol, float(stop_loss))
tp_adj = round_price_to_exchange(exchange_symbol, float(take_profit))
if sl_adj is not None:
stop_loss = float(sl_adj)
if tp_adj is not None:
take_profit = float(tp_adj)
planned_rr = calc_rr_ratio(direction, entry_price, stop_loss, take_profit)
if planned_rr is None or planned_rr <= KEY_AUTO_MIN_PLANNED_RR:
rr_txt = f"{planned_rr:.4f}" if planned_rr is not None else "无法计算"
return False, f"计划盈亏比 {rr_txt}:1 未达要求(>{KEY_AUTO_MIN_PLANNED_RR}:1", None
risk_percent = max(0.01, float(RISK_PERCENT))
if is_full_margin_mode(POSITION_SIZING_MODE):
ok_flat, flat_msg = full_margin_requires_flat_position(get_active_position_count(conn))
if not ok_flat:
return False, flat_msg, None
leverage = leverage_for_full_margin(symbol, BTC_LEVERAGE, ALT_LEVERAGE)
sizing, sizing_err = compute_full_margin_sizing(
symbol=symbol,
available_usdt=available_usdt if available_usdt is not None else 0.0,
capital_base=capital_base,
buffer_ratio=FULL_MARGIN_BUFFER_RATIO,
btc_leverage=BTC_LEVERAGE,
alt_leverage=ALT_LEVERAGE,
funds_decimals=2,
)
if sizing_err:
return False, sizing_err, None
margin_capital = float(sizing["margin_capital"])
notional_value = float(sizing["notional_value"])
position_ratio = float(sizing["position_ratio"])
risk_amount = margin_capital
else:
default_leverage = get_synced_leverage(exchange_symbol, direction) or infer_leverage(symbol)
leverage = int(default_leverage) if default_leverage else 5
if leverage <= 0:
leverage = 5
risk_fraction = calc_risk_fraction(direction, entry_price, stop_loss)
if risk_fraction is None:
return False, "止损方向不合法(相对计划入场价)", None
risk_amount = round(capital_base * risk_percent / 100.0, 4)
notional_value = round(risk_amount / risk_fraction, 4)
margin_capital = round(notional_value / leverage, 4)
if capital_base and margin_capital > capital_base:
return False, "以损定仓后保证金超过当前交易资金", None
if available_usdt is not None:
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
if margin_capital > max_margin:
return (
False,
f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {round(max_margin, 2)}U",
None,
)
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
try:
amount, quote_price = prepare_order_amount(exchange_symbol, margin_capital, leverage, live_price)
contract_size = get_contract_size(exchange_symbol)
base_amount = round(float(amount) * contract_size, 8)
order_resp = place_exchange_order(
exchange_symbol, direction, amount, leverage,
stop_loss=stop_loss, take_profit=take_profit,
)
open_order_id = order_resp.get("id", "")
tpsl_attached = bool(order_resp.get("tpsl_attached"))
trigger_price = resolve_order_entry_price(order_resp, exchange_symbol, quote_price)
except Exception as e:
return False, friendly_exchange_error(e, available_usdt=available_usdt), None
trigger_price = round_price_to_exchange(exchange_symbol, trigger_price)
stop_loss = round_price_to_exchange(exchange_symbol, stop_loss)
take_profit = round_price_to_exchange(exchange_symbol, take_profit)
opened_at_bj = app_now_str()
opened_at_ms = _to_ms_with_fallback(None, opened_at_bj)
planned_rr_fill = calc_rr_ratio(direction, trigger_price, stop_loss, take_profit)
breakeven_rr_trigger = float(BREAKEVEN_RR_TRIGGER)
breakeven_offset_pct = float(BREAKEVEN_OFFSET_PCT)
breakeven_step_r = float(BREAKEVEN_STEP_R) if float(BREAKEVEN_STEP_R) > 0 else 1.0
risk_amount_final = calc_risk_amount_from_plan(direction, trigger_price, stop_loss, margin_capital, leverage)
if risk_amount_final is None:
risk_amount_final = risk_amount
else:
try:
risk_amount_final = round(float(risk_amount_final), 4)
except (TypeError, ValueError):
risk_amount_final = risk_amount
if direction == "short":
breakeven_raw = float(trigger_price) * (1 - breakeven_offset_pct / 100.0)
else:
breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
be_enabled = 1 if int(breakeven_enabled or 0) != 0 else 0
tc_en, tc_h, tc_at = time_close_insert_values(time_close_enabled, time_close_hours, opened_at_ms)
risk_percent_db = risk_percent_for_storage(POSITION_SIZING_MODE, risk_percent)
conn.execute(
"INSERT INTO order_monitors "
"(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, "
"margin_capital, leverage, trade_style, risk_percent, risk_amount, "
"breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, "
"notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, key_signal_type, "
"time_close_enabled, time_close_hours, time_close_at_ms) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol,
exchange_symbol,
direction,
trigger_price,
stop_loss,
stop_loss,
take_profit,
margin_capital,
leverage,
trade_style,
risk_percent_db,
risk_amount_final,
breakeven_rr_trigger,
breakeven_offset_pct,
breakeven_step_r,
0,
breakeven_price,
be_enabled,
notional_value,
position_ratio,
base_amount,
amount,
open_order_id,
opened_at_bj,
opened_at_ms,
trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO,
stored_key_signal_type(TRIGGER_ENTRY_MONITOR_TYPE),
tc_en,
tc_h,
tc_at,
),
)
new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
try_persist_exchange_margin_for_order(conn, new_order_id, exchange_symbol, direction, order_leverage=leverage)
opens_today_after = count_opens_for_trading_day(conn, trading_day)
return True, None, {
"new_order_id": new_order_id,
"open_order_id": open_order_id,
"trigger_price": trigger_price,
"planned_rr_fill": planned_rr_fill,
"risk_amount_final": risk_amount_final,
"margin_capital": margin_capital,
"leverage": leverage,
"amount": amount,
"tpsl_attached": tpsl_attached,
"opens_today_before": opens_today_before,
"opens_today_after": opens_today_after,
"trading_day": trading_day,
"stop_loss": stop_loss,
"take_profit": take_profit,
}
def _execute_trigger_entry_cross(conn, row):
"""标记价触达计划入场:先删监控行防重复触发,再市价开仓。"""
symbol = row["symbol"]
direction = (row["direction"] or "long").lower()
ex_sym = normalize_exchange_symbol(symbol)
entry = float(_sqlite_row_val(row, "fib_entry_price") or 0)
sl = float(_sqlite_row_val(row, "fib_stop_loss") or 0)
tp = float(_sqlite_row_val(row, "fib_take_profit") or 0)
be_en = breakeven_enabled_from_row(row, 0)
tc_en, tc_h, _ = time_close_settings_from_row(row)
kid = int(row["id"])
conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,))
conn.commit()
ok, err, det = _market_open_for_trigger_entry(
conn,
symbol,
direction,
ex_sym,
entry,
sl,
tp,
breakeven_enabled=be_en,
time_close_enabled=tc_en,
time_close_hours=tc_h,
)
if ok and det:
rr_txt = format_wechat_scalar_2dp(det.get("planned_rr_fill")) if det.get("planned_rr_fill") is not None else "-"
msg = (
f"# ✅ {symbol} 触价开仓成交\n"
f"**账户:{_wechat_account_label()}**\n"
f"- 来源:{ORDER_MONITOR_TYPE_KEY_AUTO}(程序触价 @ E\n"
f"- 类型:{TRIGGER_ENTRY_MONITOR_TYPE}{_wechat_direction_text(direction)}\n"
f"- 订单 ID**{det.get('new_order_id')}**\n"
f"- 计划入场:{format_price_for_symbol(symbol, entry)}\n"
f"- 成交价:{format_price_for_symbol(symbol, det.get('trigger_price'))}\n"
f"- 止损:{format_wechat_scalar_2dp(det.get('stop_loss'))}|止盈:{format_price_for_symbol(symbol, det.get('take_profit'))}\n"
f"- 计划 RR{rr_txt}:1\n"
f"- {'已挂交易所 TP/SL' if det.get('tpsl_attached') else 'TP/SL 未挂上'}\n"
)
send_wechat_msg(msg)
insert_key_monitor_history(conn, row, 0, msg, TRIGGER_ENTRY_CLOSE_FILLED)
return True, None
fail_msg = err or "触价触发后开仓失败"
send_wechat_msg(
f"# ❌ {symbol} 触价开仓失败\n"
f"**账户:{_wechat_account_label()}**\n"
f"- 计划入场:{format_price_for_symbol(symbol, entry)}\n"
f"- 原因:{fail_msg}\n"
)
insert_key_monitor_history(conn, row, 0, fail_msg, TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED)
return False, fail_msg
def check_trigger_entry_key_monitors():
conn = get_db()
rows = conn.execute("SELECT * FROM key_monitors WHERE monitor_type=?", (TRIGGER_ENTRY_MONITOR_TYPE,)).fetchall()
now_dt = app_now()
for r in rows:
symbol = r["symbol"]
direction = (r["direction"] or "long").lower()
entry = float(_sqlite_row_val(r, "fib_entry_price") or 0)
sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0)
tp = float(_sqlite_row_val(r, "fib_take_profit") or 0)
if entry <= 0 or sl <= 0 or tp <= 0:
_finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid")
continue
mark = get_symbol_mark_price(symbol)
if mark is None:
continue
if is_trigger_entry_expired(r["created_at"], now_dt, hours=TRIGGER_ENTRY_VALIDITY_HOURS):
exp_txt = trigger_entry_expires_at_text(r["created_at"], hours=TRIGGER_ENTRY_VALIDITY_HOURS)
msg = (
f"# ⚠️ {symbol} 触价开仓已过期\n"
f"**账户:{_wechat_account_label()}**\n"
f"- 类型:{TRIGGER_ENTRY_MONITOR_TYPE}{_wechat_direction_text(direction)}\n"
f"- 有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h(应于 {exp_txt} 前触发)\n"
)
send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_EXPIRED)
continue
if trigger_entry_invalidate_by_tp(direction, mark, tp):
msg = (
f"# ⚠️ {symbol} 触价开仓失效\n"
f"**账户:{_wechat_account_label()}**\n"
f"- 标记价 {format_price_for_symbol(symbol, mark)} 已触达止盈侧(未成交)\n"
)
send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, r, msg, TRIGGER_ENTRY_CLOSE_TP_INVALIDATE)
continue
if trigger_entry_reached(direction, mark, entry):
try:
_execute_trigger_entry_cross(conn, r)
except Exception as e:
fail_msg = friendly_exchange_error(e)
try:
insert_key_monitor_history(conn, r, 0, fail_msg, TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED)
except Exception:
pass
send_wechat_msg(
f"# ❌ {symbol} 触价开仓异常\n**账户:{_wechat_account_label()}**\n- {fail_msg}\n"
)
conn.commit()
conn.close()
conn = get_db()
rows = conn.execute("SELECT * FROM key_monitors").fetchall()
for r in rows:
@@ -5879,6 +6361,7 @@ def background_task():
conn.close()
force_close_before_reset()
check_fib_key_monitors()
check_trigger_entry_key_monitors()
_roll_cfg = app.extensions.get("strategy_roll_cfg")
if _roll_cfg:
from strategy_roll_monitor_lib import check_roll_monitors
@@ -6276,6 +6759,7 @@ def render_main_page(page="trade"):
key_stop_outside_breakout_pct=KEY_STOP_OUTSIDE_BREAKOUT_PCT,
key_trend_stop_outside_pct=KEY_TREND_STOP_OUTSIDE_PCT,
false_breakout_validity_hours=FALSE_BREAKOUT_VALIDITY_HOURS,
trigger_entry_validity_hours=TRIGGER_ENTRY_VALIDITY_HOURS,
)
strategy_extra = {}
if page in ("strategy", "strategy_trend", "strategy_roll", "strategy_records"):
@@ -6451,7 +6935,7 @@ def api_account_snapshot():
def api_price_snapshot():
conn = get_db()
key_rows = conn.execute(
"SELECT id,symbol,monitor_type,direction,upper,lower,fib_entry_price,fib_limit_order_id,created_at FROM key_monitors"
"SELECT id,symbol,monitor_type,direction,upper,lower,fib_entry_price,fib_stop_loss,fib_take_profit,fib_limit_order_id,created_at FROM key_monitors"
).fetchall()
order_rows = conn.execute(
"SELECT id,symbol,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,"
@@ -6491,7 +6975,8 @@ def api_price_snapshot():
for r in key_rows:
is_fib = is_fib_key_monitor_type(r["monitor_type"])
is_fb = is_false_breakout_key_monitor_type(r["monitor_type"])
if is_fib or is_fb:
is_te = is_trigger_entry_key_monitor_type(r["monitor_type"])
if is_fib or is_fb or is_te:
price = get_symbol_mark_price(r["symbol"])
else:
price = prices.get(r["symbol"])
@@ -6525,6 +7010,24 @@ def api_price_snapshot():
gate_summary = prev.get("summary") or "-"
gate_metrics = prev.get("metrics") or ""
fb_gate_ok = bool(prev.get("gate_ok"))
elif is_te:
direction = (r["direction"] or "long").lower()
entry = _sqlite_row_val(r, "fib_entry_price")
tp_v = _sqlite_row_val(r, "fib_take_profit")
entry_txt = format_price_for_symbol(r["symbol"], entry) if entry else "-"
tp_txt = format_price_for_symbol(r["symbol"], tp_v) if tp_v else "-"
tp_inv = trigger_entry_invalidate_by_tp(direction, price, float(tp_v)) if tp_v else False
prev = trigger_entry_gate_preview(
entry_display=entry_txt,
take_profit_display=tp_txt,
created_at=_sqlite_row_val(r, "created_at"),
now=app_now(),
tp_invalidated=tp_inv,
hours=TRIGGER_ENTRY_VALIDITY_HOURS,
)
gate_summary = prev.get("summary") or "-"
gate_metrics = prev.get("metrics") or ""
fib_gate_ok = bool(prev.get("gate_ok"))
elif (r["monitor_type"] or "").strip() in KEY_MONITOR_RS_TYPES:
try:
prev = _key_rs_gate_preview(r["symbol"], r["upper"], r["lower"])
@@ -7123,14 +7626,15 @@ def add_key():
+ tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
+ tuple(FIB_KEY_MONITOR_TYPES)
+ (FALSE_BREAKOUT_MONITOR_TYPE,)
+ (TRIGGER_ENTRY_MONITOR_TYPE,)
)
if mt not in allowed_types:
flash("监控类型无效")
return redirect("/key_monitor")
if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt):
flash(
"全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;"
"请改用阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。"
"全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;"
"可使用「触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。"
)
return redirect("/key_monitor")
skip_volume_rank = is_false_breakout_key_monitor_type(mt)
@@ -7166,6 +7670,41 @@ def add_key():
tc_h = parse_time_close_hours_form(d.get("time_close_hours")) if tc_en else None
if tc_en and not tc_h:
tc_en = 0
if is_trigger_entry_key_monitor_type(mt):
if direction_sel not in ("long", "short"):
conn.close()
conn = None
flash("触价开仓请选择做多或做空")
return redirect("/key_monitor")
try:
entry_px = float(d.get("trigger_entry") or 0)
sl_px = float(d.get("trigger_sl") or 0)
tp_px = float(d.get("trigger_tp") or 0)
except (TypeError, ValueError):
entry_px = sl_px = tp_px = 0
if entry_px <= 0 or sl_px <= 0 or tp_px <= 0:
conn.close()
conn = None
flash("触价开仓须填写有效的入场价、止损价、止盈价")
return redirect("/key_monitor")
ok_te, err_te = _add_trigger_entry_key_monitor(
conn, symbol, direction_sel, entry_px, sl_px, tp_px, breakeven_enabled=be_flag,
time_close_enabled=tc_en, time_close_hours=tc_h,
)
conn.commit()
conn.close()
conn = None
if not ok_te:
flash(err_te or "触价开仓监控添加失败")
return redirect("/key_monitor")
flash(
f"触价开仓已添加({symbol} 日成交量排名 {rank}/{total}"
f"|有效期 {TRIGGER_ENTRY_VALIDITY_HOURS}h"
f"|标记价触达入场价后下一轮询市价开仓"
f"|移动保本:{'' if be_flag else ''}"
+ (f"{time_close_label(tc_h)}" if tc_en else "")
)
return redirect("/key_monitor")
if is_false_breakout_key_monitor_type(mt):
fb_sym = normalize_false_breakout_symbol(symbol)
if not fb_sym:
+19 -7
View File
@@ -1520,14 +1520,19 @@ function syncKeyMonitorFormFields(){
const autoTypes = new Set(["箱体突破","收敛突破"]);
const fibTypes = new Set(["斐波回调0.618","斐波回调0.786"]);
const fbTypes = new Set(["假突破"]);
const teTypes = new Set(["触价开仓"]);
const rsTypes = new Set(["关键阻力位","关键支撑位"]);
const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb;
const showTe = teTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb || showTe;
const showDir = !rsTypes.has(t);
const upperEl = document.getElementById("key-upper");
const lowerEl = document.getElementById("key-lower");
const fbPriceEl = document.getElementById("key-fb-price");
const teEntryEl = document.getElementById("key-trigger-entry");
const teSlEl = document.getElementById("key-trigger-sl");
const teTpEl = document.getElementById("key-trigger-tp");
if(dirEl){
dirEl.style.display = showDir ? "" : "none";
dirEl.required = showDir;
@@ -1541,15 +1546,16 @@ function syncKeyMonitorFormFields(){
}
if(beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if(window.TimeCloseUI) TimeCloseUI.syncKeyTimeCloseVisibility(showBe);
const hideBounds = showFb || showTe;
if(upperEl){
upperEl.style.display = showFb ? "none" : "";
upperEl.required = !showFb;
if(showFb) upperEl.value = "";
upperEl.style.display = hideBounds ? "none" : "";
upperEl.required = !hideBounds;
if(hideBounds) upperEl.value = "";
}
if(lowerEl){
lowerEl.style.display = showFb ? "none" : "";
lowerEl.required = !showFb;
if(showFb) lowerEl.value = "";
lowerEl.style.display = hideBounds ? "none" : "";
lowerEl.required = !hideBounds;
if(hideBounds) lowerEl.value = "";
}
if(fbPriceEl){
fbPriceEl.style.display = showFb ? "" : "none";
@@ -1557,6 +1563,12 @@ function syncKeyMonitorFormFields(){
if(!showFb) fbPriceEl.value = "";
fbPriceEl.placeholder = (dirEl && dirEl.value === "short") ? "高点(阻力)" : ((dirEl && dirEl.value === "long") ? "低点(支撑)" : "做空填高点/做多填低点");
}
[teEntryEl, teSlEl, teTpEl].forEach((el)=>{
if(!el) return;
el.style.display = showTe ? "" : "none";
el.required = showTe;
if(!showTe) el.value = "";
});
}
const keyTypeSel = document.querySelector('#key-form [name="type"]');
const keyModeSel = document.getElementById("key-sl-tp-mode");
+3 -2
View File
@@ -65,9 +65,10 @@
| **收敛突破** | 同上(自动开仓类)。 |
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 |
| **关键支撑位** | 同上(仅提醒)。 |
| **触价开仓** | **不挂交易所限价**;标记价触达计划入场价后 **下一轮询市价开仓**RR 门槛同关键位 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h**;全仓杠杆模式可用。 |
3. **方向**:做多 / 做空(选)。
4. **上沿 / 下沿**:必填;保存时会按交易所 **价格精度** 取整
3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。
4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**
**限制:**
活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**(默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**」;仍可添加「**关键阻力位 / 支撑位**」。