sqa 0.0.15 → 0.0.17

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fcc7dbda5b62549fa1fb9691bf7ef9af1a0ba5dfcff63494ce2cdf6ca7e578a
4
- data.tar.gz: 9527508fa1573a09536ef4bf735671761d722d1611494d3aeb8615ba25385f2c
3
+ metadata.gz: 911775606b7e0fa046261c5a9bc4d3be21ca9caf38c37011141f1393ac6e5063
4
+ data.tar.gz: d70ae996a39dbe7c386750286cb4cc437681d584945b31104e91b06c30b1600f
5
5
  SHA512:
6
- metadata.gz: c5ce2164cd95cf91c62e054dad4fef84ed105d7713207bf5f5e39fbad282785184217ed303fe73b24801b871e9ca0db371c0f514949803aace37b788ebec6140
7
- data.tar.gz: f7336d71e365388d571267e87fbdd82a66dbd532da8e386e9670e1a9f7034d2279202e6df16ce30d14410b2ab39923ecf4bb78e0941b554bf11ee3101fdb9a41
6
+ metadata.gz: '0074688f69947d20d5aae7c81b090a91fb4f06862084b2349b248ecdf2e17376f47d5a1acffbfb7b2700865cf25f5ff5ba054514d00ed88750f46224b4c9360e'
7
+ data.tar.gz: 38d1ebf511e9dfa2b87084ed77093e2fc03ccf6eeb6a00e42d1060acca82e135d0b4c0d36a25d9f9bc4a2dd9adf91749a6640aab14b304e33f24fee0c3eb7e15
data/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ **Replacing Daru** with Hashie::Mash
2
+
3
+ This is branch hashie_df
4
+
1
5
  # SQA - Simple Qualitative Analysis
2
6
 
3
7
  This is a very simplistic set of tools for running technical analysis on a stock portfolio. Simplistic means it is not reliable nor intended for any kind of financial use. Think of it as a training tool. I do. Its helping me understand why I need professional help from people who know what they are doing.
@@ -0,0 +1 @@
1
+ 2ee94a54d6ac3d13685dc9b91a2bae0fe75feab6148e1aa9a9d4096961b9b7b577b7ce9d1264f0cce260640515ddd86d5fd5fd2b66f49175844c903581ff6fd9
@@ -5,9 +5,8 @@
5
5
  #
6
6
 
7
7
 
8
- class SQA::DataFrame < Daru::DataFrame
8
+ class SQA::DataFrame
9
9
  class AlphaVantage
10
- API_KEY = Nenv.av_api_key
11
10
  CONNECTION = Faraday.new(url: 'https://www.alphavantage.co')
12
11
  HEADERS = YahooFinance::HEADERS
13
12
 
@@ -24,47 +23,16 @@ class SQA::DataFrame < Daru::DataFrame
24
23
  "volume" => HEADERS[6]
25
24
  }
26
25
 
26
+ TRANSFORMERS = {
27
+ HEADERS[1] => -> (v) { v.to_f.round(3) },
28
+ HEADERS[2] => -> (v) { v.to_f.round(3) },
29
+ HEADERS[3] => -> (v) { v.to_f.round(3) },
30
+ HEADERS[4] => -> (v) { v.to_f.round(3) },
31
+ HEADERS[5] => -> (v) { v.to_f.round(3) },
32
+ HEADERS[6] => -> (v) { v.to_i }
33
+ }
27
34
 
28
35
  ################################################################
29
- # Load a Dataframe from a csv file
30
- def self.load(ticker, type="csv")
31
- filepath = SQA.data_dir + "#{ticker}.#{type}"
32
-
33
- if filepath.exist?
34
- df = normalize_vector_names SQA::DataFrame.load(ticker, type)
35
- else
36
- df = recent(ticker, full: true)
37
- df.send("to_#{type}",filepath)
38
- end
39
-
40
- df
41
- end
42
-
43
-
44
- # Normalize the vector (aka column) names as
45
- # symbols using the standard names set by
46
- # Yahoo Finance ... since it was the first one
47
- # not because its anything special.
48
- #
49
- def self.normalize_vector_names(df)
50
- headers = df.vectors.to_a
51
-
52
- # convert vector names to symbols
53
- # when they are strings. They become stings
54
- # when the data frame is saved to a CSV file
55
- # and then loaded back in.
56
-
57
- if headers.first == HEADERS.first.to_s
58
- a_hash = {}
59
- HEADERS.each {|k| a_hash[k.to_s] = k}
60
- df.rename_vectors(a_hash) # renames from String to Symbol
61
- else
62
- df.rename_vectors(HEADER_MAPPING)
63
- end
64
-
65
- df
66
- end
67
-
68
36
 
69
37
  # Get recent data from JSON API
70
38
  #
@@ -82,7 +50,8 @@ class SQA::DataFrame < Daru::DataFrame
82
50
  # and adding that to the data frame as if it were
83
51
  # adjusted.
84
52
  #
85
- def self.recent(ticker, full: false)
53
+ def self.recent(ticker, full: false, from_date: nil)
54
+
86
55
  # NOTE: Using the CSV format because the JSON format has
87
56
  # really silly key values. The column names for the
88
57
  # CSV format are much better.
@@ -90,7 +59,7 @@ class SQA::DataFrame < Daru::DataFrame
90
59
  "/query?" +
91
60
  "function=TIME_SERIES_DAILY&" +
92
61
  "symbol=#{ticker.upcase}&" +
93
- "apikey=#{API_KEY}&" +
62
+ "apikey=#{SQA.av.key}&" +
94
63
  "datatype=csv&" +
