Class: SQA::RiskManager
- Inherits:
-
Object
- Object
- SQA::RiskManager
- Defined in:
- lib/sqa/risk_manager.rb
Overview
RiskManager - Comprehensive risk management and position sizing
Provides methods for:
-
Value at Risk (VaR): Historical, Parametric, Monte Carlo
-
Conditional VaR (CVaR / Expected Shortfall)
-
Position sizing: Kelly Criterion, Fixed Fractional, Percent Volatility
-
Risk metrics: Sharpe, Sortino, Calmar, Maximum Drawdown
-
Stop loss calculations
Class Method Summary collapse
-
.atr_stop_loss(current_price:, atr:, multiplier: 2.0, direction: :long) ⇒ Float
Calculate stop loss price based on ATR (Average True Range).
-
.calmar_ratio(returns, periods_per_year: 252) ⇒ Float
Calculate Calmar Ratio.
-
.cvar(returns, confidence: 0.95) ⇒ Float
Calculate Conditional Value at Risk (CVaR / Expected Shortfall).
-
.fixed_fractional(capital:, risk_fraction: 0.02) ⇒ Float
Calculate position size using Fixed Fractional method.
-
.kelly_criterion(win_rate:, avg_win:, avg_loss:, capital:, max_fraction: 0.25) ⇒ Float
Calculate position size using Kelly Criterion.
-
.max_drawdown(prices) ⇒ Hash
Calculate maximum drawdown from price series.
-
.monte_carlo_simulation(initial_capital:, returns:, periods:, simulations: 1000) ⇒ Hash
Monte Carlo simulation for portfolio value.
-
.percent_volatility(capital:, returns:, target_volatility: 0.15, current_price:) ⇒ Integer
Calculate position size using Percent Volatility method.
-
.sharpe_ratio(returns, risk_free_rate: 0.02, periods_per_year: 252) ⇒ Float
Calculate Sharpe Ratio.
-
.sortino_ratio(returns, target_return: 0.0, periods_per_year: 252) ⇒ Float
Calculate Sortino Ratio.
-
.var(returns, confidence: 0.95, method: :historical, simulations: 10_000) ⇒ Float
Calculate Value at Risk (VaR) using historical method.
Class Method Details
.atr_stop_loss(current_price:, atr:, multiplier: 2.0, direction: :long) ⇒ Float
Calculate stop loss price based on ATR (Average True Range)
204 205 206 207 208 209 210 |
# File 'lib/sqa/risk_manager.rb', line 204 def atr_stop_loss(current_price:, atr:, multiplier: 2.0, direction: :long) if direction == :long current_price - (atr * multiplier) else current_price + (atr * multiplier) end end |
.calmar_ratio(returns, periods_per_year: 252) ⇒ Float
Calculate Calmar Ratio
Ratio of annualized return to maximum drawdown.
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 |
# File 'lib/sqa/risk_manager.rb', line 325 def calmar_ratio(returns, periods_per_year: 252) return 0.0 if returns.empty? # Annualized return total_return = returns.inject(1.0) { |product, r| product * (1 + r) } periods = returns.size annualized_return = (total_return ** (periods_per_year.to_f / periods)) - 1.0 # Convert returns to prices for drawdown calculation prices = returns.inject([100.0]) { |acc, r| acc << acc.last * (1 + r) } max_dd = max_drawdown(prices)[:max_drawdown].abs return 0.0 if max_dd.zero? annualized_return / max_dd end |
.cvar(returns, confidence: 0.95) ⇒ Float
Calculate Conditional Value at Risk (CVaR / Expected Shortfall)
CVaR is the expected loss given that the loss exceeds the VaR threshold. It provides a more conservative risk measure than VaR.
77 78 79 80 81 82 83 84 85 |
# File 'lib/sqa/risk_manager.rb', line 77 def cvar(returns, confidence: 0.95) return nil if returns.empty? var_threshold = var(returns, confidence: confidence, method: :historical) tail_losses = returns.select { |r| r <= var_threshold } return nil if tail_losses.empty? tail_losses.sum / tail_losses.size.to_f end |
.fixed_fractional(capital:, risk_fraction: 0.02) ⇒ Float
Calculate position size using Fixed Fractional method
Risk a fixed percentage of capital on each trade. Simple and conservative approach.
146 147 148 |
# File 'lib/sqa/risk_manager.rb', line 146 def fixed_fractional(capital:, risk_fraction: 0.02) capital * risk_fraction end |
.kelly_criterion(win_rate:, avg_win:, avg_loss:, capital:, max_fraction: 0.25) ⇒ Float
Calculate position size using Kelly Criterion
Kelly Criterion calculates the optimal fraction of capital to risk based on win rate and win/loss ratio.
Formula: f = (p * b - q) / b where:
f = fraction of capital to bet
p = probability of winning
q = probability of losing (1 - p)
b = win/loss ratio (avg_win / avg_loss)
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/sqa/risk_manager.rb', line 116 def kelly_criterion(win_rate:, avg_win:, avg_loss:, capital:, max_fraction: 0.25) return 0.0 if avg_loss.zero? || win_rate >= 1.0 || win_rate <= 0.0 lose_rate = 1.0 - win_rate win_loss_ratio = avg_win / avg_loss # Kelly formula kelly_fraction = (win_rate * win_loss_ratio - lose_rate) / win_loss_ratio # Cap at max_fraction (Kelly can be aggressive) kelly_fraction = [kelly_fraction, max_fraction].min kelly_fraction = [kelly_fraction, 0.0].max # No negative positions capital * kelly_fraction end |
.max_drawdown(prices) ⇒ Hash
Calculate maximum drawdown from price series
Drawdown is the peak-to-trough decline in portfolio value.
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/sqa/risk_manager.rb', line 224 def max_drawdown(prices) return { max_drawdown: 0.0, peak_idx: 0, trough_idx: 0 } if prices.size < 2 max_dd = 0.0 peak_idx = 0 trough_idx = 0 running_peak = prices.first running_peak_idx = 0 prices.each_with_index do |price, idx| if price > running_peak running_peak = price running_peak_idx = idx end drawdown = (price - running_peak) / running_peak if drawdown < max_dd max_dd = drawdown peak_idx = running_peak_idx trough_idx = idx end end { max_drawdown: max_dd, peak_idx: peak_idx, trough_idx: trough_idx, peak_value: prices[peak_idx], trough_value: prices[trough_idx] } end |
.monte_carlo_simulation(initial_capital:, returns:, periods:, simulations: 1000) ⇒ Hash
Monte Carlo simulation for portfolio value
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/sqa/risk_manager.rb', line 360 def monte_carlo_simulation(initial_capital:, returns:, periods:, simulations: 1000) return nil if returns.empty? final_values = simulations.times.map do value = initial_capital periods.times do random_return = returns.sample value *= (1 + random_return) end value end final_values.sort! { mean: final_values.sum / final_values.size.to_f, median: final_values[final_values.size / 2], percentile_5: final_values[(final_values.size * 0.05).floor], percentile_25: final_values[(final_values.size * 0.25).floor], percentile_75: final_values[(final_values.size * 0.75).floor], percentile_95: final_values[(final_values.size * 0.95).floor], min: final_values.first, max: final_values.last, all_values: final_values } end |
.percent_volatility(capital:, returns:, target_volatility: 0.15, current_price:) ⇒ Integer
Calculate position size using Percent Volatility method
Adjust position size based on recent volatility. Higher volatility = smaller position size.
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/sqa/risk_manager.rb', line 170 def percent_volatility(capital:, returns:, target_volatility: 0.15, current_price:) return 0 if returns.empty? || current_price.zero? # Calculate recent volatility (annualized) std_dev = standard_deviation(returns) annualized_volatility = std_dev * Math.sqrt(252) # Assume 252 trading days return 0 if annualized_volatility.zero? # Calculate position size position_value = capital * (target_volatility / annualized_volatility) shares = (position_value / current_price).floor [shares, 0].max # No negative shares end |
.sharpe_ratio(returns, risk_free_rate: 0.02, periods_per_year: 252) ⇒ Float
Calculate Sharpe Ratio
Measures risk-adjusted return (excess return per unit of risk).
269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/sqa/risk_manager.rb', line 269 def sharpe_ratio(returns, risk_free_rate: 0.02, periods_per_year: 252) return 0.0 if returns.empty? excess_returns = returns.map { |r| r - (risk_free_rate / periods_per_year) } mean_excess = excess_returns.sum / excess_returns.size.to_f std_excess = standard_deviation(excess_returns) return 0.0 if std_excess.zero? (mean_excess / std_excess) * Math.sqrt(periods_per_year) end |
.sortino_ratio(returns, target_return: 0.0, periods_per_year: 252) ⇒ Float
Calculate Sortino Ratio
Like Sharpe ratio but only penalizes downside volatility.
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/sqa/risk_manager.rb', line 294 def sortino_ratio(returns, target_return: 0.0, periods_per_year: 252) return 0.0 if returns.empty? excess_returns = returns.map { |r| r - target_return } mean_excess = excess_returns.sum / excess_returns.size.to_f # Downside deviation (only negative returns) downside_returns = excess_returns.select { |r| r < 0 } return Float::INFINITY if downside_returns.empty? downside_deviation = Math.sqrt( downside_returns.map { |r| r**2 }.sum / downside_returns.size.to_f ) return 0.0 if downside_deviation.zero? (mean_excess / downside_deviation) * Math.sqrt(periods_per_year) end |
.var(returns, confidence: 0.95, method: :historical, simulations: 10_000) ⇒ Float
Calculate Value at Risk (VaR) using historical method
VaR represents the maximum expected loss over a given time period at a specified confidence level.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/sqa/risk_manager.rb', line 48 def var(returns, confidence: 0.95, method: :historical, simulations: 10_000) return nil if returns.empty? case method when :historical historical_var(returns, confidence) when :parametric parametric_var(returns, confidence) when :monte_carlo monte_carlo_var(returns, confidence, simulations) else raise ArgumentError, "Unknown VaR method: #{method}" end end |