sqa 0.0.24 → 0.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.goose/memory/development.txt +3 -0
- data/.semver +6 -0
- data/ARCHITECTURE.md +648 -0
- data/CHANGELOG.md +82 -0
- data/CLAUDE.md +653 -0
- data/COMMITS.md +196 -0
- data/DATAFRAME_ARCHITECTURE_REVIEW.md +421 -0
- data/NEXT-STEPS.md +154 -0
- data/README.md +812 -262
- data/TASKS.md +358 -0
- data/TEST_RESULTS.md +140 -0
- data/TODO.md +42 -0
- data/_notes.txt +25 -0
- data/bin/sqa-console +11 -0
- data/data/talk_talk.json +103284 -0
- data/develop_summary.md +313 -0
- data/docs/advanced/backtesting.md +206 -0
- data/docs/advanced/ensemble.md +68 -0
- data/docs/advanced/fpop.md +153 -0
- data/docs/advanced/index.md +112 -0
- data/docs/advanced/multi-timeframe.md +67 -0
- data/docs/advanced/pattern-matcher.md +75 -0
- data/docs/advanced/portfolio-optimizer.md +79 -0
- data/docs/advanced/portfolio.md +166 -0
- data/docs/advanced/risk-management.md +210 -0
- data/docs/advanced/strategy-generator.md +158 -0
- data/docs/advanced/streaming.md +209 -0
- data/docs/ai_and_ml.md +80 -0
- data/docs/api/dataframe.md +1115 -0
- data/docs/api/index.md +126 -0
- data/docs/assets/css/custom.css +88 -0
- data/docs/assets/js/mathjax.js +18 -0
- data/docs/concepts/index.md +68 -0
- data/docs/contributing/index.md +60 -0
- data/docs/data-sources/index.md +66 -0
- data/docs/data_frame.md +317 -97
- data/docs/factors_that_impact_price.md +26 -0
- data/docs/finviz.md +11 -0
- data/docs/fx_pro_bit.md +25 -0
- data/docs/genetic_programming.md +104 -0
- data/docs/getting-started/index.md +123 -0
- data/docs/getting-started/installation.md +229 -0
- data/docs/getting-started/quick-start.md +244 -0
- data/docs/i_gotta_an_idea.md +22 -0
- data/docs/index.md +163 -0
- data/docs/indicators/index.md +97 -0
- data/docs/indicators.md +110 -24
- data/docs/options.md +8 -0
- data/docs/strategies/bollinger-bands.md +146 -0
- data/docs/strategies/consensus.md +64 -0
- data/docs/strategies/custom.md +310 -0
- data/docs/strategies/ema.md +53 -0
- data/docs/strategies/index.md +92 -0
- data/docs/strategies/kbs.md +164 -0
- data/docs/strategies/macd.md +96 -0
- data/docs/strategies/market-profile.md +54 -0
- data/docs/strategies/mean-reversion.md +58 -0
- data/docs/strategies/rsi.md +95 -0
- data/docs/strategies/sma.md +55 -0
- data/docs/strategies/stochastic.md +63 -0
- data/docs/strategies/volume-breakout.md +54 -0
- data/docs/tags.md +7 -0
- data/docs/true_strength_index.md +46 -0
- data/docs/weighted_moving_average.md +48 -0
- data/examples/README.md +354 -0
- data/examples/advanced_features_example.rb +350 -0
- data/examples/fpop_analysis_example.rb +191 -0
- data/examples/genetic_programming_example.rb +148 -0
- data/examples/kbs_strategy_example.rb +208 -0
- data/examples/pattern_context_example.rb +300 -0
- data/examples/rails_app/Gemfile +34 -0
- data/examples/rails_app/README.md +416 -0
- data/examples/rails_app/app/assets/javascripts/application.js +107 -0
- data/examples/rails_app/app/assets/stylesheets/application.css +659 -0
- data/examples/rails_app/app/controllers/analysis_controller.rb +11 -0
- data/examples/rails_app/app/controllers/api/v1/stocks_controller.rb +227 -0
- data/examples/rails_app/app/controllers/application_controller.rb +22 -0
- data/examples/rails_app/app/controllers/backtest_controller.rb +11 -0
- data/examples/rails_app/app/controllers/dashboard_controller.rb +21 -0
- data/examples/rails_app/app/controllers/portfolio_controller.rb +7 -0
- data/examples/rails_app/app/views/analysis/show.html.erb +209 -0
- data/examples/rails_app/app/views/backtest/show.html.erb +171 -0
- data/examples/rails_app/app/views/dashboard/index.html.erb +118 -0
- data/examples/rails_app/app/views/dashboard/show.html.erb +408 -0
- data/examples/rails_app/app/views/errors/show.html.erb +17 -0
- data/examples/rails_app/app/views/layouts/application.html.erb +60 -0
- data/examples/rails_app/app/views/portfolio/index.html.erb +33 -0
- data/examples/rails_app/bin/rails +6 -0
- data/examples/rails_app/config/application.rb +45 -0
- data/examples/rails_app/config/boot.rb +5 -0
- data/examples/rails_app/config/database.yml +18 -0
- data/examples/rails_app/config/environment.rb +11 -0
- data/examples/rails_app/config/routes.rb +26 -0
- data/examples/rails_app/config.ru +8 -0
- data/examples/realtime_stream_example.rb +274 -0
- data/examples/sinatra_app/Gemfile +22 -0
- data/examples/sinatra_app/QUICKSTART.md +159 -0
- data/examples/sinatra_app/README.md +461 -0
- data/examples/sinatra_app/app.rb +344 -0
- data/examples/sinatra_app/config.ru +5 -0
- data/examples/sinatra_app/public/css/style.css +659 -0
- data/examples/sinatra_app/public/js/app.js +107 -0
- data/examples/sinatra_app/views/analyze.erb +306 -0
- data/examples/sinatra_app/views/backtest.erb +325 -0
- data/examples/sinatra_app/views/dashboard.erb +419 -0
- data/examples/sinatra_app/views/error.erb +58 -0
- data/examples/sinatra_app/views/index.erb +118 -0
- data/examples/sinatra_app/views/layout.erb +61 -0
- data/examples/sinatra_app/views/portfolio.erb +43 -0
- data/examples/strategy_generator_example.rb +346 -0
- data/hsa_portfolio.csv +11 -0
- data/justfile +0 -0
- data/lib/api/alpha_vantage_api.rb +462 -0
- data/lib/sqa/backtest.rb +329 -0
- data/lib/sqa/data_frame/alpha_vantage.rb +43 -65
- data/lib/sqa/data_frame/data.rb +92 -0
- data/lib/sqa/data_frame/yahoo_finance.rb +35 -43
- data/lib/sqa/data_frame.rb +148 -243
- data/lib/sqa/ensemble.rb +359 -0
- data/lib/sqa/fpop.rb +199 -0
- data/lib/sqa/gp.rb +259 -0
- data/lib/sqa/indicator.rb +5 -8
- data/lib/sqa/init.rb +15 -8
- data/lib/sqa/market_regime.rb +240 -0
- data/lib/sqa/multi_timeframe.rb +379 -0
- data/lib/sqa/pattern_matcher.rb +497 -0
- data/lib/sqa/portfolio.rb +260 -6
- data/lib/sqa/portfolio_optimizer.rb +377 -0
- data/lib/sqa/risk_manager.rb +442 -0
- data/lib/sqa/seasonal_analyzer.rb +209 -0
- data/lib/sqa/sector_analyzer.rb +300 -0
- data/lib/sqa/stock.rb +67 -125
- data/lib/sqa/strategy/bollinger_bands.rb +42 -0
- data/lib/sqa/strategy/consensus.rb +5 -2
- data/lib/sqa/strategy/kbs_strategy.rb +470 -0
- data/lib/sqa/strategy/macd.rb +46 -0
- data/lib/sqa/strategy/mp.rb +1 -1
- data/lib/sqa/strategy/stochastic.rb +60 -0
- data/lib/sqa/strategy/volume_breakout.rb +57 -0
- data/lib/sqa/strategy.rb +5 -0
- data/lib/sqa/strategy_generator.rb +947 -0
- data/lib/sqa/stream.rb +361 -0
- data/lib/sqa/version.rb +1 -7
- data/lib/sqa.rb +23 -16
- data/main.just +81 -0
- data/mkdocs.yml +288 -0
- data/trace.log +0 -0
- metadata +261 -51
- data/bin/sqa +0 -6
- data/lib/patches/dry-cli.rb +0 -228
- data/lib/sqa/activity.rb +0 -10
- data/lib/sqa/cli.rb +0 -62
- data/lib/sqa/commands/analysis.rb +0 -309
- data/lib/sqa/commands/base.rb +0 -139
- data/lib/sqa/commands/web.rb +0 -199
- data/lib/sqa/commands.rb +0 -22
- data/lib/sqa/constants.rb +0 -23
- data/lib/sqa/indicator/average_true_range.rb +0 -33
- data/lib/sqa/indicator/bollinger_bands.rb +0 -28
- data/lib/sqa/indicator/candlestick_pattern_recognizer.rb +0 -60
- data/lib/sqa/indicator/donchian_channel.rb +0 -29
- data/lib/sqa/indicator/double_top_bottom_pattern.rb +0 -34
- data/lib/sqa/indicator/elliott_wave_theory.rb +0 -57
- data/lib/sqa/indicator/exponential_moving_average.rb +0 -25
- data/lib/sqa/indicator/exponential_moving_average_trend.rb +0 -36
- data/lib/sqa/indicator/fibonacci_retracement.rb +0 -23
- data/lib/sqa/indicator/head_and_shoulders_pattern.rb +0 -26
- data/lib/sqa/indicator/market_profile.rb +0 -32
- data/lib/sqa/indicator/mean_reversion.rb +0 -37
- data/lib/sqa/indicator/momentum.rb +0 -28
- data/lib/sqa/indicator/moving_average_convergence_divergence.rb +0 -29
- data/lib/sqa/indicator/peaks_and_valleys.rb +0 -29
- data/lib/sqa/indicator/predict_next_value.rb +0 -202
- data/lib/sqa/indicator/relative_strength_index.rb +0 -47
- data/lib/sqa/indicator/simple_moving_average.rb +0 -24
- data/lib/sqa/indicator/simple_moving_average_trend.rb +0 -32
- data/lib/sqa/indicator/stochastic_oscillator.rb +0 -68
- data/lib/sqa/indicator/true_range.rb +0 -39
- data/lib/sqa/trade.rb +0 -26
data/lib/sqa/ensemble.rb
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# lib/sqa/ensemble.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SQA
|
|
5
|
+
##
|
|
6
|
+
# Ensemble - Combine multiple trading strategies
|
|
7
|
+
#
|
|
8
|
+
# Provides methods for:
|
|
9
|
+
# - Majority voting
|
|
10
|
+
# - Weighted voting based on past performance
|
|
11
|
+
# - Meta-learning (strategy selection)
|
|
12
|
+
# - Strategy rotation based on market conditions
|
|
13
|
+
# - Confidence-based aggregation
|
|
14
|
+
#
|
|
15
|
+
# @example Simple majority voting
|
|
16
|
+
# ensemble = SQA::Ensemble.new(
|
|
17
|
+
# strategies: [SQA::Strategy::RSI, SQA::Strategy::MACD, SQA::Strategy::Bollinger]
|
|
18
|
+
# )
|
|
19
|
+
# signal = ensemble.vote(vector)
|
|
20
|
+
# # => :buy (if 2 out of 3 say :buy)
|
|
21
|
+
#
|
|
22
|
+
class Ensemble
|
|
23
|
+
attr_accessor :strategies, :weights, :performance_history
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# Initialize ensemble
|
|
27
|
+
#
|
|
28
|
+
# @param strategies [Array<Class>] Array of strategy classes
|
|
29
|
+
# @param voting_method [Symbol] :majority, :weighted, :unanimous, :confidence
|
|
30
|
+
# @param weights [Array<Float>] Optional weights for weighted voting
|
|
31
|
+
#
|
|
32
|
+
def initialize(strategies:, voting_method: :majority, weights: nil)
|
|
33
|
+
@strategies = strategies
|
|
34
|
+
@voting_method = voting_method
|
|
35
|
+
@weights = weights || Array.new(strategies.size, 1.0 / strategies.size)
|
|
36
|
+
@performance_history = Hash.new { |h, k| h[k] = [] }
|
|
37
|
+
@confidence_scores = Hash.new(0.5)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# Generate ensemble signal
|
|
42
|
+
#
|
|
43
|
+
# @param vector [OpenStruct] Market data vector
|
|
44
|
+
# @return [Symbol] :buy, :sell, or :hold
|
|
45
|
+
#
|
|
46
|
+
def signal(vector)
|
|
47
|
+
case @voting_method
|
|
48
|
+
when :majority
|
|
49
|
+
majority_vote(vector)
|
|
50
|
+
when :weighted
|
|
51
|
+
weighted_vote(vector)
|
|
52
|
+
when :unanimous
|
|
53
|
+
unanimous_vote(vector)
|
|
54
|
+
when :confidence
|
|
55
|
+
confidence_vote(vector)
|
|
56
|
+
else
|
|
57
|
+
majority_vote(vector)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# Majority voting
|
|
63
|
+
#
|
|
64
|
+
# @param vector [OpenStruct] Market data
|
|
65
|
+
# @return [Symbol] Signal with most votes
|
|
66
|
+
#
|
|
67
|
+
def majority_vote(vector)
|
|
68
|
+
votes = collect_votes(vector)
|
|
69
|
+
|
|
70
|
+
# Count votes
|
|
71
|
+
vote_counts = { buy: 0, sell: 0, hold: 0 }
|
|
72
|
+
votes.each { |v| vote_counts[v] += 1 }
|
|
73
|
+
|
|
74
|
+
# Return signal with most votes
|
|
75
|
+
vote_counts.max_by { |_signal, count| count }.first
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
# Weighted voting based on strategy performance
|
|
80
|
+
#
|
|
81
|
+
# @param vector [OpenStruct] Market data
|
|
82
|
+
# @return [Symbol] Weighted signal
|
|
83
|
+
#
|
|
84
|
+
def weighted_vote(vector)
|
|
85
|
+
votes = collect_votes(vector)
|
|
86
|
+
|
|
87
|
+
# Weighted scores
|
|
88
|
+
scores = { buy: 0.0, sell: 0.0, hold: 0.0 }
|
|
89
|
+
|
|
90
|
+
votes.each_with_index do |vote, idx|
|
|
91
|
+
scores[vote] += @weights[idx]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Return signal with highest weighted score
|
|
95
|
+
scores.max_by { |_signal, score| score }.first
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
# Unanimous voting (all strategies must agree)
|
|
100
|
+
#
|
|
101
|
+
# @param vector [OpenStruct] Market data
|
|
102
|
+
# @return [Symbol] :buy/:sell only if unanimous, otherwise :hold
|
|
103
|
+
#
|
|
104
|
+
def unanimous_vote(vector)
|
|
105
|
+
votes = collect_votes(vector)
|
|
106
|
+
|
|
107
|
+
# All must agree
|
|
108
|
+
if votes.all? { |v| v == :buy }
|
|
109
|
+
:buy
|
|
110
|
+
elsif votes.all? { |v| v == :sell }
|
|
111
|
+
:sell
|
|
112
|
+
else
|
|
113
|
+
:hold
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
# Confidence-based voting
|
|
119
|
+
#
|
|
120
|
+
# Weight votes by strategy confidence scores.
|
|
121
|
+
#
|
|
122
|
+
# @param vector [OpenStruct] Market data
|
|
123
|
+
# @return [Symbol] Signal weighted by confidence
|
|
124
|
+
#
|
|
125
|
+
def confidence_vote(vector)
|
|
126
|
+
votes = collect_votes(vector)
|
|
127
|
+
|
|
128
|
+
scores = { buy: 0.0, sell: 0.0, hold: 0.0 }
|
|
129
|
+
|
|
130
|
+
votes.each_with_index do |vote, idx|
|
|
131
|
+
strategy_class = @strategies[idx]
|
|
132
|
+
confidence = @confidence_scores[strategy_class] || 0.5
|
|
133
|
+
scores[vote] += confidence
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
scores.max_by { |_signal, score| score }.first
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
##
|
|
140
|
+
# Update strategy weights based on performance
|
|
141
|
+
#
|
|
142
|
+
# Adjust weights to favor better-performing strategies.
|
|
143
|
+
#
|
|
144
|
+
# @param strategy_index [Integer] Index of strategy
|
|
145
|
+
# @param performance [Float] Performance metric (e.g., return)
|
|
146
|
+
#
|
|
147
|
+
def update_weight(strategy_index, performance)
|
|
148
|
+
@performance_history[@strategies[strategy_index]] << performance
|
|
149
|
+
|
|
150
|
+
# Recalculate weights based on recent performance
|
|
151
|
+
recalculate_weights
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
##
|
|
155
|
+
# Update confidence score for strategy
|
|
156
|
+
#
|
|
157
|
+
# @param strategy_class [Class] Strategy class
|
|
158
|
+
# @param correct [Boolean] Was the prediction correct?
|
|
159
|
+
#
|
|
160
|
+
def update_confidence(strategy_class, correct)
|
|
161
|
+
current = @confidence_scores[strategy_class]
|
|
162
|
+
|
|
163
|
+
# Exponential moving average of correctness
|
|
164
|
+
alpha = 0.1
|
|
165
|
+
@confidence_scores[strategy_class] = if correct
|
|
166
|
+
current + alpha * (1.0 - current)
|
|
167
|
+
else
|
|
168
|
+
current - alpha * current
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
##
|
|
173
|
+
# Select best strategy for current market conditions
|
|
174
|
+
#
|
|
175
|
+
# Meta-learning approach: choose the strategy most likely to succeed.
|
|
176
|
+
#
|
|
177
|
+
# @param market_regime [Symbol] Current market regime (:bull, :bear, :sideways)
|
|
178
|
+
# @param volatility [Symbol] Volatility regime (:low, :medium, :high)
|
|
179
|
+
# @return [Class] Best strategy class for conditions
|
|
180
|
+
#
|
|
181
|
+
def select_strategy(market_regime:, volatility: :medium)
|
|
182
|
+
# Strategy performance by market condition
|
|
183
|
+
# This could be learned from historical data
|
|
184
|
+
strategy_preferences = {
|
|
185
|
+
bull: {
|
|
186
|
+
low: SQA::Strategy::EMA,
|
|
187
|
+
medium: SQA::Strategy::MACD,
|
|
188
|
+
high: SQA::Strategy::Bollinger
|
|
189
|
+
},
|
|
190
|
+
bear: {
|
|
191
|
+
low: SQA::Strategy::RSI,
|
|
192
|
+
medium: SQA::Strategy::RSI,
|
|
193
|
+
high: SQA::Strategy::Bollinger
|
|
194
|
+
},
|
|
195
|
+
sideways: {
|
|
196
|
+
low: SQA::Strategy::MR,
|
|
197
|
+
medium: SQA::Strategy::MR,
|
|
198
|
+
high: SQA::Strategy::Bollinger
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Return preferred strategy or fall back to best performer
|
|
203
|
+
strategy_preferences.dig(market_regime, volatility) ||
|
|
204
|
+
best_performing_strategy
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
##
|
|
208
|
+
# Rotate strategies based on market conditions
|
|
209
|
+
#
|
|
210
|
+
# @param stock [SQA::Stock] Stock object
|
|
211
|
+
# @return [Class] Strategy to use
|
|
212
|
+
#
|
|
213
|
+
def rotate(stock)
|
|
214
|
+
regime_data = SQA::MarketRegime.detect(stock)
|
|
215
|
+
|
|
216
|
+
select_strategy(
|
|
217
|
+
market_regime: regime_data[:type],
|
|
218
|
+
volatility: regime_data[:volatility]
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
##
|
|
223
|
+
# Get ensemble statistics
|
|
224
|
+
#
|
|
225
|
+
# @return [Hash] Performance statistics
|
|
226
|
+
#
|
|
227
|
+
def statistics
|
|
228
|
+
{
|
|
229
|
+
num_strategies: @strategies.size,
|
|
230
|
+
weights: @weights,
|
|
231
|
+
confidence_scores: @confidence_scores,
|
|
232
|
+
best_strategy: best_performing_strategy,
|
|
233
|
+
worst_strategy: worst_performing_strategy,
|
|
234
|
+
performance_history: @performance_history
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
##
|
|
239
|
+
# Backtest ensemble vs individual strategies
|
|
240
|
+
#
|
|
241
|
+
# @param stock [SQA::Stock] Stock to backtest
|
|
242
|
+
# @param initial_capital [Float] Starting capital
|
|
243
|
+
# @return [Hash] Comparison results
|
|
244
|
+
#
|
|
245
|
+
def backtest_comparison(stock, initial_capital: 10_000)
|
|
246
|
+
results = {}
|
|
247
|
+
|
|
248
|
+
# Backtest ensemble
|
|
249
|
+
ensemble_backtest = SQA::Backtest.new(
|
|
250
|
+
stock: stock,
|
|
251
|
+
strategy: self,
|
|
252
|
+
initial_capital: initial_capital
|
|
253
|
+
)
|
|
254
|
+
results[:ensemble] = ensemble_backtest.run
|
|
255
|
+
|
|
256
|
+
# Backtest each individual strategy
|
|
257
|
+
@strategies.each do |strategy_class|
|
|
258
|
+
individual_backtest = SQA::Backtest.new(
|
|
259
|
+
stock: stock,
|
|
260
|
+
strategy: strategy_class,
|
|
261
|
+
initial_capital: initial_capital
|
|
262
|
+
)
|
|
263
|
+
results[strategy_class.name] = individual_backtest.run
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
results
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
##
|
|
270
|
+
# Make ensemble compatible with Backtest (acts like a strategy)
|
|
271
|
+
#
|
|
272
|
+
# @param vector [OpenStruct] Market data
|
|
273
|
+
# @return [Symbol] Trading signal
|
|
274
|
+
#
|
|
275
|
+
def self.trade(vector)
|
|
276
|
+
# This won't work for class method, use instance instead
|
|
277
|
+
raise NotImplementedError, "Use ensemble instance, not class"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
##
|
|
281
|
+
# Instance method for compatibility
|
|
282
|
+
#
|
|
283
|
+
def trade(vector)
|
|
284
|
+
signal(vector)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
private
|
|
288
|
+
|
|
289
|
+
##
|
|
290
|
+
# Collect votes from all strategies
|
|
291
|
+
#
|
|
292
|
+
def collect_votes(vector)
|
|
293
|
+
@strategies.map do |strategy_class|
|
|
294
|
+
begin
|
|
295
|
+
strategy_class.trade(vector)
|
|
296
|
+
rescue StandardError => e
|
|
297
|
+
# If strategy fails, default to :hold
|
|
298
|
+
:hold
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
##
|
|
304
|
+
# Recalculate weights based on performance history
|
|
305
|
+
#
|
|
306
|
+
def recalculate_weights
|
|
307
|
+
# Use recent performance (last 10 trades)
|
|
308
|
+
recent_performance = @strategies.map do |strategy_class|
|
|
309
|
+
history = @performance_history[strategy_class]
|
|
310
|
+
recent = history.last(10)
|
|
311
|
+
recent.empty? ? 0.0 : recent.sum / recent.size.to_f
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Convert to positive values (shift if negative)
|
|
315
|
+
min_perf = recent_performance.min
|
|
316
|
+
if min_perf < 0
|
|
317
|
+
recent_performance = recent_performance.map { |p| p - min_perf + 0.01 }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Normalize to sum to 1.0
|
|
321
|
+
total = recent_performance.sum
|
|
322
|
+
@weights = if total.zero?
|
|
323
|
+
Array.new(@strategies.size, 1.0 / @strategies.size)
|
|
324
|
+
else
|
|
325
|
+
recent_performance.map { |p| p / total }
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
##
|
|
330
|
+
# Find best performing strategy
|
|
331
|
+
#
|
|
332
|
+
def best_performing_strategy
|
|
333
|
+
return @strategies.first if @performance_history.empty?
|
|
334
|
+
|
|
335
|
+
avg_performance = @strategies.map do |strategy_class|
|
|
336
|
+
history = @performance_history[strategy_class]
|
|
337
|
+
avg = history.empty? ? 0.0 : history.sum / history.size.to_f
|
|
338
|
+
[strategy_class, avg]
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
avg_performance.max_by { |_strategy, avg| avg }&.first || @strategies.first
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
##
|
|
345
|
+
# Find worst performing strategy
|
|
346
|
+
#
|
|
347
|
+
def worst_performing_strategy
|
|
348
|
+
return @strategies.first if @performance_history.empty?
|
|
349
|
+
|
|
350
|
+
avg_performance = @strategies.map do |strategy_class|
|
|
351
|
+
history = @performance_history[strategy_class]
|
|
352
|
+
avg = history.empty? ? 0.0 : history.sum / history.size.to_f
|
|
353
|
+
[strategy_class, avg]
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
avg_performance.min_by { |_strategy, avg| avg }&.first || @strategies.first
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
data/lib/sqa/fpop.rb
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SQA::FPOP - Future Period of Performance Analysis
|
|
4
|
+
#
|
|
5
|
+
# This module provides utilities for analyzing potential future price movements
|
|
6
|
+
# from any given point in a price series. It calculates the range of possible
|
|
7
|
+
# outcomes (min/max deltas) during a future period, along with risk metrics
|
|
8
|
+
# and directional classification.
|
|
9
|
+
#
|
|
10
|
+
# Key Concepts:
|
|
11
|
+
# - FPL (Future Period Loss/Profit): Min and max percentage change during fpop
|
|
12
|
+
# - Risk: Volatility range (max_delta - min_delta)
|
|
13
|
+
# - Direction: Classification of movement (UP/DOWN/UNCERTAIN/FLAT)
|
|
14
|
+
# - Magnitude: Average expected movement
|
|
15
|
+
#
|
|
16
|
+
# Example:
|
|
17
|
+
# prices = [100, 102, 98, 105, 110, 115, 120]
|
|
18
|
+
#
|
|
19
|
+
# # Get raw FPL data
|
|
20
|
+
# fpl_data = SQA::FPOP.fpl(prices, fpop: 3)
|
|
21
|
+
# # => [[-2.0, 10.0], [-4.9, 17.6], ...]
|
|
22
|
+
#
|
|
23
|
+
# # Get detailed analysis
|
|
24
|
+
# analysis = SQA::FPOP.fpl_analysis(prices, fpop: 3)
|
|
25
|
+
# # => [{min_delta: -2.0, max_delta: 10.0, risk: 12.0, direction: :UNCERTAIN, ...}, ...]
|
|
26
|
+
|
|
27
|
+
module SQA
|
|
28
|
+
module FPOP
|
|
29
|
+
class << self
|
|
30
|
+
# Calculate Future Period Loss/Profit for each point in price series
|
|
31
|
+
#
|
|
32
|
+
# For each price point, looks ahead fpop periods and calculates:
|
|
33
|
+
# - Minimum percentage change (worst loss)
|
|
34
|
+
# - Maximum percentage change (best gain)
|
|
35
|
+
#
|
|
36
|
+
# @param price [Array<Numeric>] Array of prices
|
|
37
|
+
# @param fpop [Integer] Future Period of Performance (days to look ahead)
|
|
38
|
+
# @return [Array<Array<Float, Float>>] Array of [min_delta, max_delta] pairs
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# SQA::FPOP.fpl([100, 105, 95, 110], fpop: 2)
|
|
42
|
+
# # => [[-5.0, 5.0], [-9.52, 4.76], [15.79, 15.79]]
|
|
43
|
+
#
|
|
44
|
+
def fpl(price, fpop: 14)
|
|
45
|
+
validate_fpl_inputs(price, fpop)
|
|
46
|
+
|
|
47
|
+
price_floats = price.map(&:to_f)
|
|
48
|
+
result = []
|
|
49
|
+
|
|
50
|
+
price_floats.each_with_index do |current_price, index|
|
|
51
|
+
future_prices = price_floats[(index + 1)..(index + fpop)]
|
|
52
|
+
break if future_prices.nil? || future_prices.empty?
|
|
53
|
+
|
|
54
|
+
deltas = future_prices.map { |p| ((p - current_price) / current_price) * 100.0 }
|
|
55
|
+
result << [deltas.min, deltas.max]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Perform comprehensive FPL analysis with risk metrics and classification
|
|
62
|
+
#
|
|
63
|
+
# @param price [Array<Numeric>] Array of prices
|
|
64
|
+
# @param fpop [Integer] Future Period of Performance
|
|
65
|
+
# @return [Array<Hash>] Array of analysis hashes containing:
|
|
66
|
+
# - :min_delta - Worst percentage loss during fpop
|
|
67
|
+
# - :max_delta - Best percentage gain during fpop
|
|
68
|
+
# - :risk - Volatility range (max - min)
|
|
69
|
+
# - :direction - Movement classification (:UP, :DOWN, :UNCERTAIN, :FLAT)
|
|
70
|
+
# - :magnitude - Average expected movement
|
|
71
|
+
# - :interpretation - Human-readable summary
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# analysis = SQA::FPOP.fpl_analysis([100, 110, 120], fpop: 2)
|
|
75
|
+
# analysis.first
|
|
76
|
+
# # => {
|
|
77
|
+
# # min_delta: 10.0,
|
|
78
|
+
# # max_delta: 20.0,
|
|
79
|
+
# # risk: 10.0,
|
|
80
|
+
# # direction: :UP,
|
|
81
|
+
# # magnitude: 15.0,
|
|
82
|
+
# # interpretation: "UP: 15.0% (±5.0% risk)"
|
|
83
|
+
# # }
|
|
84
|
+
#
|
|
85
|
+
def fpl_analysis(price, fpop: 14)
|
|
86
|
+
validate_fpl_inputs(price, fpop)
|
|
87
|
+
|
|
88
|
+
fpl_results = fpl(price, fpop: fpop)
|
|
89
|
+
|
|
90
|
+
fpl_results.map do |min_delta, max_delta|
|
|
91
|
+
{
|
|
92
|
+
min_delta: min_delta,
|
|
93
|
+
max_delta: max_delta,
|
|
94
|
+
risk: (max_delta - min_delta).abs,
|
|
95
|
+
direction: determine_direction(min_delta, max_delta),
|
|
96
|
+
magnitude: calculate_magnitude(min_delta, max_delta),
|
|
97
|
+
interpretation: build_interpretation(min_delta, max_delta)
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Determine directional bias from min/max deltas
|
|
103
|
+
#
|
|
104
|
+
# @param min_delta [Float] Minimum percentage change
|
|
105
|
+
# @param max_delta [Float] Maximum percentage change
|
|
106
|
+
# @return [Symbol] :UP, :DOWN, :UNCERTAIN, or :FLAT
|
|
107
|
+
#
|
|
108
|
+
def determine_direction(min_delta, max_delta)
|
|
109
|
+
if min_delta > 0 && max_delta > 0
|
|
110
|
+
:UP
|
|
111
|
+
elsif min_delta < 0 && max_delta < 0
|
|
112
|
+
:DOWN
|
|
113
|
+
elsif min_delta < 0 && max_delta > 0
|
|
114
|
+
:UNCERTAIN
|
|
115
|
+
else
|
|
116
|
+
:FLAT
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Calculate average expected movement (magnitude)
|
|
121
|
+
#
|
|
122
|
+
# @param min_delta [Float] Minimum percentage change
|
|
123
|
+
# @param max_delta [Float] Maximum percentage change
|
|
124
|
+
# @return [Float] Average of min and max deltas
|
|
125
|
+
#
|
|
126
|
+
def calculate_magnitude(min_delta, max_delta)
|
|
127
|
+
(min_delta + max_delta) / 2.0
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Build human-readable interpretation string
|
|
131
|
+
#
|
|
132
|
+
# @param min_delta [Float] Minimum percentage change
|
|
133
|
+
# @param max_delta [Float] Maximum percentage change
|
|
134
|
+
# @return [String] Formatted interpretation
|
|
135
|
+
#
|
|
136
|
+
def build_interpretation(min_delta, max_delta)
|
|
137
|
+
direction = determine_direction(min_delta, max_delta)
|
|
138
|
+
magnitude = calculate_magnitude(min_delta, max_delta)
|
|
139
|
+
risk = (max_delta - min_delta).abs
|
|
140
|
+
|
|
141
|
+
"#{direction}: #{magnitude.round(2)}% (±#{(risk / 2).round(2)}% risk)"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Filter FPL analysis results by criteria
|
|
145
|
+
#
|
|
146
|
+
# Useful for finding high-quality trading opportunities
|
|
147
|
+
#
|
|
148
|
+
# @param analysis [Array<Hash>] FPL analysis results
|
|
149
|
+
# @param min_magnitude [Float] Minimum average movement (default: nil)
|
|
150
|
+
# @param max_risk [Float] Maximum acceptable risk (default: nil)
|
|
151
|
+
# @param directions [Array<Symbol>] Acceptable directions (default: [:UP, :DOWN, :UNCERTAIN, :FLAT])
|
|
152
|
+
# @return [Array<Integer>] Indices of points that meet criteria
|
|
153
|
+
#
|
|
154
|
+
# @example Find low-risk bullish opportunities
|
|
155
|
+
# analysis = SQA::FPOP.fpl_analysis(prices, fpop: 10)
|
|
156
|
+
# indices = SQA::FPOP.filter_by_quality(
|
|
157
|
+
# analysis,
|
|
158
|
+
# min_magnitude: 5.0,
|
|
159
|
+
# max_risk: 10.0,
|
|
160
|
+
# directions: [:UP]
|
|
161
|
+
# )
|
|
162
|
+
#
|
|
163
|
+
def filter_by_quality(analysis, min_magnitude: nil, max_risk: nil, directions: [:UP, :DOWN, :UNCERTAIN, :FLAT])
|
|
164
|
+
indices = []
|
|
165
|
+
|
|
166
|
+
analysis.each_with_index do |result, idx|
|
|
167
|
+
next if min_magnitude && result[:magnitude] < min_magnitude
|
|
168
|
+
next if max_risk && result[:risk] > max_risk
|
|
169
|
+
next unless directions.include?(result[:direction])
|
|
170
|
+
|
|
171
|
+
indices << idx
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
indices
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Calculate risk-reward ratio for each analysis point
|
|
178
|
+
#
|
|
179
|
+
# @param analysis [Array<Hash>] FPL analysis results
|
|
180
|
+
# @return [Array<Float>] Risk-reward ratios (magnitude / risk)
|
|
181
|
+
#
|
|
182
|
+
def risk_reward_ratios(analysis)
|
|
183
|
+
analysis.map do |result|
|
|
184
|
+
result[:risk] > 0 ? result[:magnitude].abs / result[:risk] : 0.0
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
# Validate FPL input parameters
|
|
191
|
+
def validate_fpl_inputs(price, fpop)
|
|
192
|
+
raise ArgumentError, "price must be an Array" unless price.is_a?(Array)
|
|
193
|
+
raise ArgumentError, "price cannot be empty" if price.empty?
|
|
194
|
+
raise ArgumentError, "fpop must be a positive integer" unless fpop.is_a?(Integer) && fpop > 0
|
|
195
|
+
raise ArgumentError, "prices must not contain zero or negative values" if price.any? { |p| p <= 0 }
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|