95
64
  "outputsize=#{full ? 'full' : 'compact'}"
96
65
  ).to_hash
@@ -100,18 +69,19 @@ class SQA::DataFrame < Daru::DataFrame
100
69
  end
101
70
 
102
71
  raw = response[:body].split
103
-
104
72
  headers = raw.shift.split(',')
73
+
105
74
  headers[0] = 'date' # website returns "timestamp" but that
106
75
  # has an unintended side-effect when
107
76
  # the names are normalized.
77
+ # SMELL: IS THIS STILL TRUE?
108
78
 
109
79
  close_inx = headers.size - 2
110
80
  adj_close_inx = close_inx + 1
111
81
 
112
82
  headers.insert(adj_close_inx, 'adjusted_close')
113
83
 
114
- data = raw.map do |e|
84
+ aofh = raw.map do |e|
115
85
  e2 = e.split(',')
116
86
  e2[1..-2] = e2[1..-2].map(&:to_f) # converting open, high, low, close
117
87
  e2[-1] = e2[-1].to_i # converting volumn
@@ -119,35 +89,20 @@ class SQA::DataFrame < Daru::DataFrame
119
89
  headers.zip(e2).to_h
120
90
  end
121
91
 
122
- # What oldest data first in the data frame
123
- normalize_vector_names Daru::DataFrame.new(data.reverse)
124
- end
125
-
92
+ if from_date
93
+ aofh.reject!{|e| Date.parse(e['date']) < from_date}
94
+ end
126
95
 
127
- # Append update_df rows to the base_df
128
- #
129
- # base_df is ascending on timestamp
130
- # update_df is descending on timestamp
131
- #
132
- # base_df content came from CSV file downloaded
133
- # from Yahoo Finance.
134
- #
135
- # update_df came from scraping the webpage
136
- # at Yahoo Finance for the recent history.
137
- #
138
- # Returns a combined DataFrame.
139
- #
140
- def self.append(base_df, updates_df)
141
- last_timestamp = Date.parse base_df.timestamp.last
142
- filtered_df = updates_df.filter_rows { |row| Date.parse(row[:timestamp]) > last_timestamp }
96
+ return nil if aofh.empty?
143
97
 
144
- last_inx = filtered_df.size - 1
98
+ # ensure tha the data frame is
99
+ # always sorted oldest to newest.
145
100
 
146
- (0..last_inx).each do |x|
147
- base_df.add_row filtered_df.row[last_inx-x]
101
+ if aofh.first['date'] > aofh.last['date']
102
+ aofh.reverse!
148
103
  end
149
104
 
150
- base_df
105
+ SQA::DataFrame.from_aofh(aofh, mapping: HEADER_MAPPING, transformers: TRANSFORMERS)
151
106
  end
152
107
  end
153
108
  end
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
 
5
- class SQA::DataFrame < Daru::DataFrame
5
+ class SQA::DataFrame
6
6
  class YahooFinance
7
7
  CONNECTION = Faraday.new(url: 'https://finance.yahoo.com')
8
8
  HEADERS = [
@@ -30,21 +30,6 @@ class SQA::DataFrame < Daru::DataFrame
30
30
  }
31
31
 
32
32
  ################################################################
33
- def self.load(filename, options={}, &block)
34
- df = SQA::DataFrame.load(filename, options={}, &block)
35
-
36
- headers = df.vectors
37
-
38
- if headers.first == HEADERS.first.to_s
39
- a_hash = {}
40
- HEADERS.each {|k| a_hash[k.to_s] = k}
41
- df.rename_vectors(a_hash)
42
- else
43
- df.rename_vectors(HEADER_MAPPING)
44
- end
45
-
46
- df
47
- end
48
33
 
49
34
 
50
35
  # Scrape the Yahoo Finance website to get recent
@@ -62,7 +47,7 @@ class SQA::DataFrame < Daru::DataFrame
62
47
 
63
48
  rows = table.css('tbody tr')
64
49
 
65
- data = []
50
+ aofh = []
66
51
 
67
52
  rows.each do |row|
68
53
  cols = row.css('td').map{|c| c.children[0].text}
@@ -80,37 +65,10 @@ class SQA::DataFrame < Daru::DataFrame
80
65
  cols[0] = Date.parse(cols[0]).to_s
81
66
  cols[6] = cols[6].tr(',','').to_i
82
67
  (1..5).each {|x| cols[x] = cols[x].to_f}
83
- data << HEADERS.zip(cols).to_h
84
- end
85
-
86
- Daru::DataFrame.new(data)
87
- end
88
-
89
-
90
- # Append update_df rows to the base_df
91
- #
92
- # base_df is ascending on timestamp
93
- # update_df is descending on timestamp
94
- #
95
- # base_df content came from CSV file downloaded
96
- # from Yahoo Finance.
97
- #
98
- # update_df came from scraping the webpage
99
- # at Yahoo Finance for the recent history.
100
- #
101
- # Returns a combined DataFrame.
102
- #
103
- def self.append(base_df, updates_df)
104
- last_timestamp = Date.parse base_df.timestamp.last
105
- filtered_df = updates_df.filter_rows { |row| Date.parse(row[:timestamp]) > last_timestamp }
106
-
107
- last_inx = filtered_df.size - 1
108
-
109
- (0..last_inx).each do |x|
110
- base_df.add_row filtered_df.row[last_inx-x]
68
+ aofh << HEADERS.zip(cols).to_h
111
69
  end
112
70
 
113
- base_df
71
+ aofh
114
72
  end
115
73
  end
116
74
  end
