sqa 0.0.32 → 0.0.38
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/CHANGELOG.md +154 -1
- data/README.md +4 -0
- data/Rakefile +52 -10
- data/docs/advanced/index.md +1 -13
- data/docs/api/index.md +547 -61
- data/docs/api-reference/alphavantageapi.md +1057 -0
- data/docs/api-reference/apierror.md +31 -0
- data/docs/api-reference/index.md +221 -0
- data/docs/api-reference/notimplemented.md +27 -0
- data/docs/api-reference/sqa.md +267 -0
- data/docs/api-reference/sqa_backtest.md +171 -0
- data/docs/api-reference/sqa_backtest_results.md +530 -0
- data/docs/api-reference/sqa_badparametererror.md +13 -0
- data/docs/api-reference/sqa_config.md +538 -0
- data/docs/api-reference/sqa_configurationerror.md +13 -0
- data/docs/api-reference/sqa_datafetcherror.md +56 -0
- data/docs/api-reference/sqa_dataframe.md +779 -0
- data/docs/api-reference/sqa_dataframe_alphavantage.md +30 -0
- data/docs/api-reference/sqa_dataframe_data.md +325 -0
- data/docs/api-reference/sqa_dataframe_yahoofinance.md +25 -0
- data/docs/api-reference/sqa_ensemble.md +413 -0
- data/docs/api-reference/sqa_fpop.md +211 -0
- data/docs/api-reference/sqa_geneticprogram.md +325 -0
- data/docs/api-reference/sqa_geneticprogram_individual.md +114 -0
- data/docs/api-reference/sqa_marketregime.md +212 -0
- data/docs/api-reference/sqa_multitimeframe.md +227 -0
- data/docs/api-reference/sqa_patternmatcher.md +195 -0
- data/docs/api-reference/sqa_pluginmanager.md +55 -0
- data/docs/api-reference/sqa_portfolio.md +512 -0
- data/docs/api-reference/sqa_portfolio_position.md +220 -0
- data/docs/api-reference/sqa_portfolio_trade.md +332 -0
- data/docs/api-reference/sqa_portfoliooptimizer.md +248 -0
- data/docs/api-reference/sqa_riskmanager.md +388 -0
- data/docs/api-reference/sqa_seasonalanalyzer.md +121 -0
- data/docs/api-reference/sqa_sectoranalyzer.md +163 -0
- data/docs/api-reference/sqa_stock.md +661 -0
- data/docs/api-reference/sqa_strategy.md +178 -0
- data/docs/api-reference/sqa_strategy_bollingerbands.md +26 -0
- data/docs/api-reference/sqa_strategy_common.md +29 -0
- data/docs/api-reference/sqa_strategy_consensus.md +129 -0
- data/docs/api-reference/sqa_strategy_ema.md +41 -0
- data/docs/api-reference/sqa_strategy_kbs.md +154 -0
- data/docs/api-reference/sqa_strategy_macd.md +26 -0
- data/docs/api-reference/sqa_strategy_mp.md +41 -0
- data/docs/api-reference/sqa_strategy_mr.md +41 -0
- data/docs/api-reference/sqa_strategy_random.md +41 -0
- data/docs/api-reference/sqa_strategy_rsi.md +41 -0
- data/docs/api-reference/sqa_strategy_sma.md +41 -0
- data/docs/api-reference/sqa_strategy_stochastic.md +26 -0
- data/docs/api-reference/sqa_strategy_volumebreakout.md +26 -0
- data/docs/api-reference/sqa_strategygenerator.md +298 -0
- data/docs/api-reference/sqa_strategygenerator_pattern.md +264 -0
- data/docs/api-reference/sqa_strategygenerator_patterncontext.md +326 -0
- data/docs/api-reference/sqa_strategygenerator_profitablepoint.md +424 -0
- data/docs/api-reference/sqa_stream.md +256 -0
- data/docs/api-reference/sqa_ticker.md +175 -0
- data/docs/api-reference/string.md +135 -0
- data/docs/assets/images/advanced-workflow.svg +89 -0
- data/docs/assets/images/architecture.svg +107 -0
- data/docs/assets/images/data-flow.svg +138 -0
- data/docs/assets/images/getting-started-workflow.svg +88 -0
- data/docs/assets/images/strategy-flow.svg +78 -0
- data/docs/assets/images/system-architecture.svg +150 -0
- data/docs/concepts/index.md +292 -19
- data/docs/file_formats.md +250 -0
- data/docs/getting-started/index.md +1 -14
- data/docs/index.md +26 -23
- data/docs/llms.txt +109 -0
- data/docs/strategies/kbs.md +15 -14
- data/docs/strategy.md +381 -3
- data/docs/terms_of_use.md +1 -1
- data/examples/README.md +10 -0
- data/lib/api/alpha_vantage_api.rb +3 -7
- data/lib/sqa/backtest.rb +32 -0
- data/lib/sqa/config.rb +109 -28
- data/lib/sqa/data_frame/data.rb +13 -1
- data/lib/sqa/data_frame.rb +193 -26
- data/lib/sqa/errors.rb +79 -17
- data/lib/sqa/init.rb +70 -15
- data/lib/sqa/pattern_matcher.rb +4 -4
- data/lib/sqa/portfolio.rb +55 -1
- data/lib/sqa/sector_analyzer.rb +3 -11
- data/lib/sqa/stock.rb +180 -15
- data/lib/sqa/strategy.rb +62 -4
- data/lib/sqa/ticker.rb +106 -48
- data/lib/sqa/version.rb +1 -1
- data/lib/sqa.rb +4 -4
- data/mkdocs.yml +69 -81
- metadata +89 -21
- data/docs/README.md +0 -43
- data/examples/sinatra_app/Gemfile +0 -42
- data/examples/sinatra_app/Gemfile.lock +0 -268
- data/examples/sinatra_app/QUICKSTART.md +0 -169
- data/examples/sinatra_app/README.md +0 -471
- data/examples/sinatra_app/RUNNING_WITHOUT_TALIB.md +0 -90
- data/examples/sinatra_app/TROUBLESHOOTING.md +0 -95
- data/examples/sinatra_app/app.rb +0 -404
- data/examples/sinatra_app/config.ru +0 -5
- data/examples/sinatra_app/public/css/style.css +0 -723
- data/examples/sinatra_app/public/debug_macd.html +0 -82
- data/examples/sinatra_app/public/js/app.js +0 -107
- data/examples/sinatra_app/start.sh +0 -53
- data/examples/sinatra_app/views/analyze.erb +0 -306
- data/examples/sinatra_app/views/backtest.erb +0 -325
- data/examples/sinatra_app/views/dashboard.erb +0 -831
- data/examples/sinatra_app/views/error.erb +0 -58
- data/examples/sinatra_app/views/index.erb +0 -118
- data/examples/sinatra_app/views/layout.erb +0 -61
- data/examples/sinatra_app/views/portfolio.erb +0 -43
data/docs/strategy.md
CHANGED
|
@@ -1,5 +1,383 @@
|
|
|
1
|
-
# Strategy
|
|
1
|
+
# Strategy Framework
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A comprehensive guide to SQA's trading strategy architecture.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
A strategy in SQA is a set of rules that analyze technical indicators and market data to generate trading signals. The `SQA::Strategy` class provides the framework for managing and executing multiple strategies.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## The Strategy Pattern
|
|
14
|
+
|
|
15
|
+
All SQA strategies follow a common interface:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
class SQA::Strategy::YourStrategy
|
|
19
|
+
def self.trade(vector)
|
|
20
|
+
# Analyze the vector (OpenStruct with indicator data)
|
|
21
|
+
# Return :buy, :sell, or :hold
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### The Vector
|
|
27
|
+
|
|
28
|
+
The vector is an `OpenStruct` containing all the data a strategy needs to make a decision:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require 'ostruct'
|
|
32
|
+
|
|
33
|
+
vector = OpenStruct.new(
|
|
34
|
+
rsi: { trend: :over_sold, value: 28.5 },
|
|
35
|
+
macd: { crossover: :bullish, histogram: 0.5 },
|
|
36
|
+
prices: prices_array,
|
|
37
|
+
sma_20: sma_20.last,
|
|
38
|
+
sma_50: sma_50.last
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Using the Strategy Framework
|
|
43
|
+
|
|
44
|
+
### Managing Multiple Strategies
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# Create a strategy manager
|
|
48
|
+
strategy = SQA::Strategy.new
|
|
49
|
+
|
|
50
|
+
# Add strategies
|
|
51
|
+
strategy.add SQA::Strategy::RSI
|
|
52
|
+
strategy.add SQA::Strategy::MACD
|
|
53
|
+
strategy.add SQA::Strategy::BollingerBands
|
|
54
|
+
|
|
55
|
+
# Execute all strategies
|
|
56
|
+
signals = strategy.execute(vector)
|
|
57
|
+
# => [:buy, :hold, :sell]
|
|
58
|
+
|
|
59
|
+
# Count votes for consensus
|
|
60
|
+
buy_votes = signals.count(:buy)
|
|
61
|
+
sell_votes = signals.count(:sell)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Auto-Loading Strategies
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# Load all built-in strategies
|
|
68
|
+
strategy = SQA::Strategy.new
|
|
69
|
+
strategy.auto_load
|
|
70
|
+
|
|
71
|
+
# Load specific strategies only
|
|
72
|
+
strategy.auto_load(only: [:rsi, :macd])
|
|
73
|
+
|
|
74
|
+
# Load all except certain strategies
|
|
75
|
+
strategy.auto_load(except: [:random, :common])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Listing Available Strategies
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
strategy = SQA::Strategy.new
|
|
82
|
+
available = strategy.available
|
|
83
|
+
# => [SQA::Strategy::RSI, SQA::Strategy::MACD, ...]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Built-in Strategies
|
|
87
|
+
|
|
88
|
+
SQA includes 13+ built-in trading strategies:
|
|
89
|
+
|
|
90
|
+
### Trend-Following
|
|
91
|
+
|
|
92
|
+
| Strategy | Description | Best For |
|
|
93
|
+
|----------|-------------|----------|
|
|
94
|
+
| **SMA** | Simple Moving Average crossovers | Trending markets |
|
|
95
|
+
| **EMA** | Exponential Moving Average crossovers | Faster trend detection |
|
|
96
|
+
| **MACD** | Moving Average Convergence Divergence | Momentum + trend |
|
|
97
|
+
|
|
98
|
+
### Momentum
|
|
99
|
+
|
|
100
|
+
| Strategy | Description | Best For |
|
|
101
|
+
|----------|-------------|----------|
|
|
102
|
+
| **RSI** | Relative Strength Index (oversold/overbought) | Range-bound markets |
|
|
103
|
+
| **Stochastic** | Stochastic oscillator crossovers | Short-term reversals |
|
|
104
|
+
|
|
105
|
+
### Volatility
|
|
106
|
+
|
|
107
|
+
| Strategy | Description | Best For |
|
|
108
|
+
|----------|-------------|----------|
|
|
109
|
+
| **Bollinger Bands** | Price touching volatility bands | Volatile markets |
|
|
110
|
+
| **Volume Breakout** | High volume price breakouts | Breakout trading |
|
|
111
|
+
|
|
112
|
+
### Advanced
|
|
113
|
+
|
|
114
|
+
| Strategy | Description | Best For |
|
|
115
|
+
|----------|-------------|----------|
|
|
116
|
+
| **KBS** | Knowledge-Based System with RETE engine | Complex rule combinations |
|
|
117
|
+
| **Consensus** | Aggregates multiple strategy signals | Reducing noise |
|
|
118
|
+
| **Mean Reversion** | Statistical mean reversion | Range-bound markets |
|
|
119
|
+
|
|
120
|
+
## Creating Custom Strategies
|
|
121
|
+
|
|
122
|
+
### Basic Template
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# lib/my_strategies/awesome_strategy.rb
|
|
126
|
+
|
|
127
|
+
class SQA::Strategy::AwesomeStrategy
|
|
128
|
+
def self.trade(vector)
|
|
129
|
+
# Your trading logic here
|
|
130
|
+
if buy_condition?(vector)
|
|
131
|
+
:buy
|
|
132
|
+
elsif sell_condition?(vector)
|
|
133
|
+
:sell
|
|
134
|
+
else
|
|
135
|
+
:hold
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def self.buy_condition?(vector)
|
|
142
|
+
vector.rsi[:value] < 30 &&
|
|
143
|
+
vector.macd[:crossover] == :bullish
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.sell_condition?(vector)
|
|
147
|
+
vector.rsi[:value] > 70 &&
|
|
148
|
+
vector.macd[:crossover] == :bearish
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Using Your Strategy
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
require_relative 'my_strategies/awesome_strategy'
|
|
157
|
+
|
|
158
|
+
# Execute directly
|
|
159
|
+
signal = SQA::Strategy::AwesomeStrategy.trade(vector)
|
|
160
|
+
|
|
161
|
+
# Or add to strategy manager
|
|
162
|
+
strategy = SQA::Strategy.new
|
|
163
|
+
strategy.add SQA::Strategy::AwesomeStrategy
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Strategy with Configuration
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class SQA::Strategy::ConfigurableRSI
|
|
170
|
+
@oversold_threshold = 30
|
|
171
|
+
@overbought_threshold = 70
|
|
172
|
+
|
|
173
|
+
class << self
|
|
174
|
+
attr_accessor :oversold_threshold, :overbought_threshold
|
|
175
|
+
|
|
176
|
+
def trade(vector)
|
|
177
|
+
rsi_value = vector.rsi[:value] || vector.rsi
|
|
178
|
+
|
|
179
|
+
if rsi_value < oversold_threshold
|
|
180
|
+
:buy
|
|
181
|
+
elsif rsi_value > overbought_threshold
|
|
182
|
+
:sell
|
|
183
|
+
else
|
|
184
|
+
:hold
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Configure before using
|
|
191
|
+
SQA::Strategy::ConfigurableRSI.oversold_threshold = 25
|
|
192
|
+
SQA::Strategy::ConfigurableRSI.overbought_threshold = 75
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Combining Strategies
|
|
196
|
+
|
|
197
|
+
### Consensus Strategy
|
|
198
|
+
|
|
199
|
+
The built-in Consensus strategy aggregates signals from multiple strategies:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# Simple majority vote
|
|
203
|
+
signals = strategy.execute(vector)
|
|
204
|
+
# => [:buy, :buy, :hold, :sell]
|
|
205
|
+
|
|
206
|
+
consensus = signals.group_by(&:itself)
|
|
207
|
+
.transform_values(&:count)
|
|
208
|
+
.max_by { |_, v| v }
|
|
209
|
+
.first
|
|
210
|
+
# => :buy (2 votes vs 1 each for hold/sell)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Weighted Voting
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
# Assign weights to strategies
|
|
217
|
+
weights = {
|
|
218
|
+
SQA::Strategy::RSI => 2.0, # RSI gets double weight
|
|
219
|
+
SQA::Strategy::MACD => 1.5, # MACD gets 1.5x weight
|
|
220
|
+
SQA::Strategy::SMA => 1.0 # SMA gets normal weight
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Calculate weighted consensus
|
|
224
|
+
weighted_votes = { buy: 0.0, sell: 0.0, hold: 0.0 }
|
|
225
|
+
signals.each_with_index do |signal, i|
|
|
226
|
+
weight = weights.values[i]
|
|
227
|
+
weighted_votes[signal] += weight
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
final_signal = weighted_votes.max_by { |_, v| v }.first
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Testing Strategies
|
|
234
|
+
|
|
235
|
+
### Unit Testing
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
# test/strategy/awesome_strategy_test.rb
|
|
239
|
+
require 'minitest/autorun'
|
|
240
|
+
require 'ostruct'
|
|
241
|
+
|
|
242
|
+
class AwesomeStrategyTest < Minitest::Test
|
|
243
|
+
def test_buy_signal_on_oversold
|
|
244
|
+
vector = OpenStruct.new(
|
|
245
|
+
rsi: { value: 25, trend: :over_sold },
|
|
246
|
+
macd: { crossover: :bullish }
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
assert_equal :buy, SQA::Strategy::AwesomeStrategy.trade(vector)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def test_sell_signal_on_overbought
|
|
253
|
+
vector = OpenStruct.new(
|
|
254
|
+
rsi: { value: 75, trend: :over_bought },
|
|
255
|
+
macd: { crossover: :bearish }
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
assert_equal :sell, SQA::Strategy::AwesomeStrategy.trade(vector)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Backtesting
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
backtest = SQA::Backtest.new(
|
|
267
|
+
stock: stock,
|
|
268
|
+
strategy: SQA::Strategy::AwesomeStrategy,
|
|
269
|
+
initial_cash: 10_000
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
results = backtest.run
|
|
273
|
+
puts "Return: #{results.total_return}%"
|
|
274
|
+
puts "Sharpe: #{results.sharpe_ratio}"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Strategy Internals
|
|
278
|
+
|
|
279
|
+
### How Strategies Process Data
|
|
280
|
+
|
|
281
|
+
1. **Data Preparation**: Raw price data is converted to indicators
|
|
282
|
+
2. **Vector Creation**: Indicators are packaged into an OpenStruct
|
|
283
|
+
3. **Strategy Execution**: Each strategy analyzes the vector
|
|
284
|
+
4. **Signal Generation**: Strategy returns `:buy`, `:sell`, or `:hold`
|
|
285
|
+
5. **Aggregation**: Signals can be combined for consensus
|
|
286
|
+
|
|
287
|
+
### Data Ordering
|
|
288
|
+
|
|
289
|
+
All indicator data in SQA follows **ascending chronological order** (oldest first):
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
# prices[0] = oldest price
|
|
293
|
+
# prices[-1] = most recent price
|
|
294
|
+
|
|
295
|
+
prices = stock.df["adj_close_price"].to_a
|
|
296
|
+
rsi = SQAI.rsi(prices, period: 14)
|
|
297
|
+
|
|
298
|
+
# rsi.last is the most recent RSI value
|
|
299
|
+
current_rsi = rsi.last
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Best Practices
|
|
303
|
+
|
|
304
|
+
### 1. Keep Strategies Simple
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
# GOOD: Clear, single-purpose logic
|
|
308
|
+
def self.trade(vector)
|
|
309
|
+
vector.rsi[:value] < 30 ? :buy : :hold
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# BAD: Complex nested conditions
|
|
313
|
+
def self.trade(vector)
|
|
314
|
+
if vector.rsi[:value] < 30
|
|
315
|
+
if vector.macd[:histogram] > 0
|
|
316
|
+
if vector.volume > vector.avg_volume * 1.5
|
|
317
|
+
# ... more nesting ...
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### 2. Use Named Parameters in Vectors
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
# GOOD: Self-documenting
|
|
328
|
+
vector = OpenStruct.new(
|
|
329
|
+
rsi_value: rsi.last,
|
|
330
|
+
rsi_trend: rsi.last < 30 ? :oversold : :neutral,
|
|
331
|
+
sma_20: sma_20.last,
|
|
332
|
+
sma_50: sma_50.last
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# BAD: Magic numbers
|
|
336
|
+
vector = OpenStruct.new(
|
|
337
|
+
val1: 28.5,
|
|
338
|
+
val2: 150.0,
|
|
339
|
+
val3: 148.0
|
|
340
|
+
)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### 3. Handle Edge Cases
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
def self.trade(vector)
|
|
347
|
+
return :hold if vector.rsi.nil?
|
|
348
|
+
return :hold if vector.prices.empty?
|
|
349
|
+
|
|
350
|
+
# Main logic here
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### 4. Document Your Strategy
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
# Dual Moving Average Crossover Strategy
|
|
358
|
+
#
|
|
359
|
+
# Generates buy signals when short MA crosses above long MA,
|
|
360
|
+
# sell signals when short MA crosses below long MA.
|
|
361
|
+
#
|
|
362
|
+
# Parameters:
|
|
363
|
+
# vector.sma_short - Short-term SMA (e.g., 20-day)
|
|
364
|
+
# vector.sma_long - Long-term SMA (e.g., 50-day)
|
|
365
|
+
#
|
|
366
|
+
# Returns:
|
|
367
|
+
# :buy - Short MA > Long MA (bullish crossover)
|
|
368
|
+
# :sell - Short MA < Long MA (bearish crossover)
|
|
369
|
+
# :hold - No clear signal
|
|
370
|
+
#
|
|
371
|
+
class SQA::Strategy::DualMA
|
|
372
|
+
def self.trade(vector)
|
|
373
|
+
# ...
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Related Documentation
|
|
379
|
+
|
|
380
|
+
- [Trading Strategies Reference](strategies/index.md) - Details on each built-in strategy
|
|
381
|
+
- [Custom Strategies](strategies/custom.md) - Guide to creating your own
|
|
382
|
+
- [Backtesting](advanced/backtesting.md) - Test strategies on historical data
|
|
383
|
+
- [Technical Indicators](indicators/index.md) - Calculate indicator values
|
data/docs/terms_of_use.md
CHANGED
|
@@ -32,7 +32,7 @@ Your affirmative act of using our Ruby Gem ("library") located at https://github
|
|
|
32
32
|
- [20. House rules](#20-house-rules)
|
|
33
33
|
- [21. Third Party Software](#21-third-party-software)
|
|
34
34
|
- [22. Scripts](#22-scripts)
|
|
35
|
-
- [23. Publications - No Recommendation or Advice Status](#23-publications
|
|
35
|
+
- [23. Publications - No Recommendation or Advice Status](#23-publications-no-recommendation-or-advice-status)
|
|
36
36
|
|
|
37
37
|
<!-- Tocer[finish]: Auto-generated, don't remove. -->
|
|
38
38
|
|
data/examples/README.md
CHANGED
|
@@ -335,6 +335,15 @@ Examples may create output files in `/tmp/`:
|
|
|
335
335
|
- `/tmp/sqa_evolution_history.csv` - GP evolution history
|
|
336
336
|
- `/tmp/sqa_backtest_results.csv` - Backtest results
|
|
337
337
|
|
|
338
|
+
## Web Demo Application
|
|
339
|
+
|
|
340
|
+
For a complete web-based demonstration of SQA's capabilities, see the **[sqa_demo-sinatra](https://github.com/MadBomber/sqa_demo-sinatra)** gem. This Sinatra application provides a visual interface for:
|
|
341
|
+
|
|
342
|
+
- Stock analysis dashboard
|
|
343
|
+
- Technical indicator visualization
|
|
344
|
+
- Strategy backtesting
|
|
345
|
+
- Portfolio management
|
|
346
|
+
|
|
338
347
|
## Next Steps
|
|
339
348
|
|
|
340
349
|
After running these examples:
|
|
@@ -344,6 +353,7 @@ After running these examples:
|
|
|
344
353
|
3. **Walk-Forward Validation**: Test on out-of-sample data
|
|
345
354
|
4. **Combine Techniques**: Use strategy generator + GP + KBS together
|
|
346
355
|
5. **Production Deployment**: Connect to real WebSocket data feeds
|
|
356
|
+
6. **Try the Web Demo**: Install [sqa_demo-sinatra](https://github.com/MadBomber/sqa_demo-sinatra) for a visual interface
|
|
347
357
|
|
|
348
358
|
## Educational Disclaimer
|
|
349
359
|
|
|
@@ -3,13 +3,9 @@
|
|
|
3
3
|
require 'faraday'
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# Trading
|
|
10
|
-
# Economic Indicators
|
|
11
|
-
# Digital and Forex
|
|
12
|
-
#
|
|
6
|
+
# Alpha Vantage API wrapper
|
|
7
|
+
# Categories: Market Data, Technical Indicators, Trading, Economic Indicators, Digital/Forex
|
|
8
|
+
# See: https://www.alphavantage.co/documentation/
|
|
13
9
|
|
|
14
10
|
|
|
15
11
|
class AlphaVantageAPI
|
data/lib/sqa/backtest.rb
CHANGED
|
@@ -99,6 +99,38 @@ class SQA::Backtest
|
|
|
99
99
|
|
|
100
100
|
# Run the backtest
|
|
101
101
|
# @return [Results] Backtest results
|
|
102
|
+
#
|
|
103
|
+
# @example Run backtest with RSI strategy
|
|
104
|
+
# stock = SQA::Stock.new(ticker: 'AAPL')
|
|
105
|
+
# backtest = SQA::Backtest.new(
|
|
106
|
+
# stock: stock,
|
|
107
|
+
# strategy: SQA::Strategy::RSI,
|
|
108
|
+
# initial_capital: 10_000,
|
|
109
|
+
# commission: 1.0
|
|
110
|
+
# )
|
|
111
|
+
# results = backtest.run
|
|
112
|
+
# puts results.summary
|
|
113
|
+
# # => Total Return: 15.5%
|
|
114
|
+
# # Sharpe Ratio: 1.2
|
|
115
|
+
# # Max Drawdown: -8.3%
|
|
116
|
+
# # Win Rate: 65%
|
|
117
|
+
#
|
|
118
|
+
# @example Backtest with custom date range
|
|
119
|
+
# backtest = SQA::Backtest.new(
|
|
120
|
+
# stock: stock,
|
|
121
|
+
# strategy: SQA::Strategy::MACD,
|
|
122
|
+
# start_date: '2023-01-01',
|
|
123
|
+
# end_date: '2023-12-31'
|
|
124
|
+
# )
|
|
125
|
+
# results = backtest.run
|
|
126
|
+
# results.total_return # => 0.155 (15.5%)
|
|
127
|
+
#
|
|
128
|
+
# @example Access equity curve for plotting
|
|
129
|
+
# results = backtest.run
|
|
130
|
+
# backtest.equity_curve.each do |point|
|
|
131
|
+
# puts "#{point[:date]}: $#{point[:value]}"
|
|
132
|
+
# end
|
|
133
|
+
#
|
|
102
134
|
def run
|
|
103
135
|
# Get data for the backtest period
|
|
104
136
|
df = @stock.df.data
|
data/lib/sqa/config.rb
CHANGED
|
@@ -1,30 +1,63 @@
|
|
|
1
1
|
# lib/sqa/config.rb
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
3
|
+
# Configuration management for SQA with hierarchical value resolution.
|
|
4
|
+
# Values are resolved in this order (later overrides earlier):
|
|
5
|
+
# 1. default values
|
|
6
|
+
# 2. environment variables (SQA_ prefix)
|
|
7
|
+
# 3. config file (YAML, TOML, or JSON)
|
|
8
|
+
# 4. command line parameters
|
|
9
|
+
#
|
|
10
|
+
# @example Basic configuration
|
|
11
|
+
# SQA.init
|
|
12
|
+
# SQA.config.data_dir = "~/my_data"
|
|
13
|
+
# SQA.config.debug = true
|
|
14
|
+
#
|
|
15
|
+
# @example Using config file
|
|
16
|
+
# SQA.config.config_file = "~/.sqa.yml"
|
|
17
|
+
# SQA.config.from_file
|
|
18
|
+
#
|
|
19
|
+
# @example Environment variables
|
|
20
|
+
# # Set SQA_DATA_DIR, SQA_DEBUG, etc. before requiring sqa
|
|
21
|
+
#
|
|
22
|
+
|
|
23
|
+
require 'fileutils'
|
|
9
24
|
require 'yaml'
|
|
10
25
|
require 'toml-rb'
|
|
11
26
|
|
|
12
27
|
module SQA
|
|
13
|
-
# class
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
# Configuration class for SQA settings.
|
|
29
|
+
# Extends Hashie::Dash for property-based configuration with coercion.
|
|
30
|
+
#
|
|
31
|
+
# @!attribute [rw] command
|
|
32
|
+
# @return [String, nil] Current command (nil, 'analysis', or 'web')
|
|
33
|
+
# @!attribute [rw] config_file
|
|
34
|
+
# @return [String, nil] Path to configuration file
|
|
35
|
+
# @!attribute [rw] dump_config
|
|
36
|
+
# @return [String, nil] Path to dump current configuration
|
|
37
|
+
# @!attribute [rw] data_dir
|
|
38
|
+
# @return [String] Directory for data storage (default: ~/sqa_data)
|
|
39
|
+
# @!attribute [rw] portfolio_filename
|
|
40
|
+
# @return [String] Portfolio CSV filename (default: portfolio.csv)
|
|
41
|
+
# @!attribute [rw] trades_filename
|
|
42
|
+
# @return [String] Trades CSV filename (default: trades.csv)
|
|
43
|
+
# @!attribute [rw] log_level
|
|
44
|
+
# @return [Symbol] Log level (:debug, :info, :warn, :error, :fatal)
|
|
45
|
+
# @!attribute [rw] debug
|
|
46
|
+
# @return [Boolean] Enable debug mode
|
|
47
|
+
# @!attribute [rw] verbose
|
|
48
|
+
# @return [Boolean] Enable verbose output
|
|
49
|
+
# @!attribute [rw] plotting_library
|
|
50
|
+
# @return [Symbol] Plotting library to use (:gruff)
|
|
51
|
+
# @!attribute [rw] lazy_update
|
|
52
|
+
# @return [Boolean] Skip API updates if cached data exists
|
|
53
|
+
#
|
|
18
54
|
class Config < Hashie::Dash
|
|
19
55
|
include Hashie::Extensions::Dash::PropertyTranslation
|
|
20
56
|
include Hashie::Extensions::MethodAccess
|
|
21
57
|
include Hashie::Extensions::Coercion
|
|
22
58
|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# Looks like it is only used for the log level. Should
|
|
26
|
-
# able to work around that.
|
|
27
|
-
#
|
|
59
|
+
# NOTE: PredefinedValues extension disabled due to compatibility issues.
|
|
60
|
+
# Log level validation is handled via the `values:` option on the property instead.
|
|
28
61
|
# include Hashie::Extensions::Dash::PredefinedValues
|
|
29
62
|
|
|
30
63
|
property :command # a String currently, nil, analysis or web
|
|
@@ -33,18 +66,17 @@ module SQA
|
|
|
33
66
|
|
|
34
67
|
property :data_dir, default: Nenv.home + "/sqa_data"
|
|
35
68
|
|
|
36
|
-
#
|
|
37
|
-
# data directory, otherwise, use the given path
|
|
69
|
+
# Relative filenames are resolved against data_dir; absolute paths used as-is
|
|
38
70
|
property :portfolio_filename, from: :portfolio, default: "portfolio.csv"
|
|
39
71
|
property :trades_filename, from: :trades, default: "trades.csv"
|
|
40
72
|
|
|
41
73
|
property :log_level, default: :info, coerce: Symbol, values: %i[debug info warn error fatal]
|
|
42
74
|
|
|
43
|
-
#
|
|
44
|
-
property :debug, default: false
|
|
45
|
-
property :verbose, default: false
|
|
75
|
+
# Boolean coercion handled via coerce_key blocks below (no Boolean class in Ruby)
|
|
76
|
+
property :debug, default: false
|
|
77
|
+
property :verbose, default: false
|
|
46
78
|
|
|
47
|
-
#
|
|
79
|
+
# Plotting library - gruff is default; svggraph support could be added in future
|
|
48
80
|
property :plotting_library, from: :plot_lib, default: :gruff, coerce: Symbol
|
|
49
81
|
property :lazy_update, from: :lazy, default: false
|
|
50
82
|
|
|
@@ -71,17 +103,41 @@ module SQA
|
|
|
71
103
|
end
|
|
72
104
|
end
|
|
73
105
|
|
|
106
|
+
coerce_key :log_level, ->(v) do
|
|
107
|
+
v.is_a?(String) ? v.to_sym : v
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
coerce_key :plotting_library, ->(v) do
|
|
111
|
+
v.is_a?(String) ? v.to_sym : v
|
|
112
|
+
end
|
|
113
|
+
|
|
74
114
|
########################################################
|
|
115
|
+
|
|
116
|
+
# Creates a new Config instance with optional initial values.
|
|
117
|
+
# Automatically applies environment variable overrides.
|
|
118
|
+
#
|
|
119
|
+
# @param a_hash [Hash] Initial configuration values
|
|
75
120
|
def initialize(a_hash={})
|
|
76
121
|
super(a_hash)
|
|
77
122
|
override_with_envars
|
|
78
123
|
end
|
|
79
124
|
|
|
125
|
+
# Returns whether debug mode is enabled.
|
|
126
|
+
# @return [Boolean] true if debug mode is on
|
|
80
127
|
def debug? = debug
|
|
128
|
+
|
|
129
|
+
# Returns whether verbose mode is enabled.
|
|
130
|
+
# @return [Boolean] true if verbose mode is on
|
|
81
131
|
def verbose? = verbose
|
|
82
132
|
|
|
83
133
|
|
|
84
134
|
########################################################
|
|
135
|
+
|
|
136
|
+
# Loads configuration from a file.
|
|
137
|
+
# Supports YAML (.yml, .yaml), TOML (.toml), and JSON (.json) formats.
|
|
138
|
+
#
|
|
139
|
+
# @return [void]
|
|
140
|
+
# @raise [BadParameterError] If config file is invalid or unsupported format
|
|
85
141
|
def from_file
|
|
86
142
|
return if config_file.nil?
|
|
87
143
|
|
|
@@ -93,8 +149,7 @@ module SQA
|
|
|
93
149
|
type = "invalid"
|
|
94
150
|
end
|
|
95
151
|
|
|
96
|
-
#
|
|
97
|
-
|
|
152
|
+
# Config file format detection (YAML is most common)
|
|
98
153
|
if ".json" == type
|
|
99
154
|
incoming = form_json
|
|
100
155
|
|
|
@@ -108,19 +163,24 @@ module SQA
|
|
|
108
163
|
raise BadParameterError, "Invalid Config File: #{config_file}"
|
|
109
164
|
end
|
|
110
165
|
|
|
111
|
-
if incoming.
|
|
166
|
+
if incoming.key?(:data_dir)
|
|
112
167
|
incoming[:data_dir] = incoming[:data_dir].gsub(/^~/, Nenv.home)
|
|
113
168
|
end
|
|
114
169
|
|
|
115
170
|
merge! incoming
|
|
116
171
|
end
|
|
117
172
|
|
|
173
|
+
# Writes current configuration to a file.
|
|
174
|
+
# Format is determined by file extension.
|
|
175
|
+
#
|
|
176
|
+
# @return [void]
|
|
177
|
+
# @raise [BadParameterError] If config file is not set or unsupported format
|
|
118
178
|
def dump_file
|
|
119
179
|
if config_file.nil?
|
|
120
180
|
raise BadParameterError, "No config file given"
|
|
121
181
|
end
|
|
122
182
|
|
|
123
|
-
|
|
183
|
+
FileUtils.touch(config_file)
|
|
124
184
|
# unless File.exist?(config_file)
|
|
125
185
|
|
|
126
186
|
type = File.extname(config_file).downcase
|
|
@@ -139,7 +199,10 @@ module SQA
|
|
|
139
199
|
end
|
|
140
200
|
end
|
|
141
201
|
|
|
142
|
-
#
|
|
202
|
+
# Injects additional properties from plugins.
|
|
203
|
+
# Allows external code to register new configuration options.
|
|
204
|
+
#
|
|
205
|
+
# @return [void]
|
|
143
206
|
def inject_additional_properties
|
|
144
207
|
SQA::PluginManager.registered_properties.each do |prop, options|
|
|
145
208
|
self.class.property(prop, options)
|
|
@@ -185,11 +248,29 @@ module SQA
|
|
|
185
248
|
|
|
186
249
|
#####################################
|
|
187
250
|
class << self
|
|
251
|
+
# Resets the configuration to default values.
|
|
252
|
+
# Creates a new Config instance and assigns it to SQA.config.
|
|
253
|
+
#
|
|
254
|
+
# @return [SQA::Config] The new config instance
|
|
188
255
|
def reset
|
|
256
|
+
@initialized = true
|
|
189
257
|
SQA.config = new
|
|
190
258
|
end
|
|
259
|
+
|
|
260
|
+
# Returns whether the configuration has been initialized.
|
|
261
|
+
#
|
|
262
|
+
# @return [Boolean] true if reset has been called
|
|
263
|
+
def initialized?
|
|
264
|
+
@initialized ||= false
|
|
265
|
+
end
|
|
191
266
|
end
|
|
192
267
|
end
|
|
193
268
|
end
|
|
194
269
|
|
|
195
|
-
|
|
270
|
+
# Auto-initialization with deprecation warning
|
|
271
|
+
# This will be removed in v1.0.0 - applications should call SQA.init explicitly
|
|
272
|
+
unless SQA::Config.initialized?
|
|
273
|
+
warn "[SQA DEPRECATION] Auto-initialization at require time will be removed in v1.0. " \
|
|
274
|
+
"Please call SQA.init explicitly in your application startup." if $VERBOSE
|
|
275
|
+
SQA::Config.reset
|
|
276
|
+
end
|