TradingAgents/tradingagents/simulation/economic_conditions.py

1178 lines
41 KiB
Python

"""Economic Conditions Module for regime tagging and evaluation.
This module provides economic and market regime analysis including:
- Market regime detection (bull, bear, sideways)
- Scenario tagging by economic conditions
- Performance evaluation by regime
- Regime-specific recommendations
Issue #35: [SIM-34] Economic conditions - regime tagging, evaluation
Design Principles:
- Compatible with ScenarioRunner and StrategyComparator
- Statistical regime detection
- Comprehensive performance breakdown by regime
"""
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from decimal import Decimal
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import statistics
import uuid
# ============================================================================
# Enums
# ============================================================================
class MarketRegime(str, Enum):
"""Market regime classifications based on price action."""
BULL = "bull" # Strong uptrend (>20% annualized)
MODERATE_BULL = "moderate_bull" # Modest uptrend (5-20% annualized)
SIDEWAYS = "sideways" # Range-bound (-5% to 5% annualized)
MODERATE_BEAR = "moderate_bear" # Modest downtrend (-20% to -5%)
BEAR = "bear" # Strong downtrend (<-20% annualized)
class VolatilityRegime(str, Enum):
"""Volatility regime classifications."""
LOW = "low" # VIX < 15 or vol < 10%
NORMAL = "normal" # VIX 15-20 or vol 10-20%
ELEVATED = "elevated" # VIX 20-30 or vol 20-30%
HIGH = "high" # VIX > 30 or vol > 30%
class TrendStrength(str, Enum):
"""Strength of the detected trend."""
STRONG = "strong" # Clear, persistent trend
MODERATE = "moderate" # Identifiable trend with noise
WEAK = "weak" # Marginal trend
NONE = "none" # No discernible trend
class RegimeConfidence(str, Enum):
"""Confidence level in regime classification."""
HIGH = "high" # Strong statistical support
MEDIUM = "medium" # Moderate support
LOW = "low" # Marginal classification
# ============================================================================
# Data Classes
# ============================================================================
@dataclass
class RegimeTag:
"""Tag for a scenario or time period with regime information.
Attributes:
tag_id: Unique identifier for this tag
start_date: Start of the tagged period
end_date: End of the tagged period
market_regime: Bull/bear/sideways classification
volatility_regime: Low/normal/elevated/high volatility
trend_strength: Strength of the detected trend
confidence: Confidence in the regime classification
annualized_return: Return during this period (annualized)
volatility: Volatility during this period (annualized)
max_drawdown: Maximum drawdown during period
metadata: Additional tag data
"""
tag_id: str = field(default_factory=lambda: str(uuid.uuid4()))
start_date: Optional[date] = None
end_date: Optional[date] = None
market_regime: MarketRegime = MarketRegime.SIDEWAYS
volatility_regime: VolatilityRegime = VolatilityRegime.NORMAL
trend_strength: TrendStrength = TrendStrength.NONE
confidence: RegimeConfidence = RegimeConfidence.MEDIUM
annualized_return: Decimal = Decimal("0")
volatility: Decimal = Decimal("0")
max_drawdown: Decimal = Decimal("0")
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class RegimePerformance:
"""Performance summary for a specific regime.
Attributes:
regime: The regime this performance is for
period_count: Number of periods in this regime
total_days: Total trading days in this regime
avg_return: Average return across periods
total_return: Cumulative return
volatility: Average volatility in this regime
avg_drawdown: Average max drawdown
worst_drawdown: Worst max drawdown seen
win_rate: Percentage of winning periods
sharpe_ratio: Sharpe ratio for this regime
best_period_return: Best single period return
worst_period_return: Worst single period return
consistency_score: How consistent returns are (0-1)
"""
regime: Union[MarketRegime, VolatilityRegime]
period_count: int = 0
total_days: int = 0
avg_return: Decimal = Decimal("0")
total_return: Decimal = Decimal("0")
volatility: Decimal = Decimal("0")
avg_drawdown: Decimal = Decimal("0")
worst_drawdown: Decimal = Decimal("0")
win_rate: Decimal = Decimal("0")
sharpe_ratio: Optional[Decimal] = None
best_period_return: Decimal = Decimal("0")
worst_period_return: Decimal = Decimal("0")
consistency_score: Decimal = Decimal("0")
@dataclass
class RegimeTransition:
"""Record of a transition between regimes.
Attributes:
transition_id: Unique identifier
transition_date: When the transition occurred
from_regime: Previous regime
to_regime: New regime
transition_return: Return during transition period
transition_volatility: Volatility during transition
days_in_prior_regime: How long in prior regime
"""
transition_id: str = field(default_factory=lambda: str(uuid.uuid4()))
transition_date: Optional[date] = None
from_regime: Optional[MarketRegime] = None
to_regime: Optional[MarketRegime] = None
transition_return: Decimal = Decimal("0")
transition_volatility: Decimal = Decimal("0")
days_in_prior_regime: int = 0
@dataclass
class RegimeRecommendation:
"""Strategy recommendation for a specific regime.
Attributes:
regime: The regime this recommendation is for
allocation_adjustment: Suggested allocation change (-1 to 1)
risk_adjustment: Suggested risk adjustment (-1 to 1)
position_sizing: Recommended position size factor (0.5-1.5)
strategy_notes: Specific strategy recommendations
cautions: Warnings for this regime
opportunities: Potential opportunities
"""
regime: MarketRegime
allocation_adjustment: Decimal = Decimal("0")
risk_adjustment: Decimal = Decimal("0")
position_sizing: Decimal = Decimal("1")
strategy_notes: List[str] = field(default_factory=list)
cautions: List[str] = field(default_factory=list)
opportunities: List[str] = field(default_factory=list)
@dataclass
class RegimeEvaluationResult:
"""Complete result of regime-based evaluation.
Attributes:
evaluation_id: Unique identifier
strategy_id: ID of evaluated strategy
strategy_name: Name of the strategy
start_date: Evaluation period start
end_date: Evaluation period end
current_regime: Currently detected regime
current_volatility: Current volatility regime
regime_tags: All regime tags detected
performance_by_market_regime: Performance in each market regime
performance_by_volatility: Performance in each volatility regime
transitions: Regime transitions detected
recommendations: Regime-specific recommendations
overall_regime_score: How well strategy handles regimes (0-100)
regime_adaptability: How adaptive strategy is to changes (0-100)
metadata: Additional evaluation data
"""
evaluation_id: str = field(default_factory=lambda: str(uuid.uuid4()))
strategy_id: str = ""
strategy_name: str = ""
start_date: Optional[date] = None
end_date: Optional[date] = None
current_regime: MarketRegime = MarketRegime.SIDEWAYS
current_volatility: VolatilityRegime = VolatilityRegime.NORMAL
regime_tags: List[RegimeTag] = field(default_factory=list)
performance_by_market_regime: Dict[MarketRegime, RegimePerformance] = field(
default_factory=dict
)
performance_by_volatility: Dict[VolatilityRegime, RegimePerformance] = field(
default_factory=dict
)
transitions: List[RegimeTransition] = field(default_factory=list)
recommendations: Dict[MarketRegime, RegimeRecommendation] = field(
default_factory=dict
)
overall_regime_score: Decimal = Decimal("0")
regime_adaptability: Decimal = Decimal("0")
metadata: Dict[str, Any] = field(default_factory=dict)
# ============================================================================
# RegimeDetector Class
# ============================================================================
class RegimeDetector:
"""Detects market and volatility regimes from price/return data.
Uses statistical analysis of returns to classify market conditions
into bull, bear, or sideways regimes with volatility assessment.
Attributes:
bull_threshold: Annualized return above this = bull
bear_threshold: Annualized return below this = bear
vol_low_threshold: Vol below this = low volatility
vol_high_threshold: Vol above this = high volatility
min_periods: Minimum periods needed for regime detection
"""
def __init__(
self,
bull_threshold: Decimal = Decimal("0.20"),
bear_threshold: Decimal = Decimal("-0.20"),
moderate_bull_threshold: Decimal = Decimal("0.05"),
moderate_bear_threshold: Decimal = Decimal("-0.05"),
vol_low_threshold: Decimal = Decimal("0.10"),
vol_normal_threshold: Decimal = Decimal("0.20"),
vol_high_threshold: Decimal = Decimal("0.30"),
min_periods: int = 20,
):
"""Initialize regime detector with thresholds.
Args:
bull_threshold: Return above this = strong bull (default 20%)
bear_threshold: Return below this = strong bear (default -20%)
moderate_bull_threshold: Return above this = moderate bull (default 5%)
moderate_bear_threshold: Return below this = moderate bear (default -5%)
vol_low_threshold: Volatility below this = low (default 10%)
vol_normal_threshold: Vol above this = elevated (default 20%)
vol_high_threshold: Vol above this = high (default 30%)
min_periods: Minimum periods for detection (default 20)
"""
self.bull_threshold = bull_threshold
self.bear_threshold = bear_threshold
self.moderate_bull_threshold = moderate_bull_threshold
self.moderate_bear_threshold = moderate_bear_threshold
self.vol_low_threshold = vol_low_threshold
self.vol_normal_threshold = vol_normal_threshold
self.vol_high_threshold = vol_high_threshold
self.min_periods = min_periods
def detect_market_regime(
self,
returns: List[Decimal],
periods_per_year: int = 252,
) -> Tuple[MarketRegime, RegimeConfidence]:
"""Detect market regime from return series.
Args:
returns: List of periodic returns (daily, weekly, etc.)
periods_per_year: Number of periods in a year (252 for daily)
Returns:
Tuple of (MarketRegime, RegimeConfidence)
"""
if len(returns) < self.min_periods:
return MarketRegime.SIDEWAYS, RegimeConfidence.LOW
# Calculate annualized return
avg_return = sum(returns) / len(returns)
annualized = avg_return * Decimal(str(periods_per_year))
# Calculate confidence based on consistency
positive_count = sum(1 for r in returns if r > 0)
win_rate = Decimal(str(positive_count)) / Decimal(str(len(returns)))
# Determine regime
if annualized > self.bull_threshold:
regime = MarketRegime.BULL
confidence = self._calculate_confidence(win_rate, Decimal("0.55"))
elif annualized > self.moderate_bull_threshold:
regime = MarketRegime.MODERATE_BULL
confidence = self._calculate_confidence(win_rate, Decimal("0.52"))
elif annualized < self.bear_threshold:
regime = MarketRegime.BEAR
confidence = self._calculate_confidence(
Decimal("1") - win_rate, Decimal("0.55")
)
elif annualized < self.moderate_bear_threshold:
regime = MarketRegime.MODERATE_BEAR
confidence = self._calculate_confidence(
Decimal("1") - win_rate, Decimal("0.52")
)
else:
regime = MarketRegime.SIDEWAYS
# Sideways confidence based on how close to zero
deviation = abs(annualized)
if deviation < Decimal("0.02"):
confidence = RegimeConfidence.HIGH
elif deviation < Decimal("0.04"):
confidence = RegimeConfidence.MEDIUM
else:
confidence = RegimeConfidence.LOW
return regime, confidence
def detect_volatility_regime(
self,
returns: List[Decimal],
periods_per_year: int = 252,
) -> Tuple[VolatilityRegime, Decimal]:
"""Detect volatility regime from return series.
Args:
returns: List of periodic returns
periods_per_year: Number of periods in a year
Returns:
Tuple of (VolatilityRegime, annualized_volatility)
"""
if len(returns) < self.min_periods:
return VolatilityRegime.NORMAL, Decimal("0.15")
# Calculate standard deviation of returns
float_returns = [float(r) for r in returns]
if len(set(float_returns)) == 1:
# All returns identical, zero volatility
return VolatilityRegime.LOW, Decimal("0")
period_vol = Decimal(str(statistics.stdev(float_returns)))
annual_vol = period_vol * Decimal(str(periods_per_year)).sqrt()
if annual_vol < self.vol_low_threshold:
regime = VolatilityRegime.LOW
elif annual_vol < self.vol_normal_threshold:
regime = VolatilityRegime.NORMAL
elif annual_vol < self.vol_high_threshold:
regime = VolatilityRegime.ELEVATED
else:
regime = VolatilityRegime.HIGH
return regime, annual_vol
def detect_trend_strength(
self,
returns: List[Decimal],
) -> TrendStrength:
"""Detect strength of trend from returns.
Uses autocorrelation and directional consistency.
Args:
returns: List of periodic returns
Returns:
TrendStrength classification
"""
if len(returns) < self.min_periods:
return TrendStrength.NONE
# Calculate directional consistency
positive_count = sum(1 for r in returns if r > 0)
consistency = abs(
Decimal(str(positive_count)) / Decimal(str(len(returns))) - Decimal("0.5")
)
# Strong trend: >65% consistent direction
if consistency > Decimal("0.15"):
return TrendStrength.STRONG
elif consistency > Decimal("0.10"):
return TrendStrength.MODERATE
elif consistency > Decimal("0.05"):
return TrendStrength.WEAK
else:
return TrendStrength.NONE
def _calculate_confidence(
self,
rate: Decimal,
threshold: Decimal,
) -> RegimeConfidence:
"""Calculate confidence from win/loss rate.
Args:
rate: Observed win rate
threshold: Threshold for high confidence
Returns:
RegimeConfidence level
"""
if rate > threshold + Decimal("0.10"):
return RegimeConfidence.HIGH
elif rate > threshold:
return RegimeConfidence.MEDIUM
else:
return RegimeConfidence.LOW
def tag_period(
self,
returns: List[Decimal],
start_date: Optional[date] = None,
end_date: Optional[date] = None,
periods_per_year: int = 252,
) -> RegimeTag:
"""Create a regime tag for a time period.
Args:
returns: List of returns for the period
start_date: Period start date
end_date: Period end date
periods_per_year: Trading periods per year
Returns:
RegimeTag with detected regimes
"""
market_regime, confidence = self.detect_market_regime(
returns, periods_per_year
)
volatility_regime, vol = self.detect_volatility_regime(
returns, periods_per_year
)
trend = self.detect_trend_strength(returns)
# Calculate metrics
if returns:
avg_return = sum(returns) / len(returns)
annualized = avg_return * Decimal(str(periods_per_year))
max_dd = self._calculate_max_drawdown(returns)
else:
annualized = Decimal("0")
max_dd = Decimal("0")
return RegimeTag(
start_date=start_date,
end_date=end_date,
market_regime=market_regime,
volatility_regime=volatility_regime,
trend_strength=trend,
confidence=confidence,
annualized_return=annualized,
volatility=vol,
max_drawdown=max_dd,
)
def _calculate_max_drawdown(self, returns: List[Decimal]) -> Decimal:
"""Calculate maximum drawdown from returns.
Args:
returns: List of periodic returns
Returns:
Maximum drawdown as negative decimal
"""
if not returns:
return Decimal("0")
cumulative = Decimal("1")
peak = Decimal("1")
max_dd = Decimal("0")
for ret in returns:
cumulative *= (Decimal("1") + ret)
if cumulative > peak:
peak = cumulative
drawdown = (cumulative - peak) / peak
if drawdown < max_dd:
max_dd = drawdown
return max_dd
# ============================================================================
# RegimeEvaluator Class
# ============================================================================
class RegimeEvaluator:
"""Evaluates strategy performance across different regimes.
Provides comprehensive analysis of how strategies perform in
different market and volatility conditions.
Attributes:
detector: RegimeDetector for regime classification
lookback_periods: Periods to look back for regime detection
"""
def __init__(
self,
detector: Optional[RegimeDetector] = None,
lookback_periods: int = 60,
):
"""Initialize regime evaluator.
Args:
detector: RegimeDetector instance (creates default if None)
lookback_periods: Periods for rolling regime detection
"""
self.detector = detector or RegimeDetector()
self.lookback_periods = lookback_periods
def evaluate_strategy(
self,
strategy_id: str,
strategy_name: str,
returns: List[Decimal],
dates: Optional[List[date]] = None,
periods_per_year: int = 252,
benchmark_returns: Optional[List[Decimal]] = None,
) -> RegimeEvaluationResult:
"""Evaluate a strategy across all detected regimes.
Args:
strategy_id: Unique strategy identifier
strategy_name: Human-readable strategy name
returns: List of strategy returns
dates: Optional list of dates for each return
periods_per_year: Periods per year (252 for daily)
benchmark_returns: Optional benchmark returns for regime detection
Returns:
RegimeEvaluationResult with complete analysis
"""
if not returns:
return RegimeEvaluationResult(
strategy_id=strategy_id,
strategy_name=strategy_name,
)
# Use benchmark returns for regime detection if provided
regime_returns = benchmark_returns if benchmark_returns else returns
# Detect regime tags using rolling windows
regime_tags = self._detect_rolling_regimes(
regime_returns, dates, periods_per_year
)
# Calculate performance by market regime
perf_by_market = self._calculate_performance_by_regime(
returns, dates, regime_tags, "market", periods_per_year
)
# Calculate performance by volatility regime
perf_by_vol = self._calculate_performance_by_regime(
returns, dates, regime_tags, "volatility", periods_per_year
)
# Detect transitions
transitions = self._detect_transitions(regime_tags)
# Generate recommendations
recommendations = self._generate_recommendations(
perf_by_market, perf_by_vol
)
# Get current regime (most recent)
current_regime = MarketRegime.SIDEWAYS
current_vol = VolatilityRegime.NORMAL
if regime_tags:
current_regime = regime_tags[-1].market_regime
current_vol = regime_tags[-1].volatility_regime
# Calculate overall scores
regime_score = self._calculate_regime_score(perf_by_market)
adaptability = self._calculate_adaptability(transitions, perf_by_market)
# Determine date range
start = dates[0] if dates else None
end = dates[-1] if dates else None
return RegimeEvaluationResult(
strategy_id=strategy_id,
strategy_name=strategy_name,
start_date=start,
end_date=end,
current_regime=current_regime,
current_volatility=current_vol,
regime_tags=regime_tags,
performance_by_market_regime=perf_by_market,
performance_by_volatility=perf_by_vol,
transitions=transitions,
recommendations=recommendations,
overall_regime_score=regime_score,
regime_adaptability=adaptability,
)
def compare_strategies_by_regime(
self,
strategies: List[Tuple[str, str, List[Decimal]]],
dates: Optional[List[date]] = None,
periods_per_year: int = 252,
benchmark_returns: Optional[List[Decimal]] = None,
) -> Dict[str, RegimeEvaluationResult]:
"""Compare multiple strategies by regime performance.
Args:
strategies: List of (id, name, returns) tuples
dates: Optional shared date list
periods_per_year: Periods per year
benchmark_returns: Benchmark for regime detection
Returns:
Dict mapping strategy_id to RegimeEvaluationResult
"""
results = {}
for strategy_id, strategy_name, returns in strategies:
results[strategy_id] = self.evaluate_strategy(
strategy_id=strategy_id,
strategy_name=strategy_name,
returns=returns,
dates=dates,
periods_per_year=periods_per_year,
benchmark_returns=benchmark_returns,
)
return results
def get_best_strategy_for_regime(
self,
evaluations: Dict[str, RegimeEvaluationResult],
regime: MarketRegime,
) -> Optional[str]:
"""Find the best performing strategy for a specific regime.
Args:
evaluations: Dict of strategy evaluations
regime: The regime to find best strategy for
Returns:
strategy_id of best strategy, or None
"""
best_id = None
best_sharpe = None
for strategy_id, result in evaluations.items():
if regime in result.performance_by_market_regime:
perf = result.performance_by_market_regime[regime]
if perf.sharpe_ratio is not None:
if best_sharpe is None or perf.sharpe_ratio > best_sharpe:
best_sharpe = perf.sharpe_ratio
best_id = strategy_id
return best_id
def _detect_rolling_regimes(
self,
returns: List[Decimal],
dates: Optional[List[date]],
periods_per_year: int,
) -> List[RegimeTag]:
"""Detect regimes using rolling windows.
Args:
returns: Return series
dates: Optional date list
periods_per_year: Periods per year
Returns:
List of RegimeTag for each window
"""
tags = []
window_size = self.lookback_periods
if len(returns) < window_size:
# Single tag for entire period
tag = self.detector.tag_period(
returns=returns,
start_date=dates[0] if dates else None,
end_date=dates[-1] if dates else None,
periods_per_year=periods_per_year,
)
return [tag]
# Rolling windows with step = half window
step = max(1, window_size // 2)
for i in range(0, len(returns) - window_size + 1, step):
window_returns = returns[i:i + window_size]
start = dates[i] if dates else None
end = dates[i + window_size - 1] if dates else None
tag = self.detector.tag_period(
returns=window_returns,
start_date=start,
end_date=end,
periods_per_year=periods_per_year,
)
tags.append(tag)
return tags
def _calculate_performance_by_regime(
self,
returns: List[Decimal],
dates: Optional[List[date]],
tags: List[RegimeTag],
regime_type: str, # "market" or "volatility"
periods_per_year: int,
) -> Dict:
"""Calculate performance breakdown by regime.
Args:
returns: Strategy returns
dates: Date list
tags: Regime tags
regime_type: "market" or "volatility"
periods_per_year: Periods per year
Returns:
Dict mapping regime to RegimePerformance
"""
if regime_type == "market":
regimes = list(MarketRegime)
else:
regimes = list(VolatilityRegime)
result = {}
for regime in regimes:
# Collect returns for this regime
regime_returns = []
regime_days = 0
for i, tag in enumerate(tags):
if regime_type == "market":
tag_regime = tag.market_regime
else:
tag_regime = tag.volatility_regime
if tag_regime == regime:
# Add returns from this tag's period
# Approximate: divide returns evenly across tags
start_idx = i * (self.lookback_periods // 2)
end_idx = min(
start_idx + self.lookback_periods,
len(returns)
)
regime_returns.extend(returns[start_idx:end_idx])
regime_days += end_idx - start_idx
if not regime_returns:
continue
# Calculate metrics
avg_ret = sum(regime_returns) / len(regime_returns)
total_ret = self._calculate_cumulative_return(regime_returns)
# Volatility
if len(regime_returns) > 1:
float_rets = [float(r) for r in regime_returns]
period_vol = Decimal(str(statistics.stdev(float_rets)))
vol = period_vol * Decimal(str(periods_per_year)).sqrt()
else:
vol = Decimal("0")
# Win rate
wins = sum(1 for r in regime_returns if r > 0)
win_rate = Decimal(str(wins)) / Decimal(str(len(regime_returns)))
# Max drawdown
max_dd = self.detector._calculate_max_drawdown(regime_returns)
# Sharpe ratio
if vol > 0:
annual_ret = avg_ret * Decimal(str(periods_per_year))
sharpe = annual_ret / vol
else:
sharpe = None
# Best/worst
best = max(regime_returns)
worst = min(regime_returns)
# Consistency (inverse of return std)
if len(regime_returns) > 1:
ret_std = Decimal(str(statistics.stdev(float_rets)))
consistency = max(Decimal("0"), Decimal("1") - ret_std * 10)
else:
consistency = Decimal("0.5")
result[regime] = RegimePerformance(
regime=regime,
period_count=len([t for t in tags if (
t.market_regime if regime_type == "market"
else t.volatility_regime
) == regime]),
total_days=regime_days,
avg_return=avg_ret * Decimal(str(periods_per_year)),
total_return=total_ret,
volatility=vol,
avg_drawdown=max_dd,
worst_drawdown=max_dd,
win_rate=win_rate,
sharpe_ratio=sharpe,
best_period_return=best,
worst_period_return=worst,
consistency_score=consistency,
)
return result
def _calculate_cumulative_return(
self,
returns: List[Decimal],
) -> Decimal:
"""Calculate cumulative return from periodic returns.
Args:
returns: List of periodic returns
Returns:
Total cumulative return
"""
cumulative = Decimal("1")
for ret in returns:
cumulative *= (Decimal("1") + ret)
return cumulative - Decimal("1")
def _detect_transitions(
self,
tags: List[RegimeTag],
) -> List[RegimeTransition]:
"""Detect regime transitions from tag sequence.
Args:
tags: List of regime tags
Returns:
List of RegimeTransition
"""
transitions = []
for i in range(1, len(tags)):
prev_regime = tags[i - 1].market_regime
curr_regime = tags[i].market_regime
if prev_regime != curr_regime:
transitions.append(RegimeTransition(
transition_date=tags[i].start_date,
from_regime=prev_regime,
to_regime=curr_regime,
transition_return=tags[i].annualized_return,
transition_volatility=tags[i].volatility,
days_in_prior_regime=self.lookback_periods,
))
return transitions
def _generate_recommendations(
self,
perf_by_market: Dict[MarketRegime, RegimePerformance],
perf_by_vol: Dict[VolatilityRegime, RegimePerformance],
) -> Dict[MarketRegime, RegimeRecommendation]:
"""Generate regime-specific recommendations.
Args:
perf_by_market: Performance by market regime
perf_by_vol: Performance by volatility regime
Returns:
Dict of recommendations per regime
"""
recommendations = {}
for regime in MarketRegime:
rec = self._create_recommendation_for_regime(
regime, perf_by_market.get(regime)
)
recommendations[regime] = rec
return recommendations
def _create_recommendation_for_regime(
self,
regime: MarketRegime,
perf: Optional[RegimePerformance],
) -> RegimeRecommendation:
"""Create recommendation for a specific regime.
Args:
regime: The market regime
perf: Performance in this regime
Returns:
RegimeRecommendation
"""
rec = RegimeRecommendation(regime=regime)
if regime == MarketRegime.BULL:
rec.allocation_adjustment = Decimal("0.2")
rec.position_sizing = Decimal("1.2")
rec.strategy_notes = [
"Increase equity exposure",
"Consider momentum strategies",
"Reduce cash holdings",
]
rec.opportunities = [
"Growth stocks outperform",
"Risk-on positioning favored",
]
elif regime == MarketRegime.MODERATE_BULL:
rec.allocation_adjustment = Decimal("0.1")
rec.position_sizing = Decimal("1.1")
rec.strategy_notes = [
"Maintain balanced exposure",
"Focus on quality growth",
]
elif regime == MarketRegime.SIDEWAYS:
rec.allocation_adjustment = Decimal("0")
rec.position_sizing = Decimal("1.0")
rec.strategy_notes = [
"Range-trading strategies may work",
"Consider covered calls for income",
"Reduce directional bets",
]
rec.cautions = [
"Low returns expected",
"Transaction costs eat into profits",
]
elif regime == MarketRegime.MODERATE_BEAR:
rec.allocation_adjustment = Decimal("-0.1")
rec.risk_adjustment = Decimal("-0.2")
rec.position_sizing = Decimal("0.9")
rec.strategy_notes = [
"Increase defensive allocation",
"Add quality names on dips",
]
rec.cautions = [
"Trend may accelerate lower",
]
elif regime == MarketRegime.BEAR:
rec.allocation_adjustment = Decimal("-0.3")
rec.risk_adjustment = Decimal("-0.4")
rec.position_sizing = Decimal("0.7")
rec.strategy_notes = [
"Maximize defensive allocation",
"Consider hedging strategies",
"Hold cash for opportunities",
]
rec.cautions = [
"Capital preservation is priority",
"Avoid catching falling knives",
]
rec.opportunities = [
"Build watchlist for recovery",
"Quality assets on sale",
]
# Adjust based on actual performance
if perf:
if perf.sharpe_ratio and perf.sharpe_ratio < Decimal("0"):
rec.cautions.append(
f"Strategy underperforms in {regime.value} markets"
)
rec.position_sizing = max(
Decimal("0.5"),
rec.position_sizing - Decimal("0.2")
)
if perf.win_rate < Decimal("0.4"):
rec.cautions.append(
f"Low win rate ({float(perf.win_rate):.1%}) in this regime"
)
if perf.worst_drawdown < Decimal("-0.15"):
rec.cautions.append(
f"Large drawdowns ({float(perf.worst_drawdown):.1%}) observed"
)
return rec
def _calculate_regime_score(
self,
perf_by_market: Dict[MarketRegime, RegimePerformance],
) -> Decimal:
"""Calculate overall regime handling score.
Considers performance across all regimes weighted by severity.
Args:
perf_by_market: Performance by market regime
Returns:
Score from 0 to 100
"""
if not perf_by_market:
return Decimal("50")
# Weight regimes by difficulty
weights = {
MarketRegime.BULL: Decimal("1.0"),
MarketRegime.MODERATE_BULL: Decimal("1.0"),
MarketRegime.SIDEWAYS: Decimal("1.2"),
MarketRegime.MODERATE_BEAR: Decimal("1.5"),
MarketRegime.BEAR: Decimal("2.0"),
}
total_weight = Decimal("0")
weighted_score = Decimal("0")
for regime, perf in perf_by_market.items():
weight = weights.get(regime, Decimal("1"))
# Score based on Sharpe
if perf.sharpe_ratio is not None:
# Convert Sharpe to 0-100 score
# Sharpe of 2 = 100, Sharpe of -1 = 0
sharpe_score = min(
Decimal("100"),
max(
Decimal("0"),
(perf.sharpe_ratio + Decimal("1")) * Decimal("33.33")
)
)
else:
sharpe_score = Decimal("50")
weighted_score += weight * sharpe_score
total_weight += weight
if total_weight > 0:
return weighted_score / total_weight
return Decimal("50")
def _calculate_adaptability(
self,
transitions: List[RegimeTransition],
perf_by_market: Dict[MarketRegime, RegimePerformance],
) -> Decimal:
"""Calculate strategy adaptability score.
Measures how well strategy handles regime changes.
Args:
transitions: List of regime transitions
perf_by_market: Performance by regime
Returns:
Score from 0 to 100
"""
if not transitions or not perf_by_market:
return Decimal("50")
# Check performance variance across regimes
sharpes = [
p.sharpe_ratio for p in perf_by_market.values()
if p.sharpe_ratio is not None
]
if len(sharpes) < 2:
return Decimal("50")
float_sharpes = [float(s) for s in sharpes]
sharpe_std = Decimal(str(statistics.stdev(float_sharpes)))
# Lower std = more adaptive
# Std of 0 = 100, Std of 2 = 0
adaptability = max(
Decimal("0"),
Decimal("100") - sharpe_std * Decimal("50")
)
return adaptability
def generate_regime_report(
self,
result: RegimeEvaluationResult,
) -> str:
"""Generate a formatted regime evaluation report.
Args:
result: RegimeEvaluationResult to report on
Returns:
Formatted markdown report
"""
lines = [
"# Regime Evaluation Report",
f"**Strategy**: {result.strategy_name}",
f"**Period**: {result.start_date} to {result.end_date}",
"",
"## Current Conditions",
f"- **Market Regime**: {result.current_regime.value}",
f"- **Volatility**: {result.current_volatility.value}",
f"- **Overall Score**: {float(result.overall_regime_score):.1f}/100",
f"- **Adaptability**: {float(result.regime_adaptability):.1f}/100",
"",
"## Performance by Market Regime",
"",
"| Regime | Periods | Avg Return | Sharpe | Win Rate | Max DD |",
"|--------|---------|------------|--------|----------|--------|",
]
for regime in MarketRegime:
if regime in result.performance_by_market_regime:
perf = result.performance_by_market_regime[regime]
sharpe = f"{float(perf.sharpe_ratio):.2f}" if perf.sharpe_ratio else "N/A"
lines.append(
f"| {regime.value} | {perf.period_count} | "
f"{float(perf.avg_return):.1%} | {sharpe} | "
f"{float(perf.win_rate):.1%} | {float(perf.worst_drawdown):.1%} |"
)
lines.extend([
"",
"## Performance by Volatility",
"",
"| Vol Regime | Periods | Avg Return | Sharpe | Win Rate |",
"|------------|---------|------------|--------|----------|",
])
for regime in VolatilityRegime:
if regime in result.performance_by_volatility:
perf = result.performance_by_volatility[regime]
sharpe = f"{float(perf.sharpe_ratio):.2f}" if perf.sharpe_ratio else "N/A"
lines.append(
f"| {regime.value} | {perf.period_count} | "
f"{float(perf.avg_return):.1%} | {sharpe} | "
f"{float(perf.win_rate):.1%} |"
)
if result.transitions:
lines.extend([
"",
"## Regime Transitions",
f"Total transitions detected: {len(result.transitions)}",
"",
])
for t in result.transitions[:5]:
lines.append(
f"- {t.transition_date}: "
f"{t.from_regime.value if t.from_regime else 'N/A'}"
f"{t.to_regime.value if t.to_regime else 'N/A'}"
)
# Add recommendation for current regime
if result.current_regime in result.recommendations:
rec = result.recommendations[result.current_regime]
lines.extend([
"",
f"## Recommendations for {result.current_regime.value.title()} Market",
"",
"### Strategy Notes",
])
for note in rec.strategy_notes:
lines.append(f"- {note}")
if rec.cautions:
lines.append("")
lines.append("### Cautions")
for caution in rec.cautions:
lines.append(f"- ⚠️ {caution}")
if rec.opportunities:
lines.append("")
lines.append("### Opportunities")
for opp in rec.opportunities:
lines.append(f"- ✓ {opp}")
return "\n".join(lines)