feat: math core v3 engine upgrade
This commit is contained in:
@@ -4,11 +4,14 @@ Computes dollar allocation and share quantity for a trade by applying
|
||||
a sequential adjustment pipeline: confidence gate, correlation reduction,
|
||||
sector exposure, diversification bonus, earnings proximity, portfolio
|
||||
heat check, active-pool minimum, absolute cap, and share rounding.
|
||||
|
||||
Also provides v3 fractional Kelly position sizing (Requirements 14.1–14.7).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from services.trading.models import (
|
||||
@@ -345,3 +348,111 @@ class PositionSizer:
|
||||
return new_dollar, new_pct
|
||||
|
||||
return dollar_amount, allocation_pct
|
||||
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# v3 Fractional Kelly Position Sizing (Requirements 14.1–14.7)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KellySizingResult:
|
||||
"""Result of v3 fractional Kelly position sizing computation."""
|
||||
|
||||
portfolio_pct: float
|
||||
f_kelly: float
|
||||
reward_ratio: float
|
||||
downgrade: bool
|
||||
downgrade_reason: str
|
||||
|
||||
|
||||
def _clamp(value: float, lo: float, hi: float) -> float:
|
||||
"""Clamp value to [lo, hi]."""
|
||||
return max(lo, min(hi, value))
|
||||
|
||||
|
||||
def compute_reward_ratio(
|
||||
confidence: float, strength: float, contradiction: float
|
||||
) -> float:
|
||||
"""Compute reward ratio b = clamp(1.2 + 2.0*confidence + 1.0*strength - contradiction, 1.2, 3.0).
|
||||
|
||||
Requirements: 14.2
|
||||
"""
|
||||
raw = 1.2 + 2.0 * confidence + 1.0 * strength - contradiction
|
||||
return _clamp(raw, 1.2, 3.0)
|
||||
|
||||
|
||||
def compute_kelly_sizing(
|
||||
p_win: float,
|
||||
b: float,
|
||||
confidence: float,
|
||||
data_quality: float,
|
||||
contradiction: float,
|
||||
max_position_pct: float,
|
||||
available_caps: dict[str, float],
|
||||
) -> KellySizingResult:
|
||||
"""Compute fractional Kelly position sizing.
|
||||
|
||||
f_kelly = (p_win * b - (1 - p_win)) / b
|
||||
portfolio_pct = clamp(max(0, f_kelly) * 0.25 * confidence * data_quality * (1 - contradiction), 0, max_position_pct)
|
||||
|
||||
Apply min of all capacity constraints from available_caps:
|
||||
- sector_capacity
|
||||
- correlation_capacity (0 if avg corr > 0.80)
|
||||
- heat_capacity
|
||||
|
||||
Downgrade rules:
|
||||
- If f_kelly <= 0 → portfolio_pct = 0, downgrade with reason "negative_edge"
|
||||
- If portfolio_pct < 0.005 → downgrade with reason "position_below_minimum"
|
||||
|
||||
Requirements: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7
|
||||
"""
|
||||
# Compute Kelly fraction (Req 14.3)
|
||||
f_kelly = (p_win * b - (1.0 - p_win)) / b
|
||||
|
||||
# Negative edge → immediate downgrade (Req 14.7)
|
||||
if f_kelly <= 0.0:
|
||||
return KellySizingResult(
|
||||
portfolio_pct=0.0,
|
||||
f_kelly=f_kelly,
|
||||
reward_ratio=b,
|
||||
downgrade=True,
|
||||
downgrade_reason="negative_edge",
|
||||
)
|
||||
|
||||
# Apply fractional Kelly with dampening factors (Req 14.4)
|
||||
raw_pct = f_kelly * 0.25 * confidence * data_quality * (1.0 - contradiction)
|
||||
portfolio_pct = _clamp(max(0.0, raw_pct), 0.0, max_position_pct)
|
||||
|
||||
# Apply capacity constraints (Req 14.5)
|
||||
sector_capacity = available_caps.get("sector_capacity", max_position_pct)
|
||||
correlation_capacity = available_caps.get("correlation_capacity", max_position_pct)
|
||||
heat_capacity = available_caps.get("heat_capacity", max_position_pct)
|
||||
|
||||
# Correlation capacity of 0 means avg corr > 0.80 → force zero
|
||||
portfolio_pct = min(
|
||||
portfolio_pct,
|
||||
max_position_pct,
|
||||
sector_capacity,
|
||||
correlation_capacity,
|
||||
heat_capacity,
|
||||
)
|
||||
|
||||
# Position below minimum threshold → downgrade (Req 14.6)
|
||||
if portfolio_pct < 0.005:
|
||||
return KellySizingResult(
|
||||
portfolio_pct=0.0,
|
||||
f_kelly=f_kelly,
|
||||
reward_ratio=b,
|
||||
downgrade=True,
|
||||
downgrade_reason="position_below_minimum",
|
||||
)
|
||||
|
||||
return KellySizingResult(
|
||||
portfolio_pct=portfolio_pct,
|
||||
f_kelly=f_kelly,
|
||||
reward_ratio=b,
|
||||
downgrade=False,
|
||||
downgrade_reason="",
|
||||
)
|
||||
|
||||
@@ -5,12 +5,15 @@ re-evaluates levels when volatility or market conditions change, detects
|
||||
price crossings that should trigger exits, and tightens stops under
|
||||
high-heat or high-severity-event conditions.
|
||||
|
||||
Also provides v3 regime-aware stop loss and take profit (Requirements 16.1–16.5).
|
||||
|
||||
All public methods are synchronous (pure computation, no DB access).
|
||||
Persistence is handled by the caller (engine.py).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from services.trading.models import (
|
||||
@@ -20,6 +23,100 @@ from services.trading.models import (
|
||||
StopTrigger,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# v3 Regime-Aware Stop Loss and Take Profit (Requirements 16.1–16.5)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class V3StopLevels:
|
||||
"""v3 stop-loss and take-profit levels computed from regime-aware volatility."""
|
||||
|
||||
stop_loss: float
|
||||
take_profit: float
|
||||
stop_distance_pct: float
|
||||
reward_ratio: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TrailingStopResult:
|
||||
"""Result of trailing stop computation."""
|
||||
|
||||
trailing_stop: float
|
||||
activated: bool
|
||||
|
||||
|
||||
def compute_v3_stops(
|
||||
entry_price: float,
|
||||
atr_pct: float,
|
||||
regime_atr_mult: float,
|
||||
sigma_h: float,
|
||||
reward_ratio: float,
|
||||
) -> V3StopLevels:
|
||||
"""Compute regime-aware stop loss and take profit.
|
||||
|
||||
stop_distance_pct = max(ATR_pct × regime_ATR_mult, sigma_h × 1.25, 0.005)
|
||||
stop_loss = entry_price × (1 - stop_distance_pct)
|
||||
take_profit = entry_price × (1 + b × stop_distance_pct)
|
||||
|
||||
Requirements: 16.1, 16.2, 16.3
|
||||
"""
|
||||
# Requirement 16.1: stop distance from regime-aware volatility
|
||||
z_stop = 1.25
|
||||
min_stop_pct = 0.005
|
||||
stop_distance_pct = max(atr_pct * regime_atr_mult, sigma_h * z_stop, min_stop_pct)
|
||||
|
||||
# Requirement 16.2: stop loss for long position
|
||||
stop_loss = entry_price * (1.0 - stop_distance_pct)
|
||||
|
||||
# Requirement 16.3: take profit using dynamic reward ratio
|
||||
take_profit = entry_price * (1.0 + reward_ratio * stop_distance_pct)
|
||||
|
||||
return V3StopLevels(
|
||||
stop_loss=stop_loss,
|
||||
take_profit=take_profit,
|
||||
stop_distance_pct=stop_distance_pct,
|
||||
reward_ratio=reward_ratio,
|
||||
)
|
||||
|
||||
|
||||
def compute_trailing_stop(
|
||||
existing_stop: float,
|
||||
current_price: float,
|
||||
entry_price: float,
|
||||
take_profit: float,
|
||||
atr_pct: float,
|
||||
trailing_atr_mult: float,
|
||||
sigma_h: float,
|
||||
) -> TrailingStopResult:
|
||||
"""Compute trailing stop level.
|
||||
|
||||
Activated when unrealized_gain >= 0.50 × TP distance.
|
||||
trailing_stop = max(existing_stop, current_price × (1 - trailing_distance_pct))
|
||||
trailing_distance_pct = max(ATR_pct × trailing_ATR_mult, sigma_h × 0.75)
|
||||
|
||||
Trailing stop is monotonically non-decreasing.
|
||||
|
||||
Requirements: 16.4, 16.5
|
||||
"""
|
||||
# Requirement 16.4: activation check
|
||||
take_profit_distance = take_profit - entry_price
|
||||
unrealized_gain = current_price - entry_price
|
||||
|
||||
# Activation threshold: gain >= 50% of TP distance
|
||||
if take_profit_distance <= 0 or unrealized_gain < 0.50 * take_profit_distance:
|
||||
# Not activated — return existing stop unchanged
|
||||
return TrailingStopResult(trailing_stop=existing_stop, activated=False)
|
||||
|
||||
# Requirement 16.5: compute trailing stop
|
||||
trailing_distance_pct = max(atr_pct * trailing_atr_mult, sigma_h * 0.75)
|
||||
candidate_stop = current_price * (1.0 - trailing_distance_pct)
|
||||
|
||||
# Monotonically non-decreasing: never lower than existing stop
|
||||
trailing_stop = max(existing_stop, candidate_stop)
|
||||
|
||||
return TrailingStopResult(trailing_stop=trailing_stop, activated=True)
|
||||
|
||||
|
||||
class StopLossManager:
|
||||
"""Compute and maintain dynamic stop-loss / take-profit levels."""
|
||||
|
||||
Reference in New Issue
Block a user