sqa-tai 0.1.1 → 0.4.0
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/.claude/settings.local.json +33 -0
- data/.envrc +6 -1
- data/.loki +10 -0
- data/.quality/reek_baseline.txt +0 -0
- data/.rubocop.yml +221 -0
- data/CHANGELOG.md +37 -0
- data/Gemfile +0 -8
- data/Gemfile.local +19 -0
- data/README.md +37 -10
- data/Rakefile +225 -0
- data/lib/sqa/tai/cycle_indicators.rb +7 -19
- data/lib/sqa/tai/help/data.json +697 -0
- data/lib/sqa/tai/help/resource.rb +67 -0
- data/lib/sqa/tai/help.rb +46 -0
- data/lib/sqa/tai/momentum_indicators.rb +51 -110
- data/lib/sqa/tai/native.rb +497 -0
- data/lib/sqa/tai/overlap_studies.rb +13 -22
- data/lib/sqa/tai/pattern_recognition.rb +67 -61
- data/lib/sqa/tai/price_transform.rb +10 -4
- data/lib/sqa/tai/statistical_functions.rb +21 -21
- data/lib/sqa/tai/version.rb +1 -1
- data/lib/sqa/tai/volatility_indicators.rb +27 -31
- data/lib/sqa/tai/volume_indicators.rb +9 -3
- data/lib/sqa/tai.rb +79 -5
- data/mkdocs.yml +154 -15
- data/sqa-tai.gemspec +54 -0
- metadata +134 -13
- data/Gemfile.lock +0 -71
- data/lib/extensions/ta_lib_ffi.rb +0 -154
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae6d7d288fb0bea0c3664ce0daf6cb02d73fb0d9590637431cd276c0b00e9efc
|
|
4
|
+
data.tar.gz: 8c10225300c771ace26256f9724f37ca84261bb718ebe9e8b061fbc51c7ad1dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de3f11e6ca8fdf99034d951a74a35d8930585325a0cd91098330e6765cae71d1600c1b7fae05e79a7a4f50f9a1e2f164e4998520037179fca6ff5ea7ca03a479
|
|
7
|
+
data.tar.gz: 8f6c5bff1286e2833ec9c846b7b6243f9d313b9d17fe49da857567ae97adcfff70b1ab3adf50a17ef5425d24c79840d658563b600de77b3332879515432f2f9f
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(find:*)",
|
|
5
|
+
"Bash(bundle exec rake:*)",
|
|
6
|
+
"Bash(ruby -e:*)",
|
|
7
|
+
"Bash(gem which:*)",
|
|
8
|
+
"Bash(xargs dirname:*)",
|
|
9
|
+
"Bash(xargs:*)",
|
|
10
|
+
"Bash(gem info:*)",
|
|
11
|
+
"Bash(ruby analyze_indicators.rb:*)",
|
|
12
|
+
"Bash(bundle exec rspec:*)",
|
|
13
|
+
"Bash(ruby count_indicators.rb:*)",
|
|
14
|
+
"Bash(ruby test_new_indicators.rb:*)",
|
|
15
|
+
"Bash(ruby verify_count.rb:*)",
|
|
16
|
+
"Bash(ruby generate_docs.rb:*)",
|
|
17
|
+
"Bash(mkdocs build:*)",
|
|
18
|
+
"Bash(test:*)",
|
|
19
|
+
"Bash(for file in cdl_counterattack.md cdl_darkcloudcover.md cdl_dojistar.md cdl_dragonflydoji.md cdl_eveningdojistar.md cdl_eveningstar.md cdl_gapsidesidewhite.md cdl_gravestonedoji.md cdl_hammer.md cdl_hangingman.md)",
|
|
20
|
+
"Bash(done)",
|
|
21
|
+
"Bash(wc:*)",
|
|
22
|
+
"Bash(for file in cdl_sticksandwich cdl_matchinglow cdl_identical3crows cdl_unique3river cdl_tristar cdl_stalledpattern cdl_tasukigap cdl_xsidegap3methods cdl_upsidegap2crows cdl_longline cdl_shortline)",
|
|
23
|
+
"Bash(do echo \"Processing $file\")",
|
|
24
|
+
"Bash(ls:*)",
|
|
25
|
+
"Bash(ruby:*)",
|
|
26
|
+
"Bash(git commit:*)",
|
|
27
|
+
"Bash(mkdir:*)",
|
|
28
|
+
"Bash(bundle install:*)"
|
|
29
|
+
],
|
|
30
|
+
"deny": [],
|
|
31
|
+
"ask": []
|
|
32
|
+
}
|
|
33
|
+
}
|
data/.envrc
CHANGED
data/.loki
ADDED
|
File without changes
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
SuggestExtensions: false
|
|
4
|
+
TargetRubyVersion: 4.0
|
|
5
|
+
Exclude:
|
|
6
|
+
- 'examples/**/*'
|
|
7
|
+
- 'docs/**/*'
|
|
8
|
+
- 'site/**/*'
|
|
9
|
+
- 'pkg/**/*'
|
|
10
|
+
- 'coverage/**/*'
|
|
11
|
+
# Untracked scratch/dev scripts left at repo root in sqa-tai (not part of the shipped gem)
|
|
12
|
+
- 'add_array_order_note.rb'
|
|
13
|
+
- 'complete_all_patterns.rb'
|
|
14
|
+
- 'generate_remaining_patterns.rb'
|
|
15
|
+
|
|
16
|
+
# ── Style: disabled cops ───────────────────────────────────────────────────
|
|
17
|
+
Style/StringLiterals:
|
|
18
|
+
Enabled: false
|
|
19
|
+
|
|
20
|
+
Style/StringLiteralsInInterpolation:
|
|
21
|
+
Enabled: false
|
|
22
|
+
|
|
23
|
+
Style/Documentation:
|
|
24
|
+
Enabled: false
|
|
25
|
+
|
|
26
|
+
# Ruby 4.0 freezes string literals by default
|
|
27
|
+
Style/FrozenStringLiteralComment:
|
|
28
|
+
Enabled: false
|
|
29
|
+
|
|
30
|
+
Style/IfUnlessModifier:
|
|
31
|
+
Enabled: false
|
|
32
|
+
|
|
33
|
+
Style/RescueModifier:
|
|
34
|
+
Enabled: false
|
|
35
|
+
|
|
36
|
+
Style/TrivialAccessors:
|
|
37
|
+
Enabled: false
|
|
38
|
+
|
|
39
|
+
Style/MultilineTernaryOperator:
|
|
40
|
+
Enabled: false
|
|
41
|
+
|
|
42
|
+
Style/SafeNavigation:
|
|
43
|
+
Enabled: false
|
|
44
|
+
|
|
45
|
+
Style/EmptyClassDefinition:
|
|
46
|
+
Enabled: false
|
|
47
|
+
|
|
48
|
+
Style/ClassAndModuleChildren:
|
|
49
|
+
Enabled: false
|
|
50
|
+
|
|
51
|
+
Style/RescueStandardError:
|
|
52
|
+
Enabled: false
|
|
53
|
+
|
|
54
|
+
Style/OneClassPerFile:
|
|
55
|
+
Enabled: false
|
|
56
|
+
|
|
57
|
+
# Both % and format/sprintf are acceptable
|
|
58
|
+
Style/FormatString:
|
|
59
|
+
Enabled: false
|
|
60
|
+
|
|
61
|
+
# String concatenation and interpolation are both acceptable
|
|
62
|
+
Style/StringConcatenation:
|
|
63
|
+
Enabled: false
|
|
64
|
+
|
|
65
|
+
# OpenStruct is a deliberate architectural choice throughout this ecosystem —
|
|
66
|
+
# strategy/backtest/stream vectors are built as OpenStructs on purpose (see
|
|
67
|
+
# sqa/CLAUDE.md). Not a candidate for a Struct/class/test-double rewrite.
|
|
68
|
+
Style/OpenStructUse:
|
|
69
|
+
Enabled: false
|
|
70
|
+
|
|
71
|
+
# $DEBUG_ME (debug_me gem convention) and test fixture globals like $data are
|
|
72
|
+
# established patterns here, not accidental global-variable leaks.
|
|
73
|
+
Style/GlobalVars:
|
|
74
|
+
Enabled: false
|
|
75
|
+
|
|
76
|
+
# ── Layout ─────────────────────────────────────────────────────────────────
|
|
77
|
+
Layout/LineLength:
|
|
78
|
+
Max: 140
|
|
79
|
+
|
|
80
|
+
Layout/ExtraSpacing:
|
|
81
|
+
Enabled: false
|
|
82
|
+
|
|
83
|
+
Layout/HashAlignment:
|
|
84
|
+
Enabled: false
|
|
85
|
+
|
|
86
|
+
Layout/FirstHashElementIndentation:
|
|
87
|
+
Enabled: false
|
|
88
|
+
|
|
89
|
+
Layout/EmptyLineAfterGuardClause:
|
|
90
|
+
Enabled: false
|
|
91
|
+
|
|
92
|
+
# ── Naming ─────────────────────────────────────────────────────────────────
|
|
93
|
+
# Single-char params (c, e, n) are acceptable throughout
|
|
94
|
+
Naming/MethodParameterName:
|
|
95
|
+
Enabled: false
|
|
96
|
+
|
|
97
|
+
# sma_20, percentile_95, and similar parameterized names are more readable
|
|
98
|
+
# than the normalcase default (sma20, percentile95). alpha_vantage_api.rb is
|
|
99
|
+
# excluded entirely: it mirrors Alpha Vantage's literal API function/parameter
|
|
100
|
+
# names (t3, timeperiod1/2/3, ...) verbatim, which we can't rename. Same
|
|
101
|
+
# reasoning for sqa-tai's overlap_studies.rb (t3) and momentum_indicators.rb
|
|
102
|
+
# (ultosc's time_period1/2/3) — TA-Lib's own C function/parameter names.
|
|
103
|
+
Naming/VariableNumber:
|
|
104
|
+
EnforcedStyle: snake_case
|
|
105
|
+
Exclude:
|
|
106
|
+
- 'test/**/*'
|
|
107
|
+
- 'lib/api/alpha_vantage_api.rb'
|
|
108
|
+
- 'lib/sqa/tai/overlap_studies.rb'
|
|
109
|
+
- 'lib/sqa/tai/momentum_indicators.rb'
|
|
110
|
+
- 'lib/sqa/data_frame/yahoo_finance.rb' # period1/period2 mirror Yahoo's own chart API query params verbatim
|
|
111
|
+
|
|
112
|
+
# sqa-cli's gem entry point must be lib/sqa-cli.rb (hyphen) so that
|
|
113
|
+
# `require 'sqa-cli'` resolves by exact match on the gem name; renaming to
|
|
114
|
+
# snake_case would break the require path. (Only sqa-cli has such a file.)
|
|
115
|
+
Naming/FileName:
|
|
116
|
+
Exclude:
|
|
117
|
+
- 'lib/sqa-cli.rb'
|
|
118
|
+
|
|
119
|
+
Naming/RescuedExceptionsVariableName:
|
|
120
|
+
Enabled: false
|
|
121
|
+
|
|
122
|
+
# set_results and similar explicit setters are clear and conventional
|
|
123
|
+
Naming/AccessorMethodName:
|
|
124
|
+
Enabled: false
|
|
125
|
+
|
|
126
|
+
# has_tool_calls? and similar are clear and conventional
|
|
127
|
+
Naming/PredicatePrefix:
|
|
128
|
+
Enabled: false
|
|
129
|
+
|
|
130
|
+
# Test helper methods don't need to follow predicate naming rules
|
|
131
|
+
Naming/PredicateMethod:
|
|
132
|
+
Exclude:
|
|
133
|
+
- 'test/**/*'
|
|
134
|
+
|
|
135
|
+
# ── Lint: relax noisy cops on intentional patterns ─────────────────────────
|
|
136
|
+
Lint/EmptyBlock:
|
|
137
|
+
Exclude:
|
|
138
|
+
- 'test/**/*'
|
|
139
|
+
|
|
140
|
+
# Library and framework methods commonly accept args for API/documentation purposes
|
|
141
|
+
Lint/UnusedMethodArgument:
|
|
142
|
+
Enabled: false
|
|
143
|
+
|
|
144
|
+
Lint/ConstantDefinitionInBlock:
|
|
145
|
+
Exclude:
|
|
146
|
+
- 'Rakefile'
|
|
147
|
+
- 'test/**/*'
|
|
148
|
+
|
|
149
|
+
# ── Gemspec ────────────────────────────────────────────────────────────────
|
|
150
|
+
# All 5 sqa gem repos declare dev deps directly in the gemspec (not a Gemfile
|
|
151
|
+
# group) — the opposite convention from robot_lab_project, so match reality.
|
|
152
|
+
Gemspec/DevelopmentDependencies:
|
|
153
|
+
EnforcedStyle: gemspec
|
|
154
|
+
|
|
155
|
+
Gemspec/RequiredRubyVersion:
|
|
156
|
+
Enabled: false
|
|
157
|
+
|
|
158
|
+
Gemspec/OrderedDependencies:
|
|
159
|
+
Enabled: false
|
|
160
|
+
|
|
161
|
+
# ── Metrics ────────────────────────────────────────────────────────────────
|
|
162
|
+
# Flog is the primary complexity gate (see repo_dev.loki flog_check) — these
|
|
163
|
+
# RuboCop thresholds catch only egregious outliers without duplicating flog's
|
|
164
|
+
# more precise per-method scoring.
|
|
165
|
+
|
|
166
|
+
Metrics/MethodLength:
|
|
167
|
+
Max: 35
|
|
168
|
+
CountAsOne:
|
|
169
|
+
- heredoc
|
|
170
|
+
- array
|
|
171
|
+
- hash
|
|
172
|
+
Exclude:
|
|
173
|
+
- 'test/**/*'
|
|
174
|
+
- 'bin/**/*'
|
|
175
|
+
|
|
176
|
+
Metrics/AbcSize:
|
|
177
|
+
Max: 40
|
|
178
|
+
Exclude:
|
|
179
|
+
- 'test/**/*'
|
|
180
|
+
- 'bin/**/*'
|
|
181
|
+
|
|
182
|
+
Metrics/ClassLength:
|
|
183
|
+
Max: 650 # matches robot_lab_project: central orchestrator classes are legitimately large
|
|
184
|
+
Exclude:
|
|
185
|
+
- 'test/**/*'
|
|
186
|
+
- 'bin/**/*'
|
|
187
|
+
|
|
188
|
+
Metrics/ModuleLength:
|
|
189
|
+
Max: 200
|
|
190
|
+
Exclude:
|
|
191
|
+
- 'test/**/*'
|
|
192
|
+
- 'bin/**/*'
|
|
193
|
+
# Many independent thin TA-Lib indicator wrappers — naturally long by breadth, not complexity
|
|
194
|
+
- 'lib/sqa/tai/pattern_recognition.rb'
|
|
195
|
+
- 'lib/sqa/tai/momentum_indicators.rb'
|
|
196
|
+
# Cohesive groups of route helpers extracted during the flog complexity pass
|
|
197
|
+
- 'lib/sqa_demo/sinatra/helpers/api_helpers.rb'
|
|
198
|
+
- 'lib/sqa_demo/sinatra/helpers/stock_loader.rb'
|
|
199
|
+
|
|
200
|
+
Metrics/CyclomaticComplexity:
|
|
201
|
+
Max: 20
|
|
202
|
+
Exclude:
|
|
203
|
+
- 'test/**/*'
|
|
204
|
+
- 'bin/**/*'
|
|
205
|
+
|
|
206
|
+
Metrics/PerceivedComplexity:
|
|
207
|
+
Max: 20
|
|
208
|
+
Exclude:
|
|
209
|
+
- 'test/**/*'
|
|
210
|
+
- 'bin/**/*'
|
|
211
|
+
|
|
212
|
+
# Long method signatures with keyword args are common in this codebase
|
|
213
|
+
Metrics/ParameterLists:
|
|
214
|
+
Enabled: false
|
|
215
|
+
|
|
216
|
+
Metrics/BlockLength:
|
|
217
|
+
Exclude:
|
|
218
|
+
- 'Rakefile'
|
|
219
|
+
- '*.gemspec'
|
|
220
|
+
- 'test/**/*'
|
|
221
|
+
- 'bin/**/*'
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-09-18
|
|
11
|
+
### Changed
|
|
12
|
+
- Dropped the unmaintained `ta_lib_ffi` gem dependency. `sqa-tai` now talks
|
|
13
|
+
to the installed TA-Lib C library directly via a vendored Fiddle binding
|
|
14
|
+
(`SQA::TAI::Native`), using TA-Lib's own abstract interface
|
|
15
|
+
(`TA_GetFuncInfo`/`TA_Get{Input,OptInput,Output}ParameterInfo`) to
|
|
16
|
+
discover each function's signature at runtime instead of depending on
|
|
17
|
+
hand-written, unmaintained bindings.
|
|
18
|
+
### Fixed
|
|
19
|
+
- Fixed an ABI mismatch where `ta_lib_ffi`'s `TA_FuncInfo` struct declared
|
|
20
|
+
a `camelCaseName` field that doesn't exist in current TA-Lib builds
|
|
21
|
+
(Homebrew's 0.8.x community fork), which misaligned every subsequent
|
|
22
|
+
struct field and caused any indicator call to hang while allocating
|
|
23
|
+
unbounded memory.
|
|
24
|
+
- Fixed indicators whose TA-Lib input is `TA_Input_Price` (all OHLC(V)
|
|
25
|
+
indicators and all 61 candlestick pattern functions) to bundle their
|
|
26
|
+
price arrays into a single argument (e.g. `atr([high, low, close], ...)`)
|
|
27
|
+
as the C API requires, instead of passing them as separate positional
|
|
28
|
+
arguments — this had previously been worked around with a monkey patch
|
|
29
|
+
(`lib/extensions/ta_lib_ffi.rb`, now removed) that patched the wrong
|
|
30
|
+
layer instead of fixing the call sites.
|
|
31
|
+
|
|
32
|
+
## [0.3.0] - 2026-07-02
|
|
33
|
+
- Coordinated version bump to v0.3.0 across the SQA workspace.
|
|
34
|
+
|
|
35
|
+
## [0.1.2] 2025-11-13
|
|
36
|
+
### Added
|
|
37
|
+
- Help system for accessing indicator documentation (`SQA::TAI.help`)
|
|
38
|
+
- Returns URLs to online documentation for any indicator
|
|
39
|
+
- Supports searching by name, filtering by category, and listing all indicators
|
|
40
|
+
- Auto-generated from mkdocs documentation files via `rake help:generate`
|
|
41
|
+
- Flexible API returns Help::Resource objects with formatted output
|
|
42
|
+
- Optional browser integration to open documentation directly
|
|
43
|
+
- Zeitwerk-compatible namespace structure with `help/resource.rb` and `help/data.json`
|
|
44
|
+
- Lazy-loaded JSON data for improved gem load performance
|
|
45
|
+
- Formatted `to_s` output displaying indicator name, category, and website URL
|
|
46
|
+
|
|
10
47
|
## [0.1.1] 2025-11-13
|
|
11
48
|
### Added
|
|
12
49
|
- Intraday Momentum Index (IMI) indicator
|
data/Gemfile
CHANGED
|
@@ -4,14 +4,6 @@ source "https://rubygems.org"
|
|
|
4
4
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
|
-
group :development, :test do
|
|
8
|
-
gem "rake", "~> 13.0"
|
|
9
|
-
gem "minitest", "~> 5.0"
|
|
10
|
-
gem "minitest-reporters"
|
|
11
|
-
gem "simplecov", require: false
|
|
12
|
-
gem "debug"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
7
|
group :documentation do
|
|
16
8
|
# MkDocs is Python-based, managed separately
|
|
17
9
|
# See docs/README.md for setup instructions
|
data/Gemfile.local
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Local cross-repo development Gemfile.
|
|
4
|
+
#
|
|
5
|
+
# sqa-tai is the leaf of the SQA dependency chain (it has no sibling
|
|
6
|
+
# SQA-ecosystem dependencies), so this file is identical to Gemfile.
|
|
7
|
+
# It exists so that `BUNDLE_GEMFILE=Gemfile.local` works uniformly in
|
|
8
|
+
# every repo of the workspace.
|
|
9
|
+
#
|
|
10
|
+
# Activate with: BUNDLE_GEMFILE=Gemfile.local bundle <command>
|
|
11
|
+
|
|
12
|
+
source "https://rubygems.org"
|
|
13
|
+
|
|
14
|
+
gemspec
|
|
15
|
+
|
|
16
|
+
group :documentation do
|
|
17
|
+
# MkDocs is Python-based, managed separately
|
|
18
|
+
# See docs/README.md for setup instructions
|
|
19
|
+
end
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>SQA::TAI - Technical Analysis Indicators</h1>
|
|
3
3
|
|
|
4
|
-
<p>Ruby wrapper around [TA-Lib](https://ta-lib.org/) providing **
|
|
4
|
+
<p>Ruby wrapper around [TA-Lib](https://ta-lib.org/) providing **134 technical analysis indicators** for stock market analysis.<br/>
|
|
5
5
|
Part of the [SQA](https://github.com/MadBomber/sqa) (Simple Qualitative Analysis) ecosystem.</p>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
@@ -15,14 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
|
-
- 🚀 **
|
|
19
|
-
- ⚡ **Blazing Fast** - C library
|
|
18
|
+
- 🚀 **134 Indicators** - Comprehensive coverage with 94% of trading-relevant TA-Lib indicators
|
|
19
|
+
- ⚡ **Blazing Fast** - Talks directly to the TA-Lib C library via a vendored Fiddle binding, no unmaintained FFI gem in between
|
|
20
20
|
- 🎯 **Clean API** - Simple, intuitive Ruby interface with keyword arguments
|
|
21
21
|
- 📊 **Comprehensive** - Overlap studies, momentum, volatility, volume, cycles, stats, patterns
|
|
22
22
|
- 🔌 **Easy Integration** - Works seamlessly with existing Ruby trading tools
|
|
23
23
|
- ✅ **Well Tested** - 73 tests, 332 assertions, 100% passing
|
|
24
24
|
- 📚 **Documented** - Full documentation at [madbomber.github.io/sqa-tai](https://madbomber.github.io/sqa-tai)
|
|
25
|
-
- 🔧 **Bug Fixed** - Includes monkey patch for ta_lib_ffi 0.3.0 multi-array parameter bug
|
|
26
25
|
|
|
27
26
|
</td>
|
|
28
27
|
</tr>
|
|
@@ -86,17 +85,45 @@ if SQA::TAI.available?
|
|
|
86
85
|
end
|
|
87
86
|
```
|
|
88
87
|
|
|
89
|
-
##
|
|
88
|
+
## Getting Help on Indicators
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
Access comprehensive documentation for any indicator directly from your code:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# Get documentation URL for an indicator
|
|
94
|
+
help = SQA::TAI.help(:sma)
|
|
95
|
+
puts help.url
|
|
96
|
+
# => https://madbomber.github.io/sqa-tai/indicators/overlap/sma/
|
|
97
|
+
|
|
98
|
+
# Open documentation in browser
|
|
99
|
+
SQA::TAI.help(:rsi, open: true)
|
|
100
|
+
|
|
101
|
+
# Search for indicators
|
|
102
|
+
SQA::TAI.help(search: "momentum")
|
|
103
|
+
# => {mom: "https://...", cmo: "https://...", ...}
|
|
104
|
+
|
|
105
|
+
# List all indicators in a category
|
|
106
|
+
SQA::TAI.help(category: :momentum_indicators)
|
|
107
|
+
# => {rsi: "https://...", macd: "https://...", ...}
|
|
108
|
+
|
|
109
|
+
# Get all available indicators
|
|
110
|
+
SQA::TAI.help(:all)
|
|
111
|
+
# => {sma: "https://...", ema: "https://...", ...}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
See the [Help System Guide](https://madbomber.github.io/sqa-tai/getting-started/help/) for more details.
|
|
115
|
+
|
|
116
|
+
## Available Indicators (134 Total)
|
|
117
|
+
|
|
118
|
+
### Overlap Studies (17)
|
|
119
|
+
- **SMA, EMA, WMA, MA** - Moving Averages
|
|
93
120
|
- **DEMA, TEMA, TRIMA** - Advanced Moving Averages
|
|
94
121
|
- **KAMA, T3, MAMA** - Adaptive Moving Averages
|
|
95
|
-
- **BBANDS** -
|
|
122
|
+
- **BBANDS, ACCBANDS** - Bands
|
|
96
123
|
- **SAREXT, HT_TRENDLINE** - Trend indicators
|
|
97
124
|
- **MIDPOINT, MIDPRICE, MAVP** - Price calculations
|
|
98
125
|
|
|
99
|
-
### Momentum Indicators (
|
|
126
|
+
### Momentum Indicators (31)
|
|
100
127
|
- **RSI** - Relative Strength Index
|
|
101
128
|
- **MACD, MACDEXT, MACDFIX** - MACD variants
|
|
102
129
|
- **STOCH, STOCHF, STOCHRSI** - Stochastic variants
|
|
@@ -110,6 +137,7 @@ end
|
|
|
110
137
|
- **BOP** - Balance of Power
|
|
111
138
|
- **CMO** - Chande Momentum Oscillator
|
|
112
139
|
- **MFI** - Money Flow Index
|
|
140
|
+
- **IMI** - Intraday Momentum Index
|
|
113
141
|
- **PLUS_DI, MINUS_DI, PLUS_DM, MINUS_DM** - Directional indicators
|
|
114
142
|
- **TRIX** - Triple Smooth EMA
|
|
115
143
|
- **ULTOSC** - Ultimate Oscillator
|
|
@@ -282,7 +310,6 @@ The gem is available as open source under the terms of the [MIT License](LICENSE
|
|
|
282
310
|
## Acknowledgments
|
|
283
311
|
|
|
284
312
|
- [TA-Lib](https://ta-lib.org/) - The underlying C library
|
|
285
|
-
- [ta_lib_ffi](https://github.com/TA-Lib/ta-lib-ruby) - Ruby FFI wrapper
|
|
286
313
|
|
|
287
314
|
## Support
|
|
288
315
|
|
data/Rakefile
CHANGED
|
@@ -11,3 +11,228 @@ Rake::TestTask.new(:test) do |t|
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
task default: :test
|
|
14
|
+
|
|
15
|
+
desc "Check code style with RuboCop"
|
|
16
|
+
task :rubocop do
|
|
17
|
+
sh "bundle exec rubocop --format simple"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "Auto-correct RuboCop offenses"
|
|
21
|
+
task :rubocop_fix do
|
|
22
|
+
sh "bundle exec rubocop -A"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "Check code complexity with Flog (warn >=20, fail >=50)"
|
|
26
|
+
task :flog_check do
|
|
27
|
+
require "flog"
|
|
28
|
+
|
|
29
|
+
warn_threshold = 20.0
|
|
30
|
+
fail_threshold = 50.0
|
|
31
|
+
|
|
32
|
+
flogger = Flog.new(all: true)
|
|
33
|
+
flogger.flog(*Dir.glob("lib/**/*.rb"))
|
|
34
|
+
|
|
35
|
+
warnings = []
|
|
36
|
+
failures = []
|
|
37
|
+
|
|
38
|
+
flogger.each_by_score do |method, score|
|
|
39
|
+
next if method.end_with?("#none")
|
|
40
|
+
|
|
41
|
+
if score > fail_threshold
|
|
42
|
+
failures << "#{format("%.1f", score)}: #{method}"
|
|
43
|
+
elsif score > warn_threshold
|
|
44
|
+
warnings << "#{format("%.1f", score)}: #{method}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless warnings.empty?
|
|
49
|
+
puts "\nFlog warnings (#{warn_threshold}–#{fail_threshold}) — target for future refactoring:"
|
|
50
|
+
warnings.each { |v| puts " #{v}" }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if failures.empty?
|
|
54
|
+
puts "\nFlog: no methods exceed the failure threshold (>=#{fail_threshold})"
|
|
55
|
+
else
|
|
56
|
+
puts "\nFlog failures (>=#{fail_threshold}) — must be refactored:"
|
|
57
|
+
failures.each { |v| puts " #{v}" }
|
|
58
|
+
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{fail_threshold}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc "Check for structural code duplication with Flay (mass >= 50)"
|
|
63
|
+
task :flay_check do
|
|
64
|
+
require "flay"
|
|
65
|
+
|
|
66
|
+
mass_threshold = 50
|
|
67
|
+
|
|
68
|
+
flay = Flay.new(mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60)
|
|
69
|
+
flay.process(*Dir.glob("lib/**/*.rb"))
|
|
70
|
+
flay.analyze
|
|
71
|
+
|
|
72
|
+
if flay.hashes.empty?
|
|
73
|
+
puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
|
|
74
|
+
else
|
|
75
|
+
puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
|
|
76
|
+
flay.report
|
|
77
|
+
abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
desc "Check code smells with Reek (fails only on new/worsened files vs .quality/reek_baseline.txt)"
|
|
82
|
+
task :reek_check do
|
|
83
|
+
require "reek"
|
|
84
|
+
require "reek/configuration/app_configuration"
|
|
85
|
+
|
|
86
|
+
# Resolve .reek.yml by walking up from cwd: a gem-level config wins,
|
|
87
|
+
# otherwise the shared workspace-level one is used.
|
|
88
|
+
config_file = nil
|
|
89
|
+
dir = Pathname.new(Dir.pwd).expand_path
|
|
90
|
+
loop do
|
|
91
|
+
candidate = dir.join(".reek.yml")
|
|
92
|
+
if candidate.exist?
|
|
93
|
+
config_file = candidate.to_s
|
|
94
|
+
break
|
|
95
|
+
end
|
|
96
|
+
break if dir.root?
|
|
97
|
+
|
|
98
|
+
dir = dir.parent
|
|
99
|
+
end
|
|
100
|
+
config = config_file ? Reek::Configuration::AppConfiguration.from_path(Pathname.new(config_file)) : nil
|
|
101
|
+
|
|
102
|
+
# Smell count per file (only files with at least one smell).
|
|
103
|
+
current = Dir.glob("lib/**/*.rb").each_with_object({}) do |file, acc|
|
|
104
|
+
count = Reek::Examiner.new(File.read(file), configuration: config).smells.size
|
|
105
|
+
acc[file] = count if count.positive?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Grandfathered smell counts from .quality/reek_baseline.txt ("count\tfile").
|
|
109
|
+
baseline_path = File.join(".quality", "reek_baseline.txt")
|
|
110
|
+
baseline = {}
|
|
111
|
+
if File.exist?(baseline_path)
|
|
112
|
+
File.readlines(baseline_path).each do |line|
|
|
113
|
+
count, file = line.strip.split("\t", 2)
|
|
114
|
+
baseline[file] = count.to_i if file && !file.empty?
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
new_files = current.reject { |file, _| baseline.key?(file) }
|
|
119
|
+
worsened = current.select { |file, count| baseline.key?(file) && count > baseline[file] }
|
|
120
|
+
|
|
121
|
+
puts "\nReek: #{current.values.sum} warning(s) across #{current.size} file(s) " \
|
|
122
|
+
"(#{baseline.values.sum} grandfathered across #{baseline.size} file(s))."
|
|
123
|
+
|
|
124
|
+
if new_files.empty? && worsened.empty?
|
|
125
|
+
puts "Reek: no new or worsened files (quality gate passed)"
|
|
126
|
+
else
|
|
127
|
+
puts "\nReek quality gate failed:"
|
|
128
|
+
new_files.each { |file, count| puts " NEW #{count.to_s.rjust(3)} warning(s): #{file}" }
|
|
129
|
+
worsened.each { |file, count| puts " WORSENED #{baseline[file].to_s.rjust(3)} -> #{count.to_s.rjust(3)}: #{file}" }
|
|
130
|
+
abort "\nReek quality gate failed: #{new_files.size + worsened.size} file(s) regressed. " \
|
|
131
|
+
"Fix smells, or run `asgard reek_baseline` if intentional."
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
desc "Run all quality checks: tests (with coverage), Flog, Flay, and Reek"
|
|
136
|
+
task :quality do
|
|
137
|
+
results = {}
|
|
138
|
+
|
|
139
|
+
puts "\n#{"=" * 60}"
|
|
140
|
+
puts "Quality Gate: Tests + Coverage"
|
|
141
|
+
puts "=" * 60
|
|
142
|
+
results[:tests] = system("bundle exec rake test") ? :pass : :fail
|
|
143
|
+
|
|
144
|
+
puts "\n#{"=" * 60}"
|
|
145
|
+
puts "Quality Gate: Flog Complexity"
|
|
146
|
+
puts "=" * 60
|
|
147
|
+
results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
|
|
148
|
+
|
|
149
|
+
puts "\n#{"=" * 60}"
|
|
150
|
+
puts "Quality Gate: Flay Duplication"
|
|
151
|
+
puts "=" * 60
|
|
152
|
+
results[:flay] = system("bundle exec rake flay_check") ? :pass : :fail
|
|
153
|
+
|
|
154
|
+
puts "\n#{"=" * 60}"
|
|
155
|
+
puts "Quality Gate: Reek Smells"
|
|
156
|
+
puts "=" * 60
|
|
157
|
+
results[:reek] = system("bundle exec rake reek_check") ? :pass : :fail
|
|
158
|
+
|
|
159
|
+
puts "\n#{"=" * 60}"
|
|
160
|
+
puts "Quality Summary"
|
|
161
|
+
puts "=" * 60
|
|
162
|
+
results.each do |gate, status|
|
|
163
|
+
icon = status == :pass ? "PASS" : "FAIL"
|
|
164
|
+
puts " [#{icon}] #{gate}"
|
|
165
|
+
end
|
|
166
|
+
puts "=" * 60
|
|
167
|
+
|
|
168
|
+
abort "\nQuality gate failed" if results.values.any?(:fail)
|
|
169
|
+
puts "\nAll quality gates passed."
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
namespace :help do
|
|
173
|
+
desc "Generate help mappings from documentation files"
|
|
174
|
+
task :generate do
|
|
175
|
+
require "fileutils"
|
|
176
|
+
|
|
177
|
+
indicators = {}
|
|
178
|
+
category_mapping = {
|
|
179
|
+
"overlap" => :overlap_studies,
|
|
180
|
+
"momentum" => :momentum_indicators,
|
|
181
|
+
"volatility" => :volatility_indicators,
|
|
182
|
+
"volume" => :volume_indicators,
|
|
183
|
+
"price_transform" => :price_transform,
|
|
184
|
+
"statistical" => :statistical_functions,
|
|
185
|
+
"cycle" => :cycle_indicators,
|
|
186
|
+
"patterns" => :pattern_recognition
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Scan docs directory
|
|
190
|
+
Dir.glob("docs/indicators/**/*.md").each do |file|
|
|
191
|
+
next if file.include?("index.md")
|
|
192
|
+
|
|
193
|
+
# Extract indicator name from filename
|
|
194
|
+
indicator = File.basename(file, ".md").to_sym
|
|
195
|
+
|
|
196
|
+
# Extract category from path
|
|
197
|
+
path_category = File.dirname(file).split("/").last
|
|
198
|
+
category = category_mapping[path_category] || path_category.to_sym
|
|
199
|
+
|
|
200
|
+
# Read first heading for name
|
|
201
|
+
name = nil
|
|
202
|
+
File.open(file, "r") do |f|
|
|
203
|
+
f.each_line do |line|
|
|
204
|
+
next unless line.start_with?("# ")
|
|
205
|
+
name = line.gsub(/^#\s*/, "").strip
|
|
206
|
+
# Remove content after parentheses if present
|
|
207
|
+
name = name.split("(").first.strip if name.include?("(")
|
|
208
|
+
break
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Build path relative to docs/
|
|
213
|
+
path = file.gsub("docs/", "").gsub(".md", "")
|
|
214
|
+
|
|
215
|
+
indicators[indicator] = {
|
|
216
|
+
name: name || indicator.to_s.upcase,
|
|
217
|
+
category: category,
|
|
218
|
+
path: path
|
|
219
|
+
}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Convert to JSON-friendly format (strings instead of symbols)
|
|
223
|
+
json_data = indicators.transform_keys(&:to_s).transform_values do |meta|
|
|
224
|
+
{
|
|
225
|
+
"name" => meta[:name],
|
|
226
|
+
"category" => meta[:category].to_s,
|
|
227
|
+
"path" => meta[:path]
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Write to JSON file
|
|
232
|
+
require "json"
|
|
233
|
+
json_output = JSON.pretty_generate(json_data)
|
|
234
|
+
File.write("lib/sqa/tai/help/data.json", json_output)
|
|
235
|
+
puts "✓ Generated help data for #{indicators.size} indicators"
|
|
236
|
+
puts " File: lib/sqa/tai/help/data.json"
|
|
237
|
+
end
|
|
238
|
+
end
|