sqa 0.0.12 → 0.0.14

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: 39185fb1deccd8cc248d4ad776b724ea5ebe1a7e93fdc1d6ada4f53661a55bba
4
- data.tar.gz: 3b394f997cdcdd67ec9eced2cd7bc08c1af2b407b8945e1a76e0383591ca3495
3
+ metadata.gz: 41df647860d7465185b8f069d1fb51edcb540d4114cfd56c898e3373426e0bb8
4
+ data.tar.gz: 130b1023a4530a645ea90bf323c16dd4fb22a887222e073209b8edbfeeb6731f
5
5
  SHA512:
6
- metadata.gz: 3d805f6db11375738db0cdad8385f7a74c26c3fba39f0a7aa88593532e1127af7c4c690844eae56f6203696743e1b655bb9d996f2e71745cefb4f04227bbe622
7
- data.tar.gz: c52327bfbf15f25159a14af31be0881112e169dc73dbdab7d7c7ac7519208e366314e4ab6bd31d44134fbda5238dc7b56e553dcc8c7d231938b78af3ec17d137
6
+ metadata.gz: b28c7bf4231e3ced4046e83f5afd256a0dc155500493b8d2c3cee43695e8ef203534a0642dc4028830f3d068653d6a9016a785b3343611a18209cb1cfb6f4694
7
+ data.tar.gz: 20ee19fe13781e473953ed4fcae73ce8e687ce1808fc74fa02d834364b7b02df14c7dab4cc0390efbae0f48328d71f27aab96318fad28052588d7b623abf4fe3
@@ -0,0 +1 @@
1
+ 524fd82ea9501f1b1b1a3bf3e0f1a500e675d3f4664d342b8b377995f310c1e40a597f61d8e0bf8d2fa8dd83202f7a415b517df371dbf370c25f59fb0fb42d96
@@ -0,0 +1 @@
1
+ c5eea4fb74e6ab7b7b1715a81598585ec540a3eb1e9b902ca339ff0a2ba9cc7624ae3f43e00b5fbbad1d3d510ed938e8f91154c0bee2e14d6503564b1b0ccfb1
data/lib/sqa/cli.rb CHANGED
@@ -140,6 +140,7 @@ module SQA
140
140
 
141
141
  elsif params[:config_file]
142
142
  # Override the defaults <- envars <- config file content
143
+ params[:config_file] = SQA.homify params[:config_file]
143
144
  SQA.config.config_file = params[:config_file]
144
145
  SQA.config.from_file
145
146
  end
