This commit is contained in:
aaron 2026-05-28 01:07:33 +08:00
parent 476cfef193
commit 954e6ad660
4 changed files with 53 additions and 10 deletions

View File

@ -83,6 +83,8 @@ ALPHAX_PAPER_ENTRY_MIN_REC_SCORE=50
ALPHAX_PAPER_MIN_RR=1.5 ALPHAX_PAPER_MIN_RR=1.5
ALPHAX_PAPER_ENTRY_MIN_RR=1.5 ALPHAX_PAPER_ENTRY_MIN_RR=1.5
ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT=20 ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT=20
ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED=1
ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN=3
ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT=3 ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT=3
ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES=3 ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES=3
ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS=6 ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS=6

View File

@ -181,6 +181,8 @@ def default_paper_trading_config():
"min_rr": paper_min_rr, "min_rr": paper_min_rr,
"entry_min_rr": _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", paper_min_rr), "entry_min_rr": _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", paper_min_rr),
"max_stop_loss_leverage_risk_pct": _env_float("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", 20.0), "max_stop_loss_leverage_risk_pct": _env_float("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", 20.0),
"dynamic_leverage_enabled": _env_bool("ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED", True),
"dynamic_leverage_min": _env_float("ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN", 3.0),
"max_account_drawdown_pause_pct": _env_float("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", 3.0), "max_account_drawdown_pause_pct": _env_float("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", 3.0),
"pause_after_weak_entries": _env_int("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", 3), "pause_after_weak_entries": _env_int("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", 3),
"weak_entry_window_hours": _env_float("ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS", 6.0), "weak_entry_window_hours": _env_float("ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS", 6.0),
@ -234,6 +236,8 @@ def _paper_trading_env_overrides():
"ALPHAX_PAPER_ENTRY_MIN_REC_SCORE": ("entry_min_rec_score", lambda: _env_float("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", 50.0)), "ALPHAX_PAPER_ENTRY_MIN_REC_SCORE": ("entry_min_rec_score", lambda: _env_float("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", 50.0)),
"ALPHAX_PAPER_ENTRY_MIN_RR": ("entry_min_rr", lambda: _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", overrides.get("min_rr", 1.5))), "ALPHAX_PAPER_ENTRY_MIN_RR": ("entry_min_rr", lambda: _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", overrides.get("min_rr", 1.5))),
"ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT": ("max_stop_loss_leverage_risk_pct", lambda: _env_float("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", 20.0)), "ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT": ("max_stop_loss_leverage_risk_pct", lambda: _env_float("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", 20.0)),
"ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED": ("dynamic_leverage_enabled", lambda: _env_bool("ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED", True)),
"ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN": ("dynamic_leverage_min", lambda: _env_float("ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN", 3.0)),
"ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT": ("max_account_drawdown_pause_pct", lambda: _env_float("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", 3.0)), "ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT": ("max_account_drawdown_pause_pct", lambda: _env_float("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", 3.0)),
"ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES": ("pause_after_weak_entries", lambda: _env_int("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", 3)), "ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES": ("pause_after_weak_entries", lambda: _env_int("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", 3)),
"ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS": ("weak_entry_window_hours", lambda: _env_float("ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS", 6.0)), "ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS": ("weak_entry_window_hours", lambda: _env_float("ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS", 6.0)),

View File

