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,470 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'kbs'
|
|
4
|
+
require 'kbs/dsl'
|
|
5
|
+
|
|
6
|
+
=begin
|
|
7
|
+
|
|
8
|
+
Knowledge-Based Strategy using RETE Forward Chaining
|
|
9
|
+
|
|
10
|
+
This strategy uses a rule-based system with the RETE algorithm for
|
|
11
|
+
forward-chaining inference. It allows defining complex trading rules
|
|
12
|
+
that react to market conditions.
|
|
13
|
+
|
|
14
|
+
The strategy asserts facts about market conditions (RSI, trends, volume, etc.)
|
|
15
|
+
and fires rules when patterns are matched.
|
|
16
|
+
|
|
17
|
+
DSL Keywords:
|
|
18
|
+
- on : Assert a condition (fact must exist)
|
|
19
|
+
- without : Negated condition (fact must NOT exist)
|
|
20
|
+
- perform : Define action to execute when rule fires
|
|
21
|
+
- execute : Alias for perform
|
|
22
|
+
- action : Alias for perform
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
strategy = SQA::Strategy::KBS.new
|
|
26
|
+
|
|
27
|
+
# Capture kb for use in perform blocks
|
|
28
|
+
kb = strategy.kb
|
|
29
|
+
|
|
30
|
+
# Define custom rules using the DSL
|
|
31
|
+
strategy.add_rule :buy_oversold_uptrend do
|
|
32
|
+
on :rsi, { level: :oversold }
|
|
33
|
+
on :trend, { direction: :up }
|
|
34
|
+
without :position
|
|
35
|
+
|
|
36
|
+
perform do
|
|
37
|
+
kb.assert(:signal, { action: :buy, confidence: :high })
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Execute strategy
|
|
42
|
+
signal = strategy.trade(vector)
|
|
43
|
+
|
|
44
|
+
Note: Use 'kb.assert' (not just 'assert') in perform blocks to access the knowledge base.
|
|
45
|
+
|
|
46
|
+
=end
|
|
47
|
+
|
|
48
|
+
module SQA
|
|
49
|
+
class Strategy
|
|
50
|
+
class KBS
|
|
51
|
+
attr_reader :kb, :default_rules_loaded
|
|
52
|
+
|
|
53
|
+
def initialize(load_defaults: true)
|
|
54
|
+
@kb = ::KBS::DSL::KnowledgeBase.new
|
|
55
|
+
@default_rules_loaded = false
|
|
56
|
+
@last_signal = :hold
|
|
57
|
+
|
|
58
|
+
load_default_rules if load_defaults
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Main strategy interface - compatible with SQA::Strategy framework
|
|
62
|
+
def self.trade(vector)
|
|
63
|
+
strategy = new
|
|
64
|
+
strategy.execute(vector)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Execute strategy with given market data
|
|
68
|
+
def execute(vector)
|
|
69
|
+
# Reset working memory
|
|
70
|
+
@kb.reset
|
|
71
|
+
|
|
72
|
+
# Assert facts from vector
|
|
73
|
+
assert_market_facts(vector)
|
|
74
|
+
|
|
75
|
+
# Run the inference engine
|
|
76
|
+
@kb.run
|
|
77
|
+
|
|
78
|
+
# Query for trading signal
|
|
79
|
+
determine_signal
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Add a custom trading rule
|
|
83
|
+
#
|
|
84
|
+
# Example:
|
|
85
|
+
# add_rule :buy_dip do
|
|
86
|
+
# on :rsi, { value: ->(v) { v < 30 } }
|
|
87
|
+
# on :macd, { signal: :bullish }
|
|
88
|
+
# perform { kb.assert(:signal, { action: :buy, confidence: :high }) }
|
|
89
|
+
# end
|
|
90
|
+
#
|
|
91
|
+
# Note: Use `kb.assert` in perform blocks, not just `assert`
|
|
92
|
+
def add_rule(name, &block)
|
|
93
|
+
# Capture kb reference for use in perform blocks
|
|
94
|
+
kb = @kb
|
|
95
|
+
|
|
96
|
+
# Define the rule with kb available in closure
|
|
97
|
+
@kb.instance_eval do
|
|
98
|
+
rule(name, &block)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Assert a fact into working memory
|
|
105
|
+
def assert_fact(type, attributes = {})
|
|
106
|
+
@kb.assert(type, attributes)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Query facts from working memory
|
|
110
|
+
def query_facts(type, pattern = {})
|
|
111
|
+
@kb.query(type, pattern)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Print current working memory (for debugging)
|
|
115
|
+
def print_facts
|
|
116
|
+
@kb.print_facts
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Print all rules (for debugging)
|
|
120
|
+
def print_rules
|
|
121
|
+
@kb.print_rules
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Assert market condition facts from the data vector
|
|
127
|
+
def assert_market_facts(vector)
|
|
128
|
+
# RSI facts
|
|
129
|
+
if vector.respond_to?(:rsi) && vector.rsi
|
|
130
|
+
rsi_value = Array(vector.rsi).last
|
|
131
|
+
|
|
132
|
+
assert_fact(:rsi, {
|
|
133
|
+
value: rsi_value,
|
|
134
|
+
level: rsi_level(rsi_value)
|
|
135
|
+
})
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# MACD facts
|
|
139
|
+
if vector.respond_to?(:macd) && vector.macd
|
|
140
|
+
macd_line, signal_line, histogram = vector.macd
|
|
141
|
+
|
|
142
|
+
if macd_line && signal_line
|
|
143
|
+
current_macd = Array(macd_line).last
|
|
144
|
+
current_signal = Array(signal_line).last
|
|
145
|
+
|
|
146
|
+
assert_fact(:macd, {
|
|
147
|
+
line: current_macd,
|
|
148
|
+
signal: current_signal,
|
|
149
|
+
histogram: Array(histogram).last,
|
|
150
|
+
crossover: macd_crossover(macd_line, signal_line)
|
|
151
|
+
})
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Price trend facts
|
|
156
|
+
if vector.respond_to?(:prices) && vector.prices
|
|
157
|
+
prices = vector.prices
|
|
158
|
+
|
|
159
|
+
if prices.size >= 20
|
|
160
|
+
recent_trend = price_trend(prices.last(20))
|
|
161
|
+
medium_trend = price_trend(prices.last(50)) if prices.size >= 50
|
|
162
|
+
|
|
163
|
+
assert_fact(:trend, {
|
|
164
|
+
short_term: recent_trend,
|
|
165
|
+
medium_term: medium_trend || recent_trend,
|
|
166
|
+
strength: trend_strength(prices)
|
|
167
|
+
})
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# SMA facts
|
|
172
|
+
if vector.respond_to?(:sma_short) && vector.respond_to?(:sma_long)
|
|
173
|
+
if vector.sma_short && vector.sma_long
|
|
174
|
+
short_sma = Array(vector.sma_short).last
|
|
175
|
+
long_sma = Array(vector.sma_long).last
|
|
176
|
+
|
|
177
|
+
assert_fact(:sma_crossover, {
|
|
178
|
+
short: short_sma,
|
|
179
|
+
long: long_sma,
|
|
180
|
+
signal: sma_crossover_signal(short_sma, long_sma)
|
|
181
|
+
})
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Volume facts
|
|
186
|
+
if vector.respond_to?(:volume) && vector.volume
|
|
187
|
+
volumes = Array(vector.volume)
|
|
188
|
+
current_volume = volumes.last
|
|
189
|
+
avg_volume = volumes.last(20).sum / 20.0 if volumes.size >= 20
|
|
190
|
+
|
|
191
|
+
if avg_volume
|
|
192
|
+
assert_fact(:volume, {
|
|
193
|
+
current: current_volume,
|
|
194
|
+
average: avg_volume,
|
|
195
|
+
level: volume_level(current_volume, avg_volume)
|
|
196
|
+
})
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Stochastic facts
|
|
201
|
+
if vector.respond_to?(:stoch_k) && vector.respond_to?(:stoch_d)
|
|
202
|
+
if vector.stoch_k && vector.stoch_d
|
|
203
|
+
k_value = Array(vector.stoch_k).last
|
|
204
|
+
d_value = Array(vector.stoch_d).last
|
|
205
|
+
|
|
206
|
+
assert_fact(:stochastic, {
|
|
207
|
+
k: k_value,
|
|
208
|
+
d: d_value,
|
|
209
|
+
zone: stoch_zone(k_value),
|
|
210
|
+
crossover: stoch_crossover(vector.stoch_k, vector.stoch_d)
|
|
211
|
+
})
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Bollinger Bands facts
|
|
216
|
+
if vector.respond_to?(:bb_upper) && vector.respond_to?(:bb_lower) && vector.respond_to?(:prices)
|
|
217
|
+
if vector.bb_upper && vector.bb_lower && vector.prices
|
|
218
|
+
current_price = Array(vector.prices).last
|
|
219
|
+
upper = Array(vector.bb_upper).last
|
|
220
|
+
lower = Array(vector.bb_lower).last
|
|
221
|
+
middle = Array(vector.bb_middle).last if vector.respond_to?(:bb_middle)
|
|
222
|
+
|
|
223
|
+
assert_fact(:bollinger, {
|
|
224
|
+
price: current_price,
|
|
225
|
+
upper: upper,
|
|
226
|
+
lower: lower,
|
|
227
|
+
middle: middle,
|
|
228
|
+
position: bb_position(current_price, lower, upper)
|
|
229
|
+
})
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Determine final trading signal from asserted signal facts
|
|
235
|
+
def determine_signal
|
|
236
|
+
# Query for signal facts
|
|
237
|
+
buy_signals = query_facts(:signal, { action: :buy })
|
|
238
|
+
sell_signals = query_facts(:signal, { action: :sell })
|
|
239
|
+
|
|
240
|
+
# Count confidence levels
|
|
241
|
+
buy_confidence = calculate_confidence(buy_signals)
|
|
242
|
+
sell_confidence = calculate_confidence(sell_signals)
|
|
243
|
+
|
|
244
|
+
# Determine signal based on confidence
|
|
245
|
+
if buy_confidence > sell_confidence && buy_confidence >= 0.5
|
|
246
|
+
@last_signal = :buy
|
|
247
|
+
elsif sell_confidence > buy_confidence && sell_confidence >= 0.5
|
|
248
|
+
@last_signal = :sell
|
|
249
|
+
else
|
|
250
|
+
@last_signal = :hold
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
@last_signal
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Calculate aggregate confidence from multiple signals
|
|
257
|
+
def calculate_confidence(signals)
|
|
258
|
+
return 0.0 if signals.empty?
|
|
259
|
+
|
|
260
|
+
total_confidence = signals.sum do |fact|
|
|
261
|
+
case fact.attributes[:confidence]
|
|
262
|
+
when :high then 1.0
|
|
263
|
+
when :medium then 0.6
|
|
264
|
+
when :low then 0.3
|
|
265
|
+
else 0.5
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
total_confidence / signals.size.to_f
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Helper methods for fact classification
|
|
273
|
+
|
|
274
|
+
def rsi_level(value)
|
|
275
|
+
return :oversold if value < 30
|
|
276
|
+
return :overbought if value > 70
|
|
277
|
+
:neutral
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def macd_crossover(macd_line, signal_line)
|
|
281
|
+
return :none if macd_line.size < 2 || signal_line.size < 2
|
|
282
|
+
|
|
283
|
+
curr_macd = macd_line.last
|
|
284
|
+
prev_macd = macd_line[-2]
|
|
285
|
+
curr_signal = signal_line.last
|
|
286
|
+
prev_signal = signal_line[-2]
|
|
287
|
+
|
|
288
|
+
if prev_macd <= prev_signal && curr_macd > curr_signal
|
|
289
|
+
:bullish
|
|
290
|
+
elsif prev_macd >= prev_signal && curr_macd < curr_signal
|
|
291
|
+
:bearish
|
|
292
|
+
else
|
|
293
|
+
:none
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def price_trend(prices)
|
|
298
|
+
return :neutral if prices.size < 2
|
|
299
|
+
|
|
300
|
+
first_half = prices[0...prices.size/2]
|
|
301
|
+
second_half = prices[prices.size/2..-1]
|
|
302
|
+
|
|
303
|
+
avg_first = first_half.sum / first_half.size.to_f
|
|
304
|
+
avg_second = second_half.sum / second_half.size.to_f
|
|
305
|
+
|
|
306
|
+
if avg_second > avg_first * 1.02
|
|
307
|
+
:up
|
|
308
|
+
elsif avg_second < avg_first * 0.98
|
|
309
|
+
:down
|
|
310
|
+
else
|
|
311
|
+
:neutral
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def trend_strength(prices)
|
|
316
|
+
return :weak if prices.size < 10
|
|
317
|
+
|
|
318
|
+
changes = prices.each_cons(2).map { |a, b| (b - a) / a.to_f }
|
|
319
|
+
avg_change = changes.sum / changes.size.to_f
|
|
320
|
+
|
|
321
|
+
return :strong if avg_change.abs > 0.02
|
|
322
|
+
return :moderate if avg_change.abs > 0.01
|
|
323
|
+
:weak
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def sma_crossover_signal(short_sma, long_sma)
|
|
327
|
+
return :bullish if short_sma > long_sma
|
|
328
|
+
return :bearish if short_sma < long_sma
|
|
329
|
+
:neutral
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def volume_level(current, average)
|
|
333
|
+
ratio = current / average.to_f
|
|
334
|
+
return :high if ratio > 1.5
|
|
335
|
+
return :low if ratio < 0.5
|
|
336
|
+
:normal
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def stoch_zone(value)
|
|
340
|
+
return :oversold if value < 20
|
|
341
|
+
return :overbought if value > 80
|
|
342
|
+
:neutral
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def stoch_crossover(k_values, d_values)
|
|
346
|
+
return :none if k_values.size < 2 || d_values.size < 2
|
|
347
|
+
|
|
348
|
+
curr_k = k_values.last
|
|
349
|
+
prev_k = k_values[-2]
|
|
350
|
+
curr_d = d_values.last
|
|
351
|
+
prev_d = d_values[-2]
|
|
352
|
+
|
|
353
|
+
if prev_k <= prev_d && curr_k > curr_d
|
|
354
|
+
:bullish
|
|
355
|
+
elsif prev_k >= prev_d && curr_k < curr_d
|
|
356
|
+
:bearish
|
|
357
|
+
else
|
|
358
|
+
:none
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def bb_position(price, lower, upper)
|
|
363
|
+
return :below if price < lower
|
|
364
|
+
return :above if price > upper
|
|
365
|
+
:inside
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Load default trading rules
|
|
369
|
+
def load_default_rules
|
|
370
|
+
return if @default_rules_loaded
|
|
371
|
+
|
|
372
|
+
# Capture kb reference for use in perform blocks
|
|
373
|
+
kb = @kb
|
|
374
|
+
|
|
375
|
+
# Rule 1: Buy on RSI oversold in uptrend
|
|
376
|
+
add_rule :buy_oversold_uptrend do
|
|
377
|
+
on :rsi, { level: :oversold }
|
|
378
|
+
on :trend, { short_term: :up }
|
|
379
|
+
perform do
|
|
380
|
+
kb.assert(:signal, { action: :buy, confidence: :high, reason: :oversold_uptrend })
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Rule 2: Sell on RSI overbought in downtrend
|
|
385
|
+
add_rule :sell_overbought_downtrend do
|
|
386
|
+
on :rsi, { level: :overbought }
|
|
387
|
+
on :trend, { short_term: :down }
|
|
388
|
+
perform do
|
|
389
|
+
kb.assert(:signal, { action: :sell, confidence: :high, reason: :overbought_downtrend })
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Rule 3: Buy on bullish MACD crossover
|
|
394
|
+
add_rule :buy_macd_bullish do
|
|
395
|
+
on :macd, { crossover: :bullish }
|
|
396
|
+
on :trend, { medium_term: :up }
|
|
397
|
+
perform do
|
|
398
|
+
kb.assert(:signal, { action: :buy, confidence: :medium, reason: :macd_crossover })
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Rule 4: Sell on bearish MACD crossover
|
|
403
|
+
add_rule :sell_macd_bearish do
|
|
404
|
+
on :macd, { crossover: :bearish }
|
|
405
|
+
on :trend, { medium_term: :down }
|
|
406
|
+
perform do
|
|
407
|
+
kb.assert(:signal, { action: :sell, confidence: :medium, reason: :macd_crossover })
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Rule 5: Buy at lower Bollinger Band
|
|
412
|
+
add_rule :buy_bb_lower do
|
|
413
|
+
on :bollinger, { position: :below }
|
|
414
|
+
on :trend, { short_term: :up }
|
|
415
|
+
perform do
|
|
416
|
+
kb.assert(:signal, { action: :buy, confidence: :medium, reason: :bollinger_bounce })
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Rule 6: Sell at upper Bollinger Band
|
|
421
|
+
add_rule :sell_bb_upper do
|
|
422
|
+
on :bollinger, { position: :above }
|
|
423
|
+
on :trend, { short_term: :down }
|
|
424
|
+
perform do
|
|
425
|
+
kb.assert(:signal, { action: :sell, confidence: :medium, reason: :bollinger_resistance })
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Rule 7: Buy on stochastic oversold crossover
|
|
430
|
+
add_rule :buy_stoch_oversold do
|
|
431
|
+
on :stochastic, { zone: :oversold, crossover: :bullish }
|
|
432
|
+
on :volume, { level: :high }
|
|
433
|
+
perform do
|
|
434
|
+
kb.assert(:signal, { action: :buy, confidence: :high, reason: :stoch_oversold_volume })
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Rule 8: Sell on stochastic overbought crossover
|
|
439
|
+
add_rule :sell_stoch_overbought do
|
|
440
|
+
on :stochastic, { zone: :overbought, crossover: :bearish }
|
|
441
|
+
on :volume, { level: :high }
|
|
442
|
+
perform do
|
|
443
|
+
kb.assert(:signal, { action: :sell, confidence: :high, reason: :stoch_overbought_volume })
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Rule 9: SMA golden cross (buy)
|
|
448
|
+
add_rule :buy_golden_cross do
|
|
449
|
+
on :sma_crossover, { signal: :bullish }
|
|
450
|
+
on :volume, { level: :high }
|
|
451
|
+
perform do
|
|
452
|
+
kb.assert(:signal, { action: :buy, confidence: :high, reason: :golden_cross })
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Rule 10: SMA death cross (sell)
|
|
457
|
+
add_rule :sell_death_cross do
|
|
458
|
+
on :sma_crossover, { signal: :bearish }
|
|
459
|
+
on :volume, { level: :high }
|
|
460
|
+
perform do
|
|
461
|
+
kb.assert(:signal, { action: :sell, confidence: :high, reason: :death_cross })
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
@default_rules_loaded = true
|
|
466
|
+
self
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# lib/sqa/strategy/macd.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# MACD (Moving Average Convergence Divergence) crossover strategy
|
|
5
|
+
# Buy when MACD line crosses above signal line (bullish)
|
|
6
|
+
# Sell when MACD line crosses below signal line (bearish)
|
|
7
|
+
#
|
|
8
|
+
class SQA::Strategy::MACD
|
|
9
|
+
def self.trade(vector)
|
|
10
|
+
return :hold unless vector.respond_to?(:prices) && vector.prices&.size >= 35
|
|
11
|
+
|
|
12
|
+
prices = vector.prices
|
|
13
|
+
|
|
14
|
+
# Calculate MACD using SQAI (returns macd, signal, histogram)
|
|
15
|
+
macd_line, signal_line, histogram = SQAI.macd(
|
|
16
|
+
prices,
|
|
17
|
+
fast_period: 12,
|
|
18
|
+
slow_period: 26,
|
|
19
|
+
signal_period: 9
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return :hold if macd_line.nil? || signal_line.nil? || macd_line.size < 2
|
|
23
|
+
|
|
24
|
+
# Get current and previous values
|
|
25
|
+
current_macd = macd_line[-1]
|
|
26
|
+
current_signal = signal_line[-1]
|
|
27
|
+
prev_macd = macd_line[-2]
|
|
28
|
+
prev_signal = signal_line[-2]
|
|
29
|
+
|
|
30
|
+
# Bullish crossover: MACD crosses above signal
|
|
31
|
+
if prev_macd <= prev_signal && current_macd > current_signal
|
|
32
|
+
:buy
|
|
33
|
+
|
|
34
|
+
# Bearish crossover: MACD crosses below signal
|
|
35
|
+
elsif prev_macd >= prev_signal && current_macd < current_signal
|
|
36
|
+
:sell
|
|
37
|
+
|
|
38
|
+
# No crossover
|
|
39
|
+
else
|
|
40
|
+
:hold
|
|
41
|
+
end
|
|
42
|
+
rescue => e
|
|
43
|
+
warn "MACD strategy error: #{e.message}"
|
|
44
|
+
:hold
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/sqa/strategy/mp.rb
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# lib/sqa/strategy/stochastic.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Stochastic Oscillator crossover strategy
|
|
5
|
+
# Buy when %K crosses above %D in oversold territory (< 20)
|
|
6
|
+
# Sell when %K crosses below %D in overbought territory (> 80)
|
|
7
|
+
#
|
|
8
|
+
class SQA::Strategy::Stochastic
|
|
9
|
+
def self.trade(vector)
|
|
10
|
+
return :hold unless vector.respond_to?(:prices) && vector.prices&.size >= 14
|
|
11
|
+
|
|
12
|
+
prices = vector.prices
|
|
13
|
+
|
|
14
|
+
# We need high, low, close arrays for stochastic
|
|
15
|
+
# For simplicity, use prices as close, and approximate high/low from recent range
|
|
16
|
+
# In a real scenario, you'd get actual high/low from the stock data
|
|
17
|
+
high = prices.dup
|
|
18
|
+
low = prices.dup
|
|
19
|
+
close = prices
|
|
20
|
+
|
|
21
|
+
# Calculate Stochastic using SQAI
|
|
22
|
+
# Returns fastk and fastd (or slowk and slowd depending on the function)
|
|
23
|
+
stoch_k, stoch_d = SQAI.stoch(
|
|
24
|
+
high,
|
|
25
|
+
low,
|
|
26
|
+
close,
|
|
27
|
+
fastk_period: 14,
|
|
28
|
+
slowk_period: 3,
|
|
29
|
+
slowd_period: 3
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return :hold if stoch_k.nil? || stoch_d.nil? || stoch_k.size < 2
|
|
33
|
+
|
|
34
|
+
# Get current and previous values
|
|
35
|
+
current_k = stoch_k[-1]
|
|
36
|
+
current_d = stoch_d[-1]
|
|
37
|
+
prev_k = stoch_k[-2]
|
|
38
|
+
prev_d = stoch_d[-2]
|
|
39
|
+
|
|
40
|
+
# Oversold threshold
|
|
41
|
+
oversold = 20.0
|
|
42
|
+
# Overbought threshold
|
|
43
|
+
overbought = 80.0
|
|
44
|
+
|
|
45
|
+
# Buy signal: %K crosses above %D in oversold territory
|
|
46
|
+
if current_k < oversold && prev_k <= prev_d && current_k > current_d
|
|
47
|
+
:buy
|
|
48
|
+
|
|
49
|
+
# Sell signal: %K crosses below %D in overbought territory
|
|
50
|
+
elsif current_k > overbought && prev_k >= prev_d && current_k < current_d
|
|
51
|
+
:sell
|
|
52
|
+
|
|
53
|
+
else
|
|
54
|
+
:hold
|
|
55
|
+
end
|
|
56
|
+
rescue => e
|
|
57
|
+
warn "Stochastic strategy error: #{e.message}"
|
|
58
|
+
:hold
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# lib/sqa/strategy/volume_breakout.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Volume Breakout strategy
|
|
5
|
+
# Buy when price breaks above resistance with high volume
|
|
6
|
+
# Sell when price breaks below support with high volume
|
|
7
|
+
#
|
|
8
|
+
class SQA::Strategy::VolumeBreakout
|
|
9
|
+
def self.trade(vector)
|
|
10
|
+
return :hold unless vector.respond_to?(:prices) &&
|
|
11
|
+
vector.respond_to?(:volumes) &&
|
|
12
|
+
vector.prices&.size >= 20 &&
|
|
13
|
+
vector.volumes&.size >= 20
|
|
14
|
+
|
|
15
|
+
prices = vector.prices
|
|
16
|
+
volumes = vector.volumes
|
|
17
|
+
|
|
18
|
+
# Calculate moving averages
|
|
19
|
+
sma_20 = SQAI.sma(prices, period: 20)
|
|
20
|
+
return :hold if sma_20.nil?
|
|
21
|
+
|
|
22
|
+
current_price = prices.last
|
|
23
|
+
prev_price = prices[-2]
|
|
24
|
+
current_volume = volumes.last
|
|
25
|
+
|
|
26
|
+
# Calculate average volume
|
|
27
|
+
avg_volume = volumes.last(20).sum / 20.0
|
|
28
|
+
|
|
29
|
+
# High volume threshold (1.5x average)
|
|
30
|
+
volume_threshold = avg_volume * 1.5
|
|
31
|
+
|
|
32
|
+
# Get recent high and low (resistance and support) from previous prices
|
|
33
|
+
# Exclude current price to allow breakout detection
|
|
34
|
+
lookback_prices = prices[...-1].last(20) # Last 20 prices excluding current
|
|
35
|
+
recent_high = lookback_prices.max
|
|
36
|
+
recent_low = lookback_prices.min
|
|
37
|
+
|
|
38
|
+
# Buy signal: price breaks above recent high with high volume
|
|
39
|
+
if current_price > recent_high &&
|
|
40
|
+
prev_price <= recent_high &&
|
|
41
|
+
current_volume > volume_threshold
|
|
42
|
+
:buy
|
|
43
|
+
|
|
44
|
+
# Sell signal: price breaks below recent low with high volume
|
|
45
|
+
elsif current_price < recent_low &&
|
|
46
|
+
prev_price >= recent_low &&
|
|
47
|
+
current_volume > volume_threshold
|
|
48
|
+
:sell
|
|
49
|
+
|
|
50
|
+
else
|
|
51
|
+
:hold
|
|
52
|
+
end
|
|
53
|
+
rescue => e
|
|
54
|
+
warn "VolumeBreakout strategy error: #{e.message}"
|
|
55
|
+
:hold
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/sqa/strategy.rb
CHANGED
|
@@ -62,4 +62,9 @@ require_relative 'strategy/mr'
|
|
|
62
62
|
require_relative 'strategy/random'
|
|
63
63
|
require_relative 'strategy/rsi'
|
|
64
64
|
require_relative 'strategy/sma'
|
|
65
|
+
require_relative 'strategy/bollinger_bands'
|
|
66
|
+
require_relative 'strategy/macd'
|
|
67
|
+
require_relative 'strategy/stochastic'
|
|
68
|
+
require_relative 'strategy/volume_breakout'
|
|
69
|
+
require_relative 'strategy/kbs_strategy'
|
|
65
70
|
|