@@ -1,52 +1,302 @@
1
1
  # lib/sqa/data_frame.rb
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative 'data_frame/yahoo_finance'
5
- require_relative 'data_frame/alpha_vantage'
4
+ require 'forwardable'
6
5
 
7
- class Daru::DataFrame
6
+ require_relative 'data_frame/yahoo_finance'
7
+ require_relative 'data_frame/alpha_vantage'
8
8
 
9
- def to_csv(path_to_file, opts={})
10
- options = {
11
- headers: true,
12
- converters: :numeric
13
- }.merge(opts)
9
+ class SQA::DataFrame
10
+ class Data < Hashie::Mash
11
+ # SNELL: Are all of these needed?
12
+ include Hashie::Extensions::Mash::KeepOriginalKeys
13
+ # include Hashie::Extensions::Mash::PermissiveRespondTo
14
+ include Hashie::Extensions::Mash::SafeAssignment
15
+ include Hashie::Extensions::Mash::SymbolizeKeys
16
+ # include Hashie::Extensions::Mash::DefineAccessors
17
+ end
18
+
19
+ extend Forwardable
14
20
 
15
- writer = ::CSV.open(path_to_file, 'wb')
21
+ # @data is of class Data
22
+ attr_accessor :data
23
+
24
+ # Expects a Hash of Arrays (hofa)
25
+ def initialize(a_hash={})
26
+ @data = Data.new(a_hash)
27
+ end
16
28
 
17
- writer << vectors.to_a if options[:headers]
18
29
 
19
- each_row do |row|
20
- writer << if options[:convert_comma]
21
- row.map { |v| v.to_s.tr('.', ',') }
22
- else
23
- row.to_a
24
- end
30
+ def to_csv(path_to_file)
31
+ CSV.open(path_to_file, 'w') do |csv|
32
+ csv << keys
33
+ size.times do |x|
34
+ csv << row(x)
35
+ end
25
36
  end
37
+ end
38
+
26
39
 
27
- writer.close
40
+ def to_json(path_to_file)
41
+ NotImplemented.raise
28
42
  end
29
- end
30
43
 
31
44
 
45
+ def to_aofh
46
+ NotImplemented.raise
47
+ end
32
48
 
33
49
 
34
- class SQA::DataFrame < Daru::DataFrame
50
+ def_delegator :@data, :to_h, :to_hofa
51
+ alias_method :to_h, :to_hofa
52
+
53
+
54
+ # The number of data rows
55
+ def size
56
+ data[@data.keys[0]].size
57
+ end
58
+ alias_method :nrows, :size
59
+ alias_method :length, :size
60
+
61
+
62
+ def_delegator :@data, :keys
63
+ alias_method :vectors, :keys
64
+ alias_method :columns, :keys
65
+
66
+
67
+ def ncols
68
+ keys.size
69
+ end
70
+
71
+
72
+ def_delegator :@data, :values, :values
73
+ def_delegator :@data, :[], :[]
74
+ def_delegator :@data, :[]=, :[]=
75
+
76
+
77
+ def rows
78
+ result = []
79
+ (0..size - 1).each do |x|
80
+ entry = row(x)
81
+ result << entry
82
+ end
83
+ result
84
+ end
85
+ alias_method :to_a, :rows
86
+
87
+
88
+ def row(x)
89
+ if x.is_a?(Integer)
90
+ raise BadParameterError if x < 0 || x >= size
91
+
92
+ elsif x.is_a?(Hash)
93
+ raise BadParameterError, "x is #{x}" if x.size > 1
94
+ key = x.keys[0]
95
+ x = @data[key].index(x[key])
96
+ raise BadParameterError, 'Not Found #{x}' if x.nil?
97
+ return keys.zip(row(x)).to_h
35
98
 
36
- #################################################
37
- def self.load(ticker, type="csv", options={}, &block)
38
- source = SQA.data_dir + "#{ticker}.#{type}"
39
-
40
- if :csv == type
41
- from_csv(source, options={}, &block)
42
- elsif :json == type
43
- from_json(source, options={}, &block)
44
- elsif %i[txt dat].include?(type)
45
- from_plaintext(source, options={}, &block)
46
- elsif :xls == type
47
- from_excel(source, options={}, &block)
48
99
  else
