769 lines
27 KiB
Python
769 lines
27 KiB
Python
"""
|
|
Risk Manager
|
|
============
|
|
|
|
Comprehensive risk management with enforcement of position limits,
|
|
loss limits, and portfolio risk metrics.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, List, Optional, Any, Tuple
|
|
from datetime import datetime, timedelta, date
|
|
from decimal import Decimal
|
|
from enum import Enum
|
|
from dataclasses import dataclass, field
|
|
import numpy as np
|
|
from collections import defaultdict
|
|
|
|
from pydantic import BaseModel, Field, validator
|
|
|
|
from .database import (
|
|
DatabaseManager, Position, Order, Trade,
|
|
OrderStatus, PerformanceMetric
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RiskLevel(str, Enum):
|
|
"""Risk level classification"""
|
|
LOW = "LOW"
|
|
MEDIUM = "MEDIUM"
|
|
HIGH = "HIGH"
|
|
CRITICAL = "CRITICAL"
|
|
|
|
|
|
class RiskViolationType(str, Enum):
|
|
"""Types of risk violations"""
|
|
POSITION_SIZE = "position_size"
|
|
DAILY_LOSS = "daily_loss"
|
|
CONCENTRATION = "concentration"
|
|
CORRELATION = "correlation"
|
|
VOLATILITY = "volatility"
|
|
MARGIN = "margin"
|
|
PATTERN_DAY_TRADER = "pattern_day_trader"
|
|
|
|
|
|
@dataclass
|
|
class RiskMetrics:
|
|
"""Portfolio risk metrics"""
|
|
total_exposure: Decimal = Decimal('0')
|
|
max_position_size: Decimal = Decimal('0')
|
|
concentration_risk: Decimal = Decimal('0')
|
|
portfolio_beta: float = 0.0
|
|
portfolio_volatility: float = 0.0
|
|
value_at_risk_95: Decimal = Decimal('0')
|
|
value_at_risk_99: Decimal = Decimal('0')
|
|
sharpe_ratio: float = 0.0
|
|
sortino_ratio: float = 0.0
|
|
max_drawdown: Decimal = Decimal('0')
|
|
current_drawdown: Decimal = Decimal('0')
|
|
daily_pnl: Decimal = Decimal('0')
|
|
realized_pnl: Decimal = Decimal('0')
|
|
unrealized_pnl: Decimal = Decimal('0')
|
|
margin_used: Decimal = Decimal('0')
|
|
margin_available: Decimal = Decimal('0')
|
|
correlation_risk: float = 0.0
|
|
sector_concentration: Dict[str, float] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class RiskLimits:
|
|
"""Risk limits configuration"""
|
|
max_position_size: Decimal = Decimal('0.20') # 20% per position
|
|
max_daily_loss: Decimal = Decimal('0.05') # 5% daily loss
|
|
max_total_exposure: Decimal = Decimal('1.0') # 100% exposure
|
|
max_concentration: Decimal = Decimal('0.30') # 30% in single stock
|
|
max_sector_exposure: Decimal = Decimal('0.40') # 40% per sector
|
|
max_correlation: float = 0.70 # Max correlation between positions
|
|
max_volatility: float = 0.30 # 30% annualized volatility
|
|
min_sharpe_ratio: float = 0.5 # Minimum Sharpe ratio
|
|
max_drawdown: Decimal = Decimal('0.15') # 15% max drawdown
|
|
max_orders_per_day: int = 50
|
|
max_trades_per_symbol_per_day: int = 4 # PDT rule
|
|
min_position_hold_time: int = 60 # Seconds
|
|
required_stop_loss: bool = True
|
|
max_leverage: Decimal = Decimal('2.0') # 2x leverage
|
|
|
|
|
|
class RiskCheckResult(BaseModel):
|
|
"""Result of risk check"""
|
|
approved: bool
|
|
risk_score: float = Field(ge=0, le=100)
|
|
risk_level: RiskLevel
|
|
violations: List[RiskViolationType] = Field(default_factory=list)
|
|
reasons: List[str] = Field(default_factory=list)
|
|
position_impact: Dict[str, Any] = Field(default_factory=dict)
|
|
recommendations: List[str] = Field(default_factory=list)
|
|
|
|
|
|
class RiskManager:
|
|
"""
|
|
Comprehensive risk management system
|
|
"""
|
|
|
|
def __init__(self,
|
|
db_manager: DatabaseManager,
|
|
limits: Optional[RiskLimits] = None,
|
|
enable_enforcement: bool = True):
|
|
"""
|
|
Initialize risk manager
|
|
|
|
Args:
|
|
db_manager: Database manager
|
|
limits: Risk limits configuration
|
|
enable_enforcement: Whether to enforce limits
|
|
"""
|
|
self.db = db_manager
|
|
self.limits = limits or RiskLimits()
|
|
self.enable_enforcement = enable_enforcement
|
|
|
|
# Cache for performance
|
|
self._position_cache: Dict[str, Position] = {}
|
|
self._metrics_cache: Optional[RiskMetrics] = None
|
|
self._cache_timestamp: Optional[datetime] = None
|
|
self._cache_ttl = 60 # Seconds
|
|
|
|
# Track daily metrics
|
|
self._daily_trades: List[Trade] = []
|
|
self._daily_orders: List[Order] = []
|
|
self._starting_portfolio_value: Optional[Decimal] = None
|
|
|
|
# Sector mapping (simplified)
|
|
self.sector_map = {
|
|
'AAPL': 'Technology', 'MSFT': 'Technology', 'GOOGL': 'Technology',
|
|
'NVDA': 'Technology', 'AVGO': 'Technology', 'TSM': 'Technology',
|
|
'MU': 'Technology', 'META': 'Technology',
|
|
'JPM': 'Financial', 'BAC': 'Financial', 'GS': 'Financial',
|
|
'XOM': 'Energy', 'CVX': 'Energy',
|
|
'JNJ': 'Healthcare', 'PFE': 'Healthcare',
|
|
# Add more mappings as needed
|
|
}
|
|
|
|
async def check_order(self,
|
|
ticker: str,
|
|
side: str,
|
|
quantity: int,
|
|
price: float,
|
|
stop_loss: Optional[float] = None) -> RiskCheckResult:
|
|
"""
|
|
Check if an order passes risk management rules
|
|
|
|
Args:
|
|
ticker: Stock symbol
|
|
side: 'BUY' or 'SELL'
|
|
quantity: Number of shares
|
|
price: Order price
|
|
stop_loss: Stop loss price
|
|
|
|
Returns:
|
|
Risk check result
|
|
"""
|
|
violations = []
|
|
reasons = []
|
|
recommendations = []
|
|
|
|
# Get current metrics
|
|
metrics = await self.calculate_risk_metrics()
|
|
|
|
# Calculate order value
|
|
order_value = Decimal(str(price * quantity))
|
|
|
|
# Get portfolio value
|
|
portfolio_value = await self._get_portfolio_value()
|
|
|
|
if portfolio_value <= 0:
|
|
return RiskCheckResult(
|
|
approved=False,
|
|
risk_score=100,
|
|
risk_level=RiskLevel.CRITICAL,
|
|
reasons=["Invalid portfolio value"]
|
|
)
|
|
|
|
# === Check 1: Position Size ===
|
|
position_pct = order_value / portfolio_value
|
|
|
|
if position_pct > self.limits.max_position_size:
|
|
violations.append(RiskViolationType.POSITION_SIZE)
|
|
reasons.append(
|
|
f"Position size {position_pct:.1%} exceeds limit "
|
|
f"{self.limits.max_position_size:.1%}"
|
|
)
|
|
recommendations.append(
|
|
f"Reduce quantity to {int(quantity * float(self.limits.max_position_size / position_pct))}"
|
|
)
|
|
|
|
# === Check 2: Daily Loss Limit ===
|
|
if metrics.daily_pnl < 0:
|
|
daily_loss_pct = abs(metrics.daily_pnl / portfolio_value)
|
|
if daily_loss_pct >= self.limits.max_daily_loss:
|
|
violations.append(RiskViolationType.DAILY_LOSS)
|
|
reasons.append(
|
|
f"Daily loss {daily_loss_pct:.1%} at limit "
|
|
f"{self.limits.max_daily_loss:.1%}"
|
|
)
|
|
recommendations.append("Stop trading for the day")
|
|
|
|
# === Check 3: Concentration Risk ===
|
|
existing_position = await self._get_position(ticker)
|
|
if existing_position and side == 'BUY':
|
|
new_position_value = (existing_position.market_value +
|
|
order_value)
|
|
concentration = new_position_value / portfolio_value
|
|
|
|
if concentration > self.limits.max_concentration:
|
|
violations.append(RiskViolationType.CONCENTRATION)
|
|
reasons.append(
|
|
f"Concentration {concentration:.1%} exceeds limit "
|
|
f"{self.limits.max_concentration:.1%}"
|
|
)
|
|
recommendations.append(f"Diversify into other stocks")
|
|
|
|
# === Check 4: Sector Concentration ===
|
|
sector = self.sector_map.get(ticker, 'Other')
|
|
sector_exposure = metrics.sector_concentration.get(sector, 0)
|
|
|
|
if side == 'BUY':
|
|
new_sector_exposure = sector_exposure + float(position_pct)
|
|
if new_sector_exposure > float(self.limits.max_sector_exposure):
|
|
violations.append(RiskViolationType.CONCENTRATION)
|
|
reasons.append(
|
|
f"Sector exposure {new_sector_exposure:.1%} exceeds limit "
|
|
f"{self.limits.max_sector_exposure:.1%}"
|
|
)
|
|
recommendations.append("Diversify into other sectors")
|
|
|
|
# === Check 5: Stop Loss Required ===
|
|
if self.limits.required_stop_loss and side == 'BUY':
|
|
if not stop_loss:
|
|
reasons.append("Stop loss required for buy orders")
|
|
recommendations.append(
|
|
f"Add stop loss at {price * 0.97:.2f} (-3%)"
|
|
)
|
|
|
|
# === Check 6: Pattern Day Trader Rule ===
|
|
day_trades_count = await self._count_day_trades(ticker)
|
|
if day_trades_count >= self.limits.max_trades_per_symbol_per_day:
|
|
violations.append(RiskViolationType.PATTERN_DAY_TRADER)
|
|
reasons.append(
|
|
f"PDT rule: {day_trades_count} trades today in {ticker}"
|
|
)
|
|
|
|
# === Check 7: Volatility ===
|
|
if metrics.portfolio_volatility > self.limits.max_volatility:
|
|
violations.append(RiskViolationType.VOLATILITY)
|
|
reasons.append(
|
|
f"Portfolio volatility {metrics.portfolio_volatility:.1%} "
|
|
f"exceeds limit {self.limits.max_volatility:.1%}"
|
|
)
|
|
recommendations.append("Reduce position sizes or add hedges")
|
|
|
|
# === Check 8: Margin ===
|
|
if metrics.margin_available < order_value:
|
|
violations.append(RiskViolationType.MARGIN)
|
|
reasons.append(
|
|
f"Insufficient margin: need ${order_value:,.2f}, "
|
|
f"have ${metrics.margin_available:,.2f}"
|
|
)
|
|
|
|
# === Check 9: Correlation ===
|
|
if side == 'BUY':
|
|
correlation_risk = await self._check_correlation(ticker)
|
|
if correlation_risk > self.limits.max_correlation:
|
|
violations.append(RiskViolationType.CORRELATION)
|
|
reasons.append(
|
|
f"High correlation {correlation_risk:.2f} with existing positions"
|
|
)
|
|
recommendations.append("Diversify into uncorrelated assets")
|
|
|
|
# === Check 10: Max Drawdown ===
|
|
if metrics.current_drawdown > self.limits.max_drawdown:
|
|
violations.append(RiskViolationType.VOLATILITY)
|
|
reasons.append(
|
|
f"In drawdown {metrics.current_drawdown:.1%}, "
|
|
f"limit {self.limits.max_drawdown:.1%}"
|
|
)
|
|
recommendations.append("Reduce risk until recovery")
|
|
|
|
# Calculate risk score
|
|
risk_score = self._calculate_risk_score(
|
|
violations, metrics, position_pct
|
|
)
|
|
|
|
# Determine risk level
|
|
if risk_score >= 80:
|
|
risk_level = RiskLevel.CRITICAL
|
|
elif risk_score >= 60:
|
|
risk_level = RiskLevel.HIGH
|
|
elif risk_score >= 40:
|
|
risk_level = RiskLevel.MEDIUM
|
|
else:
|
|
risk_level = RiskLevel.LOW
|
|
|
|
# Determine approval
|
|
approved = True
|
|
if self.enable_enforcement:
|
|
# Critical violations always reject
|
|
critical_violations = [
|
|
RiskViolationType.DAILY_LOSS,
|
|
RiskViolationType.MARGIN,
|
|
RiskViolationType.PATTERN_DAY_TRADER
|
|
]
|
|
if any(v in critical_violations for v in violations):
|
|
approved = False
|
|
|
|
# High risk requires override
|
|
if risk_level == RiskLevel.CRITICAL:
|
|
approved = False
|
|
|
|
return RiskCheckResult(
|
|
approved=approved,
|
|
risk_score=risk_score,
|
|
risk_level=risk_level,
|
|
violations=violations,
|
|
reasons=reasons,
|
|
position_impact={
|
|
'new_position_size': float(position_pct),
|
|
'new_total_exposure': float(metrics.total_exposure + position_pct),
|
|
'expected_volatility_change': self._estimate_volatility_impact(
|
|
ticker, position_pct
|
|
)
|
|
},
|
|
recommendations=recommendations
|
|
)
|
|
|
|
async def calculate_risk_metrics(self, force_refresh: bool = False) -> RiskMetrics:
|
|
"""
|
|
Calculate comprehensive risk metrics
|
|
|
|
Args:
|
|
force_refresh: Force cache refresh
|
|
|
|
Returns:
|
|
Risk metrics
|
|
"""
|
|
# Check cache
|
|
if not force_refresh and self._metrics_cache:
|
|
if (datetime.now() - self._cache_timestamp).seconds < self._cache_ttl:
|
|
return self._metrics_cache
|
|
|
|
metrics = RiskMetrics()
|
|
|
|
# Get positions
|
|
positions = await self._get_all_positions()
|
|
portfolio_value = await self._get_portfolio_value()
|
|
|
|
if not positions or portfolio_value <= 0:
|
|
return metrics
|
|
|
|
# Calculate exposure metrics
|
|
total_position_value = Decimal('0')
|
|
max_position_value = Decimal('0')
|
|
sector_values = defaultdict(Decimal)
|
|
|
|
for position in positions:
|
|
position_value = position.market_value
|
|
total_position_value += position_value
|
|
|
|
# Track max position
|
|
if position_value > max_position_value:
|
|
max_position_value = position_value
|
|
|
|
# Track sector exposure
|
|
sector = self.sector_map.get(position.ticker, 'Other')
|
|
sector_values[sector] += position_value
|
|
|
|
# Basic metrics
|
|
metrics.total_exposure = total_position_value / portfolio_value
|
|
metrics.max_position_size = max_position_value / portfolio_value
|
|
metrics.concentration_risk = max_position_value / total_position_value
|
|
|
|
# Sector concentration
|
|
for sector, value in sector_values.items():
|
|
metrics.sector_concentration[sector] = float(value / portfolio_value)
|
|
|
|
# P&L metrics
|
|
metrics.daily_pnl = await self._calculate_daily_pnl()
|
|
metrics.realized_pnl = await self._calculate_realized_pnl()
|
|
metrics.unrealized_pnl = sum(p.unrealized_pnl for p in positions)
|
|
|
|
# Risk metrics (simplified)
|
|
returns = await self._get_historical_returns()
|
|
if returns:
|
|
metrics.portfolio_volatility = np.std(returns) * np.sqrt(252) # Annualized
|
|
metrics.sharpe_ratio = self._calculate_sharpe_ratio(returns)
|
|
metrics.value_at_risk_95 = self._calculate_var(returns, 0.95)
|
|
metrics.value_at_risk_99 = self._calculate_var(returns, 0.99)
|
|
metrics.max_drawdown = self._calculate_max_drawdown(returns)
|
|
|
|
# Correlation risk
|
|
metrics.correlation_risk = await self._calculate_correlation_risk()
|
|
|
|
# Margin (simplified)
|
|
metrics.margin_used = total_position_value * Decimal('0.5') # 50% margin
|
|
metrics.margin_available = portfolio_value * Decimal('2') - metrics.margin_used
|
|
|
|
# Cache results
|
|
self._metrics_cache = metrics
|
|
self._cache_timestamp = datetime.now()
|
|
|
|
return metrics
|
|
|
|
async def apply_risk_adjustment(self,
|
|
ticker: str,
|
|
base_size: float,
|
|
confidence: float) -> float:
|
|
"""
|
|
Apply risk-based position sizing
|
|
|
|
Args:
|
|
ticker: Stock symbol
|
|
base_size: Base position size (% of portfolio)
|
|
confidence: Signal confidence (0-100)
|
|
|
|
Returns:
|
|
Risk-adjusted position size
|
|
"""
|
|
metrics = await self.calculate_risk_metrics()
|
|
|
|
# Start with base size
|
|
adjusted_size = base_size
|
|
|
|
# Adjust for confidence
|
|
confidence_multiplier = 0.5 + (confidence / 100) * 0.5 # 0.5x to 1.0x
|
|
adjusted_size *= confidence_multiplier
|
|
|
|
# Adjust for portfolio volatility
|
|
if metrics.portfolio_volatility > 0.20: # High volatility
|
|
adjusted_size *= 0.7
|
|
elif metrics.portfolio_volatility < 0.10: # Low volatility
|
|
adjusted_size *= 1.2
|
|
|
|
# Adjust for drawdown
|
|
if metrics.current_drawdown > Decimal('0.10'): # In drawdown
|
|
adjusted_size *= 0.5
|
|
|
|
# Adjust for concentration
|
|
if ticker in [p.ticker for p in await self._get_all_positions()]:
|
|
adjusted_size *= 0.8 # Reduce if already have position
|
|
|
|
# Kelly Criterion (simplified)
|
|
win_rate = 0.55 # Assumed win rate
|
|
avg_win_loss = 1.5 # Assumed win/loss ratio
|
|
kelly_fraction = (win_rate * avg_win_loss - (1 - win_rate)) / avg_win_loss
|
|
kelly_size = min(kelly_fraction, 0.25) # Cap at 25%
|
|
|
|
# Blend base and Kelly
|
|
adjusted_size = (adjusted_size * 0.7) + (kelly_size * 0.3)
|
|
|
|
# Ensure within limits
|
|
adjusted_size = max(
|
|
float(self.limits.max_position_size * Decimal('0.25')), # Min 25% of limit
|
|
min(adjusted_size, float(self.limits.max_position_size))
|
|
)
|
|
|
|
return adjusted_size
|
|
|
|
async def check_portfolio_health(self) -> Dict[str, Any]:
|
|
"""
|
|
Comprehensive portfolio health check
|
|
|
|
Returns:
|
|
Health report dictionary
|
|
"""
|
|
metrics = await self.calculate_risk_metrics()
|
|
health_issues = []
|
|
health_score = 100
|
|
|
|
# Check each metric
|
|
if metrics.total_exposure > Decimal('0.95'):
|
|
health_issues.append("Over-exposed (>95% invested)")
|
|
health_score -= 10
|
|
|
|
if metrics.concentration_risk > Decimal('0.40'):
|
|
health_issues.append("High concentration risk (>40% in one position)")
|
|
health_score -= 15
|
|
|
|
if metrics.portfolio_volatility > 0.30:
|
|
health_issues.append(f"High volatility ({metrics.portfolio_volatility:.1%})")
|
|
health_score -= 10
|
|
|
|
if metrics.sharpe_ratio < 0.5:
|
|
health_issues.append(f"Low Sharpe ratio ({metrics.sharpe_ratio:.2f})")
|
|
health_score -= 10
|
|
|
|
if metrics.current_drawdown > Decimal('0.10'):
|
|
health_issues.append(f"In drawdown ({metrics.current_drawdown:.1%})")
|
|
health_score -= 20
|
|
|
|
if metrics.daily_pnl < Decimal('-1000'):
|
|
health_issues.append(f"Large daily loss (${metrics.daily_pnl:,.2f})")
|
|
health_score -= 15
|
|
|
|
# Determine health status
|
|
if health_score >= 80:
|
|
status = "HEALTHY"
|
|
elif health_score >= 60:
|
|
status = "CAUTION"
|
|
elif health_score >= 40:
|
|
status = "WARNING"
|
|
else:
|
|
status = "CRITICAL"
|
|
|
|
return {
|
|
'status': status,
|
|
'score': health_score,
|
|
'issues': health_issues,
|
|
'metrics': {
|
|
'exposure': float(metrics.total_exposure),
|
|
'volatility': metrics.portfolio_volatility,
|
|
'sharpe': metrics.sharpe_ratio,
|
|
'var_95': float(metrics.value_at_risk_95),
|
|
'daily_pnl': float(metrics.daily_pnl),
|
|
'drawdown': float(metrics.current_drawdown)
|
|
},
|
|
'recommendations': self._generate_recommendations(metrics, health_issues)
|
|
}
|
|
|
|
# === Helper Methods ===
|
|
|
|
async def _get_all_positions(self) -> List[Position]:
|
|
"""Get all active positions"""
|
|
return self.db.get_active_positions()
|
|
|
|
async def _get_position(self, ticker: str) -> Optional[Position]:
|
|
"""Get specific position"""
|
|
with self.db.get_session() as session:
|
|
return session.query(Position).filter_by(ticker=ticker).first()
|
|
|
|
async def _get_portfolio_value(self) -> Decimal:
|
|
"""Get total portfolio value"""
|
|
positions = await self._get_all_positions()
|
|
return sum(p.market_value for p in positions)
|
|
|
|
async def _calculate_daily_pnl(self) -> Decimal:
|
|
"""Calculate today's P&L"""
|
|
with self.db.get_session() as session:
|
|
today_trades = session.query(Trade).filter(
|
|
Trade.executed_at >= date.today()
|
|
).all()
|
|
|
|
daily_pnl = sum(t.pnl or 0 for t in today_trades)
|
|
|
|
# Add unrealized P&L changes
|
|
positions = await self._get_all_positions()
|
|
for position in positions:
|
|
# Simplified - would compare to morning snapshot
|
|
daily_pnl += position.unrealized_pnl * Decimal('0.1') # Estimate
|
|
|
|
return daily_pnl
|
|
|
|
async def _calculate_realized_pnl(self) -> Decimal:
|
|
"""Calculate realized P&L"""
|
|
with self.db.get_session() as session:
|
|
all_trades = session.query(Trade).all()
|
|
return sum(t.pnl or 0 for t in all_trades)
|
|
|
|
async def _count_day_trades(self, ticker: str) -> int:
|
|
"""Count day trades for PDT rule"""
|
|
with self.db.get_session() as session:
|
|
today_orders = session.query(Order).filter(
|
|
Order.ticker == ticker,
|
|
Order.created_at >= date.today()
|
|
).all()
|
|
return len(today_orders)
|
|
|
|
async def _check_correlation(self, ticker: str) -> float:
|
|
"""Check correlation with existing positions"""
|
|
# Simplified correlation check
|
|
# In production, would calculate actual correlation matrix
|
|
positions = await self._get_all_positions()
|
|
|
|
if not positions:
|
|
return 0.0
|
|
|
|
# High correlation for same sector
|
|
sector = self.sector_map.get(ticker, 'Other')
|
|
same_sector_positions = [
|
|
p for p in positions
|
|
if self.sector_map.get(p.ticker, 'Other') == sector
|
|
]
|
|
|
|
if same_sector_positions:
|
|
return 0.8 # High correlation assumed for same sector
|
|
|
|
return 0.3 # Low correlation for different sectors
|
|
|
|
async def _get_historical_returns(self, days: int = 30) -> List[float]:
|
|
"""Get historical portfolio returns"""
|
|
with self.db.get_session() as session:
|
|
snapshots = session.query(PerformanceMetric).filter(
|
|
PerformanceMetric.date >= datetime.now() - timedelta(days=days)
|
|
).order_by(PerformanceMetric.date).all()
|
|
|
|
if len(snapshots) < 2:
|
|
return []
|
|
|
|
returns = []
|
|
for i in range(1, len(snapshots)):
|
|
if snapshots[i-1].total_pnl and snapshots[i].total_pnl:
|
|
daily_return = float(
|
|
(snapshots[i].total_pnl - snapshots[i-1].total_pnl) /
|
|
abs(snapshots[i-1].total_pnl)
|
|
)
|
|
returns.append(daily_return)
|
|
|
|
return returns
|
|
|
|
def _calculate_risk_score(self,
|
|
violations: List[RiskViolationType],
|
|
metrics: RiskMetrics,
|
|
position_size: Decimal) -> float:
|
|
"""Calculate risk score (0-100)"""
|
|
score = 0
|
|
|
|
# Violation weights
|
|
violation_weights = {
|
|
RiskViolationType.DAILY_LOSS: 30,
|
|
RiskViolationType.MARGIN: 25,
|
|
RiskViolationType.POSITION_SIZE: 20,
|
|
RiskViolationType.CONCENTRATION: 15,
|
|
RiskViolationType.PATTERN_DAY_TRADER: 20,
|
|
RiskViolationType.VOLATILITY: 10,
|
|
RiskViolationType.CORRELATION: 10
|
|
}
|
|
|
|
for violation in violations:
|
|
score += violation_weights.get(violation, 5)
|
|
|
|
# Add metric-based scoring
|
|
if metrics.portfolio_volatility > 0.25:
|
|
score += 10
|
|
if metrics.current_drawdown > Decimal('0.10'):
|
|
score += 15
|
|
if float(position_size) > 0.15:
|
|
score += 10
|
|
|
|
return min(score, 100)
|
|
|
|
def _calculate_sharpe_ratio(self, returns: List[float],
|
|
risk_free_rate: float = 0.03) -> float:
|
|
"""Calculate Sharpe ratio"""
|
|
if not returns:
|
|
return 0.0
|
|
|
|
mean_return = np.mean(returns) * 252 # Annualized
|
|
std_return = np.std(returns) * np.sqrt(252)
|
|
|
|
if std_return == 0:
|
|
return 0.0
|
|
|
|
return (mean_return - risk_free_rate) / std_return
|
|
|
|
def _calculate_var(self, returns: List[float],
|
|
confidence: float) -> Decimal:
|
|
"""Calculate Value at Risk"""
|
|
if not returns:
|
|
return Decimal('0')
|
|
|
|
percentile = (1 - confidence) * 100
|
|
var = np.percentile(returns, percentile)
|
|
return Decimal(str(abs(var)))
|
|
|
|
def _calculate_max_drawdown(self, returns: List[float]) -> Decimal:
|
|
"""Calculate maximum drawdown"""
|
|
if not returns:
|
|
return Decimal('0')
|
|
|
|
cumulative = np.cumprod(1 + np.array(returns))
|
|
running_max = np.maximum.accumulate(cumulative)
|
|
drawdown = (cumulative - running_max) / running_max
|
|
return Decimal(str(abs(min(drawdown))))
|
|
|
|
async def _calculate_correlation_risk(self) -> float:
|
|
"""Calculate overall portfolio correlation risk"""
|
|
# Simplified - would use actual correlation matrix
|
|
positions = await self._get_all_positions()
|
|
|
|
if len(positions) < 2:
|
|
return 0.0
|
|
|
|
# Check sector concentration
|
|
sectors = [self.sector_map.get(p.ticker, 'Other') for p in positions]
|
|
unique_sectors = len(set(sectors))
|
|
|
|
if unique_sectors == 1:
|
|
return 0.9 # All in same sector
|
|
elif unique_sectors == 2:
|
|
return 0.6
|
|
else:
|
|
return 0.3
|
|
|
|
def _estimate_volatility_impact(self, ticker: str,
|
|
position_size: Decimal) -> float:
|
|
"""Estimate impact on portfolio volatility"""
|
|
# Simplified estimate
|
|
# Individual stock volatility assumed at 30%
|
|
stock_vol = 0.30
|
|
impact = float(position_size) * stock_vol * 0.5 # Rough estimate
|
|
return impact
|
|
|
|
def _generate_recommendations(self, metrics: RiskMetrics,
|
|
issues: List[str]) -> List[str]:
|
|
"""Generate risk management recommendations"""
|
|
recommendations = []
|
|
|
|
if metrics.concentration_risk > Decimal('0.30'):
|
|
recommendations.append("Diversify portfolio - reduce largest position")
|
|
|
|
if metrics.portfolio_volatility > 0.25:
|
|
recommendations.append("Consider hedging with options or inverse ETFs")
|
|
|
|
if metrics.current_drawdown > Decimal('0.10'):
|
|
recommendations.append("Reduce position sizes until recovery")
|
|
|
|
if metrics.sharpe_ratio < 0.5:
|
|
recommendations.append("Review strategy - risk-adjusted returns are low")
|
|
|
|
if "Over-exposed" in str(issues):
|
|
recommendations.append("Keep some cash reserve for opportunities")
|
|
|
|
return recommendations
|
|
|
|
|
|
# Example usage
|
|
async def main():
|
|
"""Example of using the risk manager"""
|
|
from .database import DatabaseManager
|
|
|
|
# Initialize
|
|
db = DatabaseManager("postgresql://trader:password@localhost/trading_db")
|
|
risk_manager = RiskManager(db)
|
|
|
|
# Check an order
|
|
result = await risk_manager.check_order(
|
|
ticker="AAPL",
|
|
side="BUY",
|
|
quantity=1000,
|
|
price=150.00,
|
|
stop_loss=145.00
|
|
)
|
|
|
|
print(f"Approved: {result.approved}")
|
|
print(f"Risk Score: {result.risk_score}")
|
|
print(f"Risk Level: {result.risk_level}")
|
|
print(f"Violations: {result.violations}")
|
|
print(f"Reasons: {result.reasons}")
|
|
print(f"Recommendations: {result.recommendations}")
|
|
|
|
# Check portfolio health
|
|
health = await risk_manager.check_portfolio_health()
|
|
print(f"\nPortfolio Health: {health['status']}")
|
|
print(f"Health Score: {health['score']}")
|
|
print(f"Issues: {health['issues']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(level=logging.INFO)
|
|
import asyncio
|
|
asyncio.run(main()) |