增加关键位监控自动下单
This commit is contained in:
+411
-84
@@ -124,6 +124,13 @@ PRICE_REFRESH_SECONDS = int(os.getenv("PRICE_REFRESH_SECONDS", "5"))
|
||||
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_BREAKOUT_LIMIT_PCT = float(os.getenv("KEY_BREAKOUT_LIMIT_PCT", "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"))
|
||||
ORDER_MONITOR_TYPE_MANUAL = "下单监控"
|
||||
ORDER_MONITOR_TYPE_KEY_AUTO = "关键位监控"
|
||||
|
||||
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
|
||||
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
|
||||
AUTO_TRANSFER_ENABLED = os.getenv("AUTO_TRANSFER_ENABLED", "false").lower() == "true"
|
||||
AUTO_TRANSFER_AMOUNT = float(os.getenv("AUTO_TRANSFER_AMOUNT", "30"))
|
||||
AUTO_TRANSFER_FROM = os.getenv("AUTO_TRANSFER_FROM", "funding")
|
||||
@@ -1093,6 +1100,17 @@ def init_db():
|
||||
c.execute("ALTER TABLE order_monitors ADD COLUMN exchange_margin_usdt REAL")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c.execute(f"ALTER TABLE order_monitors ADD COLUMN monitor_type TEXT DEFAULT '{ORDER_MONITOR_TYPE_MANUAL}'")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c.execute(
|
||||
"UPDATE order_monitors SET monitor_type=? WHERE monitor_type IS NULL OR TRIM(monitor_type)=''",
|
||||
(ORDER_MONITOR_TYPE_MANUAL,),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c.execute("UPDATE order_monitors SET opened_at = datetime('now') WHERE opened_at IS NULL OR opened_at = ''")
|
||||
except: pass
|
||||
@@ -1967,6 +1985,8 @@ def enrich_order_item(raw_item, current_capital):
|
||||
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
||||
except Exception:
|
||||
item["breakeven_enabled"] = 1
|
||||
if not (item.get("monitor_type") or "").strip():
|
||||
item["monitor_type"] = ORDER_MONITOR_TYPE_MANUAL
|
||||
return item
|
||||
|
||||
|
||||
@@ -3461,113 +3481,386 @@ def calc_price_diff_pct(current_price, target_price):
|
||||
return None, None
|
||||
|
||||
|
||||
def can_notify_key_monitor(row, now_dt):
|
||||
max_notify = int(row["max_notify"] or KEY_ALERT_MAX_TIMES)
|
||||
if int(row["notification_count"] or 0) >= max_notify:
|
||||
return False
|
||||
last_at = row["last_notified_at"]
|
||||
if not last_at:
|
||||
return True
|
||||
def _finalize_key_monitor_one_shot(conn, row, last_msg, close_reason):
|
||||
"""本条关键位一次性结案:写历史并从当前表删除。"""
|
||||
n = int(row["notification_count"] or 0) + 1
|
||||
insert_key_monitor_history(conn, row, n, last_msg, close_reason)
|
||||
conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],))
|
||||
|
||||
|
||||
def _key_hard_lines_from_checks(checks):
|
||||
return [
|
||||
f"量能:{'通过' if checks['vol_ok'] else '不通过'}(突破K量 {round(checks['vol_break'], 4)} / 前20均量 {round(checks['avg20'], 4)},阈值1.3x)",
|
||||
f"突破价位:{'通过' if checks['breakout_ok'] else '不通过'}(突破K收盘 {round(float(checks['breakout_close']), 8)},关键位 {checks['edge_price']})",
|
||||
f"突破K幅度:{'通过' if checks['amp_ok'] else '不通过'}({round(checks['amp_pct'], 4)}%,要求0.03%~0.5%)",
|
||||
f"第二根确认:{'通过' if checks['confirm_ok'] else '不通过'}(确认收盘 {checks['confirm_close']},关键位 {checks['edge_price']})",
|
||||
f"日成交量排名:{'通过' if checks['rank_ok'] else '不通过'}({checks['rank']}/{checks['rank_total']},要求前30)",
|
||||
]
|
||||
|
||||
|
||||
def _key_plan_auto_sl_tp(direction, upper, lower, checks, outside_pct):
|
||||
"""
|
||||
计划 SL/TP:止损在突破 K 极值外侧 outside_pct%,止盈为确认收盘 ± 箱体高。
|
||||
返回 (E, raw_sl, raw_tp, box_h)。
|
||||
"""
|
||||
E = float(checks["confirm_close"])
|
||||
H = abs(float(upper) - float(lower))
|
||||
br_hi = float(checks["breakout_high"])
|
||||
br_lo = float(checks["breakout_low"])
|
||||
m = float(outside_pct) / 100.0
|
||||
if direction == "long":
|
||||
sl_raw = br_lo * (1.0 - m) if br_lo > 0 else 0.0
|
||||
tp_raw = E + H
|
||||
else:
|
||||
sl_raw = br_hi * (1.0 + m) if br_hi > 0 else 0.0
|
||||
tp_raw = E - H
|
||||
return E, sl_raw, tp_raw, H
|
||||
|
||||
|
||||
def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_loss, take_profit):
|
||||
"""
|
||||
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入。
|
||||
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
|
||||
"""
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
trading_day = get_trading_day(now)
|
||||
opens_today_before = conn.execute(
|
||||
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
||||
(trading_day,),
|
||||
).fetchone()[0]
|
||||
session_row = ensure_session(conn, trading_day)
|
||||
_, trading_capital_live = get_exchange_capitals(force=True)
|
||||
capital_base = float(trading_capital_live) if trading_capital_live is not None else float(session_row["current_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_price(symbol)
|
||||
if live_price is None:
|
||||
return False, "获取交易所实时价格失败(以损定仓需要当前价)", None
|
||||
try:
|
||||
last_dt = datetime.strptime(last_at, "%Y-%m-%d %H:%M:%S")
|
||||
ensure_markets_loaded()
|
||||
except Exception:
|
||||
return True
|
||||
interval_min = int(row["notify_interval_min"] or KEY_ALERT_INTERVAL_MINUTES)
|
||||
return (now_dt - last_dt).total_seconds() >= interval_min * 60
|
||||
pass
|
||||
lp_r = round_price_to_exchange(exchange_symbol, live_price)
|
||||
if lp_r is not None:
|
||||
live_price = lp_r
|
||||
|
||||
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)
|
||||
|
||||
risk_fraction = calc_risk_fraction(direction, live_price, stop_loss)
|
||||
if risk_fraction is None:
|
||||
return False, "止损方向不合法(相对当前市价);请核对上下沿与方向", None
|
||||
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, "以损定仓后保证金超过当前交易资金", 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
|
||||
|
||||
def breakout_too_far(p, edge_price, limit_pct):
|
||||
try:
|
||||
if edge_price is None or float(edge_price) <= 0:
|
||||
return False
|
||||
diff_pct = abs(float(p) - float(edge_price)) / float(edge_price) * 100
|
||||
return diff_pct > float(limit_pct)
|
||||
except Exception:
|
||||
return False
|
||||
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 = 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)
|
||||
breakeven_enabled = 1
|
||||
|
||||
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) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
symbol,
|
||||
exchange_symbol,
|
||||
direction,
|
||||
trigger_price,
|
||||
stop_loss,
|
||||
stop_loss,
|
||||
take_profit,
|
||||
margin_capital,
|
||||
leverage,
|
||||
trade_style,
|
||||
risk_percent,
|
||||
risk_amount_final,
|
||||
breakeven_rr_trigger,
|
||||
breakeven_offset_pct,
|
||||
breakeven_step_r,
|
||||
0,
|
||||
breakeven_price,
|
||||
breakeven_enabled,
|
||||
notional_value,
|
||||
position_ratio,
|
||||
base_amount,
|
||||
amount,
|
||||
open_order_id,
|
||||
opened_at_bj,
|
||||
opened_at_ms,
|
||||
trading_day,
|
||||
ORDER_MONITOR_TYPE_KEY_AUTO,
|
||||
),
|
||||
)
|
||||
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 = conn.execute(
|
||||
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
||||
(trading_day,),
|
||||
).fetchone()[0]
|
||||
|
||||
return True, None, {
|
||||
"new_order_id": new_order_id,
|
||||
"open_order_id": open_order_id,
|
||||
"trigger_price": trigger_price,
|
||||
"planned_rr_fill": planned_rr,
|
||||
"risk_amount_final": risk_amount_final,
|
||||
"margin_capital": margin_capital,
|
||||
"leverage": leverage,
|
||||
"amount": amount,
|
||||
"base_amount": base_amount,
|
||||
"notional_value": notional_value,
|
||||
"position_ratio": position_ratio,
|
||||
"tpsl_attached": tpsl_attached,
|
||||
"opens_today_before": opens_today_before,
|
||||
"opens_today_after": opens_today_after,
|
||||
"trading_day": trading_day,
|
||||
"risk_percent": risk_percent,
|
||||
"breakeven_rr_trigger": breakeven_rr_trigger,
|
||||
"breakeven_price": breakeven_price,
|
||||
"capital_base_at_open": capital_base,
|
||||
}
|
||||
|
||||
|
||||
# 关键位监控
|
||||
# 关键位监控(箱体/收敛可自动开仓;阻力/支撑位仅单次提醒结案)
|
||||
def check_key_monitors():
|
||||
conn = get_db()
|
||||
rows = conn.execute("SELECT * FROM key_monitors").fetchall()
|
||||
for r in rows:
|
||||
sym, typ, up, low = r["symbol"], r["monitor_type"], r["upper"], r["lower"]
|
||||
sym, typ_raw, up, low = r["symbol"], r["monitor_type"], r["upper"], r["lower"]
|
||||
typ = (typ_raw or "").strip()
|
||||
direction = (r["direction"] or "long").lower()
|
||||
now_dt = app_now()
|
||||
if not can_notify_key_monitor(r, now_dt):
|
||||
continue
|
||||
try:
|
||||
checks = _key_hard_checks(sym, direction, up, low, typ)
|
||||
except Exception:
|
||||
checks = {"ok": False}
|
||||
if not checks.get("ok"):
|
||||
continue
|
||||
|
||||
btc8h_status, _, _ = _status_by_ema55("BTC/USDT", "8h")
|
||||
coin4h_status, _, _ = _status_by_ema55(sym, "4h")
|
||||
risk_tip = None
|
||||
if (direction == "long" and coin4h_status == "空头") or (direction == "short" and coin4h_status == "多头"):
|
||||
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)
|
||||
if direction == "long":
|
||||
tp1 = c_close + box_h
|
||||
tp2 = c_close + box_h * 1.5
|
||||
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 = [
|
||||
f"量能:{'通过' if checks['vol_ok'] else '不通过'}(突破K量 {round(checks['vol_break'], 4)} / 前20均量 {round(checks['avg20'], 4)},阈值1.3x)",
|
||||
f"突破价位:{'通过' if checks['breakout_ok'] else '不通过'}(突破K收盘 {round(float(checks['breakout_close']), 8)},关键位 {checks['edge_price']})",
|
||||
f"突破K幅度:{'通过' if checks['amp_ok'] else '不通过'}({round(checks['amp_pct'], 4)}%,要求0.03%~0.5%)",
|
||||
f"第二根确认:{'通过' if checks['confirm_ok'] else '不通过'}(确认收盘 {checks['confirm_close']},关键位 {checks['edge_price']})",
|
||||
f"日成交量排名:{'通过' if checks['rank_ok'] else '不通过'}({checks['rank']}/{checks['rank_total']},要求前30)",
|
||||
]
|
||||
op_lines = [
|
||||
f"方案A:止盈=箱体1.0倍({round(tp1, 8) if tp1 else '-' }),止损=突破K极值外0.2%({round(sl1, 8) if sl1 else '-' })",
|
||||
f"方案B:止盈=箱体1.5倍({round(tp2, 8) if tp2 else '-' }),止损=箱体关键位外0.2%({round(sl2, 8) if sl2 else '-' })",
|
||||
]
|
||||
hard_lines = _key_hard_lines_from_checks(checks)
|
||||
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(
|
||||
symbol=sym,
|
||||
direction=direction,
|
||||
monitor_type=typ,
|
||||
trigger_time=trigger_time,
|
||||
key_price=key_price,
|
||||
confirm_close=checks["confirm_close"],
|
||||
hard_lines=hard_lines,
|
||||
btc8h_status=btc8h_status,
|
||||
coin4h_status=coin4h_status,
|
||||
swing4h_pct=checks.get("swing4h_pct") or 0.0,
|
||||
op_lines=op_lines,
|
||||
risk_tip=risk_tip,
|
||||
|
||||
alert_only = typ in KEY_MONITOR_ALERT_ONLY_TYPES or (
|
||||
typ not in KEY_MONITOR_AUTO_TYPES and typ not in KEY_MONITOR_ALERT_ONLY_TYPES
|
||||
)
|
||||
send_wechat_msg(msg)
|
||||
new_count = int(r["notification_count"] or 0) + 1
|
||||
max_n = int(r["max_notify"] or KEY_ALERT_MAX_TIMES)
|
||||
conn.execute(
|
||||
"UPDATE key_monitors SET notification_count = ?, last_notified_at = ? WHERE id = ?",
|
||||
(new_count, app_now_str(), r["id"]),
|
||||
)
|
||||
if new_count >= max_n:
|
||||
insert_key_monitor_history(conn, r, new_count, msg, "alerts_complete")
|
||||
conn.execute("DELETE FROM key_monitors WHERE id = ?", (r["id"],))
|
||||
send_wechat_msg(
|
||||
"\n".join(
|
||||
[
|
||||
f"# 🧾 {r['symbol']} 关键位监控结束",
|
||||
"",
|
||||
f"- 原因:已满 {max_n} 次提醒",
|
||||
"- 状态:已自动结束并记入历史",
|
||||
]
|
||||
)
|
||||
|
||||
if alert_only:
|
||||
op_lines = [
|
||||
"- 本条为关键阻力/支撑或非标类型:**仅单次推送**,不进行自动开仓。",
|
||||
"- 本条关键位将在推送后记入历史并从监控列表移除。",
|
||||
]
|
||||
msg = build_wechat_key_monitor_message(
|
||||
symbol=sym,
|
||||
direction=direction,
|
||||
monitor_type=typ,
|
||||
trigger_time=trigger_time,
|
||||
key_price=key_price,
|
||||
confirm_close=checks["confirm_close"],
|
||||
hard_lines=hard_lines,
|
||||
btc8h_status=btc8h_status,
|
||||
coin4h_status=coin4h_status,
|
||||
swing4h_pct=checks.get("swing4h_pct") or 0.0,
|
||||
op_lines=op_lines,
|
||||
risk_tip=risk_tip,
|
||||
)
|
||||
send_wechat_msg(msg)
|
||||
_finalize_key_monitor_one_shot(conn, r, msg, "key_level_alert_only")
|
||||
continue
|
||||
|
||||
E, sl_raw, tp_raw, box_h = _key_plan_auto_sl_tp(
|
||||
direction, up, low, checks, KEY_STOP_OUTSIDE_BREAKOUT_PCT,
|
||||
)
|
||||
exchange_symbol = normalize_exchange_symbol(sym)
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
except Exception:
|
||||
pass
|
||||
sl_px = round_price_to_exchange(exchange_symbol, sl_raw)
|
||||
tp_px = round_price_to_exchange(exchange_symbol, tp_raw)
|
||||
if sl_px is not None:
|
||||
sl_raw = float(sl_px)
|
||||
if tp_px is not None:
|
||||
tp_raw = float(tp_px)
|
||||
|
||||
planned_rr = calc_rr_ratio(direction, E, sl_raw, tp_raw)
|
||||
rr_ok = planned_rr is not None and planned_rr > KEY_AUTO_MIN_PLANNED_RR
|
||||
|
||||
if not rr_ok:
|
||||
fmt_rr = f"{planned_rr:.4f}" if planned_rr is not None else "无法计算(止损/止盈与确认价几何关系无效)"
|
||||
rr_msg = (
|
||||
f"# ⚠️ {sym} 关键位自动单:计划 RR 未达标\n"
|
||||
f"**账户:{_wechat_account_label()}**\n"
|
||||
f"- 类型:{typ}\n"
|
||||
f"- 方向:**{_wechat_direction_text(direction)}**\n"
|
||||
f"- 触发时间:`{trigger_time}`\n"
|
||||
f"- 确认K收盘(E):`{format_price_for_symbol(sym, E)}`\n"
|
||||
f"- 箱体高 H:`{format_price_for_symbol(sym, box_h)}`\n"
|
||||
f"- 计划止损(突破K外侧 {KEY_STOP_OUTSIDE_BREAKOUT_PCT}%):`{format_wechat_scalar_2dp(sl_raw)}`\n"
|
||||
f"- 计划止盈(E±1×H):`{format_price_for_symbol(sym, tp_raw)}`\n"
|
||||
f"- **计划 RR(按确认收盘 E):{fmt_rr} : 1**(要求 **>{KEY_AUTO_MIN_PLANNED_RR}:1**,未开仓)\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
|
||||
|
||||
ok_trade, trade_err, det = _market_open_for_key_monitor(
|
||||
conn, sym, direction, exchange_symbol, sl_raw, tp_raw,
|
||||
)
|
||||
planned_rr_txt = (
|
||||
format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
|
||||
)
|
||||
if not ok_trade:
|
||||
fail_msg = (
|
||||
f"# ❌ {sym} 关键位自动单失败\n"
|
||||
f"**账户:{_wechat_account_label()}**\n"
|
||||
f"- 类型:{typ}\n"
|
||||
f"- 方向:**{_wechat_direction_text(direction)}**\n"
|
||||
f"- 触发时间:`{trigger_time}`\n"
|
||||
f"- 确认K收盘(E):`{format_price_for_symbol(sym, E)}`\n"
|
||||
f"- 计划止损:`{format_wechat_scalar_2dp(sl_raw)}`\n"
|
||||
f"- 计划止盈:`{format_price_for_symbol(sym, tp_raw)}`\n"
|
||||
f"- **计划 RR(按 E):{planned_rr_txt} : 1**(已通过 RR 阈值)\n"
|
||||
f"- **失败原因:{trade_err}**\n"
|
||||
"---\n"
|
||||
"### 硬条件\n"
|
||||
+ "\n".join(f"- {x}" for x in hard_lines)
|
||||
)
|
||||
if risk_tip:
|
||||
fail_msg += f"\n---\n### 逆势风险提示\n- {risk_tip}"
|
||||
send_wechat_msg(fail_msg)
|
||||
_finalize_key_monitor_one_shot(conn, r, fail_msg, "exchange_failed")
|
||||
continue
|
||||
|
||||
tpsl_txt = (
|
||||
"已在交易所挂条件委托(止盈、止损触发单)"
|
||||
if det.get("tpsl_attached")
|
||||
else "⚠️ 条件委托挂接状态异常或未挂上"
|
||||
)
|
||||
rr_fill = det.get("planned_rr_fill")
|
||||
rr_fill_txt = format_wechat_scalar_2dp(rr_fill) if rr_fill is not None else "-"
|
||||
|
||||
succ_msg_lines = [
|
||||
f"# ✅ {sym} 关键位自动开仓成功",
|
||||
f"**账户:{_wechat_account_label()}**",
|
||||
f"- **来源:**{ORDER_MONITOR_TYPE_KEY_AUTO}(市价)",
|
||||
f"- 页面订单 ID:**{det['new_order_id']}**",
|
||||
f"- 交易所订单 ID:`{det.get('open_order_id') or '-'}`",
|
||||
f"- 类型:{typ}|{_wechat_direction_text(direction)}",
|
||||
f"- 触发时间:`{trigger_time}`",
|
||||
f"- 确认K收盘(E):{format_price_for_symbol(sym, E)}(RR 阈值按此计价)",
|
||||
f"- **计划 RR(E):{planned_rr_txt}:1**",
|
||||
f"- 开仓成交价:**{format_price_for_symbol(sym, det['trigger_price'])}**",
|
||||
f"- **成交价侧计划 RR:**{rr_fill_txt}:1",
|
||||
f"- 止损:{format_wechat_scalar_2dp(sl_raw)}",
|
||||
f"- 止盈:{format_price_for_symbol(sym, tp_raw)}",
|
||||
f"- 风险:{det.get('risk_percent')}%≈{format_wechat_scalar_2dp(det.get('risk_amount_final'))}U|基数 {format_wechat_scalar_2dp(det.get('margin_capital'))}U|杠杆 {det.get('leverage')}x",
|
||||
f"- 名义 {format_wechat_scalar_2dp(det.get('notional_value'))}U|张数 {format_wechat_scalar_2dp(det.get('amount'))}|折算标的 {det.get('base_amount')}",
|
||||
f"- **{tpsl_txt}**",
|
||||
f"- 保本触发:{det.get('breakeven_rr_trigger')}R→{format_price_for_symbol(sym, det.get('breakeven_price'))}",
|
||||
f"- 当日开仓次数:**{det.get('opens_today_after')}** / {DAILY_OPEN_ALERT_THRESHOLD}(提醒阈值)",
|
||||
]
|
||||
succ_msg_lines.extend(["---", "### 硬条件"] + [f"- {x}" for x in hard_lines])
|
||||
if risk_tip:
|
||||
succ_msg_lines.extend(["---", "### 逆势风险提示", f"- {risk_tip}"])
|
||||
succ_msg = "\n".join(succ_msg_lines)
|
||||
send_wechat_msg(succ_msg)
|
||||
_finalize_key_monitor_one_shot(conn, r, succ_msg, "auto_opened")
|
||||
|
||||
if det.get("opens_today_before", 0) < DAILY_OPEN_ALERT_THRESHOLD <= det.get("opens_today_after", 0):
|
||||
advice = ai_short_advice(
|
||||
f"用户在北京时间交易日 {det['trading_day']} 已累计开仓 {det['opens_today_after']} 次(阈值 {DAILY_OPEN_ALERT_THRESHOLD})。"
|
||||
f"最新一笔来源为关键位自动单:{sym} {direction},杠杆{det['leverage']}x。"
|
||||
f"用户自述“上头了”。请给克制提醒。"
|
||||
)
|
||||
if advice:
|
||||
send_wechat_msg(f"【AI提醒】今日开仓次数已达 {det['opens_today_after']}\n{advice[:800]}")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -4573,6 +4866,15 @@ def add_key():
|
||||
if not symbol:
|
||||
flash("symbol 不能为空")
|
||||
return redirect("/")
|
||||
direction_sel = (d.get("direction") or "").strip().lower()
|
||||
if direction_sel not in ("long", "short"):
|
||||
flash("请选择做多或做空")
|
||||
return redirect("/")
|
||||
mt = (d.get("type") or "").strip()
|
||||
allowed_types = tuple(KEY_MONITOR_AUTO_TYPES) + tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
|
||||
if mt not in allowed_types:
|
||||
flash("监控类型无效,请选择:箱体突破、收敛突破、关键阻力位、关键支撑位")
|
||||
return redirect("/")
|
||||
rank, total = _daily_volume_rank(symbol)
|
||||
if rank is None:
|
||||
flash("日成交量排名读取失败,请稍后重试")
|
||||
@@ -4581,6 +4883,16 @@ def add_key():
|
||||
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前30,已拒绝添加关键位")
|
||||
return redirect("/")
|
||||
conn = get_db()
|
||||
if mt in KEY_MONITOR_AUTO_TYPES:
|
||||
occupied = conn.execute(
|
||||
"SELECT COUNT(*) FROM order_monitors WHERE status='active'",
|
||||
).fetchone()[0]
|
||||
if occupied and int(occupied) > 0:
|
||||
conn.close()
|
||||
flash(
|
||||
"当前已有实盘持仓:无法添加「箱体突破 / 收敛突破」(会自动开仓)。请平仓后再试,或使用「关键阻力位/关键支撑位」(仅单次提醒)。"
|
||||
)
|
||||
return redirect("/")
|
||||
ex_sym_key = normalize_exchange_symbol(symbol)
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
@@ -4588,11 +4900,25 @@ def add_key():
|
||||
pass
|
||||
upper_px = round_price_to_exchange(ex_sym_key, float(d["upper"]))
|
||||
lower_px = round_price_to_exchange(ex_sym_key, float(d["lower"]))
|
||||
conn.execute("INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
|
||||
(symbol, d["type"], d.get("direction", "long"), upper_px, lower_px))
|
||||
conn.execute(
|
||||
"INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
|
||||
(symbol, mt, direction_sel, upper_px, lower_px),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
ctr = False
|
||||
try:
|
||||
coin4h_status, _, _ = _status_by_ema55(symbol, "4h")
|
||||
ctr = (direction_sel == "long" and coin4h_status == "空头") or (
|
||||
direction_sel == "short" and coin4h_status == "多头"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
flash(f"添加成功({symbol} 日成交量排名 {rank}/{total})")
|
||||
if ctr:
|
||||
flash(
|
||||
"⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。"
|
||||
)
|
||||
return redirect("/")
|
||||
|
||||
@app.route("/add_order", methods=["POST"])
|
||||
@@ -4772,12 +5098,13 @@ def add_order():
|
||||
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
|
||||
breakeven_enabled = 1 if (d.get("breakeven_enabled") or "").strip() in ("1", "true", "on", "yes") else 0
|
||||
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) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
"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) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
symbol, exchange_symbol, direction, trigger_price, stop_loss, stop_loss, take_profit,
|
||||
margin_capital, leverage, trade_style, risk_percent, risk_amount_final, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, 0, breakeven_price,
|
||||
breakeven_enabled,
|
||||
notional_value, position_ratio, base_amount, amount, open_order_id, opened_at_bj, opened_at_ms, trading_day
|
||||
notional_value, position_ratio, base_amount, amount, open_order_id, opened_at_bj, opened_at_ms, trading_day,
|
||||
ORDER_MONITOR_TYPE_MANUAL,
|
||||
)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
Reference in New Issue
Block a user