前端面板修改
This commit is contained in:
+137
-38
@@ -123,9 +123,18 @@ BALANCE_REFRESH_SECONDS = int(os.getenv("BALANCE_REFRESH_SECONDS", "60"))
|
||||
PRICE_REFRESH_SECONDS = int(os.getenv("PRICE_REFRESH_SECONDS", "5"))
|
||||
KEY_ALERT_MAX_TIMES = int(os.getenv("KEY_ALERT_MAX_TIMES", "3"))
|
||||
KEY_ALERT_INTERVAL_MINUTES = int(os.getenv("KEY_ALERT_INTERVAL_MINUTES", "5"))
|
||||
KEY_BREAKOUT_LIMIT_PCT = float(os.getenv("KEY_BREAKOUT_LIMIT_PCT", "1.5"))
|
||||
KEY_AUTO_MIN_PLANNED_RR = float(os.getenv("KEY_AUTO_MIN_PLANNED_RR", "1.5"))
|
||||
KEY_STOP_OUTSIDE_BREAKOUT_PCT = float(os.getenv("KEY_STOP_OUTSIDE_BREAKOUT_PCT", "0.5"))
|
||||
MANUAL_MIN_PLANNED_RR = float(os.getenv("MANUAL_MIN_PLANNED_RR", "1.4"))
|
||||
MAX_ACTIVE_POSITIONS = max(1, int(os.getenv("MAX_ACTIVE_POSITIONS", "1")))
|
||||
KEY_VOLUME_MA_BARS = max(1, int(os.getenv("KEY_VOLUME_MA_BARS", "20")))
|
||||
KEY_VOLUME_RATIO_MIN = float(os.getenv("KEY_VOLUME_RATIO_MIN", "1.3"))
|
||||
KEY_BREAKOUT_AMP_MIN_PCT = float(os.getenv("KEY_BREAKOUT_AMP_MIN_PCT", "0.03"))
|
||||
KEY_BREAKOUT_AMP_MAX_PCT = float(os.getenv("KEY_BREAKOUT_AMP_MAX_PCT", "0.5"))
|
||||
KEY_DAILY_VOLUME_RANK_MAX = max(1, int(os.getenv("KEY_DAILY_VOLUME_RANK_MAX", "30")))
|
||||
KEY_CONFIRM_BREAKOUT_BAR = int(os.getenv("KEY_CONFIRM_BREAKOUT_BAR", "-2"))
|
||||
KEY_CONFIRM_BAR = int(os.getenv("KEY_CONFIRM_BAR", "-1"))
|
||||
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT = os.getenv("KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT", "true").lower() == "true"
|
||||
ORDER_MONITOR_TYPE_MANUAL = "下单监控"
|
||||
ORDER_MONITOR_TYPE_KEY_AUTO = "关键位监控"
|
||||
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
|
||||
@@ -1223,6 +1232,10 @@ def init_db():
|
||||
try:
|
||||
c.execute("ALTER TABLE key_monitors ADD COLUMN breakout_limit_pct REAL DEFAULT 1.5")
|
||||
except: pass
|
||||
try:
|
||||
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
c.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS key_monitor_history
|
||||
@@ -2302,13 +2315,64 @@ def trading_day_reset_allows_new_open(now):
|
||||
return now.hour >= TRADING_DAY_RESET_HOUR
|
||||
|
||||
|
||||
def get_active_position_count(conn):
|
||||
return int(conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0])
|
||||
|
||||
|
||||
def clear_key_sizing_snapshot_if_flat(conn, session_date):
|
||||
if get_active_position_count(conn) > 0:
|
||||
return
|
||||
conn.execute(
|
||||
"UPDATE trading_sessions SET key_sizing_capital_snapshot = NULL, updated_at = CURRENT_TIMESTAMP WHERE session_date = ?",
|
||||
(session_date,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_key_sizing_capital_snapshot(conn, session_date):
|
||||
row = ensure_session(conn, session_date)
|
||||
try:
|
||||
val = row["key_sizing_capital_snapshot"]
|
||||
except (KeyError, IndexError):
|
||||
return None
|
||||
if val is None:
|
||||
return None
|
||||
try:
|
||||
return float(val)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def set_key_sizing_capital_snapshot(conn, session_date, capital):
|
||||
ensure_session(conn, session_date)
|
||||
conn.execute(
|
||||
"UPDATE trading_sessions SET key_sizing_capital_snapshot = ?, updated_at = CURRENT_TIMESTAMP WHERE session_date = ?",
|
||||
(round(float(capital), FUNDS_DECIMALS), session_date),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def resolve_capital_base_for_key_open(conn, trading_day, live_capital):
|
||||
"""关键位自动开仓:有仓时用无仓时资金快照计仓(可配置)。"""
|
||||
live = float(live_capital)
|
||||
active = get_active_position_count(conn)
|
||||
if active <= 0:
|
||||
set_key_sizing_capital_snapshot(conn, trading_day, live)
|
||||
return live
|
||||
if KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT:
|
||||
snap = get_key_sizing_capital_snapshot(conn, trading_day)
|
||||
if snap is not None and snap > 0:
|
||||
return snap
|
||||
return live
|
||||
|
||||
|
||||
def precheck_risk(conn, symbol, direction):
|
||||
now = app_now()
|
||||
if not trading_day_reset_allows_new_open(now):
|
||||
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
||||
active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0]
|
||||
if active_count > 0:
|
||||
return False, "一次只能持有一个仓位"
|
||||
active_count = get_active_position_count(conn)
|
||||
if active_count >= MAX_ACTIVE_POSITIONS:
|
||||
return False, f"已达最大持仓数({active_count}/{MAX_ACTIVE_POSITIONS})"
|
||||
if direction not in ("long", "short"):
|
||||
return False, "方向必须为 long 或 short"
|
||||
if symbol.upper().startswith("BTC") or symbol.upper().startswith("ETH"):
|
||||
@@ -3121,6 +3185,7 @@ def reconcile_external_closes(conn, days=None):
|
||||
closed_at=closed_at,
|
||||
)
|
||||
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (r["id"],))
|
||||
clear_key_sizing_snapshot_if_flat(conn, r["session_date"] or get_trading_day())
|
||||
if result in ("止盈", "止损", "保本止盈", "移动止盈", "手动平仓", "强制清仓"):
|
||||
send_wechat_msg(
|
||||
build_wechat_close_message(
|
||||
@@ -3287,21 +3352,26 @@ def _key_hard_checks(symbol, direction, upper, lower, monitor_type):
|
||||
out["reason"] = "5m K线数量不足"
|
||||
return out
|
||||
closed = bars[:-1] if len(bars) >= 3 else bars
|
||||
if len(closed) < 23:
|
||||
out["reason"] = "闭合K线不足"
|
||||
min_closed = KEY_VOLUME_MA_BARS + 3
|
||||
if len(closed) < min_closed:
|
||||
out["reason"] = f"{KLINE_TIMEFRAME} 闭合K线不足"
|
||||
return out
|
||||
breakout = closed[-2]
|
||||
confirm = closed[-1]
|
||||
prev20 = closed[-22:-2]
|
||||
avg20 = sum(float(x[5]) for x in prev20) / max(len(prev20), 1)
|
||||
try:
|
||||
breakout = closed[KEY_CONFIRM_BREAKOUT_BAR]
|
||||
confirm = closed[KEY_CONFIRM_BAR]
|
||||
except IndexError:
|
||||
out["reason"] = "确认K索引超出范围,请检查 KEY_CONFIRM_* 配置"
|
||||
return out
|
||||
prev_vol = closed[KEY_CONFIRM_BREAKOUT_BAR - KEY_VOLUME_MA_BARS : KEY_CONFIRM_BREAKOUT_BAR]
|
||||
avg20 = sum(float(x[5]) for x in prev_vol) / max(len(prev_vol), 1)
|
||||
vol_break = float(breakout[5])
|
||||
vol_ok = vol_break > avg20 * 1.3 if avg20 > 0 else False
|
||||
vol_ok = vol_break > avg20 * KEY_VOLUME_RATIO_MIN if avg20 > 0 else False
|
||||
open_b = float(breakout[1])
|
||||
close_b = float(breakout[4])
|
||||
high_b = float(breakout[2])
|
||||
low_b = float(breakout[3])
|
||||
amp_pct = abs(close_b - open_b) / open_b * 100 if open_b > 0 else 0
|
||||
amp_ok = (amp_pct > 0.03) and (amp_pct < 0.5)
|
||||
amp_ok = (amp_pct > KEY_BREAKOUT_AMP_MIN_PCT) and (amp_pct < KEY_BREAKOUT_AMP_MAX_PCT)
|
||||
cfm_close = float(confirm[4])
|
||||
# 区间极值点严格以前端录入 upper/lower 为准:做多看上沿,做空看下沿
|
||||
edge = float(upper) if direction == "long" else float(lower)
|
||||
@@ -3311,7 +3381,7 @@ def _key_hard_checks(symbol, direction, upper, lower, monitor_type):
|
||||
amp_ok = amp_ok and breakout_ok
|
||||
confirm_ok = confirm_ok_raw and breakout_ok
|
||||
rank, total = _daily_volume_rank(symbol)
|
||||
rank_ok = (rank is not None) and (rank <= 30)
|
||||
rank_ok = (rank is not None) and (rank <= KEY_DAILY_VOLUME_RANK_MAX)
|
||||
swing4h_pct = 0.0
|
||||
try:
|
||||
seg48 = closed[-48:] if len(closed) >= 48 else closed
|
||||
@@ -3424,7 +3494,8 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
|
||||
).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"])
|
||||
live_capital = float(trading_capital_live) if trading_capital_live is not None else float(session_row["current_capital"])
|
||||
capital_base = resolve_capital_base_for_key_open(conn, trading_day, live_capital)
|
||||
|
||||
trade_style = (DEFAULT_TRADE_STYLE or "trend").strip().lower()
|
||||
if trade_style not in ("trend", "swing"):
|
||||
@@ -3990,6 +4061,7 @@ def check_order_monitors():
|
||||
closed_at=closed_at,
|
||||
)
|
||||
conn.execute("UPDATE order_monitors SET status='stopped', exchange_close_order_id=? WHERE id=?", (close_order_id, pid))
|
||||
clear_key_sizing_snapshot_if_flat(conn, get_trading_day())
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -4198,7 +4270,12 @@ def render_main_page(page="trade"):
|
||||
)
|
||||
rate = round(win/total*100,2) if total else 0
|
||||
active_count = len(order_list)
|
||||
can_trade = trading_day_reset_allows_new_open(now) and active_count == 0
|
||||
can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
|
||||
key_gate_rule_text = (
|
||||
f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}、确认棒偏移 {KEY_CONFIRM_BAR}|"
|
||||
f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|"
|
||||
f"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}"
|
||||
)
|
||||
conn.close()
|
||||
return render_template(
|
||||
"index.html",
|
||||
@@ -4242,6 +4319,11 @@ def render_main_page(page="trade"):
|
||||
entry_reason_options=list(ENTRY_REASON_OPTIONS),
|
||||
entry_reason_other_value=ENTRY_REASON_OTHER,
|
||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
manual_min_planned_rr=MANUAL_MIN_PLANNED_RR,
|
||||
key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR,
|
||||
key_gate_rule_text=key_gate_rule_text,
|
||||
kline_timeframe=KLINE_TIMEFRAME,
|
||||
)
|
||||
|
||||
|
||||
@@ -4251,6 +4333,12 @@ def index():
|
||||
return redirect("/trade")
|
||||
|
||||
|
||||
@app.route("/key_monitor")
|
||||
@login_required
|
||||
def key_monitor_page():
|
||||
return render_main_page("key_monitor")
|
||||
|
||||
|
||||
@app.route("/trade")
|
||||
@login_required
|
||||
def trade_page():
|
||||
@@ -4281,9 +4369,9 @@ def api_account_snapshot():
|
||||
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
|
||||
current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
|
||||
recommended_capital = get_recommended_capital(current_capital)
|
||||
active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0]
|
||||
active_count = get_active_position_count(conn)
|
||||
conn.close()
|
||||
can_trade = trading_day_reset_allows_new_open(now) and active_count == 0
|
||||
can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
|
||||
available_trading_usdt = get_available_trading_usdt()
|
||||
return jsonify({
|
||||
"funding_usdt": funding_usdt,
|
||||
@@ -4291,7 +4379,9 @@ def api_account_snapshot():
|
||||
"available_trading_usdt": round(available_trading_usdt, FUNDS_DECIMALS) if available_trading_usdt is not None else None,
|
||||
"recommended_capital": recommended_capital,
|
||||
"active_count": active_count,
|
||||
"max_active_positions": MAX_ACTIVE_POSITIONS,
|
||||
"can_trade": can_trade,
|
||||
"manual_min_planned_rr": MANUAL_MIN_PLANNED_RR,
|
||||
"trading_day": trading_day
|
||||
})
|
||||
|
||||
@@ -4451,7 +4541,8 @@ def api_symbol_liquidity_rank():
|
||||
"symbol": symbol,
|
||||
"rank": int(rank),
|
||||
"total": int(total),
|
||||
"in_top30": bool(rank <= 30),
|
||||
"in_top30": bool(rank <= KEY_DAILY_VOLUME_RANK_MAX),
|
||||
"rank_max": KEY_DAILY_VOLUME_RANK_MAX,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4468,13 +4559,15 @@ def api_order_defaults():
|
||||
exchange_symbol = normalize_exchange_symbol(symbol)
|
||||
leverage = get_synced_leverage(exchange_symbol, direction) or infer_leverage(symbol)
|
||||
available = get_available_trading_usdt()
|
||||
last_price = get_price(symbol)
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"symbol": symbol,
|
||||
"exchange_symbol": exchange_symbol,
|
||||
"direction": direction,
|
||||
"leverage": leverage,
|
||||
"available_trading_usdt": round(available, FUNDS_DECIMALS) if available is not None else None
|
||||
"available_trading_usdt": round(available, FUNDS_DECIMALS) if available is not None else None,
|
||||
"last_price": round(float(last_price), 8) if last_price is not None else None,
|
||||
})
|
||||
|
||||
|
||||
@@ -4709,34 +4802,33 @@ def add_key():
|
||||
symbol = normalize_symbol_input(d.get("symbol"))
|
||||
if not symbol:
|
||||
flash("symbol 不能为空")
|
||||
return redirect("/")
|
||||
return redirect("/key_monitor")
|
||||
direction_sel = (d.get("direction") or "").strip().lower()
|
||||
if direction_sel not in ("long", "short"):
|
||||
flash("请选择做多或做空")
|
||||
return redirect("/")
|
||||
return redirect("/key_monitor")
|
||||
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("/")
|
||||
return redirect("/key_monitor")
|
||||
rank, total = _daily_volume_rank(symbol)
|
||||
if rank is None:
|
||||
flash("日成交量排名读取失败,请稍后重试")
|
||||
return redirect("/")
|
||||
if rank > 30:
|
||||
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前30,已拒绝添加关键位")
|
||||
return redirect("/")
|
||||
return redirect("/key_monitor")
|
||||
if rank > KEY_DAILY_VOLUME_RANK_MAX:
|
||||
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前{KEY_DAILY_VOLUME_RANK_MAX},已拒绝添加关键位")
|
||||
return redirect("/key_monitor")
|
||||
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:
|
||||
occupied = get_active_position_count(conn)
|
||||
if occupied >= MAX_ACTIVE_POSITIONS:
|
||||
conn.close()
|
||||
flash(
|
||||
"当前已有实盘持仓:无法添加「箱体突破 / 收敛突破」(会自动开仓)。请平仓后再试,或使用「关键阻力位/关键支撑位」(仅单次提醒)。"
|
||||
f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。"
|
||||
"请平仓后再试,或使用「关键阻力位/关键支撑位」(仅单次提醒)。"
|
||||
)
|
||||
return redirect("/")
|
||||
return redirect("/key_monitor")
|
||||
ex_sym_key = normalize_exchange_symbol(symbol)
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
@@ -4765,7 +4857,7 @@ def add_key():
|
||||
flash(
|
||||
"⚠️ 4h EMA55 提示:当前与所选方向逆势;「箱体突破/收敛突破」在条件满足时仍会按计划自动市价开仓,请注意仓位。"
|
||||
)
|
||||
return redirect("/")
|
||||
return redirect("/key_monitor")
|
||||
|
||||
@app.route("/add_order", methods=["POST"])
|
||||
@login_required
|
||||
@@ -4781,7 +4873,7 @@ def add_order():
|
||||
return redirect("/")
|
||||
ok, reason = precheck_risk(conn, symbol, direction)
|
||||
if not ok:
|
||||
if "一次只能持有一个仓位" in reason:
|
||||
if "已达最大持仓数" in reason:
|
||||
try:
|
||||
tp_raw = parse_positive_float(d.get("tp"))
|
||||
sl_raw = parse_positive_float(d.get("sl"))
|
||||
@@ -4797,14 +4889,14 @@ def add_order():
|
||||
stop_loss=sl_raw or 0,
|
||||
take_profit=tgt_raw or 0,
|
||||
result="错过",
|
||||
miss_reason="持仓占用:一次只能持有一个仓位",
|
||||
miss_reason=f"持仓占用:{reason}",
|
||||
opened_at=app_now_str(),
|
||||
closed_at=app_now_str(),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
flash(f"风控拒绝下单:{reason}")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
ok_live, reason_live = ensure_exchange_live_ready()
|
||||
if not ok_live:
|
||||
conn.close()
|
||||
@@ -4873,7 +4965,13 @@ def add_order():
|
||||
if stop_loss <= 0 or take_profit <= 0:
|
||||
conn.close()
|
||||
flash("价格参数必须大于0")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
planned_rr_manual = calc_rr_ratio(direction, live_price, stop_loss, take_profit)
|
||||
if planned_rr_manual is None or planned_rr_manual < MANUAL_MIN_PLANNED_RR:
|
||||
conn.close()
|
||||
rr_txt = f"{planned_rr_manual:.4f}" if planned_rr_manual is not None else "无法计算"
|
||||
flash(f"风控拒绝下单:计划盈亏比 {rr_txt}:1 低于最低要求 {MANUAL_MIN_PLANNED_RR}:1")
|
||||
return redirect("/trade")
|
||||
risk_fraction = calc_risk_fraction(direction, live_price, stop_loss)
|
||||
if risk_fraction is None:
|
||||
conn.close()
|
||||
@@ -5329,6 +5427,7 @@ def del_order(id):
|
||||
closed_at=closed_at,
|
||||
)
|
||||
conn.execute("UPDATE order_monitors SET status='stopped', exchange_close_order_id=? WHERE id=?", (close_order_id, id))
|
||||
clear_key_sizing_snapshot_if_flat(conn, session_date)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
send_wechat_msg(
|
||||
@@ -5348,7 +5447,7 @@ def del_order(id):
|
||||
)
|
||||
)
|
||||
flash("已按实盘流程手动平仓")
|
||||
return redirect("/")
|
||||
return redirect("/trade")
|
||||
except Exception as e:
|
||||
if is_no_position_error(str(e)):
|
||||
cancel_binance_futures_open_orders(row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"]))
|
||||
|
||||
Reference in New Issue
Block a user