49
- raise SQA::BadParameterError, "un-supported file type: #{type}"
100
+ raise BadParameterError, "Unknown x.class: #{x.class}"
101
+ end
102
+
103
+ entry = []
104
+
105
+ keys.each do |key|
106
+ entry << @data[key][x]
107
+ end
108
+
109
+ entry
110
+ end
111
+
112
+
113
+ def append(new_df)
114
+ raise(BadParameterError, "Key mismatch") if keys != new_df.keys
115
+
116
+ keys.each do |key|
117
+ @data[key] += new_df[key]
118
+ end
119
+ end
120
+ alias_method :concat, :append
121
+
122
+
123
+ # Creates a new instance with new keys
124
+ # based on the mapping hash where
125
+ # { old_key => new_key }
126
+ #
127
+ def rename(mapping)
128
+ SQA::DataFrame.new(
129
+ self.class.rename(
130
+ mapping,
131
+ @data.to_h
132
+ )
133
+ )
134
+ end
135
+ alias_method :rename_vectors, :rename
136
+
137
+
138
+ # Map the values of the vectors into different objects
139
+ # types is a Hash where the key is the vector name and
140
+ # the value is a proc
141
+ #
142
+ # For Example:
143
+ # {
144
+ # price: -> (v) {v.to_f.round(3)}
145
+ # }
146
+ #
147
+ def coerce_vectors(transformers)
148
+ transformers.each_pair do |key, transformer|
149
+ @data[key].map!{|v| transformer.call(v)}
150
+ end
151
+ end
152
+
153
+
154
+ def method_missing(method_name, *args, &block)
155
+ if @data.respond_to?(method_name)
156
+ self.class.send(:define_method, method_name) do |*method_args, &method_block|
157
+ @data.send(method_name, *method_args, &method_block)
158
+ end
159
+ send(method_name, *args, &block)
160
+ else
161
+ super
162
+ end
163
+ end
164
+
165
+
166
+ def respond_to_missing?(method_name, include_private = false)
167
+ @data.respond_to?(method_name) || super
168
+ end
169
+
170
+ #################################################
171
+ class << self
172
+
173
+ def append(base_df, other_df)
174
+ base_df.append(other_df)
175
+ end
176
+
177
+
178
+ # TODO: The Data class has its own load which also supports
179
+ # YAML by default. Maybe this method should
180
+ # make use of @data = Data.load(source)
181
+ #
182
+ def load(source:, mapping: {}, transformers:{})
183
+ file_type = source.extname[1..].downcase.to_sym
184
+
185
+ df = if :csv == file_type
186
+ from_csv_file(source, mapping: mapping, transformers: transformers)
187
+ elsif :json == file_type
188
+ from_json_file(source, mapping: mapping, transformers: transformers)
189
+ else
190
+ raise BadParameterError, "unsupported file type: #{file_type}"
191
+ end
192
+
193
+ unless transformers.empty?
194
+ df.coerce_vectors(transformers)
195
+ end
196
+
197
+ df
198
+ end
199
+
200
+
201
+ def from_aofh(aofh, mapping: {}, transformers: {})
202
+ new(
203
+ aofh_to_hofa(
204
+ aofh,
205
+ mapping: mapping,
206
+ transformers: transformers
207
+ )
208
+ )
209
+ end
210
+
211
+
212
+ def from_csv_file(source, mapping: {}, transformers: {})
213
+ aofh = []
214
+
215
+ CSV.foreach(source, headers: true) do |row|
216
+ aofh << row.to_h
217
+ end
218
+
219
+ from_aofh(aofh, mapping: mapping, transformers: transformers)
220
+ end
221
+
222
+
223
+ def from_json_file(source, mapping: {}, transformers: {})
224
+ aofh = JSON.parse(source.read)
225
+
226
+ from_aofh(aofh, mapping: mapping, transformers: transformers)
227
+ end
228
+
229
+
230
+ # aofh -- Array of Hashes
231
+ # hofa -- Hash of Arrays
232
+ def aofh_to_hofa(aofh, mapping: {}, transformers: {})
233
+ hofa = {}
234
+ keys = aofh.first.keys
235
+
236
+ keys.each do |key|
237
+ hofa[key] = []
238
+ end
239
+
240
+ aofh.each do |entry|
241
+ keys.each do |key|
242
+ hofa[key] << entry[key]
243
+ end
244
+ end
245
+
246
+ # SMELL: This might be necessary
247
+ normalize_keys(hofa, adapter_mapping: mapping)
248
+ end
249
+
250
+
251
+ def normalize_keys(hofa, adapter_mapping: {})
252
+ hofa = rename(adapter_mapping, hofa)
253
+ mapping = generate_mapping(hofa.keys)
254
+ rename(mapping, hofa)
255
+ end
256
+
257
+
258
+ def rename(mapping, hofa)
259
+ mapping.each_pair do |old_key, new_key|
260
+ hofa[new_key] = hofa.delete(old_key)
261
+ end
262
+
263
+ hofa
264
+ end
265
+
266
+
267
+ def generate_mapping(keys)
268
+ mapping = {}
269
+
270
+ keys.each do |key|
271
+ mapping[key] = underscore_key(sanitize_key(key)) unless key.is_a?(Symbol)
272
+ end
273
+
274
+ mapping
275
+ end
276
+
277
+
278
+ # returns a snake_case Symbol
279
+ def underscore_key(key)
280
+ key.to_s.gsub(/::/, '/').
281
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
282
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
283
+ tr("-", "_").
284
+ downcase.to_sym
285
+ end
286
+
287
+
288
+ # removes punctuation and specal characters,
289
+ # replaces space with underscore.
290
+ def sanitize_key(key)
291
+ key.tr('.():/','').gsub(/^\d+.?\s/, "").tr(' ','_')
292
+ end
293
+
294
+
295
+ # returns true if key is in a date format
296
+ # like 2023-06-03
297
+ def is_date?(key)
298
+ !/(\d{4}-\d{2}-\d{2})/.match(key.to_s).nil?
50
299
  end
51
300
  end
52
301
  end
302
+
data/lib/sqa/errors.rb CHANGED
@@ -1,6 +1,30 @@
1
1
  # lib/sqa/errors.rb
2
2
 
3
- module SQA
4
- # raised when an API contract is broken
5
- class BadParameterError < ArgumentError; end
3
+ # raised when a method is still in TODO state
4
+ class ApiError < RuntimeError
5
+ def self.raise(why)
6
+ puts "="*64
7
+ puts "== API Error"
8
+ puts why
9
+ puts
10
+ puts "Callback trace:"
11
+ puts caller
12
+ puts "="*64
13
+ super
14
+ end
6
15
  end
16
+
17
+ # raised when a method is still in TODO state
18
+ class NotImplemented < RuntimeError
19
+ def self.raise
20
+ puts "="*64
21
+ puts "== Not Yet Implemented"
22
+ puts "Callback trace:"
23
+ puts caller
24
+ puts "="*64
25
+ super
26
+ end
27
+ end
28
+
29
+ # raised when an API contract is broken
30
+ class BadParameterError < ArgumentError; end
data/lib/sqa/init.rb CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
3
  module SQA
