Class: SQA::Ensemble

Inherits:
Object
  • Object
show all
Defined in:
lib/sqa/ensemble.rb

Overview

Ensemble - Combine multiple trading strategies

Provides methods for:

  • Majority voting

  • Weighted voting based on past performance

  • Meta-learning (strategy selection)

  • Strategy rotation based on market conditions

  • Confidence-based aggregation

Examples:

Simple majority voting

ensemble = SQA::Ensemble.new(
  strategies: [SQA::Strategy::RSI, SQA::Strategy::MACD, SQA::Strategy::Bollinger]
)
signal = ensemble.vote(vector)
# => :buy (if 2 out of 3 say :buy)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(strategies:, voting_method: :majority, weights: nil) ⇒ Ensemble

Initialize ensemble

Parameters:

  • strategies (Array<Class>)

    Array of strategy classes

  • voting_method (Symbol) (defaults to: :majority)

    :majority, :weighted, :unanimous, :confidence

  • weights (Array<Float>) (defaults to: nil)

    Optional weights for weighted voting



32
33
34
35
36
37
38
# File 'lib/sqa/ensemble.rb', line 32

def initialize(strategies:, voting_method: :majority, weights: nil)
  @strategies = strategies
  @voting_method = voting_method
  @weights = weights || Array.new(strategies.size, 1.0 / strategies.size)
  @performance_history = Hash.new { |h, k| h[k] = [] }
  @confidence_scores = Hash.new(0.5)
end

Instance Attribute Details

#performance_historyObject

Returns the value of attribute performance_history.



23
24
25
# File 'lib/sqa/ensemble.rb', line 23

def performance_history
  @performance_history
end

#strategiesObject

Returns the value of attribute strategies.



23
24
25
# File 'lib/sqa/ensemble.rb', line 23

def strategies
  @strategies
end

#weightsObject

Returns the value of attribute weights.



23
24
25
# File 'lib/sqa/ensemble.rb', line 23

def weights
  @weights
end

Class Method Details

.trade(vector) ⇒ Symbol

Make ensemble compatible with Backtest (acts like a strategy)

Parameters:

  • vector (OpenStruct)

    Market data

Returns:

  • (Symbol)

    Trading signal

Raises:

  • (NotImplementedError)


275
276
277
278
# File 'lib/sqa/ensemble.rb', line 275

def self.trade(vector)
  # This won't work for class method, use instance instead
  raise NotImplementedError, "Use ensemble instance, not class"
end

Instance Method Details

#backtest_comparison(stock, initial_capital: 10_000) ⇒ Hash

Backtest ensemble vs individual strategies

Parameters:

  • stock (SQA::Stock)

    Stock to backtest

  • initial_capital (Float) (defaults to: 10_000)

    Starting capital

Returns:

  • (Hash)

    Comparison results



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/sqa/ensemble.rb', line 245

def backtest_comparison(stock, initial_capital: 10_000)
  results = {}

  # Backtest ensemble
  ensemble_backtest = SQA::Backtest.new(
    stock: stock,
    strategy: self,
    initial_capital: initial_capital
  )
  results[:ensemble] = ensemble_backtest.run

  # Backtest each individual strategy
  @strategies.each do |strategy_class|
    individual_backtest = SQA::Backtest.new(
      stock: stock,
      strategy: strategy_class,
      initial_capital: initial_capital
    )
    results[strategy_class.name] = individual_backtest.run
  end

  results
end

#confidence_vote(vector) ⇒ Symbol

Confidence-based voting

Weight votes by strategy confidence scores.

Parameters:

  • vector (OpenStruct)

    Market data

Returns:

  • (Symbol)

    Signal weighted by confidence



125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/sqa/ensemble.rb', line 125

def confidence_vote(vector)
  votes = collect_votes(vector)

  scores = { buy: 0.0, sell: 0.0, hold: 0.0 }

  votes.each_with_index do |vote, idx|
    strategy_class = @strategies[idx]
    confidence = @confidence_scores[strategy_class] || 0.5
    scores[vote] += confidence
  end

  scores.max_by { |_signal, score| score }.first
end

#majority_vote(vector) ⇒ Symbol

Majority voting

Parameters:

  • vector (OpenStruct)

    Market data

Returns:

  • (Symbol)

    Signal with most votes



67
68
69
70
71
72
73
74
75
76
# File 'lib/sqa/ensemble.rb', line 67

def majority_vote(vector)
  votes = collect_votes(vector)

  # Count votes
  vote_counts = { buy: 0, sell: 0, hold: 0 }
  votes.each { |v| vote_counts[v] += 1 }

  # Return signal with most votes
  vote_counts.max_by { |_signal, count| count }.first
end

#rotate(stock) ⇒ Class

Rotate strategies based on market conditions

Parameters:

Returns:

  • (Class)

    Strategy to use



213
214
215
216
217
218
219
220
# File 'lib/sqa/ensemble.rb', line 213

