Class: SQA::PatternMatcher
- Inherits:
-
Object
- Object
- SQA::PatternMatcher
- Defined in:
- lib/sqa/pattern_matcher.rb
Overview
PatternMatcher - Find similar historical patterns
Provides methods for:
-
Pattern similarity search (nearest-neighbor)
-
Shape-based pattern matching
-
Predict future moves based on similar past patterns
-
Pattern clustering
Uses techniques:
-
Euclidean distance
-
Dynamic Time Warping (DTW)
-
Pearson correlation
Instance Attribute Summary collapse
-
#prices ⇒ Object
readonly
Returns the value of attribute prices.
-
#stock ⇒ Object
readonly
Returns the value of attribute stock.
Instance Method Summary collapse
-
#cluster_patterns(pattern_length: 10, num_clusters: 5) ⇒ Array<Array<Hash>>
Cluster patterns by similarity.
-
#detect_chart_pattern(pattern_type) ⇒ Array<Hash>
Detect chart patterns (head & shoulders, double top, etc.).
-
#find_similar(lookback: 10, num_matches: 5, method: :euclidean, normalize: true) ⇒ Array<Hash>
Find similar historical patterns to current pattern.
-
#forecast(lookback: 10, forecast_periods: 5, num_matches: 10) ⇒ Hash
Predict future price movement based on similar patterns.
-
#initialize(stock:) ⇒ PatternMatcher
constructor
Initialize pattern matcher.
-
#pattern_quality(pattern) ⇒ Hash
Calculate pattern strength/quality.
Constructor Details
#initialize(stock:) ⇒ PatternMatcher
Initialize pattern matcher
32 33 34 35 |
# File 'lib/sqa/pattern_matcher.rb', line 32 def initialize(stock:) @stock = stock @prices = stock.df.data["adj_close_price"].to_a end |
Instance Attribute Details
#prices ⇒ Object (readonly)
Returns the value of attribute prices.
25 26 27 |
# File 'lib/sqa/pattern_matcher.rb', line 25 def prices @prices end |
#stock ⇒ Object (readonly)
Returns the value of attribute stock.
25 26 27 |
# File 'lib/sqa/pattern_matcher.rb', line 25 def stock @stock end |
Instance Method Details
#cluster_patterns(pattern_length: 10, num_clusters: 5) ⇒ Array<Array<Hash>>
Cluster patterns by similarity
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 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 205 206 |
# File 'lib/sqa/pattern_matcher.rb', line 163 def cluster_patterns(pattern_length: 10, num_clusters: 5) return [] if @prices.size < pattern_length * num_clusters # Extract all patterns patterns = [] (@prices.size - pattern_length).times do |start_idx| pattern = @prices[start_idx, pattern_length] patterns << { start_index: start_idx, pattern: normalize_pattern(pattern), raw_pattern: pattern } end # Simple k-means clustering clusters = Array.new(num_clusters) { [] } # Initialize centroids randomly centroids = patterns.sample(num_clusters).map { |p| p[:pattern] } # Iterate until convergence 10.times do # Assign to nearest centroid clusters = Array.new(num_clusters) { [] } patterns.each do |pattern| distances = centroids.map { |centroid| euclidean_distance(pattern[:pattern], centroid) } nearest_cluster = distances.index(distances.min) clusters[nearest_cluster] << pattern end # Update centroids centroids = clusters.map do |cluster| next centroids[0] if cluster.empty? # Average pattern pattern_length.times.map do |i| cluster.map { |p| p[:pattern][i] }.sum / cluster.size.to_f end end end clusters.reject(&:empty?) end |
#detect_chart_pattern(pattern_type) ⇒ Array<Hash>
Detect chart patterns (head & shoulders, double top, etc.)
141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/sqa/pattern_matcher.rb', line 141 def detect_chart_pattern(pattern_type) case pattern_type when :double_top detect_double_top when :double_bottom detect_double_bottom when :head_and_shoulders detect_head_shoulders when :triangle detect_triangle else [] end end |
#find_similar(lookback: 10, num_matches: 5, method: :euclidean, normalize: true) ⇒ Array<Hash>
Find similar historical patterns to current pattern
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 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 |
# File 'lib/sqa/pattern_matcher.rb', line 46 def find_similar(lookback: 10, num_matches: 5, method: :euclidean, normalize: true) return [] if @prices.size < lookback * 2 # Current pattern (most recent) current_pattern = @prices[-lookback..-1] current_pattern = normalize_pattern(current_pattern) if normalize similarities = [] # Search through historical data (@prices.size - lookback - 20).times do |start_idx| next if start_idx + lookback >= @prices.size - lookback # Don't compare to recent data historical_pattern = @prices[start_idx, lookback] historical_pattern = normalize_pattern(historical_pattern) if normalize distance = case method when :euclidean euclidean_distance(current_pattern, historical_pattern) when :dtw dtw_distance(current_pattern, historical_pattern) when :correlation -correlation(current_pattern, historical_pattern) # Negative so lower is better else euclidean_distance(current_pattern, historical_pattern) end # What happened next? future_start = start_idx + lookback future_end = [future_start + lookback, @prices.size - 1].min future_prices = @prices[future_start..future_end] next if future_prices.empty? future_return = (future_prices.last - @prices[start_idx + lookback - 1]) / @prices[start_idx + lookback - 1] similarities << { start_index: start_idx, end_index: start_idx + lookback - 1, distance: distance, pattern: historical_pattern, future_return: future_return, future_prices: future_prices, pattern_start_price: @prices[start_idx], pattern_end_price: @prices[start_idx + lookback - 1] } end # Sort by distance and return top matches similarities.sort_by { |s| s[:distance] }.first(num_matches) end |
#forecast(lookback: 10, forecast_periods: 5, num_matches: 10) ⇒ Hash
Predict future price movement based on similar patterns
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/sqa/pattern_matcher.rb', line 107 def forecast(lookback: 10, forecast_periods: 5, num_matches: 10) similar = find_similar(lookback: lookback, num_matches: num_matches) return nil if similar.empty? # Collect future returns from similar patterns future_returns = similar.map { |s| s[:future_return] } # Statistical forecast mean_return = future_returns.sum / future_returns.size.to_f std_return = standard_deviation(future_returns) current_price = @prices.last forecast_price = current_price * (1 + mean_return) { forecast_price: forecast_price, forecast_return: mean_return, confidence_interval_95: [ current_price * (1 + mean_return - 1.96 * std_return), current_price * (1 + mean_return + 1.96 * std_return) ], num_matches: similar.size, similar_patterns: similar, current_price: current_price } end |
#pattern_quality(pattern) ⇒ Hash
Calculate pattern strength/quality
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/sqa/pattern_matcher.rb', line 214 def pattern_quality(pattern) return nil if pattern.size < 3 # Trend strength (convert to float to avoid integer division) first = pattern.first.to_f last = pattern.last.to_f trend = (last - first) / first # Volatility returns = pattern.each_cons(2).map { |a, b| (b - a).to_f / a } volatility = standard_deviation(returns) # Smoothness (how linear is the trend?) x_values = (0...pattern.size).to_a correlation = pearson_correlation(x_values, pattern) { trend: trend, volatility: volatility, smoothness: correlation.abs, strength: correlation.abs * (1 - volatility) # Combined metric } end |