增加关键位监控自动下单
This commit is contained in:
@@ -60,6 +60,10 @@ BINANCE_TRIGGER_WORKING_TYPE=CONTRACT_PRICE
|
|||||||
# 关键位监控:5m收线突破过滤参数
|
# 关键位监控:5m收线突破过滤参数
|
||||||
KLINE_TIMEFRAME=5m
|
KLINE_TIMEFRAME=5m
|
||||||
KEY_BREAKOUT_LIMIT_PCT=1.5
|
KEY_BREAKOUT_LIMIT_PCT=1.5
|
||||||
|
# 关键位自动单:计划 RR 阈值(严格大于该值才开仓,按确认K收盘 E 计算)
|
||||||
|
KEY_AUTO_MIN_PLANNED_RR=1.5
|
||||||
|
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
|
||||||
|
KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
|
||||||
KEY_ALERT_MAX_TIMES=3
|
KEY_ALERT_MAX_TIMES=3
|
||||||
KEY_ALERT_INTERVAL_MINUTES=5
|
KEY_ALERT_INTERVAL_MINUTES=5
|
||||||
|
|
||||||
|
|||||||
+428
-84
@@ -124,6 +124,12 @@ PRICE_REFRESH_SECONDS = int(os.getenv("PRICE_REFRESH_SECONDS", "5"))
|
|||||||
KEY_ALERT_MAX_TIMES = int(os.getenv("KEY_ALERT_MAX_TIMES", "3"))
|
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_BREAKOUT_LIMIT_PCT = float(os.getenv("KEY_BREAKOUT_LIMIT_PCT", "1.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_ENABLED = os.getenv("AUTO_TRANSFER_ENABLED", "false").lower() == "true"
|
||||||
AUTO_TRANSFER_AMOUNT = float(os.getenv("AUTO_TRANSFER_AMOUNT", "30"))
|
AUTO_TRANSFER_AMOUNT = float(os.getenv("AUTO_TRANSFER_AMOUNT", "30"))
|
||||||
AUTO_TRANSFER_FROM = os.getenv("AUTO_TRANSFER_FROM", "funding")
|
AUTO_TRANSFER_FROM = os.getenv("AUTO_TRANSFER_FROM", "funding")
|
||||||
@@ -1089,6 +1095,17 @@ def init_db():
|
|||||||
c.execute("ALTER TABLE order_monitors ADD COLUMN breakeven_enabled INTEGER DEFAULT 1")
|
c.execute("ALTER TABLE order_monitors ADD COLUMN breakeven_enabled INTEGER DEFAULT 1")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
try:
|
||||||
c.execute("UPDATE order_monitors SET opened_at = datetime('now') WHERE opened_at IS NULL OR opened_at = ''")
|
c.execute("UPDATE order_monitors SET opened_at = datetime('now') WHERE opened_at IS NULL OR opened_at = ''")
|
||||||
except: pass
|
except: pass
|
||||||
@@ -1760,6 +1777,18 @@ def format_price_for_symbol(symbol, value):
|
|||||||
return text.rstrip("0").rstrip(".") if "." in text else text
|
return text.rstrip("0").rstrip(".") if "." in text else text
|
||||||
|
|
||||||
|
|
||||||
|
def round_price_to_exchange(exchange_symbol, price):
|
||||||
|
"""将价格按 U 本位永续 tick 取整;失败返回 None。"""
|
||||||
|
if price is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
ensure_markets_loaded()
|
||||||
|
sym = normalize_exchange_symbol(exchange_symbol)
|
||||||
|
return float(exchange.price_to_precision(sym, float(price)))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def format_hold_minutes(minutes):
|
def format_hold_minutes(minutes):
|
||||||
if not minutes:
|
if not minutes:
|
||||||
return "0分钟"
|
return "0分钟"
|
||||||
@@ -1975,6 +2004,8 @@ def enrich_order_item(raw_item, current_capital):
|
|||||||
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
||||||
except Exception:
|
except Exception:
|
||||||
item["breakeven_enabled"] = 1
|
item["breakeven_enabled"] = 1
|
||||||
|
if not (item.get("monitor_type") or "").strip():
|
||||||
|
item["monitor_type"] = ORDER_MONITOR_TYPE_MANUAL
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
@@ -3332,113 +3363,383 @@ def calc_price_diff_pct(current_price, target_price):
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def can_notify_key_monitor(row, now_dt):
|
def _finalize_key_monitor_one_shot(conn, row, last_msg, close_reason):
|
||||||
max_notify = int(row["max_notify"] or KEY_ALERT_MAX_TIMES)
|
"""本条关键位一次性结案:写历史并从当前表删除。"""
|
||||||
if int(row["notification_count"] or 0) >= max_notify:
|
n = int(row["notification_count"] or 0) + 1
|
||||||
return False
|
insert_key_monitor_history(conn, row, n, last_msg, close_reason)
|
||||||
last_at = row["last_notified_at"]
|
conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],))
|
||||||
if not last_at:
|
|
||||||
return True
|
|
||||||
|
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 写入(Binance U 本位)。
|
||||||
|
返回 (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:
|
try:
|
||||||
last_dt = datetime.strptime(last_at, "%Y-%m-%d %H:%M:%S")
|
ensure_markets_loaded()
|
||||||
except Exception:
|
except Exception:
|
||||||
return True
|
pass
|
||||||
interval_min = int(row["notify_interval_min"] or KEY_ALERT_INTERVAL_MINUTES)
|
lp_adj = round_price_to_exchange(exchange_symbol, live_price)
|
||||||
return (now_dt - last_dt).total_seconds() >= interval_min * 60
|
if lp_adj is not None:
|
||||||
|
live_price = float(lp_adj)
|
||||||
|
|
||||||
|
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, FUNDS_DECIMALS)
|
||||||
|
notional_value = round(risk_amount / risk_fraction, FUNDS_DECIMALS)
|
||||||
|
margin_capital = round(notional_value / leverage, FUNDS_DECIMALS)
|
||||||
|
|
||||||
|
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), FUNDS_DECIMALS)
|
||||||
|
if margin_capital > max_margin:
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"保证金不足:交易账户可用约 {round(available_usdt, FUNDS_DECIMALS)}U,当前最多建议 {max_margin}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:
|
try:
|
||||||
if edge_price is None or float(edge_price) <= 0:
|
amount, quote_price = prepare_order_amount(exchange_symbol, margin_capital, leverage, live_price)
|
||||||
return False
|
contract_size = get_contract_size(exchange_symbol)
|
||||||
diff_pct = abs(float(p) - float(edge_price)) / float(edge_price) * 100
|
base_amount = round(float(amount) * contract_size, 8)
|
||||||
return diff_pct > float(limit_pct)
|
order_resp = place_exchange_order(
|
||||||
except Exception:
|
exchange_symbol, direction, amount, leverage,
|
||||||
return False
|
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
|
||||||
|
|
||||||
|
tr_adj = round_price_to_exchange(exchange_symbol, trigger_price)
|
||||||
|
if tr_adj is not None:
|
||||||
|
trigger_price = float(tr_adj)
|
||||||
|
sl_f = round_price_to_exchange(exchange_symbol, stop_loss)
|
||||||
|
if sl_f is not None:
|
||||||
|
stop_loss = float(sl_f)
|
||||||
|
tp_f = round_price_to_exchange(exchange_symbol, take_profit)
|
||||||
|
if tp_f is not None:
|
||||||
|
take_profit = float(tp_f)
|
||||||
|
|
||||||
|
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) or risk_amount
|
||||||
|
|
||||||
|
if direction == "short":
|
||||||
|
breakeven_price = round(float(trigger_price) * (1 - breakeven_offset_pct / 100.0), 8)
|
||||||
|
else:
|
||||||
|
breakeven_price = round(float(trigger_price) * (1 + breakeven_offset_pct / 100.0), 8)
|
||||||
|
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])
|
||||||
|
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():
|
def check_key_monitors():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute("SELECT * FROM key_monitors").fetchall()
|
rows = conn.execute("SELECT * FROM key_monitors").fetchall()
|
||||||
for r in rows:
|
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()
|
direction = (r["direction"] or "long").lower()
|
||||||
now_dt = app_now()
|
|
||||||
if not can_notify_key_monitor(r, now_dt):
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
checks = _key_hard_checks(sym, direction, up, low, typ)
|
checks = _key_hard_checks(sym, direction, up, low, typ)
|
||||||
except Exception:
|
except Exception:
|
||||||
checks = {"ok": False}
|
checks = {"ok": False}
|
||||||
if not checks.get("ok"):
|
if not checks.get("ok"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
btc8h_status, _, _ = _status_by_ema55("BTC/USDT", "8h")
|
btc8h_status, _, _ = _status_by_ema55("BTC/USDT", "8h")
|
||||||
coin4h_status, _, _ = _status_by_ema55(sym, "4h")
|
coin4h_status, _, _ = _status_by_ema55(sym, "4h")
|
||||||
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":
|
hard_lines = _key_hard_lines_from_checks(checks)
|
||||||
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 '-' })",
|
|
||||||
]
|
|
||||||
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(
|
|
||||||
symbol=sym,
|
alert_only = typ in KEY_MONITOR_ALERT_ONLY_TYPES or (
|
||||||
direction=direction,
|
typ not in KEY_MONITOR_AUTO_TYPES and typ not in KEY_MONITOR_ALERT_ONLY_TYPES
|
||||||
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)
|
|
||||||
new_count = int(r["notification_count"] or 0) + 1
|
if alert_only:
|
||||||
max_n = int(r["max_notify"] or KEY_ALERT_MAX_TIMES)
|
op_lines = [
|
||||||
conn.execute(
|
"- 本条为关键阻力/支撑或非标类型:**仅单次推送**,不进行自动开仓。",
|
||||||
"UPDATE key_monitors SET notification_count = ?, last_notified_at = ? WHERE id = ?",
|
"- 本条关键位将在推送后记入历史并从监控列表移除。",
|
||||||
(new_count, app_now_str(), r["id"]),
|
]
|
||||||
)
|
msg = build_wechat_key_monitor_message(
|
||||||
if new_count >= max_n:
|
symbol=sym,
|
||||||
insert_key_monitor_history(conn, r, new_count, msg, "alerts_complete")
|
direction=direction,
|
||||||
conn.execute("DELETE FROM key_monitors WHERE id = ?", (r["id"],))
|
monitor_type=typ,
|
||||||
send_wechat_msg(
|
trigger_time=trigger_time,
|
||||||
"\n".join(
|
key_price=key_price,
|
||||||
[
|
confirm_close=checks["confirm_close"],
|
||||||
f"# 🧾 {r['symbol']} 关键位监控结束",
|
hard_lines=hard_lines,
|
||||||
"",
|
btc8h_status=btc8h_status,
|
||||||
f"- 原因:已满 {max_n} 次提醒",
|
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 = (
|
||||||
|
"已在交易所挂止盈/止损触发单(Binance U 本位条件单)"
|
||||||
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -4409,6 +4710,15 @@ def add_key():
|
|||||||
if not symbol:
|
if not symbol:
|
||||||
flash("symbol 不能为空")
|
flash("symbol 不能为空")
|
||||||
return redirect("/")
|
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)
|
rank, total = _daily_volume_rank(symbol)
|
||||||
if rank is None:
|
if rank is None:
|
||||||
flash("日成交量排名读取失败,请稍后重试")
|
flash("日成交量排名读取失败,请稍后重试")
|
||||||
@@ -4417,11 +4727,44 @@ def add_key():
|
|||||||
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前30,已拒绝添加关键位")
|
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前30,已拒绝添加关键位")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
conn.execute("INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
|
if mt in KEY_MONITOR_AUTO_TYPES:
|
||||||
(symbol, d["type"], d.get("direction", "long"), d["upper"], d["lower"]))
|
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()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
uh = round_price_to_exchange(ex_sym_key, float(d["upper"]))
|
||||||
|
lw = round_price_to_exchange(ex_sym_key, float(d["lower"]))
|
||||||
|
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"])
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
|
||||||
|
(symbol, mt, direction_sel, upper_px, lower_px),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
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})")
|
flash(f"添加成功({symbol} 日成交量排名 {rank}/{total})")
|
||||||
|
if ctr:
|
||||||
|
flash(
|
||||||
|
"⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。"
|
||||||
|
)
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
@app.route("/add_order", methods=["POST"])
|
@app.route("/add_order", methods=["POST"])
|
||||||
@@ -4578,12 +4921,13 @@ def add_order():
|
|||||||
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 if (d.get("breakeven_enabled") or "").strip() in ("1", "true", "on", "yes") else 0
|
breakeven_enabled = 1 if (d.get("breakeven_enabled") or "").strip() in ("1", "true", "on", "yes") else 0
|
||||||
conn.execute(
|
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,
|
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,
|
margin_capital, leverage, trade_style, risk_percent, risk_amount_final, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, 0, breakeven_price,
|
||||||
breakeven_enabled,
|
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()
|
conn.commit()
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
.review-card{grid-column:1/-1}
|
.review-card{grid-column:1/-1}
|
||||||
@media (min-width: 1900px){
|
@media (min-width: 1900px){
|
||||||
.container{max-width:2100px}
|
.container{max-width:2100px}
|
||||||
.monitor-card .list,.order-card .list{max-height:420px}
|
.monitor-card .list,.order-card .pos-list{max-height:420px}
|
||||||
.records-card .table-wrap{max-height:620px;overflow:auto}
|
.records-card .table-wrap{max-height:620px;overflow:auto}
|
||||||
}
|
}
|
||||||
@media (max-width: 1400px){
|
@media (max-width: 1400px){
|
||||||
@@ -112,6 +112,34 @@
|
|||||||
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
||||||
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
||||||
.key-history .list{max-height:200px}
|
.key-history .list{max-height:200px}
|
||||||
|
.pos-section{margin-top:12px}
|
||||||
|
.pos-section-title{font-size:.82rem;color:#8892b0;margin-bottom:8px;font-weight:500}
|
||||||
|
.pos-list{display:flex;flex-direction:column;gap:10px;max-height:280px;overflow:auto}
|
||||||
|
.pos-card{background:#141923;border:1px solid #2a3348;border-radius:10px;padding:12px 14px}
|
||||||
|
.pos-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||||
|
.pos-meta{font-size:.74rem;color:#8b95a8;line-height:1.45;margin-bottom:12px;display:flex;flex-wrap:wrap;align-items:center;gap:4px 0}
|
||||||
|
.pos-meta-item{display:inline-flex;align-items:center}
|
||||||
|
.pos-meta-item:not(:last-child)::after{content:'|';margin:0 8px;color:#3d4659}
|
||||||
|
.pos-meta-on{color:#6eb5ff}
|
||||||
|
.pos-meta-off{color:#7d8799}
|
||||||
|
.pos-card-symbol{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
|
||||||
|
.pos-card-symbol strong{font-size:.95rem;color:#fff;font-weight:600}
|
||||||
|
.pos-side-badge{padding:3px 8px;border-radius:6px;font-size:.72rem;font-weight:500;line-height:1.2}
|
||||||
|
.pos-side-long{background:#253a6e;color:#6eb5ff}
|
||||||
|
.pos-side-short{background:#4a2230;color:#ff8a8a}
|
||||||
|
.pos-close-btn{padding:6px 14px;background:#c45454;color:#fff;border-radius:8px;text-decoration:none;font-size:.82rem;font-weight:500;flex-shrink:0;white-space:nowrap}
|
||||||
|
.pos-close-btn:hover{background:#d66565;color:#fff}
|
||||||
|
.pos-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 14px;margin-bottom:12px}
|
||||||
|
.pos-cell{display:flex;flex-direction:column;gap:4px;min-width:0}
|
||||||
|
.pos-label{font-size:.72rem;color:#7d8799}
|
||||||
|
.pos-value{font-size:.88rem;color:#e8ecf4;font-weight:500;line-height:1.25}
|
||||||
|
.pos-val-dash{opacity:.75;color:#8b95a8}
|
||||||
|
.pos-value.price-up{color:#4cd97f}
|
||||||
|
.pos-value.price-down{color:#ff6666}
|
||||||
|
.pos-value.price-flat{color:#e8ecf4}
|
||||||
|
.pos-footer{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:.75rem;color:#6d7689}
|
||||||
|
.pos-empty{padding:18px;text-align:center;color:#8892b0;font-size:.85rem;background:#141923;border:1px dashed #2a3348;border-radius:10px}
|
||||||
|
@media (max-width:520px){.pos-grid{grid-template-columns:repeat(2,1fr)}}
|
||||||
.stats-card{grid-column:1/-1;margin-top:14px}
|
.stats-card{grid-column:1/-1;margin-top:14px}
|
||||||
.stats-card .stats-toggle{background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;padding:6px 10px;cursor:pointer}
|
.stats-card .stats-toggle{background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;padding:6px 10px;cursor:pointer}
|
||||||
.stats-card.collapsed .stats-content{display:none}
|
.stats-card.collapsed .stats-content{display:none}
|
||||||
@@ -216,7 +244,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="key-history">
|
<div class="key-history">
|
||||||
<h3>关键位历史(满次提醒或手动删除)</h3>
|
<h3>关键位历史(满次提醒或手动删除)</h3>
|
||||||
<div class="sub">满 {{ key_alert_max_times }} 次企业微信提醒后自动移入此处;手动删除也会归档。</div>
|
<div class="sub">每种关键位触发后<strong>一次性结案</strong>并写入下方历史:箱体/收敛在计划 RR 达标时<strong>自动市价开仓</strong>;阻力/支撑仅<strong>单次企业微信提醒</strong>。详见项目根目录「关键位自动下单说明.md」。满多次提醒的旧逻辑已不适用。</div>
|
||||||
<div class="list">
|
<div class="list">
|
||||||
{% for h in key_history %}
|
{% for h in key_history %}
|
||||||
<div class="list-item">
|
<div class="list-item">
|
||||||
@@ -295,24 +323,71 @@
|
|||||||
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
||||||
<button type="submit">开仓(以损定仓)</button>
|
<button type="submit">开仓(以损定仓)</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="list">
|
<div class="pos-section">
|
||||||
|
<div class="pos-section-title">实时持仓</div>
|
||||||
|
<div class="pos-list">
|
||||||
{% for o in order %}
|
{% for o in order %}
|
||||||
<div class="list-item">
|
<div class="pos-card" id="order-row-{{ o.id }}">
|
||||||
<div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div>
|
<div class="pos-card-head">
|
||||||
<div>
|
<div class="pos-card-symbol">
|
||||||
风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U
|
<strong>{{ o.exchange_symbol or o.symbol }}</strong>
|
||||||
| {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
|
||||||
<br>
|
</div>
|
||||||
成交:{{ price_fmt(o.symbol, o.trigger_price) }} 止损:{{ price_fmt(o.symbol, o.stop_loss) }} 止盈:{{ price_fmt(o.symbol, o.take_profit) }}
|
<a href="/del_order/{{ o.id }}" class="pos-close-btn" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||||
| 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span>
|
</div>
|
||||||
| 现价:<span id="order-price-{{ o.id }}">-</span>
|
<div class="pos-meta">
|
||||||
| 浮盈亏:<span id="order-pnl-{{ o.id }}">-</span>
|
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}</span>
|
||||||
| 计划基数:{{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U | 所保证金:<span id="order-ex-margin-{{ o.id }}">-</span>
|
<span class="pos-meta-item">风格: {{ o.trade_style or 'trend' }}</span>
|
||||||
| 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}%
|
<span class="pos-meta-item">风险: {{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U</span>
|
||||||
|
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
|
||||||
|
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pos-grid">
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">成交价</span>
|
||||||
|
<span class="pos-value">{{ price_fmt(o.symbol, o.trigger_price) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">止损</span>
|
||||||
|
{% if o.stop_loss %}
|
||||||
|
<span class="pos-value">{{ price_fmt(o.symbol, o.stop_loss) }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="pos-value pos-val-dash">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">止盈</span>
|
||||||
|
{% if o.take_profit %}
|
||||||
|
<span class="pos-value">{{ price_fmt(o.symbol, o.take_profit) }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="pos-value pos-val-dash">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">盈亏比</span>
|
||||||
|
<span class="pos-value" id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}{{ '%g'|format(o.rr_ratio) }}:1{% else %}-:1{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">标记价</span>
|
||||||
|
<span class="pos-value" id="order-price-{{ o.id }}">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">浮盈亏</span>
|
||||||
|
<span class="pos-value" id="order-pnl-{{ o.id }}">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pos-footer">
|
||||||
|
<span>保证金: <span id="order-ex-margin-{{ o.id }}">-</span></span>
|
||||||
|
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
|
||||||
|
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
||||||
|
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="pos-empty">暂无持仓</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -1098,6 +1173,14 @@ function formatSigned(v, digits=2){
|
|||||||
return `${sign}${n.toFixed(digits)}`;
|
return `${sign}${n.toFixed(digits)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRrRatio(rr){
|
||||||
|
if(rr === null || typeof rr === "undefined") return "-:1";
|
||||||
|
const n = Number(rr);
|
||||||
|
if(Number.isNaN(n)) return "-:1";
|
||||||
|
const body = Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(2)));
|
||||||
|
return `${body}:1`;
|
||||||
|
}
|
||||||
|
|
||||||
function paintPriceTrend(el, key, value){
|
function paintPriceTrend(el, key, value){
|
||||||
if(!el) return;
|
if(!el) return;
|
||||||
const prev = lastPriceMap[key];
|
const prev = lastPriceMap[key];
|
||||||
@@ -1180,7 +1263,7 @@ function refreshPriceSnapshot(){
|
|||||||
}
|
}
|
||||||
const rrEl = document.getElementById(`order-rr-${o.id}`);
|
const rrEl = document.getElementById(`order-rr-${o.id}`);
|
||||||
if(rrEl){
|
if(rrEl){
|
||||||
rrEl.innerText = (typeof o.rr_ratio !== "undefined" && o.rr_ratio !== null) ? `1:${Number(o.rr_ratio).toFixed(2)}` : "-";
|
rrEl.innerText = formatRrRatio(o.rr_ratio);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# 使用说明
|
||||||
|
|
||||||
|
**本文件对应仓库:`crypto_monitor_binance`(Binance U 本位永续)。**
|
||||||
|
功能、界面与 **Gate.io USDT 永续版**(目录 `crypto_monitor_gate`)基本一致,差异主要在 **`.env` 里交易所密钥与部分参数名**(`BINANCE_*` / `GATE_*`),文末有对照。
|
||||||
|
|
||||||
|
**部署、代理、PM2 等**请参考本仓库说明或 **`crypto_monitor_gate`** 下的 **`部署文档.md`**(该文以 Gate + SSH SOCKS 为例;Binance 侧将 API 与密钥改为 `BINANCE_*` 即可类比)。
|
||||||
|
**关键位自动开仓的规则、RR、结案原因**见本目录 **`关键位自动下单说明.md`**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 它能做什么
|
||||||
|
|
||||||
|
面向个人盘面的 **Web 控制台**,主要能力包括:
|
||||||
|
|
||||||
|
| 模块 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **关键位监控** | 录入上/下沿与类型,按 **5m 收线** 做硬条件过滤;符合条件后 **企业微信** 提醒,部分类型可 **自动市价开仓**(见第 4 节与专门文档)。 |
|
||||||
|
| **实盘下单监控** | 手工填止损/止盈,**以损定仓** 市价开单,挂上条件止盈止损,并在页面跟踪浮盈亏、保本逻辑等。 |
|
||||||
|
| **交易记录 / 复盘** | 平仓结果、盈亏、错过的单等归档与导出。 |
|
||||||
|
|
||||||
|
后台按 **`MONITOR_POLL_SECONDS`**(默认几秒)轮询行情与监控逻辑。**切勿**在未理解规则时同时运行两套程序共用一个实盘账户。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 运行前必须配置(`.env`)
|
||||||
|
|
||||||
|
至少检查以下项(具体键名以你仓库 `.env` 示例为准):
|
||||||
|
|
||||||
|
| 类别 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **登录网页** | `APP_PASSWORD`:打开站点后的登录口令。`FLASK_SECRET_KEY`:Session 密钥,请勿使用默认值。 |
|
||||||
|
| **企业微信** | `WECHAT_WEBHOOK`:告警与关键位推送机器人的 Webhook。 |
|
||||||
|
| **是否真下单** | `LIVE_TRADING_ENABLED=false`:**不会**向交易所发送开仓指令(适合测试流程)。改为 `true` 且密钥正确才会实盘。 |
|
||||||
|
| **交易所 API** | **本仓库:** `BINANCE_API_KEY`、`BINANCE_API_SECRET`;永续相关见 `BINANCE_MARGIN_MODE`、`BINANCE_POSITION_MODE`、`BINANCE_TRIGGER_WORKING_TYPE` 等。**勿**把 `.env` 提交到 Git。 |
|
||||||
|
| **关键位 RR / 止损外扩** | `KEY_AUTO_MIN_PLANNED_RR`、`KEY_STOP_OUTSIDE_BREAKOUT_PCT`(详见 `关键位自动下单说明.md`)。 |
|
||||||
|
|
||||||
|
网络需要代理时可配置 **`BINANCE_SOCKS_PROXY` / `BINANCE_HTTP_PROXY`**(与 Gate 版 `GATE_*_PROXY` 用法类似)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 如何启动与登录
|
||||||
|
|
||||||
|
1. 准备 Python 虚拟环境并安装依赖(如 `flask`、`requests`、`ccxt`、按需 `Pillow`、`PySocks` 等),配置好 `.env`。
|
||||||
|
2. 启动 Flask 应用(可用 **`ecosystem.config.cjs`** 交给 PM2,或本地 `python app.py` / `flask run`,以你当前脚本为准)。
|
||||||
|
3. 浏览器访问站点,打开 **`/login`**,使用 **`.env` 里的 `APP_PASSWORD`** 登录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 关键位监控(页面「关键位监控」卡片)
|
||||||
|
|
||||||
|
### 4.1 添加一条关键位
|
||||||
|
|
||||||
|
1. **币种**:如 `BTC` 或 `BTC/USDT`(会规范成内部符号)。
|
||||||
|
2. **类型**(必选其一):
|
||||||
|
|
||||||
|
| 类型 | 行为摘要 |
|
||||||
|
|------|----------|
|
||||||
|
| **箱体突破** | 通过门控且计划 RR 达标 → **自动市价开仓**(需 `LIVE_TRADING_ENABLED=true` 且无其他持仓占位)。结案后本条从列表消失并记入历史。 |
|
||||||
|
| **收敛突破** | 同上(自动开仓类)。 |
|
||||||
|
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 |
|
||||||
|
| **关键支撑位** | 同上(仅提醒)。 |
|
||||||
|
|
||||||
|
3. **方向**:做多 / 做空(必选)。
|
||||||
|
4. **上沿 / 下沿**:必填;保存时会按交易所 **价格精度** 取整。
|
||||||
|
|
||||||
|
**限制:**
|
||||||
|
当前已有 **`order_monitors` 活跃持仓** 时,**不允许**再添加「**箱体突破** / **收敛突破**」;仍可添加「**关键阻力位 / 支撑位**」。
|
||||||
|
若 **4h EMA55** 与你的方向逆势,页面会 **额外 Flash 提示**,**不阻挡**提交。
|
||||||
|
|
||||||
|
### 4.2 触发后会发生什么(简版)
|
||||||
|
|
||||||
|
- **箱体 / 收敛**:门控通过后算计划 SL/TP 与 RR;不达标 → 微信说明 + **`rr_insufficient`** 结案;达标 → **市价开仓**,成功 **`auto_opened`** / 失败 **`exchange_failed`**,均不重试同一关键位。
|
||||||
|
- **阻力 / 支撑**:仅 **单次推送** → **`key_level_alert_only`** 结案。
|
||||||
|
|
||||||
|
详细公式与字段见 **`关键位自动下单说明.md`**。
|
||||||
|
|
||||||
|
### 4.3 列表与历史
|
||||||
|
|
||||||
|
当前条目与历史记录的用法与 Gate 版相同;结案后可在历史区查阅 **`close_reason`**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 实盘下单监控(手工开仓)
|
||||||
|
|
||||||
|
- **同一时间系统只允许一笔活跃持仓监视**(与关键位自动单共用该限制)。
|
||||||
|
- 填写币种、方向、杠杆(可选)、止损/止盈(价格或百分比按表单)。
|
||||||
|
- 移动保本等选项按页面与 `.env` 默认。
|
||||||
|
|
||||||
|
开仓成功后卡片 **「来源」**:手工一般为 **下单监控**;关键位自动为 **关键位监控**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 企业微信
|
||||||
|
|
||||||
|
推送逻辑与 Gate 版一致;未配置 **`WECHAT_WEBHOOK`** 时可能没有消息,请以 **交易所端** 核对持仓与挂单。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 强烈建议的风险与运维习惯
|
||||||
|
|
||||||
|
1. **先用 `LIVE_TRADING_ENABLED=false`** 熟悉流程再实盘。
|
||||||
|
2. **API 权限**最小化,密钥勿泄露。
|
||||||
|
3. **同一账户避免多程序重复开仓**。
|
||||||
|
4. **备份数据库**后再升级迁移。
|
||||||
|
5. 升级代码后留意 **首轮启动**有无数据库迁移报错。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 常见问题(简要)
|
||||||
|
|
||||||
|
| 现象 | 可自查 |
|
||||||
|
|------|--------|
|
||||||
|
| 关键位永远不触发 | 门控五项、日成交量排名、`KLINE_TIMEFRAME`。 |
|
||||||
|
| 有信号但不自动开仓 | `LIVE_TRADING_ENABLED`、RR 阈值、是否已有持仓、API/保证金错误信息。 |
|
||||||
|
| 加不了箱体/收敛 | 是否已有持仓。 |
|
||||||
|
| 推送收不到 | Webhook、网络。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Gate 版(`crypto_monitor_gate`)差异速查
|
||||||
|
|
||||||
|
| 项目 | Binance 本仓库 | Gate 版 |
|
||||||
|
|------|----------------|--------|
|
||||||
|
| API 变量 | `BINANCE_API_KEY`、`BINANCE_API_SECRET`、`BINANCE_*` | `GATE_API_KEY`、`GATE_API_SECRET`、`GATE_*` |
|
||||||
|
| 代理示例 | `BINANCE_SOCKS_PROXY` | `GATE_SOCKS_PROXY` |
|
||||||
|
| TP/SL 实现 | `_binance_place_tp_sl_orders` | `_gate_place_tp_sl_orders`、`GATE_TPSL_*` |
|
||||||
|
| 资金舍入口径 | **`FUNDS_DECIMALS`**(与记账一致) | 以 Gate 仓库实现为准 |
|
||||||
|
|
||||||
|
业务流程(登录、四种关键位、手工单、单仓)两份程序对齐;仅需更换目录与 `.env`。
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 关键位自动下单说明
|
||||||
|
|
||||||
|
**适用仓库:`crypto_monitor_binance`|交易所:Binance U 本位永续**(Gate 版见同名的 `crypto_monitor_gate` 目录。)
|
||||||
|
|
||||||
|
本文档与 `.env`、`app.check_key_monitors`、`app.add_key`、`_market_open_for_key_monitor` 的实现一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 结构与是否自动开仓
|
||||||
|
|
||||||
|
| `key_monitors.monitor_type`(录入类型) | 自动下单 | 触发后处置 |
|
||||||
|
|---------------------------------------|----------|------------|
|
||||||
|
| **箱体突破** | 是(满足全部条件) | **一次性结案**:写 `key_monitor_history` → 从 `key_monitors` **删除** |
|
||||||
|
| **收敛突破** | 是(同上) | 同上 |
|
||||||
|
| **关键阻力位** | 否 | 企业微信 **1 次** → `close_reason=key_level_alert_only` → **失效** |
|
||||||
|
| **关键支撑位** | 否 | 同上 |
|
||||||
|
|
||||||
|
触发条件:**5m 收线硬门控** `_key_hard_checks`(量能、突破幅度、第二根收盘确认、日成交量前 30 等)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 录入限制(`/add_key`)
|
||||||
|
|
||||||
|
- 存在 **`order_monitors.status='active'`** 时:**禁止添加** 「箱体突破」「收敛突破」。
|
||||||
|
- **关键阻力位 / 关键支撑位**:不受上条限制;触发后 **仅单次微信提醒**,然后结案。
|
||||||
|
- **4h EMA55 与所选方向逆势**:**不拦截**;添加成功后 **Flash** 提示。
|
||||||
|
- 上下沿入库前经 **`round_price_to_exchange`** 按合约 **价格精度** 取整。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境与参数(`.env`)
|
||||||
|
|
||||||
|
| 变量 | 含义 | 默认 |
|
||||||
|
|------|------|------|
|
||||||
|
| `KEY_AUTO_MIN_PLANNED_RR` | 计划 RR 阈值:**仅当严格大于该值** 才自动开仓(按下方 `E` 计算) | `1.5` |
|
||||||
|
| `KEY_STOP_OUTSIDE_BREAKOUT_PCT` | 止损:突破 K 极值向外 **百分比**(多:`低×(1−p/100)`;空:`高×(1+p/100)`) | `0.5` |
|
||||||
|
|
||||||
|
**其余与本仓库手动实盘一致:** `KLINE_TIMEFRAME`、`RISK_PERCENT`、`LIVE_TRADING_ENABLED`、`BREAKEVEN_*`、`DAILY_OPEN_ALERT_THRESHOLD`,以及 **`BINANCE_*`**(密钥、`BINANCE_MARGIN_MODE`、`BINANCE_POSITION_MODE`、`BINANCE_TRIGGER_WORKING_TYPE` 等)。资金字段舍入端口径与 **`FUNDS_DECIMALS`** 一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 计价与下单口径
|
||||||
|
|
||||||
|
| 用途 | 价格 |
|
||||||
|
|------|------|
|
||||||
|
| 企业微信展示、**与 RR 门槛比较的计划 RR** | 确认 K(第二根闭合 5m)收盘 **`E`** |
|
||||||
|
| **实际开仓** | **市价**(`place_exchange_order`,与 `/add_order` 一致);成交价可能与 `E` **滑点** |
|
||||||
|
| **以损定仓** | `calc_risk_fraction(direction, 当前市价, 止损)` + `RISK_PERCENT`(保证金等 **`FUNDS_DECIMALS`** 舍入,与 `/add_order` 一致) |
|
||||||
|
|
||||||
|
- 开仓成功后:`order_monitors.monitor_type` 为 **关键位监控**;持仓卡片「来源」显示之。手动开仓为 **下单监控**。
|
||||||
|
- 持仓列表中的 **盈亏比**:按 **实际成交价** 相对 SL/TP 重算,可与「按 `E` 算的计划 RR」略有偏差。
|
||||||
|
- **本仓库止盈止损挂单**:开仓后由 **`_binance_place_tp_sl_orders`** 挂载(与手动一致:U 本位条件/Algo 类触发单;具体类型以 ccxt / 交易所为准)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自动单止盈 / 止损(仅箱体突破、收敛突破)
|
||||||
|
|
||||||
|
设箱体高度 **`H = |upper − lower|`**(录入上下沿)。
|
||||||
|
|
||||||
|
| 方向 | 止损 SL | 止盈 TP |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 多 | 突破 K **最低价** × (1 − `KEY_STOP_OUTSIDE_BREAKOUT_PCT`/100) | **`E + 1×H`** |
|
||||||
|
| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`/100) | **`E − 1×H`** |
|
||||||
|
|
||||||
|
计划 **`RR = calc_rr_ratio(direction, E, SL, TP)`**。若为 `None` 或 **RR ≤ `KEY_AUTO_MIN_PLANNED_RR`** → **不下单**,走 `rr_insufficient` 结案。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一次性结案(`close_reason`)
|
||||||
|
|
||||||
|
以下任一发生:**按需发微信** → **`key_monitor_history`** → **从 `key_monitors` 删除**;**不会对同一条关键位重复轮询重试开仓**。
|
||||||
|
|
||||||
|
| `close_reason` | 含义 |
|
||||||
|
|----------------|------|
|
||||||
|
| `rr_insufficient` | 门控通过,但计划 RR 未达标或 SL/TP / RR **几何无效** |
|
||||||
|
| `exchange_failed` | 计划 RR 达标,但未开实盘、`LIVE_TRADING_ENABLED=false`、风控、保证金或 **交易所报错** 等导致 **开仓失败** |
|
||||||
|
| `auto_opened` | 计划 RR 达标且 **市价开仓成功**(已写 `order_monitors`,并已挂止盈止损) |
|
||||||
|
| `key_level_alert_only` | 阻力/支撑位 **仅推送**结案 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 与企业微信推送
|
||||||
|
|
||||||
|
每种结案路径 **至多一条**主业务推送(RR 不足 / 下单失败 / 开仓成功 / 阻力支撑仅提醒)。
|
||||||
|
|
||||||
|
旧版「满 `KEY_ALERT_MAX_TIMES` 次再归档」对已触发结案的路径 **不再适用**;表中 `notification_count`、`max_notify` 等字段仍可能存在,以 **导出、兼容** 为主。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关代码位置(通用)
|
||||||
|
|
||||||
|
| 说明 | 符号 |
|
||||||
|
|------|------|
|
||||||
|
| 门控与主循环 | `check_key_monitors` |
|
||||||
|
| 录入、有仓拦截、4h Flash | `add_key` |
|
||||||
|
| 市价开仓 + 写 `order_monitors` | `_market_open_for_key_monitor` |
|
||||||
|
| 计划 RR | `calc_rr_ratio(direction, E, SL, TP)` |
|
||||||
|
| 价格精度 | `round_price_to_exchange` |
|
||||||
@@ -62,6 +62,10 @@ GATE_TPSL_PRICE_TYPE=0
|
|||||||
# 关键位监控:5m收线突破过滤参数
|
# 关键位监控:5m收线突破过滤参数
|
||||||
KLINE_TIMEFRAME=5m
|
KLINE_TIMEFRAME=5m
|
||||||
KEY_BREAKOUT_LIMIT_PCT=1.5
|
KEY_BREAKOUT_LIMIT_PCT=1.5
|
||||||
|
# 关键位自动单:计划 RR 阈值(严格大于该值才开仓,按确认K收盘 E 计算)
|
||||||
|
KEY_AUTO_MIN_PLANNED_RR=1.5
|
||||||
|
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
|
||||||
|
KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
|
||||||
KEY_ALERT_MAX_TIMES=3
|
KEY_ALERT_MAX_TIMES=3
|
||||||
KEY_ALERT_INTERVAL_MINUTES=5
|
KEY_ALERT_INTERVAL_MINUTES=5
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# crypto_monitor_gate
|
||||||
|
|
||||||
|
基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统,行情与实盘接口统一走 **Gate.io USDT 永续**,通过 **ccxt** 访问。
|
||||||
|
|
||||||
|
## 文档导航
|
||||||
|
|
||||||
|
| 文档 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **[使用说明.md](./使用说明.md)** | 日常怎么用:登录、关键位四类、手工开仓、单仓与微信等 |
|
||||||
|
| **[关键位自动下单说明.md](./关键位自动下单说明.md)** | 关键位自动开仓的 RR、止盈止损、结案原因与 `.env` |
|
||||||
|
| **[部署文档.md](./部署文档.md)** | Ubuntu、PM2、**SSH SOCKS** 访问 Gate API 等 |
|
||||||
|
|
||||||
|
另:**Binance U 本位** 对等实现见同级的 **`crypto_monitor_binance`** 仓库。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 功能概要
|
||||||
|
|
||||||
|
- **关键位监控**:5m 收线硬条件、企业微信推送;**箱体 / 收敛** 在 RR 达标时可 **自动市价开仓**(见专门文档);**阻力 / 支撑** 仅单次提醒结案
|
||||||
|
- **下单监控**:本地风控(含移动保本)、止盈/止损触达后轮询尝试平仓并记账
|
||||||
|
- **实盘(可选)**:`LIVE_TRADING_ENABLED=true` 且配置 **`GATE_API_KEY` / `GATE_API_SECRET`** 时,支持开仓、挂单 TP/SL、余额与划转(权限依账户而定)
|
||||||
|
- **止盈止损(Gate)**:市价成交后经 **`_gate_place_tp_sl_orders`** 挂单;优先 **仓位类 `price_orders`**(受 `GATE_TPSL_USE_POSITION_ORDER`、`GATE_TPSL_PRICE_TYPE`、`GATE_POS_MODE` 等影响)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- Python 3.10+(建议)
|
||||||
|
- 依赖:`flask`、`requests`、`ccxt`、`werkzeug`、`PySocks`(经 SOCKS 代理时);`Pillow`(K 线导出等可选用)
|
||||||
|
|
||||||
|
安装示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||||
|
pip install flask requests ccxt werkzeug PySocks Pillow
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置(`.env`)
|
||||||
|
|
||||||
|
项目启动时加载**仓库根目录**下的 `.env`。常用项:
|
||||||
|
|
||||||
|
| 变量 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `GATE_API_KEY` / `GATE_API_SECRET` | Gate API(需合约与对应权限) |
|
||||||
|
| `LIVE_TRADING_ENABLED` | `true` 允许真实下单;`false` 仅本地与推送逻辑 |
|
||||||
|
| `GATE_MARGIN_MODE` / `GATE_POS_MODE` | 保证金与持仓模式 |
|
||||||
|
| `GATE_TPSL_USE_POSITION_ORDER` / `GATE_TPSL_PRICE_TYPE` 等 | 条件止盈止损行为 |
|
||||||
|
| `GATE_SOCKS_PROXY` | 可选;直连不稳时 SSH 动态转发(详见部署文档) |
|
||||||
|
| `APP_PASSWORD` / `FLASK_SECRET_KEY` | Web 登录与 Session |
|
||||||
|
| `WECHAT_WEBHOOK` | 企业微信机器人 |
|
||||||
|
| `EXCHANGE_DISPLAY_NAME` / `GATE_ACCOUNT_LABEL` | 页面与推送展示的账户文案 |
|
||||||
|
|
||||||
|
其余见 **`.env` 内注释** 或 **`app.py` 顶部默认值**。
|
||||||
|
|
||||||
|
## 本地运行
|
||||||
|
|
||||||
|
**Windows** 推荐使用 UTF-8 控制台脚本:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\start_utf8.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
或直接:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
端口由 **`APP_PORT`** 控制(未设置默认 **5000**)。浏览器登录 **`/login`**,口令为 **`APP_PASSWORD`**。
|
||||||
|
|
||||||
|
## 部署(Linux / PM2 / SSH SOCKS)
|
||||||
|
|
||||||
|
见 **[部署文档.md](./部署文档.md)**。
|
||||||
|
|
||||||
|
## 自检脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/verify_gate_funding.py
|
||||||
|
```
|
||||||
|
|
||||||
|
用于核对密钥前缀(不落 Secret)、资金/合约可读性等(需网络与权限)。
|
||||||
|
|
||||||
|
## 数据与脚本
|
||||||
|
|
||||||
|
- 默认 SQLite:由 **`DB_PATH`** 指定(常见为项目下 `crypto.db`)
|
||||||
|
- `scripts/fix_breakeven_labels.py`:修正「止损」但盈亏为正的记录标签(参见部署文档说明)
|
||||||
|
|
||||||
|
## 风险与合规
|
||||||
|
|
||||||
|
实盘有亏损风险。请确认 API 权限、IP 白名单、杠杆与保证金模式与 **Gate.io** 后台一致,并遵守当地法律法规与交易所用户协议。
|
||||||
+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_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_BREAKOUT_LIMIT_PCT = float(os.getenv("KEY_BREAKOUT_LIMIT_PCT", "1.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_ENABLED = os.getenv("AUTO_TRANSFER_ENABLED", "false").lower() == "true"
|
||||||
AUTO_TRANSFER_AMOUNT = float(os.getenv("AUTO_TRANSFER_AMOUNT", "30"))
|
AUTO_TRANSFER_AMOUNT = float(os.getenv("AUTO_TRANSFER_AMOUNT", "30"))
|
||||||
AUTO_TRANSFER_FROM = os.getenv("AUTO_TRANSFER_FROM", "funding")
|
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")
|
c.execute("ALTER TABLE order_monitors ADD COLUMN exchange_margin_usdt REAL")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
try:
|
||||||
c.execute("UPDATE order_monitors SET opened_at = datetime('now') WHERE opened_at IS NULL OR opened_at = ''")
|
c.execute("UPDATE order_monitors SET opened_at = datetime('now') WHERE opened_at IS NULL OR opened_at = ''")
|
||||||
except: pass
|
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
|
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
||||||
except Exception:
|
except Exception:
|
||||||
item["breakeven_enabled"] = 1
|
item["breakeven_enabled"] = 1
|
||||||
|
if not (item.get("monitor_type") or "").strip():
|
||||||
|
item["monitor_type"] = ORDER_MONITOR_TYPE_MANUAL
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
@@ -3461,113 +3481,386 @@ def calc_price_diff_pct(current_price, target_price):
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def can_notify_key_monitor(row, now_dt):
|
def _finalize_key_monitor_one_shot(conn, row, last_msg, close_reason):
|
||||||
max_notify = int(row["max_notify"] or KEY_ALERT_MAX_TIMES)
|
"""本条关键位一次性结案:写历史并从当前表删除。"""
|
||||||
if int(row["notification_count"] or 0) >= max_notify:
|
n = int(row["notification_count"] or 0) + 1
|
||||||
return False
|
insert_key_monitor_history(conn, row, n, last_msg, close_reason)
|
||||||
last_at = row["last_notified_at"]
|
conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],))
|
||||||
if not last_at:
|
|
||||||
return True
|
|
||||||
|
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:
|
try:
|
||||||
last_dt = datetime.strptime(last_at, "%Y-%m-%d %H:%M:%S")
|
ensure_markets_loaded()
|
||||||
except Exception:
|
except Exception:
|
||||||
return True
|
pass
|
||||||
interval_min = int(row["notify_interval_min"] or KEY_ALERT_INTERVAL_MINUTES)
|
lp_r = round_price_to_exchange(exchange_symbol, live_price)
|
||||||
return (now_dt - last_dt).total_seconds() >= interval_min * 60
|
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:
|
try:
|
||||||
if edge_price is None or float(edge_price) <= 0:
|
amount, quote_price = prepare_order_amount(exchange_symbol, margin_capital, leverage, live_price)
|
||||||
return False
|
contract_size = get_contract_size(exchange_symbol)
|
||||||
diff_pct = abs(float(p) - float(edge_price)) / float(edge_price) * 100
|
base_amount = round(float(amount) * contract_size, 8)
|
||||||
return diff_pct > float(limit_pct)
|
order_resp = place_exchange_order(
|
||||||
except Exception:
|
exchange_symbol, direction, amount, leverage,
|
||||||
return False
|
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():
|
def check_key_monitors():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute("SELECT * FROM key_monitors").fetchall()
|
rows = conn.execute("SELECT * FROM key_monitors").fetchall()
|
||||||
for r in rows:
|
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()
|
direction = (r["direction"] or "long").lower()
|
||||||
now_dt = app_now()
|
|
||||||
if not can_notify_key_monitor(r, now_dt):
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
checks = _key_hard_checks(sym, direction, up, low, typ)
|
checks = _key_hard_checks(sym, direction, up, low, typ)
|
||||||
except Exception:
|
except Exception:
|
||||||
checks = {"ok": False}
|
checks = {"ok": False}
|
||||||
if not checks.get("ok"):
|
if not checks.get("ok"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
btc8h_status, _, _ = _status_by_ema55("BTC/USDT", "8h")
|
btc8h_status, _, _ = _status_by_ema55("BTC/USDT", "8h")
|
||||||
coin4h_status, _, _ = _status_by_ema55(sym, "4h")
|
coin4h_status, _, _ = _status_by_ema55(sym, "4h")
|
||||||
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":
|
hard_lines = _key_hard_lines_from_checks(checks)
|
||||||
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 '-' })",
|
|
||||||
]
|
|
||||||
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(
|
|
||||||
symbol=sym,
|
alert_only = typ in KEY_MONITOR_ALERT_ONLY_TYPES or (
|
||||||
direction=direction,
|
typ not in KEY_MONITOR_AUTO_TYPES and typ not in KEY_MONITOR_ALERT_ONLY_TYPES
|
||||||
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)
|
|
||||||
new_count = int(r["notification_count"] or 0) + 1
|
if alert_only:
|
||||||
max_n = int(r["max_notify"] or KEY_ALERT_MAX_TIMES)
|
op_lines = [
|
||||||
conn.execute(
|
"- 本条为关键阻力/支撑或非标类型:**仅单次推送**,不进行自动开仓。",
|
||||||
"UPDATE key_monitors SET notification_count = ?, last_notified_at = ? WHERE id = ?",
|
"- 本条关键位将在推送后记入历史并从监控列表移除。",
|
||||||
(new_count, app_now_str(), r["id"]),
|
]
|
||||||
)
|
msg = build_wechat_key_monitor_message(
|
||||||
if new_count >= max_n:
|
symbol=sym,
|
||||||
insert_key_monitor_history(conn, r, new_count, msg, "alerts_complete")
|
direction=direction,
|
||||||
conn.execute("DELETE FROM key_monitors WHERE id = ?", (r["id"],))
|
monitor_type=typ,
|
||||||
send_wechat_msg(
|
trigger_time=trigger_time,
|
||||||
"\n".join(
|
key_price=key_price,
|
||||||
[
|
confirm_close=checks["confirm_close"],
|
||||||
f"# 🧾 {r['symbol']} 关键位监控结束",
|
hard_lines=hard_lines,
|
||||||
"",
|
btc8h_status=btc8h_status,
|
||||||
f"- 原因:已满 {max_n} 次提醒",
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -4573,6 +4866,15 @@ def add_key():
|
|||||||
if not symbol:
|
if not symbol:
|
||||||
flash("symbol 不能为空")
|
flash("symbol 不能为空")
|
||||||
return redirect("/")
|
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)
|
rank, total = _daily_volume_rank(symbol)
|
||||||
if rank is None:
|
if rank is None:
|
||||||
flash("日成交量排名读取失败,请稍后重试")
|
flash("日成交量排名读取失败,请稍后重试")
|
||||||
@@ -4581,6 +4883,16 @@ def add_key():
|
|||||||
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前30,已拒绝添加关键位")
|
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前30,已拒绝添加关键位")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
conn = get_db()
|
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)
|
ex_sym_key = normalize_exchange_symbol(symbol)
|
||||||
try:
|
try:
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
@@ -4588,11 +4900,25 @@ 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"]))
|
||||||
conn.execute("INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
|
conn.execute(
|
||||||
(symbol, d["type"], d.get("direction", "long"), upper_px, lower_px))
|
"INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
|
||||||
|
(symbol, mt, direction_sel, upper_px, lower_px),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
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})")
|
flash(f"添加成功({symbol} 日成交量排名 {rank}/{total})")
|
||||||
|
if ctr:
|
||||||
|
flash(
|
||||||
|
"⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。"
|
||||||
|
)
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
@app.route("/add_order", methods=["POST"])
|
@app.route("/add_order", methods=["POST"])
|
||||||
@@ -4772,12 +5098,13 @@ def add_order():
|
|||||||
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
|
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
|
breakeven_enabled = 1 if (d.get("breakeven_enabled") or "").strip() in ("1", "true", "on", "yes") else 0
|
||||||
conn.execute(
|
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,
|
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,
|
margin_capital, leverage, trade_style, risk_percent, risk_amount_final, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, 0, breakeven_price,
|
||||||
breakeven_enabled,
|
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()
|
conn.commit()
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
.review-card{grid-column:1/-1}
|
.review-card{grid-column:1/-1}
|
||||||
@media (min-width: 1900px){
|
@media (min-width: 1900px){
|
||||||
.container{max-width:2100px}
|
.container{max-width:2100px}
|
||||||
.monitor-card .list,.order-card .list{max-height:420px}
|
.monitor-card .list,.order-card .pos-list{max-height:420px}
|
||||||
.records-card .table-wrap{max-height:620px;overflow:auto}
|
.records-card .table-wrap{max-height:620px;overflow:auto}
|
||||||
}
|
}
|
||||||
@media (max-width: 1400px){
|
@media (max-width: 1400px){
|
||||||
@@ -112,6 +112,34 @@
|
|||||||
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
||||||
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
||||||
.key-history .list{max-height:200px}
|
.key-history .list{max-height:200px}
|
||||||
|
.pos-section{margin-top:12px}
|
||||||
|
.pos-section-title{font-size:.82rem;color:#8892b0;margin-bottom:8px;font-weight:500}
|
||||||
|
.pos-list{display:flex;flex-direction:column;gap:10px;max-height:280px;overflow:auto}
|
||||||
|
.pos-card{background:#141923;border:1px solid #2a3348;border-radius:10px;padding:12px 14px}
|
||||||
|
.pos-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||||
|
.pos-meta{font-size:.74rem;color:#8b95a8;line-height:1.45;margin-bottom:12px;display:flex;flex-wrap:wrap;align-items:center;gap:4px 0}
|
||||||
|
.pos-meta-item{display:inline-flex;align-items:center}
|
||||||
|
.pos-meta-item:not(:last-child)::after{content:'|';margin:0 8px;color:#3d4659}
|
||||||
|
.pos-meta-on{color:#6eb5ff}
|
||||||
|
.pos-meta-off{color:#7d8799}
|
||||||
|
.pos-card-symbol{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
|
||||||
|
.pos-card-symbol strong{font-size:.95rem;color:#fff;font-weight:600}
|
||||||
|
.pos-side-badge{padding:3px 8px;border-radius:6px;font-size:.72rem;font-weight:500;line-height:1.2}
|
||||||
|
.pos-side-long{background:#253a6e;color:#6eb5ff}
|
||||||
|
.pos-side-short{background:#4a2230;color:#ff8a8a}
|
||||||
|
.pos-close-btn{padding:6px 14px;background:#c45454;color:#fff;border-radius:8px;text-decoration:none;font-size:.82rem;font-weight:500;flex-shrink:0;white-space:nowrap}
|
||||||
|
.pos-close-btn:hover{background:#d66565;color:#fff}
|
||||||
|
.pos-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 14px;margin-bottom:12px}
|
||||||
|
.pos-cell{display:flex;flex-direction:column;gap:4px;min-width:0}
|
||||||
|
.pos-label{font-size:.72rem;color:#7d8799}
|
||||||
|
.pos-value{font-size:.88rem;color:#e8ecf4;font-weight:500;line-height:1.25}
|
||||||
|
.pos-val-dash{opacity:.75;color:#8b95a8}
|
||||||
|
.pos-value.price-up{color:#4cd97f}
|
||||||
|
.pos-value.price-down{color:#ff6666}
|
||||||
|
.pos-value.price-flat{color:#e8ecf4}
|
||||||
|
.pos-footer{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:.75rem;color:#6d7689}
|
||||||
|
.pos-empty{padding:18px;text-align:center;color:#8892b0;font-size:.85rem;background:#141923;border:1px dashed #2a3348;border-radius:10px}
|
||||||
|
@media (max-width:520px){.pos-grid{grid-template-columns:repeat(2,1fr)}}
|
||||||
.stats-card{grid-column:1/-1;margin-top:14px}
|
.stats-card{grid-column:1/-1;margin-top:14px}
|
||||||
.stats-card .stats-toggle{background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;padding:6px 10px;cursor:pointer}
|
.stats-card .stats-toggle{background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;padding:6px 10px;cursor:pointer}
|
||||||
.stats-card.collapsed .stats-content{display:none}
|
.stats-card.collapsed .stats-content{display:none}
|
||||||
@@ -216,7 +244,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="key-history">
|
<div class="key-history">
|
||||||
<h3>关键位历史(满次提醒或手动删除)</h3>
|
<h3>关键位历史(满次提醒或手动删除)</h3>
|
||||||
<div class="sub">满 {{ key_alert_max_times }} 次企业微信提醒后自动移入此处;手动删除也会归档。</div>
|
<div class="sub">每种关键位触发后<strong>一次性结案</strong>并写入下方历史:箱体/收敛在计划 RR 达标时<strong>自动市价开仓</strong>;阻力/支撑仅<strong>单次企业微信提醒</strong>。详见项目根目录「关键位自动下单说明.md」。满多次提醒的旧逻辑已不适用。</div>
|
||||||
<div class="list">
|
<div class="list">
|
||||||
{% for h in key_history %}
|
{% for h in key_history %}
|
||||||
<div class="list-item">
|
<div class="list-item">
|
||||||
@@ -295,24 +323,72 @@
|
|||||||
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
||||||
<button type="submit">开仓(以损定仓)</button>
|
<button type="submit">开仓(以损定仓)</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="list">
|
<div class="pos-section">
|
||||||
|
<div class="pos-section-title">实时持仓</div>
|
||||||
|
<div class="pos-list">
|
||||||
{% for o in order %}
|
{% for o in order %}
|
||||||
<div class="list-item">
|
<div class="pos-card" id="order-row-{{ o.id }}">
|
||||||
<div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div>
|
<div class="pos-card-head">
|
||||||
<div>
|
<div class="pos-card-symbol">
|
||||||
风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{% if o.risk_amount is not none %}{{ usdt_fmt(o.risk_amount) }}{% else %}-{% endif %}U
|
<strong>{{ o.exchange_symbol or o.symbol }}</strong>
|
||||||
| {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
|
||||||
<br>
|
</div>
|
||||||
成交:{{ price_fmt(o.symbol, o.trigger_price) }} 止损:{{ price_fmt(o.symbol, o.stop_loss) }} 止盈:{{ price_fmt(o.symbol, o.take_profit) }}
|
<a href="/del_order/{{ o.id }}" class="pos-close-btn" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||||
| 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span>
|
</div>
|
||||||
| 现价:<span id="order-price-{{ o.id }}">-</span>
|
<div class="pos-meta">
|
||||||
| 浮盈亏:<span id="order-pnl-{{ o.id }}">-</span>
|
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}</span>
|
||||||
| 计划基数:{% if o.margin_capital is not none %}{{ usdt_fmt(o.margin_capital) }}{% else %}-{% endif %}U | 所保证金:<span id="order-ex-margin-{{ o.id }}">-</span>
|
<span class="pos-meta-item">风格: {{ o.trade_style or 'trend' }}</span>
|
||||||
| 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}%
|
<span class="pos-meta-item">风险: {{ o.risk_percent or '-' }}%≈{% if o.risk_amount is not none %}{{ usdt_fmt(o.risk_amount) }}{% else %}-{% endif %}U</span>
|
||||||
|
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
|
||||||
|
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pos-grid">
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">成交价</span>
|
||||||
|
<span class="pos-value">{{ price_fmt(o.symbol, o.trigger_price) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">止损</span>
|
||||||
|
{% if o.stop_loss %}
|
||||||
|
<span class="pos-value">{{ price_fmt(o.symbol, o.stop_loss) }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="pos-value pos-val-dash">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">止盈</span>
|
||||||
|
{% if o.take_profit %}
|
||||||
|
<span class="pos-value">{{ price_fmt(o.symbol, o.take_profit) }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="pos-value pos-val-dash">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">盈亏比</span>
|
||||||
|
<span class="pos-value" id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}{{ '%g'|format(o.rr_ratio) }}:1{% else %}-:1{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">标记价</span>
|
||||||
|
<span class="pos-value" id="order-price-{{ o.id }}">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="pos-cell">
|
||||||
|
<span class="pos-label">浮盈亏</span>
|
||||||
|
<span class="pos-value" id="order-pnl-{{ o.id }}">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pos-footer">
|
||||||
|
<span>保证金: <span id="order-ex-margin-{{ o.id }}">-</span></span>
|
||||||
|
<span>计划基数: {% if o.margin_capital is not none %}{{ usdt_fmt(o.margin_capital) }}{% else %}-{% endif %}U</span>
|
||||||
|
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
||||||
|
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="pos-empty">暂无持仓</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -1113,6 +1189,14 @@ function formatSignedUsdt2(v){
|
|||||||
return `${sign}${n.toFixed(2)}`;
|
return `${sign}${n.toFixed(2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRrRatio(rr){
|
||||||
|
if(rr === null || typeof rr === "undefined") return "-:1";
|
||||||
|
const n = Number(rr);
|
||||||
|
if(Number.isNaN(n)) return "-:1";
|
||||||
|
const body = Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(2)));
|
||||||
|
return `${body}:1`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatJournalPnlUi(v){
|
function formatJournalPnlUi(v){
|
||||||
if(v === null || typeof v === "undefined" || v === "") return "-";
|
if(v === null || typeof v === "undefined" || v === "") return "-";
|
||||||
const raw = String(v).trim();
|
const raw = String(v).trim();
|
||||||
@@ -1197,7 +1281,7 @@ function refreshPriceSnapshot(){
|
|||||||
}
|
}
|
||||||
const rrEl = document.getElementById(`order-rr-${o.id}`);
|
const rrEl = document.getElementById(`order-rr-${o.id}`);
|
||||||
if(rrEl){
|
if(rrEl){
|
||||||
rrEl.innerText = (typeof o.rr_ratio !== "undefined" && o.rr_ratio !== null) ? `1:${Number(o.rr_ratio).toFixed(2)}` : "-";
|
rrEl.innerText = formatRrRatio(o.rr_ratio);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# 使用说明
|
||||||
|
|
||||||
|
**本文件对应仓库:`crypto_monitor_gate`(Gate.io USDT 永续)。**
|
||||||
|
功能、界面与 **Binance U 本位版**(目录 `crypto_monitor_binance`)基本一致,差异主要在 **`.env` 里交易所密钥与部分参数名**(`GATE_*` / `BINANCE_*`),文末有对照。
|
||||||
|
|
||||||
|
**更细的部署(SSH 代理、PM2、依赖安装)** 见同目录 **`部署文档.md`**。
|
||||||
|
**关键位自动开仓的规则、RR、结案原因** 见 **`关键位自动下单说明.md`**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 它能做什么
|
||||||
|
|
||||||
|
面向个人盘面的 **Web 控制台**,主要能力包括:
|
||||||
|
|
||||||
|
| 模块 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **关键位监控** | 录入上/下沿与类型,按 **5m 收线** 做硬条件过滤;符合条件后 **企业微信** 提醒,部分类型可 **自动市价开仓**(见第 4 节与专门文档)。 |
|
||||||
|
| **实盘下单监控** | 手工填止损/止盈,**以损定仓** 市价开单,挂上条件止盈止损,并在页面跟踪浮盈亏、保本逻辑等。 |
|
||||||
|
| **交易记录 / 复盘** | 平仓结果、盈亏、错过的单等归档与导出。 |
|
||||||
|
|
||||||
|
后台按 **`MONITOR_POLL_SECONDS`**(默认几秒)轮询行情与监控逻辑。**切勿**在未理解规则时同时运行两套程序共用一个实盘账户。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 运行前必须配置(`.env`)
|
||||||
|
|
||||||
|
至少检查以下项(具体键名以你仓库 `.env` 示例为准):
|
||||||
|
|
||||||
|
| 类别 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **登录网页** | `APP_PASSWORD`:打开站点后的登录口令。`FLASK_SECRET_KEY`:Session 密钥,请勿使用默认值。 |
|
||||||
|
| **企业微信** | `WECHAT_WEBHOOK`:告警与关键位推送机器人的 Webhook。 |
|
||||||
|
| **是否真下单** | `LIVE_TRADING_ENABLED=false`:**不会**向交易所发送开仓指令(适合测试流程)。改为 `true` 且密钥正确才会实盘。 |
|
||||||
|
| **交易所 API** | **本仓库:** `GATE_API_KEY`、`GATE_API_SECRET`;合约相关见 `GATE_MARGIN_MODE`、`GATE_POS_MODE`、`GATE_TPSL_*` 等。**勿**把 `.env` 提交到 Git。 |
|
||||||
|
| **关键位 RR / 止损外扩** | `KEY_AUTO_MIN_PLANNED_RR`、`KEY_STOP_OUTSIDE_BREAKOUT_PCT`(详见 `关键位自动下单说明.md`)。 |
|
||||||
|
|
||||||
|
网络不稳定时可为 Gate 配置 **`GATE_SOCKS_PROXY`** 等(见 **`部署文档.md`**)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 如何启动与登录
|
||||||
|
|
||||||
|
1. 按 **`部署文档.md`** 建好虚拟环境、安装依赖(如 `flask`、`requests`、`ccxt`、按需 `Pillow`、`PySocks` 等),配置好 `.env`。
|
||||||
|
2. 启动 Flask 应用(本仓库可用 **`ecosystem.config.cjs`** 交给 PM2,或本地 `python app.py` / `flask run`,以你当前脚本为准)。
|
||||||
|
3. 浏览器访问站点,打开 **`/login`**,使用 **`.env` 里的 `APP_PASSWORD`** 登录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 关键位监控(页面「关键位监控」卡片)
|
||||||
|
|
||||||
|
### 4.1 添加一条关键位
|
||||||
|
|
||||||
|
1. **币种**:如 `BTC` 或 `BTC/USDT`(会规范成内部符号)。
|
||||||
|
2. **类型**(必选其一):
|
||||||
|
|
||||||
|
| 类型 | 行为摘要 |
|
||||||
|
|------|----------|
|
||||||
|
| **箱体突破** | 通过门控且计划 RR 达标 → **自动市价开仓**(需 `LIVE_TRADING_ENABLED=true` 且无其他持仓占位)。结案后本条从列表消失并记入历史。 |
|
||||||
|
| **收敛突破** | 同上(自动开仓类)。 |
|
||||||
|
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 |
|
||||||
|
| **关键支撑位** | 同上(仅提醒)。 |
|
||||||
|
|
||||||
|
3. **方向**:做多 / 做空(必选)。
|
||||||
|
4. **上沿 / 下沿**:必填;保存时会按交易所 **价格精度** 取整。
|
||||||
|
|
||||||
|
**限制:**
|
||||||
|
当前已有 **`order_monitors` 活跃持仓** 时,**不允许**再添加「**箱体突破** / **收敛突破**」(否则会与「单仓 + 自动开仓」冲突);仍可添加「**关键阻力位 / 支撑位**」。
|
||||||
|
若 **4h EMA55** 与你的方向逆势,页面会 **额外 Flash 提示**,**不阻挡**提交。
|
||||||
|
|
||||||
|
### 4.2 触发后会发生什么(简版)
|
||||||
|
|
||||||
|
- **箱体 / 收敛**:门控通过后计算计划 SL/TP 与 RR;不达标则 **微信说明 + `rr_insufficient` 结案**;达标则尝试 **市价开仓**,成功 **`auto_opened`**,失败 **`exchange_failed`**——均 **不重试同一关键位**。
|
||||||
|
- **阻力 / 支撑**:仅 **单次推送** → **`key_level_alert_only`** 结案。
|
||||||
|
|
||||||
|
详细公式、结案字段、与企业微信文案口径见 **`关键位自动下单说明.md`**。
|
||||||
|
|
||||||
|
### 4.3 列表与历史
|
||||||
|
|
||||||
|
- 当前条目可 **删除**(会按规则记入历史的情形见页面说明)。
|
||||||
|
- **关键位历史**:已结案记录;可配合导出链接(若有)做备份。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 实盘下单监控(手工开仓)
|
||||||
|
|
||||||
|
用于 **自己点按钮** 开单:
|
||||||
|
|
||||||
|
- **同一时间系统只允许一笔「活跃持仓」监视**(与关键位自动单共用该限制)。
|
||||||
|
- 填写币种、方向、杠杆(可选)、止损/止盈(价格或百分比按表单说明)。
|
||||||
|
- 勾选是否启用 **移动保本** 等行为以 `.env`/页面默认值为准。
|
||||||
|
|
||||||
|
平仓通过页面 **平仓**(或等价入口),会从交易所市价处理并更新记录。**删除/误操作可能造成真实盈亏**,请先确认环境与方向。
|
||||||
|
|
||||||
|
开仓成功后持仓卡片上会显示 **「来源」**:手工单一般为 **下单监控**;来自关键位自动单的为 **关键位监控**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 企业微信会看到什么
|
||||||
|
|
||||||
|
- 关键位:按类型与结案结果推送(RR 不足、下单失败、自动开仓成功、仅阻力支撑提醒等),**每条关键位结案路径原则上一条主推送**(详见 `关键位自动下单说明.md`)。
|
||||||
|
- 手工开仓、平仓、部分异常也会在规则满足时推送(以代码与配置为准)。
|
||||||
|
|
||||||
|
若未配置 **`WECHAT_WEBHOOK`** 或网络失败,可能只是看不到推送,不代表逻辑未执行;要紧操作请以 **交易所端持仓与挂单** 为准核对。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 强烈建议的风险与运维习惯
|
||||||
|
|
||||||
|
1. **先用 `LIVE_TRADING_ENABLED=false`** 验证页面、录入、推送,再开小资金开实盘。
|
||||||
|
2. **API 权限**:仅开所需合约权限;勿泄露密钥;定期轮换。
|
||||||
|
3. **单进程控盘**:同一账户避免本程序与其他机器人 **重复开仓**。
|
||||||
|
4. **数据库**:`*.sqlite`(或你在 `.env` 里指向的库文件)包含监控与订单,**备份后再升级/迁移**。
|
||||||
|
5. **升级代码后**:启动时会跑 **数据库迁移**(如新列 `order_monitors.monitor_type`);首次启动关注一下日志或无报错页面。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 常见问题(简要)
|
||||||
|
|
||||||
|
| 现象 | 可自查 |
|
||||||
|
|------|--------|
|
||||||
|
| 关键位永远不触发 | 5m 门控是否全通过(页面门控摘要)、币种日成交量是否在规则内、`KLINE_TIMEFRAME`。 |
|
||||||
|
| 有信号但不自动开仓 | `LIVE_TRADING_ENABLED`、`KEY_AUTO_MIN_PLANNED_RR`、计划 RR、是否已有持仓、API/余额报错(微信或日志)。 |
|
||||||
|
| 加不了箱体/收敛 | 是否已有活跃持仓;先平仓或改用「阻力/支撑位」仅提醒。 |
|
||||||
|
| 推送收不到 | `WECHAT_WEBHOOK`、企业微信机器人配额与网络。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Binance 版(`crypto_monitor_binance`)差异速查
|
||||||
|
|
||||||
|
| 项目 | Gate 本仓库 | Binance 版 |
|
||||||
|
|------|-------------|------------|
|
||||||
|
| API 变量 | `GATE_API_KEY`、`GATE_API_SECRET`、`GATE_*` | `BINANCE_API_KEY`、`BINANCE_API_SECRET`、`BINANCE_*` |
|
||||||
|
| 实盘开关 | `LIVE_TRADING_ENABLED`(通用) | 同上 |
|
||||||
|
| 止盈止损挂载路径 | `_gate_place_tp_sl_orders` 与 `GATE_TPSL_*` | `_binance_place_tp_sl_orders`(U 本位条件单) |
|
||||||
|
| 资金显示舍入 | 以本仓库为准 | 与 **`FUNDS_DECIMALS`** 等一致 |
|
||||||
|
| 专门文档 | **`关键位自动下单说明.md`**(各仓库有一份,开头标明交易所) | 同左 |
|
||||||
|
|
||||||
|
操作流程(登录、关键位四类、手工单、单仓)**两份程序一致**:换目录、换 `.env` 即可对照使用。
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 关键位自动下单说明
|
||||||
|
|
||||||
|
**适用仓库:`crypto_monitor_gate`|交易所:Gate.io USDT 永续**(Binance 版见同名的 `crypto_monitor_binance` 目录。)
|
||||||
|
|
||||||
|
本文档与 `.env`、`app.check_key_monitors`、`app.add_key`、`_market_open_for_key_monitor` 的实现一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 结构与是否自动开仓
|
||||||
|
|
||||||
|
| `key_monitors.monitor_type`(录入类型) | 自动下单 | 触发后处置 |
|
||||||
|
|---------------------------------------|----------|------------|
|
||||||
|
| **箱体突破** | 是(满足全部条件) | **一次性结案**:写 `key_monitor_history` → 从 `key_monitors` **删除** |
|
||||||
|
| **收敛突破** | 是(同上) | 同上 |
|
||||||
|
| **关键阻力位** | 否 | 企业微信 **1 次** → `close_reason=key_level_alert_only` → **失效** |
|
||||||
|
| **关键支撑位** | 否 | 同上 |
|
||||||
|
|
||||||
|
触发条件:**5m 收线硬门控** `_key_hard_checks`(量能、突破幅度、第二根收盘确认、日成交量前 30 等)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 录入限制(`/add_key`)
|
||||||
|
|
||||||
|
- 存在 **`order_monitors.status='active'`** 时:**禁止添加** 「箱体突破」「收敛突破」。
|
||||||
|
- **关键阻力位 / 关键支撑位**:不受上条限制;触发后 **仅单次微信提醒**,然后结案。
|
||||||
|
- **4h EMA55 与所选方向逆势**:**不拦截**;添加成功后 **Flash** 提示。
|
||||||
|
- 上下沿入库前经 **`round_price_to_exchange`** 按合约 **价格精度** 取整。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境与参数(`.env`)
|
||||||
|
|
||||||
|
| 变量 | 含义 | 默认 |
|
||||||
|
|------|------|------|
|
||||||
|
| `KEY_AUTO_MIN_PLANNED_RR` | 计划 RR 阈值:**仅当严格大于该值** 才自动开仓(按下方 `E` 计算) | `1.5` |
|
||||||
|
| `KEY_STOP_OUTSIDE_BREAKOUT_PCT` | 止损:突破 K 极值向外 **百分比**(多:`低×(1−p/100)`;空:`高×(1+p/100)`) | `0.5` |
|
||||||
|
|
||||||
|
**其余与本仓库手动实盘一致:** `KLINE_TIMEFRAME`、`RISK_PERCENT`、`LIVE_TRADING_ENABLED`、`BREAKEVEN_*`、`DAILY_OPEN_ALERT_THRESHOLD`,以及 **`GATE_*`**(密钥、止盈止损触发、`GATE_TPSL_*` 等)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 计价与下单口径
|
||||||
|
|
||||||
|
| 用途 | 价格 |
|
||||||
|
|------|------|
|
||||||
|
| 企业微信展示、**与 RR 门槛比较的计划 RR** | 确认 K(第二根闭合 5m)收盘 **`E`** |
|
||||||
|
| **实际开仓** | **市价**(`place_exchange_order`,与 `/add_order` 一致);成交价可能与 `E` **滑点** |
|
||||||
|
| **以损定仓** | `calc_risk_fraction(direction, 当前市价, 止损)` + `RISK_PERCENT`(与 `/add_order` 一致) |
|
||||||
|
|
||||||
|
- 开仓成功后:`order_monitors.monitor_type` 为 **关键位监控**;持仓卡片「来源」显示之。手动开仓为 **下单监控**。
|
||||||
|
- 持仓列表中的 **盈亏比**:按 **实际成交价** 相对 SL/TP 重算,可与「按 `E` 算的计划 RR」略有偏差。
|
||||||
|
- **本仓库止盈止损挂单**:开仓后由 **`_gate_place_tp_sl_orders`** 挂载(仓位类 `price_orders` 或备选条件路径,逻辑与手动一致);细节受 `GATE_TPSL_USE_POSITION_ORDER`、`GATE_TPSL_PRICE_TYPE` 等影响。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自动单止盈 / 止损(仅箱体突破、收敛突破)
|
||||||
|
|
||||||
|
设箱体高度 **`H = |upper − lower|`**(录入上下沿)。
|
||||||
|
|
||||||
|
| 方向 | 止损 SL | 止盈 TP |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 多 | 突破 K **最低价** × (1 − `KEY_STOP_OUTSIDE_BREAKOUT_PCT`/100) | **`E + 1×H`** |
|
||||||
|
| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`/100) | **`E − 1×H`** |
|
||||||
|
|
||||||
|
计划 **`RR = calc_rr_ratio(direction, E, SL, TP)`**。若为 `None` 或 **RR ≤ `KEY_AUTO_MIN_PLANNED_RR`** → **不下单**,走 `rr_insufficient` 结案。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一次性结案(`close_reason`)
|
||||||
|
|
||||||
|
以下任一发生:**按需发微信** → **`key_monitor_history`** → **从 `key_monitors` 删除**;**不会对同一条关键位重复轮询重试开仓**。
|
||||||
|
|
||||||
|
| `close_reason` | 含义 |
|
||||||
|
|----------------|------|
|
||||||
|
| `rr_insufficient` | 门控通过,但计划 RR 未达标或 SL/TP / RR **几何无效** |
|
||||||
|
| `exchange_failed` | 计划 RR 达标,但未开实盘、`LIVE_TRADING_ENABLED=false`、风控、保证金或 **交易所报错** 等导致 **开仓失败** |
|
||||||
|
| `auto_opened` | 计划 RR 达标且 **市价开仓成功**(已写 `order_monitors`,并已挂止盈止损) |
|
||||||
|
| `key_level_alert_only` | 阻力/支撑位 **仅推送**结案 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 与企业微信推送
|
||||||
|
|
||||||
|
每种结案路径 **至多一条**主业务推送(RR 不足 / 下单失败 / 开仓成功 / 阻力支撑仅提醒)。
|
||||||
|
|
||||||
|
旧版「满 `KEY_ALERT_MAX_TIMES` 次再归档」对已触发结案的路径 **不再适用**;表中 `notification_count`、`max_notify` 等字段仍可能存在,以 **导出、兼容** 为主。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关代码位置(通用)
|
||||||
|
|
||||||
|
| 说明 | 符号 |
|
||||||
|
|------|------|
|
||||||
|
| 门控与主循环 | `check_key_monitors` |
|
||||||
|
| 录入、有仓拦截、4h Flash | `add_key` |
|
||||||
|
| 市价开仓 + 写 `order_monitors` | `_market_open_for_key_monitor` |
|
||||||
|
| 计划 RR | `calc_rr_ratio(direction, E, SL, TP)` |
|
||||||
|
| 价格精度 | `round_price_to_exchange` |
|
||||||
Reference in New Issue
Block a user