def rotate(stock)
  regime_data = SQA::MarketRegime.detect(stock)

  select_strategy(
    market_regime: regime_data[:type],
    volatility: regime_data[:volatility]
  )
end

#select_strategy(market_regime:, volatility: :medium) ⇒ Class

Select best strategy for current market conditions

Meta-learning approach: choose the strategy most likely to succeed.

Parameters:

  • market_regime (Symbol)

    Current market regime (:bull, :bear, :sideways)

  • volatility (Symbol) (defaults to: :medium)

    Volatility regime (:low, :medium, :high)

Returns:

  • (Class)

    Best strategy class for conditions



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
# File 'lib/sqa/ensemble.rb', line 181

def select_strategy(market_regime:, volatility: :medium)
  # Strategy performance by market condition
  # This could be learned from historical data
  strategy_preferences = {
    bull: {
      low: SQA::Strategy::EMA,
      medium: SQA::Strategy::MACD,
      high: SQA::Strategy::Bollinger
    },
    bear: {
      low: SQA::Strategy::RSI,
      medium: SQA::Strategy::RSI,
      high: SQA::Strategy::Bollinger
    },
    sideways: {
      low: SQA::Strategy::MR,
      medium: SQA::Strategy::MR,
      high: SQA::Strategy::Bollinger
    }
  }

  # Return preferred strategy or fall back to best performer
  strategy_preferences.dig(market_regime, volatility) ||
    best_performing_strategy
end

#signal(vector) ⇒ Symbol

Generate ensemble signal

Parameters:

  • vector (OpenStruct)

    Market data vector

Returns:

  • (Symbol)

    :buy, :sell, or :hold



46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/sqa/ensemble.rb', line 46

def signal(vector)
  case @voting_method
  when :majority
    majority_vote(vector)
  when :weighted
    weighted_vote(vector)
  when :unanimous
    unanimous_vote(vector)
  when :confidence
    confidence_vote(vector)
  else
    majority_vote(vector)
  end
end

#statisticsHash

Get ensemble statistics

Returns:

  • (Hash)

    Performance statistics



227
228
229
230
231
232
233
234
235
236
# File 'lib/sqa/ensemble.rb', line 227

def statistics
  {
    num_strategies: @strategies.size,
    weights: @weights,
    confidence_scores: @confidence_scores,
    best_strategy: best_performing_strategy,
    worst_strategy: worst_performing_strategy,
    performance_history: @performance_history
  }
end

#trade(vector) ⇒ Object

Instance method for compatibility



283
284
285
# File 'lib/sqa/ensemble.rb', line 283

def trade(vector)
  signal(vector)
end

#unanimous_vote(vector) ⇒ Symbol

Unanimous voting (all strategies must agree)

Parameters:

  • vector (OpenStruct)

    Market data

Returns:

  • (Symbol)

    :buy/:sell only if unanimous, otherwise :hold



104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/sqa/ensemble.rb', line 104

def unanimous_vote(vector)
  votes = collect_votes(vector)

  # All must agree
  if votes.all? { |v| v == :buy }
    :buy
  elsif votes.all? { |v| v == :sell }
    :sell
  else
    :hold
  end
end

#update_confidence(strategy_class, correct) ⇒ Object

Update confidence score for strategy

Parameters:

  • strategy_class (Class)

    Strategy class

  • correct (Boolean)

    Was the prediction correct?



160
161
162
163
164
165
166
167
168
169
170
# File 'lib/sqa/ensemble.rb', line 160

def update_confidence(strategy_class, correct)
  current = @confidence_scores[strategy_class]

  # Exponential moving average of correctness
  alpha = 0.1
  @confidence_scores[strategy_class] = if correct
                                         current + alpha * (1.0 - current)
                                       else
                                         current - alpha * current
                                       end
end

#update_weight(strategy_index, performance) ⇒ Object

Update strategy weights based on performance

Adjust weights to favor better-performing strategies.

Parameters:

  • strategy_index (Integer)

    Index of strategy

  • performance (Float)

    Performance metric (e.g., return)



147
148
149
150
151
152
# File 'lib/sqa/ensemble.rb', line 147

def update_weight(strategy_index, performance)
  @performance_history[@strategies[strategy_index]] << performance

  # Recalculate weights based on recent performance
  recalculate_weights
end

#weighted_vote(vector) ⇒ Symbol

Weighted voting based on strategy performance

Parameters:

  • vector (OpenStruct)

    Market data

Returns:

  • (Symbol)

    Weighted signal



84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/sqa/ensemble.rb', line 84

def weighted_vote(vector)
  votes = collect_votes(vector)

  # Weighted scores
  scores = { buy: 0.0, sell: 0.0, hold: 0.0 }

  votes.each_with_index do |vote, idx|
    scores[vote] += @weights[idx]
  end

  # Return signal with highest weighted score
  scores.max_by { |_signal, score| score }.first
end