@ -400,6 +400,40 @@ def _stop_loss_leverage_risk_pct(side: str, entry_price: float, stop_loss: float
return round(_stop_loss_distance_pct(side, entry_price, stop_loss) * max(1.0, _safe_float(leverage, 1.0)), 6) return round(_stop_loss_distance_pct(side, entry_price, stop_loss) * max(1.0, _safe_float(leverage, 1.0)), 6)
def _risk_adjusted_leverage(side: str, entry_price: float, stop_loss: float, leverage: float, config: dict | None = None) -> tuple[float, dict]:
cfg = _paper_cfg(config)
base_leverage = max(1.0, _safe_float(leverage, 1.0))
max_sl_risk = max(0.0, _safe_float(cfg.get("max_stop_loss_leverage_risk_pct"), 0))
distance_pct = _stop_loss_distance_pct(side, entry_price, stop_loss)
original_risk = round(distance_pct * base_leverage, 6)
detail = {
"dynamic_leverage_enabled": bool(cfg.get("dynamic_leverage_enabled", True)),
"base_leverage": base_leverage,
"leverage": base_leverage,
"stop_loss_distance_pct": distance_pct,
"stop_loss_leverage_risk_pct": original_risk,
"max_stop_loss_leverage_risk_pct": max_sl_risk,
"adjusted": False,
}
if max_sl_risk <= 0 or distance_pct <= 0 or original_risk <= max_sl_risk + 1e-12:
return base_leverage, detail
if not bool(cfg.get("dynamic_leverage_enabled", True)):
return base_leverage, detail
allowed_leverage = max_sl_risk / distance_pct
min_leverage = max(1.0, _safe_float(cfg.get("dynamic_leverage_min"), 3.0))
detail["allowed_leverage"] = round(allowed_leverage, 6)
detail["min_dynamic_leverage"] = min_leverage
if allowed_leverage + 1e-12 < min_leverage:
return base_leverage, detail
adjusted = round(min(base_leverage, allowed_leverage), 4)
detail.update({
"leverage": adjusted,
"stop_loss_leverage_risk_pct": round(distance_pct * adjusted, 6),
"adjusted": adjusted < base_leverage,
})
return adjusted, detail
def _trade_rr(side: str, entry_price: float, stop_loss: float, tp1: float) -> float: def _trade_rr(side: str, entry_price: float, stop_loss: float, tp1: float) -> float:
return _paper_order_rr(side, entry_price, stop_loss, tp1) return _paper_order_rr(side, entry_price, stop_loss, tp1)
@ -680,8 +714,10 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
rr = max([x for x in rr_candidates if x > 0], default=0.0) rr = max([x for x in rr_candidates if x > 0], default=0.0)
min_rr = max(0.0, _safe_float(cfg.get("entry_min_rr"), 0)) min_rr = max(0.0, _safe_float(cfg.get("entry_min_rr"), 0))
min_score = max(0.0, _safe_float(cfg.get("entry_min_rec_score"), 0)) min_score = max(0.0, _safe_float(cfg.get("entry_min_rec_score"), 0))
sl_risk = _stop_loss_leverage_risk_pct(side, entry_price, stop_loss, leverage) adjusted_leverage, leverage_risk = _risk_adjusted_leverage(side, entry_price, stop_loss, leverage, cfg)
max_sl_risk = max(0.0, _safe_float(cfg.get("max_stop_loss_leverage_risk_pct"), 0)) sl_risk = _safe_float(leverage_risk.get("stop_loss_leverage_risk_pct"))
max_sl_risk = _safe_float(leverage_risk.get("max_stop_loss_leverage_risk_pct"))
leverage = adjusted_leverage
entry_reasons = [] entry_reasons = []
if rec_score < min_score: if rec_score < min_score:
entry_reasons.append("rec_score_below_min") entry_reasons.append("rec_score_below_min")
@ -708,6 +744,7 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
"stop_loss": stop_loss, "stop_loss": stop_loss,
"tp1": tp1, "tp1": tp1,
"leverage": leverage, "leverage": leverage,
"leverage_risk": leverage_risk,
}, },
} }
global_ok, global_detail = _global_risk_entry_check(conn, rec, notional, cfg) global_ok, global_detail = _global_risk_entry_check(conn, rec, notional, cfg)
@ -728,6 +765,8 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
"decision": global_detail.get("decision"), "decision": global_detail.get("decision"),
} }
notional = adjusted_notional notional = adjusted_notional
if "leverage_risk" in locals() and leverage_risk.get("adjusted"):
plan["leverage_sizing"] = leverage_risk
pause_ok, pause_reason, pause_detail = _portfolio_entry_pause_check(conn, notional, event_time, cfg) pause_ok, pause_reason, pause_detail = _portfolio_entry_pause_check(conn, notional, event_time, cfg)
if not pause_ok: if not pause_ok:
return {"opened": False, "skipped": True, "reason": pause_reason, "risk_detail": pause_detail} return {"opened": False, "skipped": True, "reason": pause_reason, "risk_detail": pause_detail}
@ -739,7 +778,7 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
"reason": "cumulative_leverage_exceeded", "reason": "cumulative_leverage_exceeded",
"risk_detail": leverage_detail, "risk_detail": leverage_detail,
} }
margin = default_margin_usdt(cfg) margin = round(notional / leverage, 8) if leverage > 0 else default_margin_usdt(cfg)
qty = round(notional / entry_price, 12) if entry_price > 0 else 0 qty = round(notional / entry_price, 12) if entry_price > 0 else 0
tp2 = _safe_float(rec.get("tp2") or plan.get("tp2") or plan.get("take_profit_2")) tp2 = _safe_float(rec.get("tp2") or plan.get("tp2") or plan.get("take_profit_2"))
fee = round(notional * default_fee_rate(cfg), 8) fee = round(notional * default_fee_rate(cfg), 8)
@ -1068,13 +1107,7 @@ def _order_payload_from_rec(rec: dict, current_price: float, event_time: str, co
def _fill_paper_order(conn, order: dict, rec: dict, current_price: float, event_time: str, config: dict | None = None) -> dict: def _fill_paper_order(conn, order: dict, rec: dict, current_price: float, event_time: str, config: dict | None = None) -> dict:
fill_price = _safe_float(order.get("target_price")) or current_price fill_price = _safe_float(order.get("target_price")) or current_price
cfg = _paper_cfg(config) cfg = _paper_cfg(config)
side = str(order.get("side") or "long").lower() stop_loss = _safe_float(order.get("stop_loss") or _entry_plan(rec).get("stop_loss") or rec.get("stop_loss"))
leverage = default_leverage(cfg)
stop_loss = _safe_float(order.get("stop_loss") or rec.get("stop_loss") or _entry_plan(rec).get("stop_loss"))
sl_risk = _stop_loss_leverage_risk_pct(side, fill_price, stop_loss, leverage)
max_sl_risk = max(0.0, _safe_float(cfg.get("max_stop_loss_leverage_risk_pct"), 0))
if max_sl_risk > 0 and sl_risk > max_sl_risk:
return _cancel_paper_order(conn, order, "stop_loss_leverage_risk_exceeded", event_time)
base_notional = _safe_float(order.get("notional_usdt"), default_notional_usdt(cfg)) base_notional = _safe_float(order.get("notional_usdt"), default_notional_usdt(cfg))
global_ok, global_detail = _global_risk_entry_check(conn, rec, base_notional, cfg) global_ok, global_detail = _global_risk_entry_check(conn, rec, base_notional, cfg)
if not global_ok: if not global_ok:

View File

@ -431,6 +431,9 @@ def test_buy_now_entry_gate_uses_latest_entry_plan_rr(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "1") monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", "50") monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", "50")
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_RR", "1.5") monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_RR", "1.5")
monkeypatch.setenv("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", "20")
monkeypatch.setenv("ALPHAX_PAPER_DYNAMIC_LEVERAGE_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_DYNAMIC_LEVERAGE_MIN", "3")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0") monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db() altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation( rec_id = altcoin_db.create_recommendation(
@ -460,6 +463,7 @@ def test_buy_now_entry_gate_uses_latest_entry_plan_rr(monkeypatch):
trade = list_paper_trades()["items"][0] trade = list_paper_trades()["items"][0]
assert trade["stop_loss"] == pytest.approx(0.085064) assert trade["stop_loss"] == pytest.approx(0.085064)
assert trade["tp1"] == pytest.approx(0.098243) assert trade["tp1"] == pytest.approx(0.098243)
assert trade["leverage"] < 5
def test_buy_now_pauses_when_portfolio_drawdown_exceeded(monkeypatch, buy_now_rec): def test_buy_now_pauses_when_portfolio_drawdown_exceeded(monkeypatch, buy_now_rec):