4
4
  class << self
5
- @@config = nil
5
+ @@config = nil
6
+ @@av = ApiKeyManager::RateLimited.new(
7
+ api_keys: ENV['AV_API_KEYS'],
8
+ delay: true,
9
+ rate_count: ENV['AV_RATE_CNT'] || 5,
10
+ rate_period: ENV['AV_RATE_PER'] || 60
11
+ )
6
12
 
7
13
  # Initializes the SQA modules
8
14
  # returns the configuration
@@ -26,12 +32,11 @@ module SQA
26
32
 
27
33
  config.data_dir = homify(config.data_dir)
28
34
 
29
- Daru.lazy_update = config.lazy_update
30
- Daru.plotting_library = config.plotting_library
31
-
32
35
  config
33
36
  end
34
37
 
38
+ def av() = @@av
39
+
35
40
  def debug?() = @@config.debug?
36
41
  def verbose?() = @@config.verbose?
37
42
 
data/lib/sqa/stock.rb CHANGED
@@ -6,83 +6,143 @@
6
6
  # separate class and injected by the requiring program?
7
7
 
8
8
  class SQA::Stock
9
+ extend Forwardable
10
+
9
11
  CONNECTION = Faraday.new(url: "https://www.alphavantage.co")
10
12
 
11
- attr_accessor :company_name
12
- attr_accessor :df # The DataFrane
13
- attr_accessor :ticker
14
- attr_accessor :type # type of data store (default is CSV)
15
- attr_accessor :indicators
13
+ attr_accessor :data # General Info -- SQA::DataFrame::Data
14
+ attr_accessor :df # Historical Prices -- SQA::DataFrame::Data
15
+
16
+ attr_accessor :klass # class of historical and current prices
17
+ attr_accessor :transformers # procs for changing column values from String to Numeric
16
18
 
17
19
  def initialize(
18
20
  ticker:,
19
- source: :alpha_vantage,
20
- type: :csv
21
+ source: :alpha_vantage
21
22
  )
23
+
24
+ @ticker = ticker.downcase
25
+ @source = source
26
+
22
27
  raise "Invalid Ticker #{ticker}" unless SQA::Ticker.valid?(ticker)
23
28
 
24
- # TODO: Change API on lookup to return array instead of hash
25
- # Could this also incorporate the validation process to
26
- # save an additiona hash lookup?
29
+ @data_path = SQA.data_dir + "#{@ticker}.json"
30
+ @df_path = SQA.data_dir + "#{@ticker}.csv"
27
31
 
28
- entry = SQA::Ticker.lookup(ticker)
32
+ @klass = "SQA::DataFrame::#{@source.to_s.camelize}".constantize
33
+ @transformers = "SQA::DataFrame::#{@source.to_s.camelize}::TRANSFORMERS".constantize
29
34
 
30
- @ticker = ticker.downcase
31
- @company_name = entry[:name]
32
- @exchange = entry[:exchange]
33
- @klass = "SQA::DataFrame::#{source.to_s.camelize}".constantize
34
- @type = type
35
- @indicators = OpenStruct.new
35
+ if @data_path.exist?
36
+ load
37
+ else
38
+ create
39
+ update
40
+ save
41
+ end
36
42
 
37
43
  update_the_dataframe
38
44
  end
39
45
 
40
46
 
41
- def update_the_dataframe
42
- df1 = @klass.load(@ticker, type)
43
- df2 = @klass.recent(@ticker)
47
+ def load
48
+ @data = SQA::DataFrame::Data.new(
49
+ JSON.parse(@data_path.read)
50
+ )
51
+ end
44
52
 
45
- df1_nrows = df1.nrows
46
- @df = @klass.append(df1, df2)
47
53
 
48
- if @df.nrows > df1_nrows
49
- @df.send("to_#{@type}", SQA.data_dir + "#{ticker}.csv")
54
+ def create
55
+ @data =
56
+ SQA::DataFrame::Data.new(
57
+ {
58
+ ticker: @ticker,
59
+ source: @source,
60
+ indicators: { xyzzy: "Magic" },
61
+ }
62
+ )
63
+ end
64
+
65
+
66
+ def update
67
+ merge_overview
68
+ end
69
+
70
+
71
+ def save
72
+ @data_path.write @data.to_json
73
+ end
74
+
75
+
76
+ def_delegator :@data, :ticker, :ticker
77
+ def_delegator :@data, :name, :name
78
+ def_delegator :@data, :exchange, :exchange
79
+ def_delegator :@data, :source, :source
80
+ def_delegator :@data, :indicators, :indicators
81
+ def_delegator :@data, :indicators=, :indicators=
82
+ def_delegator :@data, :overview, :overview
83
+
84
+
85
+
86
+ def update_the_dataframe
87
+ if @df_path.exist?
88
+ @df = SQA::DataFrame.load(
89
+ source: @df_path,
90
+ transformers: @transformers
91
+ )
92
+ else
93
+ @df = klass.recent(@ticker, full: true)
94
+ @df.to_csv(@df_path)
95
+ return
50
96
  end
51
97
 
