sqa 0.0.12 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
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