Module: SQA::MarketRegime
- Defined in:
- lib/sqa/market_regime.rb
Class Method Summary collapse
-
.detect(stock, lookback: nil, window: nil) ⇒ Hash
Detect current market regime for a stock.
-
.detect_history(stock, window: 60) ⇒ Array<Hash>
Detect market regimes across entire history.
-
.detect_strength(prices) ⇒ Symbol
Detect trend strength (backward compatibility).
-
.detect_strength_with_score(prices) ⇒ Hash
Detect trend strength with numeric score.
-
.detect_trend(prices) ⇒ Symbol
Classify regime type based on trend (backward compatibility).
-
.detect_trend_with_score(prices) ⇒ Hash
Classify regime type based on trend with numeric score.
-
.detect_volatility(prices) ⇒ Symbol
Detect volatility regime (backward compatibility).
-
.detect_volatility_with_score(prices) ⇒ Hash
Detect volatility regime with numeric score.
-
.split_by_regime(stock) ⇒ Hash
Split data by regime.
Class Method Details
.detect(stock, lookback: nil, window: nil) ⇒ Hash
Detect current market regime for a stock
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/sqa/market_regime.rb', line 26 def detect(stock, lookback: nil, window: nil) # Accept both lookback and window for backward compatibility # lookback takes precedence if both provided period = lookback || window || 60 prices = stock.df["adj_close_price"].to_a return { type: :unknown } if prices.size < period recent_prices = prices.last(period) # Get both symbolic and numeric classifications trend_data = detect_trend_with_score(recent_prices) volatility_data = detect_volatility_with_score(recent_prices) strength_data = detect_strength_with_score(recent_prices) { type: trend_data[:type], trend_score: trend_data[:score], volatility: volatility_data[:type], volatility_score: volatility_data[:score], strength: strength_data[:type], strength_score: strength_data[:score], lookback_days: period, detected_at: Time.now } end |
.detect_history(stock, window: 60) ⇒ Array<Hash>
Detect market regimes across entire history
Splits historical data into regime periods
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/sqa/market_regime.rb', line 61 def detect_history(stock, window: 60) prices = stock.df["adj_close_price"].to_a regimes = [] current_regime = nil regime_start = 0 (window...prices.size).each do |i| window_prices = prices[(i - window)..i] regime_type = detect_trend(window_prices) volatility = detect_volatility(window_prices) # Check if regime changed if current_regime != regime_type # Save previous regime if current_regime regimes << { type: current_regime, start_index: regime_start, end_index: i - 1, duration: i - regime_start, volatility: volatility } end current_regime = regime_type regime_start = i end end # Add final regime if current_regime regimes << { type: current_regime, start_index: regime_start, end_index: prices.size - 1, duration: prices.size - regime_start, volatility: detect_volatility(prices[regime_start..-1]) } end regimes end |
.detect_strength(prices) ⇒ Symbol
Detect trend strength (backward compatibility)
211 212 213 |
# File 'lib/sqa/market_regime.rb', line 211 def detect_strength(prices) detect_strength_with_score(prices)[:type] end |
.detect_strength_with_score(prices) ⇒ Hash
Detect trend strength with numeric score
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/sqa/market_regime.rb', line 179 def detect_strength_with_score(prices) return { type: :unknown, score: 0.0 } if prices.size < 20 # Look at consistency of direction up_days = 0 down_days = 0 prices.each_cons(2) do |prev, curr| if curr > prev up_days += 1 else down_days += 1 end end total_days = up_days + down_days directional_pct = [up_days, down_days].max.to_f / total_days * 100 if directional_pct > 70 { type: :strong, score: directional_pct } elsif directional_pct > 55 { type: :moderate, score: directional_pct } else { type: :weak, score: directional_pct } end end |
.detect_trend(prices) ⇒ Symbol
Classify regime type based on trend (backward compatibility)
136 137 138 |
# File 'lib/sqa/market_regime.rb', line 136 def detect_trend(prices) detect_trend_with_score(prices)[:type] end |
.detect_trend_with_score(prices) ⇒ Hash
Classify regime type based on trend with numeric score
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/sqa/market_regime.rb', line 109 def detect_trend_with_score(prices) return { type: :unknown, score: 0.0 } if prices.size < 20 # Simple moving averages sma_short = prices.last(20).sum / 20.0 sma_long = prices.last(60).sum / 60.0 rescue sma_short # Price vs moving averages current_price = prices.last # Calculate trend strength (percentage above/below SMA) pct_above_sma = ((current_price - sma_long) / sma_long * 100.0) if pct_above_sma > 5 && sma_short > sma_long { type: :bull, score: pct_above_sma } elsif pct_above_sma < -5 && sma_short < sma_long { type: :bear, score: pct_above_sma } else { type: :sideways, score: pct_above_sma } end end |
.detect_volatility(prices) ⇒ Symbol
Detect volatility regime (backward compatibility)
170 171 172 |
# File 'lib/sqa/market_regime.rb', line 170 def detect_volatility(prices) detect_volatility_with_score(prices)[:type] end |
.detect_volatility_with_score(prices) ⇒ Hash
Detect volatility regime with numeric score
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/sqa/market_regime.rb', line 145 def detect_volatility_with_score(prices) return { type: :unknown, score: 0.0 } if prices.size < 20 # Calculate daily returns returns = [] prices.each_cons(2) do |prev, curr| returns << ((curr - prev) / prev * 100.0).abs end avg_volatility = returns.sum / returns.size if avg_volatility < 1.0 { type: :low, score: avg_volatility } elsif avg_volatility < 2.5 { type: :medium, score: avg_volatility } else { type: :high, score: avg_volatility } end end |
.split_by_regime(stock) ⇒ Hash
Split data by regime
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/sqa/market_regime.rb', line 220 def split_by_regime(stock) regimes = detect_history(stock) prices = stock.df["adj_close_price"].to_a grouped = { bull: [], bear: [], sideways: [] } regimes.each do |regime| regime_prices = prices[regime[:start_index]..regime[:end_index]] grouped[regime[:type]] << { prices: regime_prices, start_index: regime[:start_index], end_index: regime[:end_index], duration: regime[:duration] } end grouped end |