feat: math core v3 engine upgrade
This commit is contained in:
@@ -14,6 +14,7 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
@@ -702,3 +703,163 @@ def evaluate_order(
|
||||
state_snapshot=state,
|
||||
evaluated_at=now,
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# v3 Stop-Defined Portfolio Heat (Requirements 15.1–15.5)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def compute_portfolio_heat(
|
||||
positions: list[dict[str, float]],
|
||||
stop_distances: dict[str, float],
|
||||
) -> float:
|
||||
"""Compute total portfolio heat from stop-defined risk dollars.
|
||||
|
||||
risk_dollars = position_value × stop_distance_pct for each position.
|
||||
portfolio_heat = sum of all risk_dollars.
|
||||
|
||||
positions: list of dicts with keys "ticker" and "position_value"
|
||||
stop_distances: dict mapping ticker to stop_distance_pct
|
||||
|
||||
Requirements: 15.1, 15.2
|
||||
"""
|
||||
total_heat = 0.0
|
||||
for pos in positions:
|
||||
ticker = pos.get("ticker", "")
|
||||
position_value = pos.get("position_value", 0.0)
|
||||
stop_distance_pct = stop_distances.get(ticker, 0.0)
|
||||
risk_dollars = position_value * stop_distance_pct
|
||||
total_heat += risk_dollars
|
||||
return total_heat
|
||||
|
||||
|
||||
def check_heat_capacity(
|
||||
current_heat: float,
|
||||
new_risk_dollars: float,
|
||||
max_heat_pct: float,
|
||||
portfolio_value: float,
|
||||
) -> bool:
|
||||
"""Check if a new position would exceed heat capacity.
|
||||
|
||||
Returns True if the new entry is allowed (capacity exists).
|
||||
Returns False if it would exceed max_heat_pct × portfolio_value.
|
||||
|
||||
Requirements: 15.3, 15.4, 15.5
|
||||
"""
|
||||
max_heat_dollars = max_heat_pct * portfolio_value
|
||||
return (current_heat + new_risk_dollars) <= max_heat_dollars
|
||||
|
||||
|
||||
def compute_available_heat_capacity(
|
||||
current_heat: float,
|
||||
max_heat_pct: float,
|
||||
portfolio_value: float,
|
||||
) -> float:
|
||||
"""Compute available heat capacity for new positions.
|
||||
|
||||
available = max_heat_pct × portfolio_value - current_heat
|
||||
Returns max(0, available).
|
||||
|
||||
Requirement: 15.4
|
||||
"""
|
||||
max_heat_dollars = max_heat_pct * portfolio_value
|
||||
available = max_heat_dollars - current_heat
|
||||
return max(0.0, available)
|
||||
|
||||
|
||||
def compute_heat_capacity_pct(
|
||||
current_heat: float,
|
||||
max_heat_pct: float,
|
||||
portfolio_value: float,
|
||||
) -> float:
|
||||
"""Compute available heat capacity as a portfolio percentage for Kelly sizing.
|
||||
|
||||
This converts absolute available heat dollars into a fraction of portfolio
|
||||
value, suitable for use as `heat_capacity` in the Kelly sizing pipeline's
|
||||
`available_caps` dict.
|
||||
|
||||
Requirements: 15.4, 15.5
|
||||
"""
|
||||
if portfolio_value <= 0.0:
|
||||
return 0.0
|
||||
available_dollars = compute_available_heat_capacity(
|
||||
current_heat, max_heat_pct, portfolio_value
|
||||
)
|
||||
return available_dollars / portfolio_value
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# v3 Risk Tier Auto-Adjustment (Requirements 18.1–18.6)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TierMetrics:
|
||||
"""30-day rolling performance metrics for tier adjustment.
|
||||
|
||||
Collected once per calendar day after session close.
|
||||
|
||||
Requirements: 18.1
|
||||
"""
|
||||
|
||||
profit_factor_30d: float
|
||||
"""Gross profit / gross loss over last 30 days."""
|
||||
|
||||
max_drawdown_30d: float
|
||||
"""Largest peak-to-trough as fraction over last 30 days."""
|
||||
|
||||
calibration_error: float
|
||||
"""Mean |predicted P_up - realized outcome| over last 30 days."""
|
||||
|
||||
realized_sharpe_30d: float
|
||||
"""Annualized Sharpe ratio of daily returns over last 30 days."""
|
||||
|
||||
n_trades_30d: int
|
||||
"""Number of trades executed in last 30 days."""
|
||||
|
||||
reserve_pool_pct: float
|
||||
"""Reserve pool as fraction of total portfolio value."""
|
||||
|
||||
|
||||
def evaluate_tier_adjustment(metrics: TierMetrics) -> str:
|
||||
"""Evaluate whether to upgrade, downgrade, or hold current risk tier.
|
||||
|
||||
Decision logic:
|
||||
- Downgrade if ANY of: profit_factor < 1.0 OR max_drawdown > 0.12 OR
|
||||
calibration_error > 0.20 OR realized_sharpe < 0
|
||||
- Upgrade only if ALL of: profit_factor > 1.35 AND max_drawdown < 0.05 AND
|
||||
calibration_error < 0.12 AND reserve_pool_pct > 0.20 AND n_trades >= 20
|
||||
- Otherwise: hold
|
||||
|
||||
Downgrade is applied immediately; 7-day upgrade cooldown is enforced
|
||||
at the caller level (not in this function). Evaluation runs once per
|
||||
calendar day after session close.
|
||||
|
||||
Returns:
|
||||
'upgrade' | 'downgrade' | 'hold'
|
||||
|
||||
Requirements: 18.2, 18.3, 18.4, 18.5, 18.6
|
||||
"""
|
||||
# --- Downgrade: any single condition triggers ---
|
||||
if (
|
||||
metrics.profit_factor_30d < 1.0
|
||||
or metrics.max_drawdown_30d > 0.12
|
||||
or metrics.calibration_error > 0.20
|
||||
or metrics.realized_sharpe_30d < 0
|
||||
):
|
||||
return "downgrade"
|
||||
|
||||
# --- Upgrade: all conditions must be satisfied ---
|
||||
if (
|
||||
metrics.profit_factor_30d > 1.35
|
||||
and metrics.max_drawdown_30d < 0.05
|
||||
and metrics.calibration_error < 0.12
|
||||
and metrics.reserve_pool_pct > 0.20
|
||||
and metrics.n_trades_30d >= 20
|
||||
):
|
||||
return "upgrade"
|
||||
|
||||
# --- Hold: neither downgrade nor upgrade criteria met ---
|
||||
return "hold"
|
||||
|
||||
Reference in New Issue
Block a user