@@ -0,0 +1,155 @@
1
+ # lib/sqa/data_frame/alpha_vantage.rb
2
+ # frozen_string_literal: true
3
+ #
4
+ # Using the Alpha Vantage JSON interface
5
+ #
6
+
7
+ require 'faraday'
8
+ require 'json'
9
+
10
+ class SQA::DataFrame < Daru::DataFrame
11
+ class AlphaVantage
12
+ API_KEY = Nenv.av_api_key
13
+ CONNECTION = Faraday.new(url: 'https://www.alphavantage.co')
14
+ HEADERS = YahooFinance::HEADERS
15
+
16
+ # The Alpha Vantage headers are being remapped so that
17
+ # they match those of the Yahoo Finance CSV file.
18
+ #
19
+ HEADER_MAPPING = {
20
+ "date" => HEADERS[0],
21
+ "open" => HEADERS[1],
22
+ "high" => HEADERS[2],
23
+ "low" => HEADERS[3],
24
+ "close" => HEADERS[4],
25
+ "adjusted_close" => HEADERS[5],
26
+ "volume" => HEADERS[6]
27
+ }
28
+
29
+
30
+ ################################################################
31
+ # Load a Dataframe from a csv file
32
+ def self.load(ticker, type="csv")
33
+ filepath = SQA.data_dir + "#{ticker}.#{type}"
34
+
35
+ if filepath.exist?
36
+ df = normalize_vector_names SQA::DataFrame.load(ticker, type)
37
+ else
38
+ df = recent(ticker, full: true)
39
+ df.send("to_#{type}",filepath)
40
+ end
41
+
42
+ df
43
+ end
44
+
45
+
46
+ # Normalize the vector (aka column) names as
47
+ # symbols using the standard names set by
48
+ # Yahoo Finance ... since it was the first one
49
+ # not because its anything special.
50
+ #
51
+ def self.normalize_vector_names(df)
52
+ headers = df.vectors.to_a
53
+
54
+ # convert vector names to symbols
55
+ # when they are strings. They become stings
56
+ # when the data frame is saved to a CSV file
57
+ # and then loaded back in.
58
+
59
+ if headers.first == HEADERS.first.to_s
60
+ a_hash = {}
61
+ HEADERS.each {|k| a_hash[k.to_s] = k}
62
+ df.rename_vectors(a_hash) # renames from String to Symbol
63
+ else
64
+ df.rename_vectors(HEADER_MAPPING)
65
+ end
66
+
67
+ df
68
+ end
69
+
70
+
71
+ # Get recent data from JSON API
72
+ #
73
+ # ticker String the security to retrieve
74
+ # returns a DataFrame
75
+ #
76
+ # NOTE: The function=TIME_SERIES_DAILY_ADJUSTED
77
+ # is not a free API endpoint from Alpha Vantange.
78
+ # So we are just using the free API endpoint
79
+ # function=TIME_SERIES_DAILY
80
+ # This means that we are not getting the
81
+ # real adjusted closing price. To sync
82
+ # the columns with those from Yahoo Finance
83
+ # we are duplicating the unadjusted clossing price
84
+ # and adding that to the data frame as if it were
85
+ # adjusted.
86
+ #
87
+ def self.recent(ticker, full: false)
88
+ # NOTE: Using the CSV format because the JSON format has
89
+ # really silly key values. The column names for the
90
+ # CSV format are much better.
91
+ response = CONNECTION.get(
92
+ "/query?" +
93
+ "function=TIME_SERIES_DAILY&" +
94
+ "symbol=#{ticker.upcase}&" +
95
+ "apikey=#{API_KEY}&" +
96
+ "datatype=csv&" +
97
+ "outputsize=#{full ? 'full' : 'compact'}"
98
+ ).to_hash
99
+
100
+ unless 200 == response[:status]
101
+ raise "Bad Response: #{response[:status]}"
102
+ end
103
+
104
+ raw = response[:body].split
105
+
106
+ headers = raw.shift.split(',')
107
+ headers[0] = 'date' # website returns "timestamp" but that
108
+ # has an unintended side-effect when
109
+ # the names are normalized.
110
+
111
+ close_inx = headers.size - 2
112
+ adj_close_inx = close_inx + 1
113
+
114
+ headers.insert(adj_close_inx, 'adjusted_close')
115
+
116
+ data = raw.map do |e|
117
+ e2 = e.split(',')
118
+ e2[1..-2] = e2[1..-2].map(&:to_f) # converting open, high, low, close
119
+ e2[-1] = e2[-1].to_i # converting volumn
120
+ e2.insert(adj_close_inx, e2[close_inx]) # duplicate the close price as a fake adj close price
121
+ headers.zip(e2).to_h
122
+ end
123
+
124
+ # What oldest data first in the data frame
125
+ normalize_vector_names Daru::DataFrame.new(data.reverse)
126
+ end
127
+
128
+
129
+ # Append update_df rows to the base_df
130
+ #
131
+ # base_df is ascending on timestamp
132
+ # update_df is descending on timestamp
133
+ #
134
+ # base_df content came from CSV file downloaded
135
+ # from Yahoo Finance.
136
+ #
137
+ # update_df came from scraping the webpage
138
+ # at Yahoo Finance for the recent history.
139
+ #
140
+ # Returns a combined DataFrame.
141
+ #
142
+ def self.append(base_df, updates_df)
143
+ last_timestamp = Date.parse base_df.timestamp.last
144
+ filtered_df = updates_df.filter_rows { |row| Date.parse(row[:timestamp]) > last_timestamp }
145
+
146
+ last_inx = filtered_df.size - 1
147
+
148
+ (0..last_inx).each do |x|
149
+ base_df.add_row filtered_df.row[last_inx-x]
150
+ end
151
+
152
+ base_df
153
+ end
154
+ end
155
+ end
@@ -59,6 +59,9 @@ class SQA::DataFrame < Daru::DataFrame
59
59
  response = CONNECTION.get("/quote/#{ticker.upcase}/history")
60
60
  doc = Nokogiri::HTML(response.body)
61
61
  table = doc.css('table').first
62
+
63
+ raise "NoDataError" if table.nil?
64
+
62
65
  rows = table.css('tbody tr')
63
66
 
64
67
  data = []
@@ -2,17 +2,19 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative 'data_frame/yahoo_finance'
5
+ require_relative 'data_frame/alpha_vantage'
5
6
 
6
7
  class Daru::DataFrame
7
8
 
8
9
  def to_csv(path_to_file, opts={})
9
10
  options = {
11
+ headers: true,
10
12
  converters: :numeric
11
13
  }.merge(opts)
12
14
 
13
15
  writer = ::CSV.open(path_to_file, 'wb')