52
- # Adding a ticker vector in case I want to do
53
- # some multi-stock analysis in the same data frame.
54
- # For example to see how one stock coorelates with another.
55
- @df[:ticker] = @ticker
98
+ from_date = Date.parse(@df.timestamp.last) + 1
99
+ df2 = klass.recent(@ticker, from_date: from_date)
100
+
101
+ return if df2.nil? # CSV file is up to date.
102
+
103
+ df_nrows = @df.nrows
104
+ @df.append(df2)
105
+
106
+ if @df.nrows > df_nrows
107
+ @df.to_csv(file_path)
108
+ end
56
109
  end
57
110
 
111
+
58
112
  def to_s
59
113
  "#{ticker} with #{@df.size} data points from #{@df.timestamp.first} to #{@df.timestamp.last}"
60
114
  end
115
+ alias_method :inspect, :to_s
61
116
 
62
- # TODO: Turn this into a class Stock::Overview
63
- # which is a sub-class of Hashie::Dash
64
- def overview
65
- return @overview unless @overview.nil?
66
117
 
118
+ def merge_overview
67
119
  temp = JSON.parse(
68
- CONNECTION.get("/query?function=OVERVIEW&symbol=#{@ticker.upcase}&apikey=#{Nenv.av_api_key}")
120
+ CONNECTION.get("/query?function=OVERVIEW&symbol=#{ticker.upcase}&apikey=#{SQA.av.key}")
69
121
  .to_hash[:body]
70
122
  )
71
123
 
124
+ if temp.has_key? "Information"
125
+ ApiError.raise(temp["Information"])
126
+ end
127
+
72
128
  # TODO: CamelCase hash keys look common in Alpha Vantage
73
129
  # JSON; look at making a special Hashie-based class
74
130
  # to convert the keys to normal Ruby standards.
75
131
 
76
132
  temp2 = {}
77
133
 
78
- string_values = %w[ address asset_type cik country currency description dividend_date ex_dividend_date exchange fiscal_year_end industry latest_quarter name sector symbol ]
134
+ string_values = %w[ address asset_type cik country currency
135
+ description dividend_date ex_dividend_date
136
+ exchange fiscal_year_end industry latest_quarter
137
+ name sector symbol
138
+ ]
79
139
 
80
140
  temp.keys.each do |k|
81
141
  new_k = k.underscore
82
142
  temp2[new_k] = string_values.include?(new_k) ? temp[k] : temp[k].to_f
83
143
  end
84
144
 
85
- @overview = Hashie::Mash.new temp2
145
+ @data.overview = temp2
86
146
  end
87
147
 
88
148
 
@@ -100,7 +160,7 @@ class SQA::Stock
100
160
 
101
161
  a_hash = JSON.parse(
102
162
  CONNECTION.get(
103
- "/query?function=TOP_GAINERS_LOSERS&apikey=#{Nenv.av_api_key}"
163
+ "/query?function=TOP_GAINERS_LOSERS&apikey=#{SQA.av.key}"
104
164
  ).to_hash[:body]
105
165
  )
106
166
 
data/lib/sqa/strategy.rb CHANGED
@@ -8,7 +8,7 @@ class SQA::Strategy
8
8
  end
9
9
 
10
10
  def add(a_strategy)
11
- raise SQA::BadParameterError unless [Class, Method].include? a_strategy.class
11
+ raise BadParameterError unless [Class, Method].include? a_strategy.class
12
12
 
13
13
  a_proc = if Class == a_strategy.class
14
14
  a_strategy.method(:trade)
data/lib/sqa/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SQA
4
- VERSION = "0.0.15"
4
+ VERSION = "0.0.17"
5
5
 
6
6
  class << self
7
7
  def version
data/lib/sqa.rb CHANGED
@@ -17,9 +17,9 @@ end
17
17
  ## Additional Libraries
18
18
 
19
19
  require 'active_support/core_ext/string'
20
- require 'alphavantage' # TODO: add rate limiter to it
20
+ require 'alphavantage' # TODO: add rate limiter to it; ** PR submitted! **
21
+ require 'api_key_manager'
21
22
  require 'amazing_print'
22
- require 'daru' # TODO: Replace this gem with something better
23
23
  require 'descriptive_statistics'
24
24
  require 'faraday'
25
25
  require 'hashie'
@@ -38,7 +38,6 @@ require_relative "sqa/errors"
38
38
 
39
39
  require_relative 'sqa/init.rb'
40
40
 
41
- # require_relative "patches/daru" # TODO: extract Daru::DataFrame in new gem sqa-data_frame
42
41
 
43
42
  # TODO: Some of these components make direct calls to the
44
43
  # Alpha Vantage API. Convert them to use the
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sqa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.15
4
+ version: 0.0.17
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-25 00:00:00.000000000 Z
11
+ date: 2023-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 7.0.6
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 7.0.6
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: alphavantage
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: daru
42
+ name: api_key_manager
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -240,8 +240,8 @@ files:
240
240
  - checksums/sqa-0.0.11.gem.sha512
241
241
  - checksums/sqa-0.0.12.gem.sha512
242
242
  - checksums/sqa-0.0.13.gem.sha512
243
- - checksums/sqa-0.0.14.gem.sha512
244
243
  - checksums/sqa-0.0.15.gem.sha512
244
+ - checksums/sqa-0.0.17.gem.sha512
245
245
  - checksums/sqa-0.0.2.gem.sha512
246
246
  - checksums/sqa-0.0.3.gem.sha512
247
247
  - checksums/sqa-0.0.4.gem.sha512
@@ -276,14 +276,6 @@ files:
276
276
  - docs/stochastic_oscillator.md
277
277
  - docs/strategy.md
278
278
  - docs/true_range.md
