sqa-tai 0.1.2 → 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 +30 -2
- data/Gemfile +0 -8
- data/Gemfile.local +19 -0
- data/README.md +9 -10
- data/Rakefile +163 -7
- data/lib/sqa/tai/cycle_indicators.rb +7 -19
- data/lib/sqa/tai/help/resource.rb +4 -3
- data/lib/sqa/tai/help.rb +16 -11
- 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 +38 -29
- data/mkdocs.yml +153 -15
- data/sqa-tai.gemspec +54 -0
- metadata +131 -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,14 +7,42 @@ 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
|
|
10
36
|
### Added
|
|
11
37
|
- Help system for accessing indicator documentation (`SQA::TAI.help`)
|
|
12
38
|
- Returns URLs to online documentation for any indicator
|
|
13
39
|
- Supports searching by name, filtering by category, and listing all indicators
|
|
14
40
|
- Auto-generated from mkdocs documentation files via `rake help:generate`
|
|
15
|
-
- Flexible API returns
|
|
41
|
+
- Flexible API returns Help::Resource objects with formatted output
|
|
16
42
|
- Optional browser integration to open documentation directly
|
|
17
|
-
- Zeitwerk-compatible
|
|
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
|
|
18
46
|
|
|
19
47
|
## [0.1.1] 2025-11-13
|
|
20
48
|
### Added
|
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>
|
|
@@ -114,17 +113,17 @@ SQA::TAI.help(:all)
|
|
|
114
113
|
|
|
115
114
|
See the [Help System Guide](https://madbomber.github.io/sqa-tai/getting-started/help/) for more details.
|
|
116
115
|
|
|
117
|
-
## Available Indicators (
|
|
116
|
+
## Available Indicators (134 Total)
|
|
118
117
|
|
|
119
|
-
### Overlap Studies (
|
|
120
|
-
- **SMA, EMA, WMA** - Moving Averages
|
|
118
|
+
### Overlap Studies (17)
|
|
119
|
+
- **SMA, EMA, WMA, MA** - Moving Averages
|
|
121
120
|
- **DEMA, TEMA, TRIMA** - Advanced Moving Averages
|
|
122
121
|
- **KAMA, T3, MAMA** - Adaptive Moving Averages
|
|
123
|
-
- **BBANDS** -
|
|
122
|
+
- **BBANDS, ACCBANDS** - Bands
|
|
124
123
|
- **SAREXT, HT_TRENDLINE** - Trend indicators
|
|
125
124
|
- **MIDPOINT, MIDPRICE, MAVP** - Price calculations
|
|
126
125
|
|
|
127
|
-
### Momentum Indicators (
|
|
126
|
+
### Momentum Indicators (31)
|
|
128
127
|
- **RSI** - Relative Strength Index
|
|
129
128
|
- **MACD, MACDEXT, MACDFIX** - MACD variants
|
|
130
129
|
- **STOCH, STOCHF, STOCHRSI** - Stochastic variants
|
|
@@ -138,6 +137,7 @@ See the [Help System Guide](https://madbomber.github.io/sqa-tai/getting-started/
|
|
|
138
137
|
- **BOP** - Balance of Power
|
|
139
138
|
- **CMO** - Chande Momentum Oscillator
|
|
140
139
|
- **MFI** - Money Flow Index
|
|
140
|
+
- **IMI** - Intraday Momentum Index
|
|
141
141
|
- **PLUS_DI, MINUS_DI, PLUS_DM, MINUS_DM** - Directional indicators
|
|
142
142
|
- **TRIX** - Triple Smooth EMA
|
|
143
143
|
- **ULTOSC** - Ultimate Oscillator
|
|
@@ -310,7 +310,6 @@ The gem is available as open source under the terms of the [MIT License](LICENSE
|
|
|
310
310
|
## Acknowledgments
|
|
311
311
|
|
|
312
312
|
- [TA-Lib](https://ta-lib.org/) - The underlying C library
|
|
313
|
-
- [ta_lib_ffi](https://github.com/TA-Lib/ta-lib-ruby) - Ruby FFI wrapper
|
|
314
313
|
|
|
315
314
|
## Support
|
|
316
315
|
|
data/Rakefile
CHANGED
|
@@ -12,6 +12,163 @@ end
|
|
|
12
12
|
|
|
13
13
|
task default: :test
|
|
14
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
|
+
|
|
15
172
|
namespace :help do
|
|
16
173
|
desc "Generate help mappings from documentation files"
|
|
17
174
|
task :generate do
|
|
@@ -30,7 +187,7 @@ namespace :help do
|
|
|
30
187
|
}
|
|
31
188
|
|
|
32
189
|
# Scan docs directory
|
|
33
|
-
Dir.glob("docs/indicators/**/*.md").
|
|
190
|
+
Dir.glob("docs/indicators/**/*.md").each do |file|
|
|
34
191
|
next if file.include?("index.md")
|
|
35
192
|
|
|
36
193
|
# Extract indicator name from filename
|
|
@@ -44,12 +201,11 @@ namespace :help do
|
|
|
44
201
|
name = nil
|
|
45
202
|
File.open(file, "r") do |f|
|
|
46
203
|
f.each_line do |line|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
end
|
|
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
|
|
53
209
|
end
|
|
54
210
|
end
|
|
55
211
|
|
|
@@ -11,7 +11,7 @@ module SQA
|
|
|
11
11
|
check_available!
|
|
12
12
|
validate_prices!(prices)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Native.ht_dcperiod(prices)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Hilbert Transform - Trend vs Cycle Mode
|
|
@@ -21,7 +21,7 @@ module SQA
|
|
|
21
21
|
check_available!
|
|
22
22
|
validate_prices!(prices)
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Native.ht_trendmode(prices)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Hilbert Transform - Dominant Cycle Phase
|
|
@@ -31,7 +31,7 @@ module SQA
|
|
|
31
31
|
check_available!
|
|
32
32
|
validate_prices!(prices)
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Native.ht_dcphase(prices)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Hilbert Transform - Phasor Components
|
|
@@ -41,14 +41,8 @@ module SQA
|
|
|
41
41
|
check_available!
|
|
42
42
|
validate_prices!(prices)
|
|
43
43
|
|
|
44
|
-
result =
|
|
45
|
-
|
|
46
|
-
# Handle hash return format from newer ta_lib_ffi versions
|
|
47
|
-
if result.is_a?(Hash)
|
|
48
|
-
[result[:in_phase], result[:quadrature]]
|
|
49
|
-
else
|
|
50
|
-
result
|
|
51
|
-
end
|
|
44
|
+
result = Native.ht_phasor(prices)
|
|
45
|
+
[result[:in_phase], result[:quadrature]]
|
|
52
46
|
end
|
|
53
47
|
|
|
54
48
|
# Hilbert Transform - SineWave
|
|
@@ -58,14 +52,8 @@ module SQA
|
|
|
58
52
|
check_available!
|
|
59
53
|
validate_prices!(prices)
|
|
60
54
|
|
|
61
|
-
result =
|
|
62
|
-
|
|
63
|
-
# Handle hash return format from newer ta_lib_ffi versions
|
|
64
|
-
if result.is_a?(Hash)
|
|
65
|
-
[result[:sine], result[:lead_sine]]
|
|
66
|
-
else
|
|
67
|
-
result
|
|
68
|
-
end
|
|
55
|
+
result = Native.ht_sine(prices)
|
|
56
|
+
[result[:sine], result[:lead_sine]]
|
|
69
57
|
end
|
|
70
58
|
end
|
|
71
59
|
end
|