From 954e6ad6603bb1571297afdae4d8a655558adb2b Mon Sep 17 00:00:00 2001 From: aaron <> Date: Thu, 28 May 2026 01:07:33 +0800 Subject: [PATCH] 1 --- .env.example | 2 ++ app/config/system_config.py | 4 +++ app/db/paper_trading.py | 53 ++++++++++++++++++++++++++++++------- tests/test_paper_trading.py | 4 +++ 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index af164e7..c8c7a4e 100644 --- a/.env.example +++ b/.env.example @@ -83,6 +83,8 @@ ALPHAX_PAPER_ENTRY_MIN_REC_SCORE=50 ALPHAX_PAPER_MIN_RR=1.5 ALPHAX_PAPER_ENTRY_MIN_RR=1.5 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_PAUSE_AFTER_WEAK_ENTRIES=3 ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS=6 diff --git a/app/config/system_config.py b/app/config/system_config.py index f5a9377..22dfd63 100644 --- a/app/config/system_config.py +++ b/app/config/system_config.py @@ -181,6 +181,8 @@ def default_paper_trading_config(): "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), + "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), "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), @@ -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_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_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_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)), diff --git a/app/db/paper_trading.py b/app/db/paper_trading.py index 4b68af8..645376b 100644 --- a/app/db/paper_trading.py +++ b/app/db/paper_trading.py @@ -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) +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: 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) 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)) - sl_risk = _stop_loss_leverage_risk_pct(side, entry_price, stop_loss, leverage) - max_sl_risk = max(0.0, _safe_float(cfg.get("max_stop_loss_leverage_risk_pct"), 0)) + adjusted_leverage, leverage_risk = _risk_adjusted_leverage(side, entry_price, stop_loss, leverage, cfg) + 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 = [] if rec_score < min_score: 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, "tp1": tp1, "leverage": leverage, + "leverage_risk": leverage_risk, }, } 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"), } 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) if not pause_ok: 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", "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 tp2 = _safe_float(rec.get("tp2") or plan.get("tp2") or plan.get("take_profit_2")) 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: fill_price = _safe_float(order.get("target_price")) or current_price cfg = _paper_cfg(config) - side = str(order.get("side") or "long").lower() - 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) + stop_loss = _safe_float(order.get("stop_loss") or _entry_plan(rec).get("stop_loss") or rec.get("stop_loss")) 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) if not global_ok: diff --git a/tests/test_paper_trading.py b/tests/test_paper_trading.py index acb5417..a99d8c9 100644 --- a/tests/test_paper_trading.py +++ b/tests/test_paper_trading.py @@ -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_MIN_REC_SCORE", "50") 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") altcoin_db.init_db() 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] assert trade["stop_loss"] == pytest.approx(0.085064) assert trade["tp1"] == pytest.approx(0.098243) + assert trade["leverage"] < 5 def test_buy_now_pauses_when_portfolio_drawdown_exceeded(monkeypatch, buy_now_rec):