feat: add daily reentry policy matrix
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user