14
16
 
15
- writer << vectors.to_a unless options[:headers] == false
17
+ writer << vectors.to_a if options[:headers]
16
18
 
17
19
  each_row do |row|
18
20
  writer << if options[:convert_comma]
@@ -31,27 +33,20 @@ end
31
33
 
32
34
  class SQA::DataFrame < Daru::DataFrame
33
35
 
34
-
35
36
  #################################################
37
+ def self.load(ticker, type="csv", options={}, &block)
38
+ source = SQA.data_dir + "#{ticker}.#{type}"
36
39
 
37
- def self.path(filename)
38
- Pathname.new(SQA.config.data_dir) + filename
39
- end
40
-
41
- def self.load(filename, options={}, &block)
42
- source = path(filename)
43
- type = source.extname.downcase
44
-
45
- if ".csv" == type
40
+ if :csv == type
46
41
  from_csv(source, options={}, &block)
47
- elsif ".json" == type
42
+ elsif :json == type
48
43
  from_json(source, options={}, &block)
49
- elsif %w[.txt .dat].include?(type)
44
+ elsif %i[txt dat].include?(type)
50
45
  from_plaintext(source, options={}, &block)
51
- elsif ".xls" == type
46
+ elsif :xls == type
52
47
  from_excel(source, options={}, &block)
53
48
  else
54
- raise SQA::BadParamenterError, "un-suppod file type: #{type}"
49
+ raise SQA::BadParameterError, "un-supported file type: #{type}"
55
50
  end
56
51
  end
57
52
  end
data/lib/sqa/stock.rb CHANGED
@@ -1,17 +1,40 @@
1
1
  # lib/sqa/stock.rb
2
2
 
3
+ require 'active_support/core_ext/string' # for String#underscore
4
+ require 'hashie' # for Hashie::Mash
5
+
6
+
7
+ # SMELL: SQA::Stock is now pretty coupled to the Alpha Vantage
8
+ # API service. Should that stuff be extracted into a
9
+ # separate class and injected by the requiring program?
10
+
3
11
  class SQA::Stock
12
+ CONNECTION = Faraday.new(url: "https://www.alphavantage.co")
13
+
4
14
  attr_accessor :company_name
5
15
  attr_accessor :df # The DataFrane
6
16
  attr_accessor :ticker
17
+ attr_accessor :type # type of data store (default is CSV)
7
18
  attr_accessor :indicators
8
19
 
9
- def initialize(ticker:, source: :yahoo_finance, type: :csv)
20
+ def initialize(
21
+ ticker:,
22
+ source: :alpha_vantage,
23
+ type: :csv
24
+ )
25
+ raise "Invalid Ticker #{ticker}" unless SQA::Ticker.valid?(ticker)
26
+
27
+ # TODO: Change API on lookup to return array instead of hash
28
+ # Could this also incorporate the validation process to
29
+ # save an additiona hash lookup?
30
+
31
+ entry = SQA::Ticker.lookup(ticker)
32
+
10
33
  @ticker = ticker.downcase
11
- @company_name = "Company Name"
34
+ @company_name = entry[:name]
35
+ @exchange = entry[:exchange]
12
36
  @klass = "SQA::DataFrame::#{source.to_s.camelize}".constantize
13
37
  @type = type
14
- @filename = "#{@ticker}.#{type}"
15
38
  @indicators = OpenStruct.new
16
39
 
17
40
  update_the_dataframe
@@ -19,24 +42,91 @@ class SQA::Stock
19
42
 
20
43
 
21
44
  def update_the_dataframe
22
- df1 = @klass.load(@filename)
45
+ df1 = @klass.load(@ticker, type)
23
46
  df2 = @klass.recent(@ticker)
24
- @df = @klass.append(df1, df2)
25
47
 
26
- if @df.nrows > df1.nrows
27
- @df.send("to_#{@type}", SQA::DataFrame.path(@filename))
48
+ df1_nrows = df1.nrows
49
+ @df = @klass.append(df1, df2)
50
+
51
+ if @df.nrows > df1_nrows
52
+ @df.send("to_#{@type}", SQA.data_dir + "#{ticker}.csv")
28
53
  end
29
54
 
30
55
  # Adding a ticker vector in case I want to do
31
56
  # some multi-stock analysis in the same data frame.
57
+ # For example to see how one stock coorelates with another.
32
58
  @df[:ticker] = @ticker
33
59
  end
34
60
 
35
61
  def to_s
36
62
  "#{ticker} with #{@df.size} data points from #{@df.timestamp.first} to #{@df.timestamp.last}"
37
63
  end
38
- end
39
64
 
