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
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# lib/sqa/multi_timeframe.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SQA
|
|
5
|
+
##
|
|
6
|
+
# MultiTimeframe - Analyze patterns across multiple timeframes
|
|
7
|
+
#
|
|
8
|
+
# Provides methods for:
|
|
9
|
+
# - Timeframe conversion (daily → weekly → monthly)
|
|
10
|
+
# - Multi-timeframe signal confirmation
|
|
11
|
+
# - Trend alignment across timeframes
|
|
12
|
+
# - Support/resistance across timeframes
|
|
13
|
+
#
|
|
14
|
+
# Common timeframe strategy:
|
|
15
|
+
# - Use higher timeframe for trend direction
|
|
16
|
+
# - Use lower timeframe for entry timing
|
|
17
|
+
#
|
|
18
|
+
# @example Multi-timeframe trend alignment
|
|
19
|
+
# mta = SQA::MultiTimeframe.new(stock: stock)
|
|
20
|
+
# alignment = mta.trend_alignment
|
|
21
|
+
# # => { daily: :up, weekly: :up, monthly: :up, aligned: true }
|
|
22
|
+
#
|
|
23
|
+
class MultiTimeframe
|
|
24
|
+
attr_reader :stock, :timeframes
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# Initialize multi-timeframe analyzer
|
|
28
|
+
#
|
|
29
|
+
# @param stock [SQA::Stock] Stock object with daily data
|
|
30
|
+
#
|
|
31
|
+
def initialize(stock:)
|
|
32
|
+
@stock = stock
|
|
33
|
+
@timeframes = {}
|
|
34
|
+
|
|
35
|
+
# Convert daily data to other timeframes
|
|
36
|
+
convert_timeframes
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Convert daily data to weekly and monthly
|
|
41
|
+
#
|
|
42
|
+
def convert_timeframes
|
|
43
|
+
daily_data = @stock.df.data
|
|
44
|
+
|
|
45
|
+
@timeframes[:daily] = daily_data
|
|
46
|
+
@timeframes[:weekly] = resample(daily_data, period: 5)
|
|
47
|
+
@timeframes[:monthly] = resample(daily_data, period: 21)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Check trend alignment across timeframes
|
|
52
|
+
#
|
|
53
|
+
# @param lookback [Integer] Periods to look back for trend
|
|
54
|
+
# @return [Hash] Trend direction for each timeframe
|
|
55
|
+
#
|
|
56
|
+
def trend_alignment(lookback: 20)
|
|
57
|
+
trends = {}
|
|
58
|
+
|
|
59
|
+
@timeframes.each do |timeframe, data|
|
|
60
|
+
prices = data["adj_close_price"].to_a
|
|
61
|
+
recent = prices.last([lookback, prices.size].min)
|
|
62
|
+
|
|
63
|
+
# Simple trend: compare current to average
|
|
64
|
+
current = recent.last
|
|
65
|
+
avg = recent.sum / recent.size.to_f
|
|
66
|
+
|
|
67
|
+
trends[timeframe] = if current > avg * 1.02
|
|
68
|
+
:up
|
|
69
|
+
elsif current < avg * 0.98
|
|
70
|
+
:down
|
|
71
|
+
else
|
|
72
|
+
:sideways
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if all timeframes aligned
|
|
77
|
+
all_up = trends.values.all? { |t| t == :up }
|
|
78
|
+
all_down = trends.values.all? { |t| t == :down }
|
|
79
|
+
|
|
80
|
+
trends[:aligned] = all_up || all_down
|
|
81
|
+
trends[:direction] = if all_up
|
|
82
|
+
:bullish
|
|
83
|
+
elsif all_down
|
|
84
|
+
:bearish
|
|
85
|
+
else
|
|
86
|
+
:mixed
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
trends
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# Generate multi-timeframe signal
|
|
94
|
+
#
|
|
95
|
+
# Uses higher timeframe for trend, lower for timing.
|
|
96
|
+
#
|
|
97
|
+
# @param strategy_class [Class] Strategy to apply
|
|
98
|
+
# @param higher_timeframe [Symbol] Timeframe for trend (:weekly, :monthly)
|
|
99
|
+
# @param lower_timeframe [Symbol] Timeframe for entry (:daily, :weekly)
|
|
100
|
+
# @return [Symbol] :buy, :sell, or :hold
|
|
101
|
+
#
|
|
102
|
+
def signal(strategy_class:, higher_timeframe: :weekly, lower_timeframe: :daily)
|
|
103
|
+
# Get trend from higher timeframe
|
|
104
|
+
higher_data = @timeframes[higher_timeframe]
|
|
105
|
+
higher_prices = higher_data["adj_close_price"].to_a
|
|
106
|
+
|
|
107
|
+
higher_trend = if higher_prices.last > higher_prices[-10..-1].sum / 10.0
|
|
108
|
+
:up
|
|
109
|
+
else
|
|
110
|
+
:down
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get signal from lower timeframe
|
|
114
|
+
lower_data = @timeframes[lower_timeframe]
|
|
115
|
+
lower_vector = create_vector(lower_data)
|
|
116
|
+
|
|
117
|
+
lower_signal = strategy_class.trade(lower_vector)
|
|
118
|
+
|
|
119
|
+
# Combine: only take trades aligned with higher timeframe
|
|
120
|
+
case higher_trend
|
|
121
|
+
when :up
|
|
122
|
+
lower_signal == :buy ? :buy : :hold
|
|
123
|
+
when :down
|
|
124
|
+
lower_signal == :sell ? :sell : :hold
|
|
125
|
+
else
|
|
126
|
+
:hold
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
##
|
|
131
|
+
# Find support/resistance levels across timeframes
|
|
132
|
+
#
|
|
133
|
+
# Levels that appear on multiple timeframes are stronger.
|
|
134
|
+
#
|
|
135
|
+
# @param tolerance [Float] Price tolerance for matching levels (default: 0.02 for 2%)
|
|
136
|
+
# @return [Hash] Support and resistance levels
|
|
137
|
+
#
|
|
138
|
+
def support_resistance(tolerance: 0.02)
|
|
139
|
+
all_levels = { support: [], resistance: [] }
|
|
140
|
+
|
|
141
|
+
@timeframes.each do |timeframe, data|
|
|
142
|
+
prices = data["adj_close_price"].to_a
|
|
143
|
+
levels = find_levels(prices)
|
|
144
|
+
|
|
145
|
+
all_levels[:support] += levels[:support].map { |l| { price: l, timeframe: timeframe } }
|
|
146
|
+
all_levels[:resistance] += levels[:resistance].map { |l| { price: l, timeframe: timeframe } }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Find levels that appear on multiple timeframes
|
|
150
|
+
strong_support = cluster_levels(all_levels[:support], tolerance)
|
|
151
|
+
strong_resistance = cluster_levels(all_levels[:resistance], tolerance)
|
|
152
|
+
|
|
153
|
+
{
|
|
154
|
+
support: strong_support,
|
|
155
|
+
resistance: strong_resistance,
|
|
156
|
+
current_price: @stock.df.data["adj_close_price"].to_a.last
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
##
|
|
161
|
+
# Calculate indicators for each timeframe
|
|
162
|
+
#
|
|
163
|
+
# @param indicator [Symbol] Indicator to calculate
|
|
164
|
+
# @param options [Hash] Indicator options
|
|
165
|
+
# @return [Hash] Indicator values for each timeframe
|
|
166
|
+
#
|
|
167
|
+
def indicators(indicator:, **options)
|
|
168
|
+
results = {}
|
|
169
|
+
|
|
170
|
+
@timeframes.each do |timeframe, data|
|
|
171
|
+
prices = data["adj_close_price"].to_a
|
|
172
|
+
|
|
173
|
+
results[timeframe] = case indicator
|
|
174
|
+
when :sma
|
|
175
|
+
SQAI.sma(prices, **options)
|
|
176
|
+
when :ema
|
|
177
|
+
SQAI.ema(prices, **options)
|
|
178
|
+
when :rsi
|
|
179
|
+
SQAI.rsi(prices, **options)
|
|
180
|
+
when :macd
|
|
181
|
+
SQAI.macd(prices, **options)
|
|
182
|
+
else
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
results
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
##
|
|
191
|
+
# Detect divergence across timeframes
|
|
192
|
+
#
|
|
193
|
+
# Divergence occurs when price and indicator move in opposite directions.
|
|
194
|
+
#
|
|
195
|
+
# @return [Hash] Divergence information
|
|
196
|
+
#
|
|
197
|
+
def detect_divergence
|
|
198
|
+
divergences = {}
|
|
199
|
+
|
|
200
|
+
@timeframes.each do |timeframe, data|
|
|
201
|
+
prices = data["adj_close_price"].to_a
|
|
202
|
+
rsi = SQAI.rsi(prices, period: 14)
|
|
203
|
+
|
|
204
|
+
next if prices.size < 20 || rsi.size < 20
|
|
205
|
+
|
|
206
|
+
# Compare last 10 periods
|
|
207
|
+
price_trend = prices[-1] > prices[-10]
|
|
208
|
+
rsi_trend = rsi[-1] > rsi[-10]
|
|
209
|
+
|
|
210
|
+
divergences[timeframe] = if price_trend && !rsi_trend
|
|
211
|
+
:bearish_divergence
|
|
212
|
+
elsif !price_trend && rsi_trend
|
|
213
|
+
:bullish_divergence
|
|
214
|
+
else
|
|
215
|
+
:no_divergence
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
divergences
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
##
|
|
223
|
+
# Check if timeframes confirm each other
|
|
224
|
+
#
|
|
225
|
+
# @param strategy_class [Class] Strategy to use
|
|
226
|
+
# @return [Hash] Confirmation status
|
|
227
|
+
#
|
|
228
|
+
def confirmation(strategy_class:)
|
|
229
|
+
signals = {}
|
|
230
|
+
|
|
231
|
+
@timeframes.each do |timeframe, data|
|
|
232
|
+
vector = create_vector(data)
|
|
233
|
+
signals[timeframe] = strategy_class.trade(vector)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Check agreement
|
|
237
|
+
buy_count = signals.values.count(:buy)
|
|
238
|
+
sell_count = signals.values.count(:sell)
|
|
239
|
+
|
|
240
|
+
{
|
|
241
|
+
signals: signals,
|
|
242
|
+
confirmed: buy_count >= 2 || sell_count >= 2,
|
|
243
|
+
consensus: if buy_count >= 2
|
|
244
|
+
:buy
|
|
245
|
+
elsif sell_count >= 2
|
|
246
|
+
:sell
|
|
247
|
+
else
|
|
248
|
+
:hold
|
|
249
|
+
end
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
##
|
|
256
|
+
# Resample daily data to longer timeframes
|
|
257
|
+
#
|
|
258
|
+
# @param daily_data [Polars::DataFrame] Daily data
|
|
259
|
+
# @param period [Integer] Number of days per period
|
|
260
|
+
# @return [Polars::DataFrame] Resampled data
|
|
261
|
+
#
|
|
262
|
+
def resample(daily_data, period:)
|
|
263
|
+
prices = daily_data["adj_close_price"].to_a
|
|
264
|
+
volumes = daily_data["volume"].to_a
|
|
265
|
+
|
|
266
|
+
resampled_prices = []
|
|
267
|
+
resampled_volumes = []
|
|
268
|
+
|
|
269
|
+
(0...prices.size).step(period) do |i|
|
|
270
|
+
chunk_prices = prices[i, period]
|
|
271
|
+
chunk_volumes = volumes[i, period]
|
|
272
|
+
|
|
273
|
+
next if chunk_prices.nil? || chunk_prices.empty?
|
|
274
|
+
|
|
275
|
+
# OHLC would be better, but we'll use close for simplicity
|
|
276
|
+
resampled_prices << chunk_prices.last
|
|
277
|
+
resampled_volumes << chunk_volumes.sum
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Create new DataFrame
|
|
281
|
+
SQA::DataFrame.new(
|
|
282
|
+
{
|
|
283
|
+
"adj_close_price" => resampled_prices,
|
|
284
|
+
"volume" => resampled_volumes
|
|
285
|
+
}
|
|
286
|
+
).data
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
##
|
|
290
|
+
# Find support/resistance levels using local extrema
|
|
291
|
+
#
|
|
292
|
+
# @param prices [Array<Float>] Price array
|
|
293
|
+
# @return [Hash] Support and resistance levels
|
|
294
|
+
#
|
|
295
|
+
def find_levels(prices)
|
|
296
|
+
return { support: [], resistance: [] } if prices.size < 20
|
|
297
|
+
|
|
298
|
+
support = []
|
|
299
|
+
resistance = []
|
|
300
|
+
|
|
301
|
+
window = 5
|
|
302
|
+
|
|
303
|
+
(window...(prices.size - window)).each do |idx|
|
|
304
|
+
current = prices[idx]
|
|
305
|
+
left = prices[(idx - window)...idx]
|
|
306
|
+
right = prices[(idx + 1)..(idx + window)]
|
|
307
|
+
|
|
308
|
+
# Local minimum (support)
|
|
309
|
+
if left.all? { |p| current <= p } && right.all? { |p| current <= p }
|
|
310
|
+
support << current
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Local maximum (resistance)
|
|
314
|
+
if left.all? { |p| current >= p } && right.all? { |p| current >= p }
|
|
315
|
+
resistance << current
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
{ support: support, resistance: resistance }
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
##
|
|
323
|
+
# Cluster similar levels across timeframes
|
|
324
|
+
#
|
|
325
|
+
# @param levels [Array<Hash>] Levels with { price:, timeframe: }
|
|
326
|
+
# @param tolerance [Float] Price tolerance
|
|
327
|
+
# @return [Array<Hash>] Clustered levels
|
|
328
|
+
#
|
|
329
|
+
def cluster_levels(levels, tolerance)
|
|
330
|
+
return [] if levels.empty?
|
|
331
|
+
|
|
332
|
+
clusters = []
|
|
333
|
+
|
|
334
|
+
levels.each do |level|
|
|
335
|
+
# Find existing cluster
|
|
336
|
+
cluster = clusters.find do |c|
|
|
337
|
+
(c[:price] - level[:price]).abs / c[:price] < tolerance
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
if cluster
|
|
341
|
+
# Add to existing cluster
|
|
342
|
+
cluster[:timeframes] << level[:timeframe]
|
|
343
|
+
cluster[:count] += 1
|
|
344
|
+
# Update average price
|
|
345
|
+
cluster[:price] = (cluster[:price] * (cluster[:count] - 1) + level[:price]) / cluster[:count]
|
|
346
|
+
else
|
|
347
|
+
# Create new cluster
|
|
348
|
+
clusters << {
|
|
349
|
+
price: level[:price],
|
|
350
|
+
timeframes: [level[:timeframe]],
|
|
351
|
+
count: 1
|
|
352
|
+
}
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Sort by strength (number of timeframes)
|
|
357
|
+
clusters.sort_by { |c| -c[:count] }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
##
|
|
361
|
+
# Create vector for strategy from timeframe data
|
|
362
|
+
#
|
|
363
|
+
# @param data [Polars::DataFrame] Timeframe data
|
|
364
|
+
# @return [OpenStruct] Vector for strategy
|
|
365
|
+
#
|
|
366
|
+
def create_vector(data)
|
|
367
|
+
prices = data["adj_close_price"].to_a
|
|
368
|
+
|
|
369
|
+
require 'ostruct'
|
|
370
|
+
OpenStruct.new(
|
|
371
|
+
prices: prices,
|
|
372
|
+
close: prices.last,
|
|
373
|
+
rsi: SQAI.rsi(prices, period: 14).last,
|
|
374
|
+
sma_20: SQAI.sma(prices, period: 20).last,
|
|
375
|
+
ema_12: SQAI.ema(prices, period: 12).last
|
|
376
|
+
)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|