Class: SQA::StrategyGenerator
- Inherits:
-
Object
- Object
- SQA::StrategyGenerator
- Defined in:
- lib/sqa/strategy_generator.rb
Defined Under Namespace
Classes: Pattern, PatternContext, ProfitablePoint
Instance Attribute Summary collapse
-
#fpop ⇒ Object
readonly
Returns the value of attribute fpop.
-
#indicators_config ⇒ Object
readonly
Returns the value of attribute indicators_config.
-
#inflection_window ⇒ Object
readonly
Returns the value of attribute inflection_window.
-
#max_fpl_risk ⇒ Object
readonly
Returns the value of attribute max_fpl_risk.
-
#min_gain_percent ⇒ Object
readonly
Returns the value of attribute min_gain_percent.
-
#min_loss_percent ⇒ Object
readonly
Returns the value of attribute min_loss_percent.
-
#patterns ⇒ Object
readonly
Returns the value of attribute patterns.
-
#profitable_points ⇒ Object
readonly
Returns the value of attribute profitable_points.
-
#required_fpl_directions ⇒ Object
readonly
Returns the value of attribute required_fpl_directions.
-
#stock ⇒ Object
readonly
Returns the value of attribute stock.
Instance Method Summary collapse
-
#discover_context_aware_patterns(analyze_regime: true, analyze_seasonal: true, sector: nil) ⇒ Array<Pattern>
Discover patterns with context (regime, seasonal, sector).
-
#discover_patterns(min_pattern_frequency: 2) ⇒ Object
Main entry point: Discover patterns in historical data.
-
#export_patterns(filename) ⇒ Object
Export patterns to CSV.
-
#generate_strategies(top_n: 5, strategy_type: :class) ⇒ Object
Generate multiple strategies from top N patterns.
-
#generate_strategy(pattern_index: 0, strategy_type: :proc) ⇒ Object
Generate a trading strategy from discovered patterns.
-
#initialize(stock:, min_gain_percent: 10.0, min_loss_percent: nil, fpop: 10, inflection_window: 3, max_fpl_risk: nil, required_fpl_directions: nil) ⇒ StrategyGenerator
constructor
A new instance of StrategyGenerator.
-
#print_patterns(max_patterns: 10) ⇒ Object
Print discovered patterns.
-
#walk_forward_validate(train_size: 250, test_size: 60, step_size: 30) ⇒ Hash
Walk-forward validation - discover patterns with time-series cross-validation.
Constructor Details
#initialize(stock:, min_gain_percent: 10.0, min_loss_percent: nil, fpop: 10, inflection_window: 3, max_fpl_risk: nil, required_fpl_directions: nil) ⇒ StrategyGenerator
Returns a new instance of StrategyGenerator.
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/sqa/strategy_generator.rb', line 144 def initialize(stock:, min_gain_percent: 10.0, min_loss_percent: nil, fpop: 10, inflection_window: 3, max_fpl_risk: nil, required_fpl_directions: nil) @stock = stock @min_gain_percent = min_gain_percent @min_loss_percent = min_loss_percent || -min_gain_percent # Symmetric loss threshold @fpop = fpop # Future Period of Performance @inflection_window = inflection_window # Window for detecting local min/max @max_fpl_risk = max_fpl_risk # Optional: Filter by max acceptable risk (volatility) @required_fpl_directions = required_fpl_directions # Optional: [:UP, :DOWN, :UNCERTAIN, :FLAT] @profitable_points = [] @patterns = [] # Configure which indicators to analyze @indicators_config = { rsi: { period: 14, oversold: 30, overbought: 70 }, macd: { fast: 12, slow: 26, signal: 9 }, stoch: { k_period: 14, d_period: 3, oversold: 20, overbought: 80 }, sma_cross: { short: 20, long: 50 }, ema: { period: 20 }, bbands: { period: 20, nbdev: 2.0 }, volume: { period: 20, threshold: 1.5 } } end |
Instance Attribute Details
#fpop ⇒ Object (readonly)
Returns the value of attribute fpop.
140 141 142 |
# File 'lib/sqa/strategy_generator.rb', line 140 def fpop @fpop end |
#indicators_config ⇒ Object (readonly)
Returns the value of attribute indicators_config.
140 141 142 |
# File 'lib/sqa/strategy_generator.rb', line 140 def indicators_config @indicators_config end |
#inflection_window ⇒ Object (readonly)
Returns the value of attribute inflection_window.
140 141 142 |
# File 'lib/sqa/strategy_generator.rb', line 140 def inflection_window @inflection_window end |
#max_fpl_risk ⇒ Object (readonly)
Returns the value of attribute max_fpl_risk.
140 141 142 |
# File 'lib/sqa/strategy_generator.rb', line 140 def max_fpl_risk @max_fpl_risk end |
#min_gain_percent ⇒ Object (readonly)
Returns the value of attribute min_gain_percent.
140 141 142 |
# File 'lib/sqa/strategy_generator.rb', line 140 def min_gain_percent @min_gain_percent end |
#min_loss_percent ⇒ Object (readonly)
Returns the value of attribute min_loss_percent.
140 141 142 |
# File 'lib/sqa/strategy_generator.rb', line 140 def min_loss_percent @min_loss_percent end |
#patterns ⇒ Object (readonly)
Returns the value of attribute patterns.
140 141 142 |
# File 'lib/sqa/strategy_generator.rb', line 140 def patterns @patterns end |
#profitable_points ⇒ Object (readonly)
Returns the value of attribute profitable_points.
140 141 142 |
# File 'lib/sqa/strategy_generator.rb', line 140 def profitable_points @profitable_points end |
#required_fpl_directions ⇒ Object (readonly)
Returns the value of attribute required_fpl_directions.
140 141 142 |
# File 'lib/sqa/strategy_generator.rb', line 140 def required_fpl_directions @required_fpl_directions end |
#stock ⇒ Object (readonly)
Returns the value of attribute stock.
140 141 142 |
# File 'lib/sqa/strategy_generator.rb', line 140 def stock @stock end |
Instance Method Details
#discover_context_aware_patterns(analyze_regime: true, analyze_seasonal: true, sector: nil) ⇒ Array<Pattern>
Discover patterns with context (regime, seasonal, sector)
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 |
# File 'lib/sqa/strategy_generator.rb', line 379 def discover_context_aware_patterns(analyze_regime: true, analyze_seasonal: true, sector: nil) puts "\n" + "=" * 70 puts "Context-Aware Pattern Discovery" puts "=" * 70 # Step 1: Detect market regime if analyze_regime regime_data = SQA::MarketRegime.detect(@stock) puts "Current regime: #{regime_data[:type]} (#{regime_data[:strength]} strength)" # Split data by regime regime_splits = SQA::MarketRegime.split_by_regime(@stock) puts "\nRegime periods:" regime_splits.each do |regime, periods| total_days = periods.sum { |p| p[:duration] } puts " #{regime}: #{total_days} days across #{periods.size} periods" end end # Step 2: Analyze seasonality if analyze_seasonal seasonal_data = SQA::SeasonalAnalyzer.analyze(@stock) puts "\nSeasonal analysis:" puts " Best months: #{seasonal_data[:best_months].join(', ')}" puts " Worst months: #{seasonal_data[:worst_months].join(', ')}" puts " Best quarters: Q#{seasonal_data[:best_quarters].join(', Q')}" puts " Has seasonal pattern: #{seasonal_data[:has_seasonal_pattern]}" end # Step 3: Discover patterns normally patterns = discover_patterns # Step 4: Add context to each pattern patterns.each do |pattern| if analyze_regime pattern.context.market_regime = regime_data[:type] pattern.context.volatility_regime = regime_data[:volatility] end if analyze_seasonal && seasonal_data[:has_seasonal_pattern] pattern.context.valid_months = seasonal_data[:best_months] pattern.context.valid_quarters = seasonal_data[:best_quarters] end if sector pattern.context.sector = sector end # Add discovery period date_column = @stock.df.data.columns.include?("date") ? "date" : "timestamp" dates = @stock.df[date_column].to_a pattern.context.discovered_period = "#{dates.first} to #{dates.last}" end puts "\n" + "=" * 70 puts "Context-Aware Discovery Complete" puts " Patterns found: #{patterns.size}" puts " Patterns with context: #{patterns.count { |p| p.context.valid? }}" puts "=" * 70 patterns end |
#discover_patterns(min_pattern_frequency: 2) ⇒ Object
Main entry point: Discover patterns in historical data
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 |
# File 'lib/sqa/strategy_generator.rb', line 168 def discover_patterns(min_pattern_frequency: 2) puts "=" * 70 puts "Strategy Generator: Discovering Profitable Patterns" puts "=" * 70 puts "Target gain: ≥#{min_gain_percent}%" puts "Target loss: ≤#{min_loss_percent}%" puts "FPOP (Future Period of Performance): #{fpop} days" puts "Inflection window: #{inflection_window} days" puts # Step 1: Find profitable inflection points find_profitable_points return [] if @profitable_points.empty? # Step 2: Calculate indicators at each profitable point analyze_indicator_states # Step 3: Mine patterns from indicator states mine_patterns(min_frequency: min_pattern_frequency) # Step 4: Calculate pattern statistics calculate_pattern_statistics @patterns end |
#export_patterns(filename) ⇒ Object
Export patterns to CSV
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/sqa/strategy_generator.rb', line 241 def export_patterns(filename) require 'csv' CSV.open(filename, 'w') do |csv| csv << ['Pattern', 'Frequency', 'Avg Gain %', 'Avg Holding Days', 'Success Rate %', 'Conditions'] @patterns.each_with_index do |pattern, i| conditions_str = pattern.conditions.map { |k, v| "#{k}=#{v}" }.join('; ') csv << [ i + 1, pattern.frequency, pattern.avg_gain.round(2), pattern.avg_holding_days.round(1), pattern.success_rate.round(2), conditions_str ] end end puts "Patterns exported to #{filename}" end |
#generate_strategies(top_n: 5, strategy_type: :class) ⇒ Object
Generate multiple strategies from top N patterns
214 215 216 217 218 |
# File 'lib/sqa/strategy_generator.rb', line 214 def generate_strategies(top_n: 5, strategy_type: :class) @patterns.take(top_n).map.with_index do |pattern, i| generate_strategy(pattern_index: i, strategy_type: strategy_type) end end |
#generate_strategy(pattern_index: 0, strategy_type: :proc) ⇒ Object
Generate a trading strategy from discovered patterns
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/sqa/strategy_generator.rb', line 196 def generate_strategy(pattern_index: 0, strategy_type: :proc) return nil if @patterns.empty? pattern = @patterns[pattern_index] case strategy_type when :proc generate_proc_strategy(pattern) when :class generate_class_strategy(pattern) when :kbs generate_kbs_strategy(pattern) else raise "Unknown strategy type: #{strategy_type}" end end |
#print_patterns(max_patterns: 10) ⇒ Object
Print discovered patterns
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/sqa/strategy_generator.rb', line 221 def print_patterns(max_patterns: 10) puts "\n" + "=" * 70 puts "Discovered Patterns (Top #{[max_patterns, @patterns.size].min})" puts "=" * 70 @patterns.take(max_patterns).each_with_index do |pattern, i| puts "\nPattern ##{i + 1}:" puts " Frequency: #{pattern.frequency} occurrences" puts " Average Gain: #{pattern.avg_gain.round(2)}%" puts " Average Holding: #{pattern.avg_holding_days.round(1)} days" puts " Success Rate: #{pattern.success_rate.round(2)}%" puts " Conditions:" pattern.conditions.each do |indicator, state| puts " - #{indicator}: #{state}" end end puts end |
#walk_forward_validate(train_size: 250, test_size: 60, step_size: 30) ⇒ Hash
Walk-forward validation - discover patterns with time-series cross-validation
Splits data into train/test windows and rolls forward through history to prevent overfitting. Only keeps patterns that work out-of-sample.
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/sqa/strategy_generator.rb', line 273 def walk_forward_validate(train_size: 250, test_size: 60, step_size: 30) puts "\n" + "=" * 70 puts "Walk-Forward Validation" puts "=" * 70 puts "Training window: #{train_size} days" puts "Testing window: #{test_size} days" puts "Step size: #{step_size} days" puts prices = @stock.df["adj_close_price"].to_a date_column = @stock.df.data.columns.include?("date") ? "date" : "timestamp" dates = @stock.df[date_column].to_a.map { |d| Date.parse(d.to_s) } validated_patterns = [] validation_results = [] start_idx = 0 iteration = 0 while start_idx + train_size + test_size < prices.size iteration += 1 train_start = start_idx train_end = start_idx + train_size test_start = train_end test_end = test_start + test_size puts "\nIteration #{iteration}:" puts " Train: #{dates[train_start]} to #{dates[train_end - 1]}" puts " Test: #{dates[test_start]} to #{dates[test_end - 1]}" # Create temporary stock with training data train_data = create_stock_subset(train_start, train_end) # Discover patterns on training data temp_generator = SQA::StrategyGenerator.new( stock: train_data, min_gain_percent: @min_gain_percent, fpop: @fpop, inflection_window: @inflection_window, max_fpl_risk: @max_fpl_risk, required_fpl_directions: @required_fpl_directions ) train_patterns = temp_generator.discover_patterns(min_pattern_frequency: 2) # Test each pattern on out-of-sample data test_data = create_stock_subset(test_start, test_end) train_patterns.each do |pattern| # Generate strategy from pattern strategy = temp_generator.generate_strategy( pattern_index: train_patterns.index(pattern), strategy_type: :proc ) # Backtest on test period begin backtest = SQA::Backtest.new(stock: test_data, strategy: strategy) results = backtest.run # Store validation result validation_results << { iteration: iteration, pattern: pattern, train_period: "#{dates[train_start]} to #{dates[train_end - 1]}", test_period: "#{dates[test_start]} to #{dates[test_end - 1]}", test_return: results.total_return, test_sharpe: results.sharpe_ratio, test_max_drawdown: results.max_drawdown } # Keep pattern if it performed well out-of-sample if results.total_return > 0 && results.sharpe_ratio > 0.5 validated_patterns << pattern end rescue => e puts " Warning: Pattern validation failed: #{e.}" end end start_idx += step_size end puts "\n" + "=" * 70 puts "Validation Complete" puts " Total iterations: #{iteration}" puts " Total patterns tested: #{validation_results.size}" puts " Patterns validated: #{validated_patterns.size}" puts "=" * 70 { validated_patterns: validated_patterns, validation_results: validation_results, total_iterations: iteration } end |