40
- __END__
65
+ # TODO: Turn this into a class Stock::Overview
66
+ # which is a sub-class of Hashie::Dash
67
+ def overview
68
+ return @overview unless @overview.nil?
69
+
70
+ temp = JSON.parse(
71
+ CONNECTION.get("/query?function=OVERVIEW&symbol=#{@ticker.upcase}&apikey=#{Nenv.av_api_key}")
72
+ .to_hash[:body]
73
+ )
74
+
75
+ # TODO: CamelCase hash keys look common in Alpha Vantage
76
+ # JSON; look at making a special Hashie-based class
77
+ # to convert the keys to normal Ruby standards.
41
78
 
42
- aapl = Stock.new('aapl', SQA::Datastore::CSV)
79
+ temp2 = {}
80
+
81
+ 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 ]
82
+
83
+ temp.keys.each do |k|
84
+ new_k = k.underscore
85
+ temp2[new_k] = string_values.include?(new_k) ? temp[k] : temp[k].to_f
86
+ end
87
+
88
+ @overview = Hashie::Mash.new temp2
89
+ end
90
+
91
+
92
+ #############################################
93
+ ## Class Methods
94
+
95
+ class << self
96
+ @@top = nil
97
+
98
+ # Top Gainers, Losers and Most Active for most
99
+ # recent closed trading day.
100
+ #
101
+ def top
102
+ return @@top unless @@top.nil?
103
+
104
+ mash = Hashie::Mash.new(
105
+ JSON.parse(
106
+ CONNECTION.get(
107
+ "/query?function=TOP_GAINERS_LOSERS&apikey=#{Nenv.av_api_key}"
108
+ ).to_hash[:body]
109
+ )
110
+ )
111
+
112
+ keys = mash.top_gainers.first.keys
113
+
114
+ %w[top_gainers top_losers most_actively_traded].each do |collection|
115
+ mash.send(collection).each do |e|
116
+ keys.each do |k|
117
+ case k
118
+ when 'ticker'
119
+ # Leave it as a String
120
+ when 'volume'
121
+ e[k] = e[k].to_i
122
+ else
123
+ e[k] = e[k].to_f
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ @@top = mash
130
+ end
131
+ end
132
+ end
data/lib/sqa/ticker.rb ADDED
@@ -0,0 +1,68 @@
1
+ # sqa/lib/sqa/ticker.rb
2
+ #
3
+ # Uses the https://dumbstockapi.com/ website to download a CSV file
4
+ #
5
+ # The CSV files have names like this:
6
+ # "dumbstockapi-2023-09-21T16 39 55.165Z.csv"
7
+ #
8
+ # which has this header:
9
+ # ticker,name,is_etf,exchange
10
+ #
11
+ # Not using the is_etf columns
12
+ #
13
+ class SQA::Ticker
14
+ FILENAME_PREFIX = "dumbstockapi"
15
+ CONNECTION = Faraday.new(url: "https://dumbstockapi.com")
16
+ @@data = {}
17
+
18
+
19
+ def self.download(country="US")
20
+ response = CONNECTION.get("/stock?format=csv&countries=#{country.upcase}").to_hash
21
+
22
+ if 200 == response[:status]
23
+ filename = response[:response_headers]["content-disposition"].split('=').last.gsub('"','')
24
+ out_path = Pathname.new(SQA.config.data_dir) + filename
25
+ out_path.write response[:body]
26
+ end
27
+
28
+ response[:status]
29
+ end
30
+
31
+
32
+ def self.load
33
+ tries = 0
34
+ found = false
35
+
36
+ until(found || tries >= 3) do
37
+ files = Pathname.new(SQA.config.data_dir).children.select{|c| c.basename.to_s.start_with?(FILENAME_PREFIX)}.sort
38
+ if files.empty?
39
+ download
40
+ tries += 1
41
+ else
42
+ found = true
43
+ end
44
+ end
45
+
46
+ raise "NoDataError" if files.empty?
47
+
48
+ load_from_csv files.last
49
+ end
50
+
51
+
52
+ def self.load_from_csv(csv_path)
53
+ CSV.foreach(csv_path, headers: true) do |row|
54
+ @@data[row["ticker"]] = {
55
+ name: row["name"],
56
+ exchange: row["exchange"]
57
+ }
58
+ end
59
+
60
+ @@data
61
+ end
62
+
63
+
64
+
65
+ def self.data = @@data.empty? ? load : @@data
66
+ def self.lookup(ticker) = data[ticker.upcase]
67
+ def self.valid?(ticker) = data.has_key?(ticker.upcase)
68
+ end
data/lib/sqa/version.rb CHANGED
@@ -4,7 +4,7 @@ require 'sem_version'
4
4
  require 'sem_version/core_ext'