279
- - lib/patches/daru.rb
280
- - lib/patches/daru/category.rb
281
- - lib/patches/daru/data_frame.rb
282
- - lib/patches/daru/plotting/svg-graph.rb
283
- - lib/patches/daru/plotting/svg-graph/category.rb
284
- - lib/patches/daru/plotting/svg-graph/dataframe.rb
285
- - lib/patches/daru/plotting/svg-graph/vector.rb
286
- - lib/patches/daru/vector.rb
287
279
  - lib/sqa.rb
288
280
  - lib/sqa/activity.rb
289
281
  - lib/sqa/analysis.rb
@@ -340,7 +332,8 @@ licenses:
340
332
  metadata:
341
333
  allowed_push_host: https://rubygems.org
342
334
  homepage_uri: https://github.com/MadBomber/sqa
343
- source_code_uri: https://github.com/MadBomber/sta
335
+ source_code_uri: https://github.com/MadBomber/sqa
336
+ changelog_uri: https://github.com/MadBomber/sqa
344
337
  post_install_message:
345
338
  rdoc_options: []
346
339
  require_paths:
@@ -356,7 +349,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
356
349
  - !ruby/object:Gem::Version
357
350
  version: '0'
358
351
  requirements: []
359
- rubygems_version: 3.4.19
352
+ rubygems_version: 3.4.20
360
353
  signing_key:
361
354
  specification_version: 4
362
355
  summary: sqa - Stock Qualitative Analysis
