feat(key-monitor): add program trigger entry across four exchanges
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+543
-4
@@ -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,6 +5026,463 @@ def _finalize_fib_key_fill(conn, row):
|
||||
_finalize_key_monitor_one_shot(conn, row, succ, close_reason)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def check_fib_key_monitors():
|
||||
conn = get_db()
|
||||
rows = conn.execute("SELECT * FROM key_monitors").fetchall()
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user