5
5
 
6
6
  module SQA
7
- VERSION = "0.0.12"
7
+ VERSION = "0.0.14"
8
8
 
9
9
  class << self
10
10
  def version
data/lib/sqa.rb CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'active_support'
5
5
  require 'active_support/core_ext/string'
6
+ require 'amazing_print'
6
7
  require 'daru'
7
8
  require 'date'
8
9
  require 'descriptive_statistics'
@@ -10,6 +11,7 @@ require 'nenv'
10
11
  require 'pathname'
11
12
 
12
13
  require_relative "sqa/version"
14
+ require_relative "sqa/errors"
13
15
 
14
16
 
15
17
  unless defined?(HOME)
@@ -21,6 +23,9 @@ module SQA
21
23
  class << self
22
24
  @@config = nil
23
25
 
26
+ # Initializes the SQA modules
27
+ # returns the configuration
28
+ #
24
29
  def init(argv=ARGV)
25
30
  if argv.is_a? String
26
31
  argv = argv.split()
@@ -34,41 +39,28 @@ module SQA
34
39
  CLI.run(argv)
35
40
  else
36
41
  # There are no real command line parameters
37
- # because the sqa gem is be required within
42
+ # because the sqa gem is being required within
38
43
  # the context of a larger program.
39
44
  end
40
45
 
46
+ config.data_dir = homify(config.data_dir)
47
+
41
48
  Daru.lazy_update = config.lazy_update
42
49
  Daru.plotting_library = config.plotting_library
43
50
 
44
- if config.debug? || config.verbose?
45
- debug_me{[
46
- :config
47
- ]}
48
- end
49
-
50
- nil
51
+ config
51
52
  end
52
53
 
53
- def homify(filepath)
54
- filepath.gsub(/^~/, Nenv.home)
55
- end
54
+ def debug?() = @@config.debug?
55
+ def verbose?() = @@config.verbose?
56
56
 
57
- def config
58
- @@config
59
- end
57
+ def homify(filepath) = filepath.gsub(/^~/, Nenv.home)
58
+ def data_dir() = Pathname.new(config.data_dir)
59
+ def config() = @@config
60
60
 
61
61
  def config=(an_object)
62
62
  @@config = an_object
63
63
  end
64
-
65
- def debug?
66
- @@config.debug?
67
- end
68
-
69
- def verbose?
70
- @@config.verbose?
71
- end
72
64
  end
73
65
  end
74
66
 
@@ -77,9 +69,9 @@ end
77
69
  require_relative "sqa/config"
78
70
  require_relative "sqa/constants"
79
71
  require_relative "sqa/data_frame"
80
- require_relative "sqa/errors"
81
72
  require_relative "sqa/indicator"
82
73
  require_relative "sqa/portfolio"
83
74
  require_relative "sqa/strategy"
84
75
  require_relative "sqa/stock"
76
+ require_relative "sqa/ticker"
85
77
  require_relative "sqa/trade"
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.12
4
+ version: 0.0.14
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-20 00:00:00.000000000 Z
11
+ date: 2023-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -281,6 +281,8 @@ files:
281
281
  - checksums/sqa-0.0.10.gem.sha512
282
282
  - checksums/sqa-0.0.11.gem.sha512
283
283
  - checksums/sqa-0.0.12.gem.sha512
284
+ - checksums/sqa-0.0.13.gem.sha512
285
+ - checksums/sqa-0.0.14.gem.sha512
284
286
  - checksums/sqa-0.0.2.gem.sha512
285
287
  - checksums/sqa-0.0.3.gem.sha512
286
288
  - checksums/sqa-0.0.4.gem.sha512
@@ -329,6 +331,7 @@ files:
329
331
  - lib/sqa/config.rb
330
332
  - lib/sqa/constants.rb
331
333
  - lib/sqa/data_frame.rb
334
+ - lib/sqa/data_frame/alpha_vantage.rb
332
335
  - lib/sqa/data_frame/yahoo_finance.rb
333
336
  - lib/sqa/errors.rb
334
337
  - lib/sqa/indicator.rb
@@ -366,6 +369,7 @@ files:
366
369
  - lib/sqa/strategy/random.rb
367
370
  - lib/sqa/strategy/rsi.rb
368
371
  - lib/sqa/strategy/sma.rb
372
+ - lib/sqa/ticker.rb
369
373
  - lib/sqa/trade.rb
370
374
  - lib/sqa/version.rb
371
375
  - lib/sqa/web.rb