@@ -1 +0,0 @@
1
- ae291d1c8a3a80fc6f24a6a1194c6db1b6e1fbdbee586546ae34db6ee304a3e431ea59a154ea976af0f25f3b0d6519f2e0a1aad4ddf3c3cdf77b7d37aabf425f
@@ -1,19 +0,0 @@
1
- # lib/patches/daru/category.rb
2
-
3
- module Daru
4
- module Category
5
-
6
- def plotting_lig lib
7
- if :svg_graph = lib
8
- @plotting_library = lib
9
- if Daru.send("has_#{lib}?".to_sym)
10
- extend Module.const_get(
11
- "Daru::Plotting::Category::#{lib.to_s.capitalize}Library"
12
- )
13
- end
14
- else
15
- super
16
- end
17
- end
18
- end
19
- end
@@ -1,19 +0,0 @@
1
- # lib/patches/daru/data_frame.rb
2
-
3
- module Daru
4
- module DataFrame
5
-
6
- def plotting_lig lib
7
- if :svg_graph = lib
8
- @plotting_library = lib
9
- if Daru.send("has_#{lib}?".to_sym)
10
- extend Module.const_get(
11
- "Daru::Plotting::DataFrame::#{lib.to_s.capitalize}Library"
12
- )
13
- end
14
- else
15
- super
16
- end
17
- end
18
- end
19
- end
@@ -1,55 +0,0 @@
1
- # lib/patches/daru/plotting/svg-graph/category.rb
2
-
3
- # NOTE: Code originally from Gruff
4
- # TODO: Tailor the code to SvgGraph
5
-
6
- module Daru
7
- module Plotting
8
- module Category
9
- module SvgGraphLibrary
10
- def plot opts={}
11
- type = opts[:type] || :bar
12
- size = opts[:size] || 500
13
- case type
14
- when :bar, :pie, :sidebar
15
- plot = send("category_#{type}_plot".to_sym, size, opts[:method])
16
- else
17
- raise ArgumentError, 'This type of plot is not supported.'
18
- end
19
- yield plot if block_given?
20
- plot
21
- end
22
-
23
- private
24
-
25
- def category_bar_plot size, method
26
- plot = SvgGraph::Bar.new size
27
- method ||= :count
28
- dv = frequencies(method)
29
- plot.labels = size.times.to_a.zip(dv.index.to_a).to_h
30
- plot.data name || :vector, dv.to_a
31
- plot
32
- end
33
-
34
- def category_pie_plot size, method
35
- plot = SvgGraph::Pie.new size
36
- method ||= :count
37
- frequencies(method).each_with_index do |data, index|
38
- plot.data index, data
39
- end
40
- plot
41
- end
42
-
43
- def category_sidebar_plot size, method
44
- plot = SvgGraph::SideBar.new size
45
- plot.labels = {0 => (name.to_s || 'vector')}
46
- method ||= :count
47
- frequencies(method).each_with_index do |data, index|
48
- plot.data index, data
49
- end
50
- plot
51
- end
52
- end
53
- end
54
- end
55
- end
@@ -1,105 +0,0 @@
1
- # lib/patches/daru/plotting/svg-graph/dataframe.rb
2
-
3
- # NOTE: Code originally from Gruff
4
- # TODO: Tailor the code to SvgGraph
5
-
6
- module Daru
7
- module Plotting
8
- module DataFrame
9
- module SvgGraphLibrary
10
- def plot opts={}
11
- opts[:type] ||= :line
12
- opts[:size] ||= 500
13
-
14
- x = extract_x_vector opts[:x]
15
- y = extract_y_vectors opts[:y]
16
-
17
- opts[:type] = process_type opts[:type], opts[:categorized]
18
-
19
- type = opts[:type]
20
-
21
- if %o[line bar scatter].include? type
22
- graph = send("#{type}_plot", size, x, y)
23
-
24
- elsif :scatter_categorized == type
25
- graph = scatter_with_category(size, x, y, opts[:categorized])
26
-
27
- else
28
- raise ArgumentError, 'This type of plot is not supported.'
29
- end
30
-
31
- yield graph if block_given?
32
- graph
33
- end
34
-
35
- private
36
-
37
- def process_type type, categorized
38
- type == :scatter && categorized ? :scatter_categorized : type
39
- end
40
-
41
- ##########################################################
42
- def line_plot size, x, y
43
- plot = SvgGraph::Line.new size
44
- plot.labels = size.times.to_a.zip(x).to_h
45
- y.each do |vec|
46
- plot.data vec.name || :vector, vec.to_a
47
- end
48
- plot
49
- end
50
-
51
- ##########################################################
52
- def bar_plot size, x, y
53
- plot = SvgGraph::Bar.new size
54
- plot.labels = size.times.to_a.zip(x).to_h
55
- y.each do |vec|
56
- plot.data vec.name || :vector, vec.to_a
57
- end
58
- plot
59
- end
60
-
61
- ##########################################################
62
- def scatter_plot size, x, y
63
- plot = SvgGraph::Scatter.new size
64
- y.each do |vec|
65
- plot.data vec.name || :vector, x, vec.to_a
66
- end
67
- plot
68
- end
69
-
70
- ##########################################################
71
- def scatter_with_category size, x, y, opts
72
- x = Daru::Vector.new x
73
- y = y.first
74
- plot = SvgGraph::Scatter.new size
75
- cat_dv = self[opts[:by]]
76
-
77
- cat_dv.categories.each do |cat|
78
- bools = cat_dv.eq cat
79
- plot.data cat, x.where(bools).to_a, y.where(bools).to_a
80
- end
81
-
82
- plot
83
- end
84
-
85
- def extract_x_vector x_name
86
- x_name && self[x_name].to_a || index.to_a
87
- end
88
-
89
- def extract_y_vectors y_names
90
- y_names =
91
- case y_names
92
- when nil
93
- vectors.to_a
94
- when Array
95
- y_names
96
- else
97
- [y_names]
98
- end
99
-
100
- y_names.map { |y| self[y] }.select(&:numeric?)
101
- end
102
- end
103
- end
104
- end
105
- end
@@ -1,102 +0,0 @@
1
- # lib/patches/daru/plotting/svg-graph/vector.rb
2
-
3
- # NOTE: Code originally from Gruff
4
- # TODO: Tailor the code to SvgGraph
5
-
6
- module Daru
7
- module Plotting
8
- module Vector
9
- module SvgGraphLibrary
10
- def plot opts={}
11
- opts[:type] ||= :line
12
- opts[:size] ||= 500 # SMELL: What is this?
13
- opts[:height] ||= 720
14
- opts[:width] ||= 720
15
- opts[:title] ||= name || :vector
16
-
17
- debug_me{[
18
- :opts,
19
- :self
20
- ]}
21
-
22
- if %i[line bar pie scatter sidebar].include? type
23
- graph = send("#{type}_plot", opts)
24
- else
25
- raise ArgumentError, 'This type of plot is not supported.'
26
- end
27
-
28
- yield graph if block_given?
29
-
30
- graph
31
- end
32
-
33
- private
34
-
35
- ####################################################
36
- def line_plot opts={}
37
- graph = SVG::Graph::Line.new opts
38
-
39
- graph.add_data(
40
- data: to_a,
41
- title: opts[:title]
42
- )
43
-
44
- graph
45
- end
46
-
47
-
48
- ####################################################
49
- def bar_plot opts
50
- graph = SVG::Graph::Bar.new opts
51
-
52
- graph.add_data(
53
- data: to_a,
54
- title: opts[:title]
55
- )
56
-
57
- graph
58
- end
59
-
60
-
61
- ####################################################
62
- def pie_plot opts
63
- graph = SVG::Graph::Pie.new opts
64
-
65
- graph.add_data(
66
- data: to_a,
67
- title: opts[:title]
68
- )
69
-
70
- graph
71
- end
72
-
73
-
74
- ####################################################
75
- def scatter_plot size
76
- graph = SVG::Graph::Plot.new opts
77
-
78
-
79
- graph.add_data(
80
- data: to_a.zip(index.to_a)
81
- title: opts[:title]
82
- )
83
-
84
- graph
85
- end
86
-
87
-
88
- ####################################################
89
- def sidebar_plot size
90
- graph = SVG::Graph::BarHorizontal.new opts
91
-
92
- graph.add_data(
93
- data: to_a,
94
- title: opts[:title]
95
- )
96
-
97
- graph
98
- end
99
- end
100
- end
101
- end
102
- end
@@ -1,6 +0,0 @@
1
- # lib/patches/daru/plotting/svg-graph.rb
2
-
3
-
4
- require_relative 'SvgGraph/category.rb'
5
- require_relative 'SvgGraph/vector.rb'
6
- require_relative 'SvgGraph/dataframe.rb'
@@ -1,19 +0,0 @@
1
- # lib/patches/daru/vector.rb
2
-
3
- module Daru
4
- module Vector
5
-
6
- def plotting_lig lib
7
- if :svg_graph = lib
8
- @plotting_library = lib
9
- if Daru.send("has_#{lib}?".to_sym)
10
- extend Module.const_get(
11
- "Daru::Plotting::Vector::#{lib.to_s.capitalize}Library"
12
- )
13
- end
14
- else
15
- super
16
- end
17
- end
18
- end
19
- end
data/lib/patches/daru.rb DELETED
@@ -1,19 +0,0 @@
1
- # lib/patches/daru.rb
2
-
3
- require_relative 'daru/category'
4
- require_relative 'daru/data_frame'
5
- require_relative 'daru/vector'
6
-
7
- module Daru
8
- create_has_library :svg_graph
9
-
10
- class << self
11
- def plotting_library lib
12
- if :svg_graph = lib
13
- @plotting_library = lib
14
- else
15
- super
16
- end
17
- end
18
- end
19
- end