feat: add daily reentry policy matrix

This commit is contained in:
2026-07-17 16:11:18 +02:00
parent 13f57b2525
commit 0cd9ee7689
4 changed files with 764 additions and 5 deletions
+17 -5
View File
@@ -1405,6 +1405,7 @@ def _simulate_portfolio(
max_positions: int = SIM_MAX_POSITIONS,
risk_per_trade: float = SIM_RISK_PER_TRADE,
atr_trail_multiplier: float = ATR_TRAIL_MULTIPLIER,
cost_per_side: float = COST_PER_SIDE,
reentry_cooldown_sessions: int = 0,
initial_stop_refresh_fn: (
Callable[[str, int, float, dict, Any], float | None] | None
@@ -1436,8 +1437,12 @@ def _simulate_portfolio(
checked against the same bar. ``post_stop_reentry_fn`` turns an initial
stop-out into a stateful episode and is the only path by which that ticker
can re-enter until the callback emits a new candidate. Returns None when
there is nothing to trade.
there is nothing to trade. ``cost_per_side`` is charged on entry and exit
and therefore changes both cash availability and subsequent position sizing.
"""
cost_rate = float(cost_per_side)
if not 0.0 <= cost_rate < 1.0:
raise ValueError("cost_per_side must be between 0 (inclusive) and 1")
if qualified_fn is None:
def _default_qualified(c: dict) -> bool:
return bool(c.get("qualified"))
@@ -1565,7 +1570,7 @@ def _simulate_portfolio(
nonlocal cash
pos = positions.pop(sym)
proceeds = pos["shares"] * fill
cost = proceeds * COST_PER_SIDE
cost = proceeds * cost_rate
cash += proceeds - cost
risk = pos["entry"] - pos["initial_stop"]
trades.append({
@@ -1641,6 +1646,7 @@ def _simulate_portfolio(
"exit_fill": float(fill),
"previous_entry": float(closed_pos["entry"]),
"previous_stop": float(closed_pos["initial_stop"]),
"previous_rank": closed_pos["entry_rank"],
"gate_went_unqualified": False,
}
continue
@@ -1677,7 +1683,9 @@ def _simulate_portfolio(
equity = _marked_equity()
fixed_todays = list(entries_by_ord.get(o, ()))
reentry_todays: list[dict] = []
if post_stop_reentry_fn is not None:
if post_stop_reentry_fn is not None and (
end_ord is None or o < end_ord
):
fixed_todays = [
candidate
for candidate in fixed_todays
@@ -1718,11 +1726,11 @@ def _simulate_portfolio(
shares = min(
(equity * risk_per_trade) / risk_ps,
(equity * SIM_NOTIONAL_CAP) / entry,
max(cash, 0.0) / (entry * (1.0 + COST_PER_SIDE)),
max(cash, 0.0) / (entry * (1.0 + cost_rate)),
)
if shares * entry < 1.0: # can't fund a meaningful position
continue
entry_cost = shares * entry * COST_PER_SIDE
entry_cost = shares * entry * cost_rate
cash -= shares * entry + entry_cost
is_reentry = bool(c.get("_post_stop_reentry"))
reentry_wait_sessions: int | None = None
@@ -1748,6 +1756,9 @@ def _simulate_portfolio(
"bars_held": 0,
"last_close": entry,
"highest_close": entry,
"entry_rank": (
float(c[ranking_key]) if c.get(ranking_key) is not None else None
),
"stop_refreshes": 0,
"is_reentry": is_reentry,
"reentry_wait_sessions": reentry_wait_sessions,
@@ -1856,6 +1867,7 @@ def _simulate_portfolio(
result = {
"starting_capital": SIM_STARTING_CAPITAL,
"cost_per_side_pct": round(cost_rate * 100.0, 3),
"final_equity": round(final_equity, 2),
"total_return_pct": round(total_return_pct, 1),
"cagr_pct": round(cagr_pct, 1) if cagr_pct is not None else None,