1
This commit is contained in:
parent
476cfef193
commit
954e6ad660
@ -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
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user