sqa 0.0.10 → 0.0.12
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/checksums/sqa-0.0.11.gem.sha512 +1 -0
- data/checksums/sqa-0.0.12.gem.sha512 +1 -0
- data/lib/sqa/config.rb +12 -10
- data/lib/sqa/data_frame/yahoo_finance.rb +19 -9
- data/lib/sqa/data_frame.rb +1 -1
- data/lib/sqa/indicator/predict_next_value.rb +165 -26
- data/lib/sqa/stock.rb +5 -1
- data/lib/sqa/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 39185fb1deccd8cc248d4ad776b724ea5ebe1a7e93fdc1d6ada4f53661a55bba
|
|
4
|
+
data.tar.gz: 3b394f997cdcdd67ec9eced2cd7bc08c1af2b407b8945e1a76e0383591ca3495
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d805f6db11375738db0cdad8385f7a74c26c3fba39f0a7aa88593532e1127af7c4c690844eae56f6203696743e1b655bb9d996f2e71745cefb4f04227bbe622
|
|
7
|
+
data.tar.gz: c52327bfbf15f25159a14af31be0881112e169dc73dbdab7d7c7ac7519208e366314e4ab6bd31d44134fbda5238dc7b56e553dcc8c7d231938b78af3ec17d137
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
9fad1def1db23b0f3f28e116cac5032d263921d31f336dba356c5f5fb5cfe911aa73c69615710fcc22662560b96bcc2b683dc5db1044b4acb3337726d6244a45
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2d4a5156118549d604963b8f4dd0b311459f5ad6a1747f2c4925d45f12fe94a863eb225d3c593f82face964773ccbf59478612a014ee9fc32814baf627f730c7
|
data/lib/sqa/config.rb
CHANGED
|
@@ -86,17 +86,23 @@ module SQA
|
|
|
86
86
|
# TODO: arrange order in mostly often used
|
|
87
87
|
|
|
88
88
|
if ".json" == type
|
|
89
|
-
form_json
|
|
89
|
+
incoming = form_json
|
|
90
90
|
|
|
91
91
|
elsif %w[.yml .yaml].include?(type)
|
|
92
|
-
from_yaml
|
|
92
|
+
incoming = from_yaml
|
|
93
93
|
|
|
94
94
|
elsif ".toml" == type
|
|
95
|
-
from_toml
|
|
95
|
+
incoming = from_toml
|
|
96
96
|
|
|
97
97
|
else
|
|
98
98
|
raise BadParameterError, "Invalid Config File: #{config_file}"
|
|
99
99
|
end
|
|
100
|
+
|
|
101
|
+
if incoming.has_key? :data_dir
|
|
102
|
+
incoming[:data_dir] = incoming[:data_dir].gsub(/^~/, Nenv.home)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
merge! incoming
|
|
100
106
|
end
|
|
101
107
|
|
|
102
108
|
def dump_file
|
|
@@ -142,19 +148,15 @@ module SQA
|
|
|
142
148
|
## override values from a config file
|
|
143
149
|
|
|
144
150
|
def from_json
|
|
145
|
-
|
|
146
|
-
debug_me{[ :incoming ]}
|
|
151
|
+
::JSON.load(File.open(config_file).read).symbolize_keys
|
|
147
152
|
end
|
|
148
153
|
|
|
149
154
|
def from_toml
|
|
150
|
-
|
|
151
|
-
debug_me{[ :incoming ]}
|
|
155
|
+
TomlRB.load_file(config_file).symbolize_keys
|
|
152
156
|
end
|
|
153
157
|
|
|
154
158
|
def from_yaml
|
|
155
|
-
|
|
156
|
-
debug_me{[ :incoming ]}
|
|
157
|
-
merge! incoming
|
|
159
|
+
::YAML.load_file(config_file).symbolize_keys
|
|
158
160
|
end
|
|
159
161
|
|
|
160
162
|
|
|
@@ -7,14 +7,14 @@ require 'nokogiri'
|
|
|
7
7
|
class SQA::DataFrame < Daru::DataFrame
|
|
8
8
|
class YahooFinance
|
|
9
9
|
CONNECTION = Faraday.new(url: 'https://finance.yahoo.com')
|
|
10
|
-
HEADERS =
|
|
11
|
-
timestamp # 0
|
|
12
|
-
open_price # 1
|
|
13
|
-
high_price # 2
|
|
14
|
-
low_price # 3
|
|
15
|
-
close_price # 4
|
|
16
|
-
adj_close_price # 5
|
|
17
|
-
volume # 6
|
|
10
|
+
HEADERS = [
|
|
11
|
+
:timestamp, # 0
|
|
12
|
+
:open_price, # 1
|
|
13
|
+
:high_price, # 2
|
|
14
|
+
:low_price, # 3
|
|
15
|
+
:close_price, # 4
|
|
16
|
+
:adj_close_price, # 5
|
|
17
|
+
:volume, # 6
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
# The Yahoo Finance Headers are being remapped so that
|
|
@@ -64,8 +64,18 @@ class SQA::DataFrame < Daru::DataFrame
|
|
|
64
64
|
data = []
|
|
65
65
|
|
|
66
66
|
rows.each do |row|
|
|
67
|
-
cols = row.css('td').map{|c| c
|
|
67
|
+
cols = row.css('td').map{|c| c.children[0].text}
|
|
68
|
+
|
|
69
|
+
next unless 7 == cols.size
|
|
68
70
|
next if cols[1]&.include?("Dividend")
|
|
71
|
+
|
|
72
|
+
if cols.any?(nil)
|
|
73
|
+
debug_me('== ERROR =='){[
|
|
74
|
+
:cols
|
|
75
|
+
]}
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
|
|
69
79
|
cols[0] = Date.parse(cols[0]).to_s
|
|
70
80
|
cols[6] = cols[6].tr(',','').to_i
|
|
71
81
|
(1..5).each {|x| cols[x] = cols[x].to_f}
|
data/lib/sqa/data_frame.rb
CHANGED
|
@@ -35,7 +35,7 @@ class SQA::DataFrame < Daru::DataFrame
|
|
|
35
35
|
#################################################
|
|
36
36
|
|
|
37
37
|
def self.path(filename)
|
|
38
|
-
Pathname.new
|
|
38
|
+
Pathname.new(SQA.config.data_dir) + filename
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def self.load(filename, options={}, &block)
|
|
@@ -5,19 +5,91 @@ end
|
|
|
5
5
|
|
|
6
6
|
class SQA::Indicator; class << self
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
# Produce a Table show actual values and forecasted values
|
|
9
|
+
#
|
|
10
|
+
# actual .... Array of Float
|
|
11
|
+
# forecast .. Array of Float or Array of Array of Float
|
|
12
|
+
# entry is either a single value or
|
|
13
|
+
# an Array [high, guess, low]
|
|
14
|
+
#
|
|
15
|
+
def prediction_test(actual, forecast)
|
|
16
|
+
|
|
17
|
+
unless actual.size == forecast.size
|
|
18
|
+
debug_me("== ERROR =="){[
|
|
19
|
+
"actual.size",
|
|
20
|
+
"forecast.size"
|
|
21
|
+
]}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Method Under Test (MUT)
|
|
25
|
+
mut = caller[0][/`([^']*)'/, 1]
|
|
26
|
+
window = actual.size
|
|
27
|
+
hgl = forecast.first.is_a?(Array)
|
|
28
|
+
|
|
29
|
+
if hgl
|
|
30
|
+
headers = %w[ Actual Forecast Diff %off InRange? High Low ]
|
|
31
|
+
else
|
|
32
|
+
headers = %w[ Actual Forecast Diff %off ]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
diff = []
|
|
36
|
+
percent = []
|
|
37
|
+
values = []
|
|
38
|
+
|
|
39
|
+
actual.map!{|v| v.round(3)}
|
|
40
|
+
|
|
41
|
+
if hgl
|
|
42
|
+
high = forecast.map{|v| v[0].round(3)}
|
|
43
|
+
guess = forecast.map{|v| v[1].round(3)}
|
|
44
|
+
low = forecast.map{|v| v[2].round(3)}
|
|
45
|
+
else
|
|
46
|
+
guess = forecast.map{|v| v.round(3)}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
window.times do |x|
|
|
50
|
+
diff << (actual[x] - guess[x]).round(3)
|
|
51
|
+
percent << ((diff.last / guess[x])*100.0).round(3)
|
|
52
|
+
|
|
53
|
+
entry = [
|
|
54
|
+
actual[x], guess[x],
|
|
55
|
+
diff[x], percent[x],
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if hgl
|
|
59
|
+
entry << ( (high[x] >= actual[x] && actual[x] >= low[x]) ? "Yes" : "" )
|
|
60
|
+
entry << high[x]
|
|
61
|
+
entry << low[x]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
values << entry
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
the_table = TTY::Table.new(headers, values)
|
|
68
|
+
|
|
69
|
+
puts "\n#{mut} Result Validation"
|
|
70
|
+
|
|
71
|
+
puts the_table.render(
|
|
72
|
+
:unicode,
|
|
73
|
+
{
|
|
74
|
+
padding: [0, 0, 0, 0],
|
|
75
|
+
alignments: [:right]*values.first.size,
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
puts
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def predict_next_values(stock, window, testing=false)
|
|
83
|
+
prices = stock.df.adj_close_price.to_a
|
|
84
|
+
known = prices.pop(window) if testing
|
|
85
|
+
result = []
|
|
86
|
+
|
|
87
|
+
prices.each_cons(2) do |a, b|
|
|
16
88
|
result << b + (b - a)
|
|
17
89
|
end
|
|
18
90
|
|
|
19
|
-
if
|
|
20
|
-
(1..
|
|
91
|
+
if window > 0
|
|
92
|
+
(1..window).each do |_|
|
|
21
93
|
last_two_values = result.last(2)
|
|
22
94
|
delta = last_two_values.last - last_two_values.first
|
|
23
95
|
next_value = last_two_values.last + delta
|
|
@@ -25,38 +97,105 @@ class SQA::Indicator; class << self
|
|
|
25
97
|
end
|
|
26
98
|
end
|
|
27
99
|
|
|
28
|
-
result.last(
|
|
100
|
+
prediction_test(known, result.last(window)) if testing
|
|
101
|
+
|
|
102
|
+
result.last(window)
|
|
29
103
|
end
|
|
30
104
|
alias_method :pnv, :predict_next_values
|
|
31
105
|
|
|
32
106
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# the forecast goes. This does not produce that kind of probability
|
|
38
|
-
# cone; but, that was hwat I was thinking about
|
|
39
|
-
#
|
|
40
|
-
# array is an Array - for example historical price data
|
|
41
|
-
# predictions is an Integer for how many predictions into the future
|
|
42
|
-
#
|
|
43
|
-
def pnv2(array, predictions)
|
|
107
|
+
def pnv2(stock, window, testing=false)
|
|
108
|
+
prices = stock.df.adj_close_price.to_a
|
|
109
|
+
known = prices.pop(window) if testing
|
|
110
|
+
|
|
44
111
|
result = []
|
|
45
|
-
last_inx =
|
|
112
|
+
last_inx = prices.size - 1 # indexes are zero based
|
|
46
113
|
|
|
47
|
-
|
|
114
|
+
window.times do |x|
|
|
48
115
|
x += 1 # forecasting 1 day into the future needs 2 days of near past data
|
|
49
116
|
|
|
50
117
|
# window is the near past values
|
|
51
|
-
window =
|
|
118
|
+
window = prices[last_inx-x..]
|
|
52
119
|
|
|
53
120
|
high = window.max
|
|
54
121
|
low = window.min
|
|
55
122
|
midpoint = (high + low) / 2.0
|
|
56
123
|
|
|
57
|
-
result
|
|
124
|
+
result << [high, midpoint, low]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
prediction_test(known, result) if testing
|
|
128
|
+
|
|
129
|
+
result
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def pnv3(stock, window, testing=false)
|
|
134
|
+
prices = stock.df.adj_close_price.to_a
|
|
135
|
+
known = prices.pop(window) if testing
|
|
136
|
+
|
|
137
|
+
result = []
|
|
138
|
+
known = prices.last(window)
|
|
139
|
+
|
|
140
|
+
last_inx = prices.size - 1
|
|
141
|
+
|
|
142
|
+
(0..window-1).to_a.reverse.each do |x|
|
|
143
|
+
curr_inx = last_inx - x
|
|
144
|
+
prev_inx = curr_inx - 1
|
|
145
|
+
current_price = prices[curr_inx]
|
|
146
|
+
percentage_change = (current_price - prices[prev_inx]) / prices[prev_inx]
|
|
147
|
+
|
|
148
|
+
result << current_price + (current_price * percentage_change)
|
|
58
149
|
end
|
|
59
150
|
|
|
151
|
+
prediction_test(known, result) if testing
|
|
152
|
+
|
|
153
|
+
result
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def pnv4(stock, window, testing=false)
|
|
158
|
+
prices = stock.df.adj_close_price.to_a
|
|
159
|
+
known = prices.pop(window) if testing
|
|
160
|
+
|
|
161
|
+
result = []
|
|
162
|
+
known = prices.last(window).dup
|
|
163
|
+
current_price = known.last
|
|
164
|
+
|
|
165
|
+
# Loop through the prediction window size
|
|
166
|
+
(1..window).each do |x|
|
|
167
|
+
|
|
168
|
+
# Calculate the percentage change between the current price and its previous price
|
|
169
|
+
percentage_change = (current_price - prices[-x]) / prices[-x]
|
|
170
|
+
|
|
171
|
+
result << current_price + (current_price * percentage_change)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
prediction_test(known, result) if testing
|
|
175
|
+
|
|
176
|
+
result
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def pnv5(stock, window, testing=false)
|
|
181
|
+
prices = stock.df.adj_close_price.to_a
|
|
182
|
+
known = prices.pop(window) if testing
|
|
183
|
+
|
|
184
|
+
result = []
|
|
185
|
+
current_price = prices.last
|
|
186
|
+
|
|
187
|
+
rate = 0.9 # convert angle into percentage
|
|
188
|
+
sma_trend = stock.indicators.sma_trend
|
|
189
|
+
percentage_change = 1 + (sma_trend[:angle] / 100.0) * rate
|
|
190
|
+
|
|
191
|
+
# Assumes the SMA trend will continue
|
|
192
|
+
window.times do |_|
|
|
193
|
+
result << current_price * percentage_change
|
|
194
|
+
current_price = result.last
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
prediction_test(known, result) if testing
|
|
198
|
+
|
|
60
199
|
result
|
|
61
200
|
end
|
|
62
201
|
|
data/lib/sqa/stock.rb
CHANGED
|
@@ -4,6 +4,7 @@ class SQA::Stock
|
|
|
4
4
|
attr_accessor :company_name
|
|
5
5
|
attr_accessor :df # The DataFrane
|
|
6
6
|
attr_accessor :ticker
|
|
7
|
+
attr_accessor :indicators
|
|
7
8
|
|
|
8
9
|
def initialize(ticker:, source: :yahoo_finance, type: :csv)
|
|
9
10
|
@ticker = ticker.downcase
|
|
@@ -11,6 +12,7 @@ class SQA::Stock
|
|
|
11
12
|
@klass = "SQA::DataFrame::#{source.to_s.camelize}".constantize
|
|
12
13
|
@type = type
|
|
13
14
|
@filename = "#{@ticker}.#{type}"
|
|
15
|
+
@indicators = OpenStruct.new
|
|
14
16
|
|
|
15
17
|
update_the_dataframe
|
|
16
18
|
end
|
|
@@ -21,10 +23,12 @@ class SQA::Stock
|
|
|
21
23
|
df2 = @klass.recent(@ticker)
|
|
22
24
|
@df = @klass.append(df1, df2)
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
if @df.nrows > df1.nrows
|
|
25
27
|
@df.send("to_#{@type}", SQA::DataFrame.path(@filename))
|
|
26
28
|
end
|
|
27
29
|
|
|
30
|
+
# Adding a ticker vector in case I want to do
|
|
31
|
+
# some multi-stock analysis in the same data frame.
|
|
28
32
|
@df[:ticker] = @ticker
|
|
29
33
|
end
|
|
30
34
|
|
data/lib/sqa/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sqa
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-09-
|
|
11
|
+
date: 2023-09-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -279,6 +279,8 @@ files:
|
|
|
279
279
|
- bin/sqa
|
|
280
280
|
- checksums/sqa-0.0.1.gem.sha512
|
|
281
281
|
- checksums/sqa-0.0.10.gem.sha512
|
|
282
|
+
- checksums/sqa-0.0.11.gem.sha512
|
|
283
|
+
- checksums/sqa-0.0.12.gem.sha512
|
|
282
284
|
- checksums/sqa-0.0.2.gem.sha512
|
|
283
285
|
- checksums/sqa-0.0.3.gem.sha512
|
|
284
286
|
- checksums/sqa-0.0.4.gem.sha512
|
|
@@ -389,7 +391,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
389
391
|
- !ruby/object:Gem::Version
|
|
390
392
|
version: '0'
|
|
391
393
|
requirements: []
|
|
392
|
-
rubygems_version: 3.4.
|
|
394
|
+
rubygems_version: 3.4.19
|
|
393
395
|
signing_key:
|
|
394
396
|
specification_version: 4
|
|
395
397
|
summary: sqa - Stock Qualitative Analysis
|