sqa 0.0.1

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +4 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE +21 -0
  5. data/README.md +29 -0
  6. data/Rakefile +4 -0
  7. data/bin/sqa +6 -0
  8. data/checksums/sqa-0.0.1.gem.sha512 +1 -0
  9. data/docs/requirements.md +40 -0
  10. data/lib/sqa/activity.rb +10 -0
  11. data/lib/sqa/cli.rb +310 -0
  12. data/lib/sqa/datastore/active_record.rb +89 -0
  13. data/lib/sqa/datastore/csv/yahoo_finance.rb +51 -0
  14. data/lib/sqa/datastore/csv.rb +93 -0
  15. data/lib/sqa/datastore/sqlite.rb +7 -0
  16. data/lib/sqa/datastore.rb +6 -0
  17. data/lib/sqa/errors.rb +5 -0
  18. data/lib/sqa/indicator/README.md +33 -0
  19. data/lib/sqa/indicator/average_true_range.md +9 -0
  20. data/lib/sqa/indicator/average_true_range.rb +30 -0
  21. data/lib/sqa/indicator/bollinger_bands.md +15 -0
  22. data/lib/sqa/indicator/bollinger_bands.rb +28 -0
  23. data/lib/sqa/indicator/candlestick_pattern_recognizer.md +4 -0
  24. data/lib/sqa/indicator/candlestick_pattern_recognizer.rb +60 -0
  25. data/lib/sqa/indicator/classify_market_profile.md +4 -0
  26. data/lib/sqa/indicator/classify_market_profile.rb +33 -0
  27. data/lib/sqa/indicator/donchian_channel.md +5 -0
  28. data/lib/sqa/indicator/donchian_channel.rb +29 -0
  29. data/lib/sqa/indicator/double_top_bottom_pattern.md +3 -0
  30. data/lib/sqa/indicator/double_top_bottom_pattern.rb +34 -0
  31. data/lib/sqa/indicator/ema_analysis.md +19 -0
  32. data/lib/sqa/indicator/ema_analysis.rb +70 -0
  33. data/lib/sqa/indicator/fibonacci_retracement.md +3 -0
  34. data/lib/sqa/indicator/fibonacci_retracement.rb +25 -0
  35. data/lib/sqa/indicator/head_and_shoulders_pattern.md +3 -0
  36. data/lib/sqa/indicator/head_and_shoulders_pattern.rb +26 -0
  37. data/lib/sqa/indicator/identify_wave_condition.md +6 -0
  38. data/lib/sqa/indicator/identify_wave_condition.rb +40 -0
  39. data/lib/sqa/indicator/mean_reversion.md +8 -0
  40. data/lib/sqa/indicator/mean_reversion.rb +37 -0
  41. data/lib/sqa/indicator/momentum.md +19 -0
  42. data/lib/sqa/indicator/momentum.rb +26 -0
  43. data/lib/sqa/indicator/moving_average_convergence_divergence.md +23 -0
  44. data/lib/sqa/indicator/moving_average_convergence_divergence.rb +25 -0
  45. data/lib/sqa/indicator/relative_strength_index.md +6 -0
  46. data/lib/sqa/indicator/relative_strength_index.md.rb +47 -0
  47. data/lib/sqa/indicator/simple_moving_average.md +8 -0
  48. data/lib/sqa/indicator/simple_moving_average.rb +21 -0
  49. data/lib/sqa/indicator/simple_moving_average_trend.rb +31 -0
  50. data/lib/sqa/indicator/stochastic_oscillator.md +5 -0
  51. data/lib/sqa/indicator/stochastic_oscillator.rb +39 -0
  52. data/lib/sqa/indicator/true_range.md +12 -0
  53. data/lib/sqa/indicator/true_range.rb +37 -0
  54. data/lib/sqa/indicator.rb +11 -0
  55. data/lib/sqa/protfolio.rb +8 -0
  56. data/lib/sqa/stock.rb +29 -0
  57. data/lib/sqa/version.rb +5 -0
  58. data/lib/sqa.rb +12 -0
  59. metadata +105 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '09d2bf9cf5dcb3a0c106047638789e49da98682e1ac59a63bf994c2be48f7b8c'
4
+ data.tar.gz: befdcb0e74e149214a7b4e91b4450525b64ff86212a4a9d5b2d8ded07490b237
5
+ SHA512:
6
+ metadata.gz: 885cbffeca7bdd5a083b2742813b1a544d0340c66caee7f67c736509d6b3ba68f88a1d8defa22f764ccf5b4411145c70f7a61d16740f5f0fde9cd5c2c511a267
7
+ data.tar.gz: c37113e0c517e6d45ad30785017928ebd9c4ab2558f47e7d198e1d971a597f1a79fa2b4ab595026831d452b736ffc7afb3fa12f4d0c5e549bce38410cd2e5e3e
data/.envrc ADDED
@@ -0,0 +1,4 @@
1
+ # .envrc
2
+ # brew install direnv
3
+
4
+ export RR=`pwd`
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.0.1] - 2023-08-10
4
+
5
+ - Planting the Flag
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Dewayne VanHoozer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # SQA - Simple Qualitative Analysis
2
+
3
+ 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.
4
+
5
+ The BUY/SELL signals that it generates are part of a game. **DO NOT USE** when real money is at stake.
6
+
7
+ ## Installation
8
+
9
+ Install the gem and add to the application's Gemfile by executing:
10
+
11
+ $ bundle add sqa
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ $ gem install sqa
16
+
17
+ ## Usage
18
+
19
+ Do not use!
20
+
21
+ ## Contributing
22
+
23
+ I can always use some help on this stuff. Got an idea for a new metric or analysis? Want to improve the math? Make the signals better? Let's collaborate!
24
+
25
+ Bug reports and pull requests are welcome on GitHub at https://github.com/MadBomber/sqa.
26
+
27
+ ## License
28
+
29
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
data/bin/sqa ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sqa'
4
+ require 'sqa/cli'
5
+
6
+ SQA::CLI.new.run
@@ -0,0 +1 @@
1
+ 424c002903602ed17878b821d45c90126fb95f62f0b221c2600aa8c6f2a3084aaa97d4eb5f50a657781ea0b93a0104778b737f9ac1cca2d8488f48a5f48f3a68
@@ -0,0 +1,40 @@
1
+ # Requirements
2
+
3
+ ... otherwise know as what I want to do. Some people would call it a roadmap; but, where I'm going "we don't need no stinking roads!"
4
+
5
+ Yes, test driven development (TDD) is important. There is a place for TDD. Its not in prototyping. The prototype is used to figure out what the requirements are. Once you have a prototype then you have requirements. With requirements come contracts for APIs. The contracts drive the test specifications which in turn drives the design.
6
+
7
+ So what is it that I want to do?
8
+
9
+ * collect technical analysis indicators to apply to a stock or a set of stocks.
10
+ * define an abstraction for a stock
11
+ * evaluate different trading strategies.
12
+ * make billions on the sock market - if you have kids you know what the sock market is.
13
+ * play around with some interesting gems
14
+ * evaluate Ruby 3.3 YJIT against Crystal
15
+ * look at carious ways to support plugin components
16
+ * learn something about options trading in risk mitigation for security trades
17
+
18
+ ## Making this thing an Application Framework
19
+
20
+ * using ActiveRecord with initial models of Stock, Portfolio and Activity
21
+
22
+ - Portfolio has many stocks with FK: ticker
23
+ - Sotkc has many activities with FK: ticker
24
+ - Activity has unique constraint on (ticker, date)
25
+
26
+ * using gem csv_importer to bring in data to load into the various tables
27
+ * using sqlite3 because I have limited resources for rdbms
28
+
29
+ ## finance.yahoo.com API
30
+
31
+ v7 is used to download historical data as CSV. It requires a cookie.
32
+
33
+ v8 gets some company info and stock prices in JSON. It might require a cookie as well
34
+
35
+ Most reliable way of getting data is the scrape the website. The gem financial_data_pull attempts to do it but it is too old.
36
+
37
+
38
+ ## Extract Indicators
39
+
40
+ After sleeping on it, I think the original plan with the fin_tech gem is a better idea for how to package the indicators. I'm going to keep the name FinTech for now while I think of something better. These are indicators; but I want them to be class-level methods with established contracts in their API.
@@ -0,0 +1,10 @@
1
+ # lib/sqa/activity.rb
2
+
3
+ # Historical daily stock activity
4
+ # primary id is [ticker, date]
5
+
6
+ class SQA::Activity < ActiveRecord::Base
7
+ # belongs_to :stock using ticker as the foreign key
8
+ # need a unique constraint on [ticker, date]
9
+ # should date be saved as a Date object or string?
10
+ end
data/lib/sqa/cli.rb ADDED
@@ -0,0 +1,310 @@
1
+ # lib/sqa/cli.rb
2
+
3
+ require_relative 'stock'
4
+
5
+ module SQA
6
+ class CLI
7
+ def initialize
8
+ @args = $ARGV.dup
9
+ end
10
+
11
+ def run
12
+ stock = Stock.new('aapl')
13
+
14
+ puts <<~OUTPUT
15
+
16
+ TBD
17
+ @args => #{@args}
18
+ stock => #{stock}
19
+
20
+ OUTPUT
21
+ end
22
+ end
23
+ end
24
+
25
+ __END__
26
+
27
+ ###################################################
28
+ ## This is the old thing that got me started ...
29
+
30
+ #!/usr/bin/env ruby
31
+ # experiments/stocks/analysis.rb
32
+ #
33
+ # Some technical indicators from FinTech gem
34
+ #
35
+ # optional date CLI option in format YYYY-mm-dd
36
+ # if not present uses Date.today
37
+ #
38
+
39
+
40
+ INVEST = 1000.00
41
+
42
+ require 'pathname'
43
+
44
+ require_relative 'stock'
45
+ require_relative 'datastore'
46
+
47
+
48
+ STOCKS = Pathname.pwd + "stocks.txt"
49
+ TRADES = Pathname.pwd + "trades.csv"
50
+
51
+ TRADES_FILE = File.open(TRADES, 'a')
52
+
53
+ unless STOCKS.exist?
54
+ puts
55
+ puts "ERROR: The 'stocks.txt' file does not exist."
56
+ puts
57
+ exot(-1)
58
+ end
59
+
60
+ require 'debug_me'
61
+ include DebugMe
62
+
63
+ require 'csv'
64
+ require 'date'
65
+ require 'tty-table'
66
+
67
+ require 'fin_tech'
68
+ require 'previous_dow'
69
+
70
+ class NilClass
71
+ def blank?() = true
72
+ end
73
+
74
+ class String
75
+ def blank?() = strip().empty?
76
+ end
77
+
78
+ class Array
79
+ def blank?() = empty?
80
+ end
81
+
82
+
83
+ def tickers
84
+ return @tickers unless @tickers.blank?
85
+
86
+ @tickers = []
87
+
88
+ STOCKS.readlines.each do |a_line|
89
+ ticker_symbol = a_line.chomp.strip.split()&.first&.downcase
90
+ next if ticker_symbol.blank? || '#' == ticker_symbol
91
+ @tickers << ticker_symbol unless @tickers.include?(ticker_symbol)
92
+ end
93
+
94
+ @tickers.sort!
95
+ end
96
+
97
+ given_date = ARGV.first ? Date.parse(ARGV.first) : Date.today
98
+
99
+ start_date = Date.new(2019, 1, 1)
100
+ end_date = previous_dow(:friday, given_date)
101
+
102
+ ASOF = end_date.to_s.tr('-','')
103
+
104
+
105
+ #######################################################################
106
+ # download a CSV file from https://query1.finance.yahoo.com
107
+ # given a stock ticker symbol as a String
108
+ # start and end dates
109
+ #
110
+ # For ticker "aapl" the downloaded file will be named "aapl.csv"
111
+ # That filename will be renamed to "aapl_YYYYmmdd.csv" where the
112
+ # date suffix is the end_date of the historical data.
113
+ #
114
+ def download_historical_prices(ticker, start_date, end_date)
115
+ data_path = Pathname.pwd + "#{ticker}_#{ASOF}.csv"
116
+ return if data_path.exist?
117
+
118
+ mew_path = Pathname.pwd + "#{ticker}.csv"
119
+
120
+ start_timestamp = start_date.to_time.to_i
121
+ end_timestamp = end_date.to_time.to_i
122
+ ticker_upcase = ticker.upcase
123
+ filename = "#{ticker.downcase}.csv"
124
+
125
+ `curl -o #{filename} "https://query1.finance.yahoo.com/v7/finance/download/#{ticker_upcase}?period1=#{start_timestamp}&period2=#{end_timestamp}&interval=1d&events=history&includeAdjustedClose=true"`
126
+
127
+ mew_path.rename data_path
128
+ end
129
+
130
+
131
+ #######################################################################
132
+ # Read the CSV file associated with the give ticker symbol
133
+ # and the ASOF date.
134
+ #
135
+ def read_csv(ticker)
136
+ filename = "#{ticker.downcase}_#{ASOF}.csv"
137
+ data = []
138
+
139
+ CSV.foreach(filename, headers: true) do |row|
140
+ data << row.to_h
141
+ end
142
+
143
+ data
144
+ end
145
+
146
+ ##########################
147
+ # record a recommend trade
148
+
149
+ def trade(ticker, signal, shares, price)
150
+ TRADES_FILE.puts "#{ticker},#{ASOF},#{signal},#{shares},#{price}"
151
+ end
152
+
153
+ #######################################################################
154
+ ###
155
+ ## Main
156
+ #
157
+
158
+
159
+ tickers.each do |ticker|
160
+ download_historical_prices(ticker, start_date, end_date)
161
+ end
162
+
163
+ result = {}
164
+
165
+ mwfd = 14 # moving_window_forcast_days
166
+
167
+ headers = %w[ Ticker AdjClose Trend Slope M'tum RSI Analysis MACD Target Signal $]
168
+ values = []
169
+
170
+ tickers.each do |ticker|
171
+
172
+ data = read_csv ticker
173
+ prices = data.map{|r| r["Adj Close"].to_f}
174
+ volumes = data.map{|r| r["volume"].to_f}
175
+
176
+ if data.blank?
177
+ puts
178
+ puts "ERROR: cannot get data for #{ticker}"
179
+ puts
180
+ next
181
+ end
182
+
183
+
184
+ result[ticker] = {
185
+ date: data.last["Date"],
186
+ adj_close: data.last["Adj Close"].to_f
187
+ }
188
+
189
+ result[ticker][:market] = FinTech.classify_market_profile(
190
+ volumes.last(mwfd),
191
+ prices.last(mwfd),
192
+ prices.last(mwfd).first,
193
+ prices.last
194
+ )
195
+
196
+ fr = FinTech.fibonacci_retracement( prices.last(mwfd).first,
197
+ prices.last).map{|x| x.round(3)}
198
+
199
+
200
+ puts "\n#{result[ticker][:market]} .. #{ticker}\t#{fr}"
201
+ print "\t"
202
+ print FinTech.head_and_shoulders_pattern?(prices.last(mwfd))
203
+ print "\t"
204
+ print FinTech.double_top_bottom_pattern?(prices.last(mwfd))
205
+ print "\t"
206
+ mr = FinTech.mean_reversion?(prices, mwfd, 0.5)
207
+ print mr
208
+
209
+ if mr
210
+ print "\t"
211
+ print FinTech.mr_mean(prices, mwfd).round(3)
212
+ end
213
+
214
+ print "\t"
215
+ print FinTech.identify_wave_condition?(prices, 2*mwfd, 1.0)
216
+ puts
217
+
218
+ print "\t"
219
+ print FinTech.ema_analysis(prices, mwfd).except(:ema_values)
220
+
221
+ puts
222
+
223
+ row = [ ticker ]
224
+
225
+ # result[ticker][:moving_averages] = FinTech.sma(data, mwfd)
226
+ result[ticker][:trend] = FinTech.sma_trend(data, mwfd)
227
+ result[ticker][:momentum] = FinTech.momentum(prices, mwfd)
228
+ result[ticker][:rsi] = FinTech.rsi(data, mwfd)
229
+ result[ticker][:bollinger_bands] = FinTech.bollinger_bands(data, mwfd, 2)
230
+ result[ticker][:macd] = FinTech.macd(data, mwfd, 2*mwfd, mwfd/2)
231
+
232
+ price = result[ticker][:adj_close].round(3)
233
+
234
+ row << price
235
+ row << result[ticker][:trend][:trend]
236
+ row << result[ticker][:trend][:angle].round(3)
237
+ row << result[ticker][:momentum].round(3)
238
+ row << result[ticker][:rsi][:rsi].round(3)
239
+ row << result[ticker][:rsi][:meaning]
240
+ row << result[ticker][:macd].first.round(3)
241
+ row << result[ticker][:macd].last.round(3)
242
+
243
+ analysis = result[ticker][:rsi][:meaning]
244
+
245
+ signal = ""
246
+ macd_diff = result[ticker][:macd].first
247
+ target = result[ticker][:macd].last
248
+ current = result[ticker][:adj_close]
249
+
250
+ trend_down = "down" == result[ticker][:trend][:trend]
251
+
252
+ if current < target
253
+ signal = "buy" unless "Over Bought" == analysis
254
+ elsif (current > target) && trend_down
255
+ signal = "sell" unless "Over Sold" == analysis
256
+ end
257
+
258
+ if "buy" == signal
259
+ pps = target - price
260
+ shares = INVEST.to_i / price.to_i
261
+ upside = (shares * pps).round(2)
262
+ trade(ticker, signal, shares, price)
263
+ elsif "sell" == signal
264
+ pps = target - price
265
+ shares = INVEST.to_i / price.to_i
266
+ upside = (shares * pps).round(2)
267
+ trade(ticker, signal, shares, price)
268
+ else
269
+ upside = ""
270
+ end
271
+
272
+ row << signal
273
+ row << upside
274
+
275
+ values << row
276
+ end
277
+
278
+ # debug_me{[
279
+ # :result
280
+ # ]}
281
+
282
+
283
+ the_table = TTY::Table.new(headers, values)
284
+
285
+ puts
286
+ puts "Analysis as of Friday Close: #{end_date}"
287
+
288
+ puts the_table.render(
289
+ :unicode,
290
+ {
291
+ padding: [0, 0, 0, 0],
292
+ alignments: [
293
+ :left, # ticker
294
+ :right, # adj close
295
+ :center, # trend
296
+ :right, # slope
297
+ :right, # momentum
298
+ :right, # rsi
299
+ :center, # meaning / analysis
300
+ :right, # macd
301
+ :right, # target
302
+ :center, # signal
303
+ :right # upside
304
+ ],
305
+ }
306
+ )
307
+ puts
308
+
309
+ TRADES_FILE.close
310
+
@@ -0,0 +1,89 @@
1
+ # lib/sqa/datastore/active_record.rb
2
+
3
+
4
+ require 'active_record'
5
+ require 'sqlite3'
6
+
7
+
8
+ module SQA::Datastore
9
+ class ActiveRecord
10
+ def initialize(ticker); end
11
+ end
12
+ end
13
+
14
+
15
+ __END__
16
+
17
+ # An example of how to use active record with sqlite3 ...
18
+
19
+ #!/usr/bin/env ruby
20
+ # See: https://gist.github.com/unnitallman/944011
21
+
22
+ require 'active_record'
23
+ require 'sqlite3'
24
+
25
+ ActiveRecord::Base.logger = Logger.new(STDERR)
26
+ # TDV ActiveRecord::Base.colorize_logging = false
27
+
28
+ ActiveRecord::Base.establish_connection(
29
+ adapter: "sqlite3",
30
+ database: './database.db'
31
+ )
32
+
33
+ ActiveRecord::Schema.define do
34
+ create_table :albums do |table|
35
+ table.column :title, :string
36
+ table.column :performer, :string
37
+ end
38
+
39
+ create_table :tracks do |table|
40
+ table.column :album_id, :integer
41
+ table.column :track_number, :integer
42
+ table.column :title, :string
43
+ end
44
+ end
45
+
46
+ class Album < ActiveRecord::Base
47
+ has_many :tracks
48
+ end
49
+
50
+ class Track < ActiveRecord::Base
51
+ belongs_to :album
52
+ end
53
+
54
+ album = Album.create(:title => 'Black and Blue',
55
+ :performer => 'The Rolling Stones')
56
+ album.tracks.create(:track_number => 1, :title => 'Hot Stuff')
57
+ album.tracks.create(:track_number => 2, :title => 'Hand Of Fate')
58
+ album.tracks.create(:track_number => 3, :title => 'Cherry Oh Baby ')
59
+ album.tracks.create(:track_number => 4, :title => 'Memory Motel ')
60
+ album.tracks.create(:track_number => 5, :title => 'Hey Negrita')
61
+ album.tracks.create(:track_number => 6, :title => 'Fool To Cry')
62
+ album.tracks.create(:track_number => 7, :title => 'Crazy Mama')
63
+ album.tracks.create(:track_number => 8,
64
+ :title => 'Melody (Inspiration By Billy Preston)')
65
+
66
+ album = Album.create(:title => 'Sticky Fingers',
67
+ :performer => 'The Rolling Stones')
68
+ album.tracks.create(:track_number => 1, :title => 'Brown Sugar')
69
+ album.tracks.create(:track_number => 2, :title => 'Sway')
70
+ album.tracks.create(:track_number => 3, :title => 'Wild Horses')
71
+ album.tracks.create(:track_number => 4,
72
+ :title => 'Can\'t You Hear Me Knocking')
73
+ album.tracks.create(:track_number => 5, :title => 'You Gotta Move')
74
+ album.tracks.create(:track_number => 6, :title => 'Bitch')
75
+ album.tracks.create(:track_number => 7, :title => 'I Got The Blues')
76
+ album.tracks.create(:track_number => 8, :title => 'Sister Morphine')
77
+ album.tracks.create(:track_number => 9, :title => 'Dead Flowers')
78
+ album.tracks.create(:track_number => 10, :title => 'Moonlight Mile')
79
+
80
+ puts Album.find(1).tracks.length
81
+ puts Album.find(2).tracks.length
82
+
83
+ puts Album.find_by_title('Sticky Fingers').title
84
+ puts Track.find_by_title('Fool To Cry').album_id
85
+
86
+
87
+
88
+
89
+
@@ -0,0 +1,51 @@
1
+ # lib/sqa/datastore/csv/yahoo_finance.rb
2
+
3
+ # processes a CSV file downloaded from finance.yahoo.com
4
+ # Date,Open,High,Low,Close,Adj Close,Volume
5
+
6
+ require 'csv-importer'
7
+
8
+ class SQA::Datastore::CSV::YahooFinance
9
+ include CSVImporter
10
+
11
+ model ::SQA::Activity # an active record like model
12
+
13
+ column :date, to: ->(x) { Date.parse(x)}, required: true
14
+ column :open, to: ->(x) {x.to_f}, required: true
15
+ column :high, to: ->(x) {x.to_f}, required: true
16
+ column :low, to: ->(x) {x.to_f}, required: true
17
+ column :close, to: ->(x) {x.to_f}, required: true
18
+ column :adj_close, to: ->(x) {x.to_f}, required: true
19
+ column :volumn, to: ->(x) {x.to_i}, required: true
20
+
21
+ # TODO: make the identifier compound [ticker, date]
22
+ # so we can put all the data into a single table.
23
+
24
+ identifier :date
25
+
26
+ when_invalid :skip # or :abort
27
+
28
+
29
+ column :email, to: ->(email) { email.downcase }, required: true
30
+ column :first_name, as: [ /first.?name/i, /pr(é|e)nom/i ]
31
+ column :last_name, as: [ /last.?name/i, "nom" ]
32
+ column :published, to: ->(published, user) { user.published_at = published ? Time.now : nil }
33
+
34
+
35
+
36
+
37
+ def self.load(ticker)
38
+ import = new(file: "#{ticker.upcase}.csv")
39
+
40
+ import.valid_header? # => false
41
+ import.report.message # => "The following columns are required: email"
42
+
43
+ # Assuming the header was valid, let's run the import!
44
+
45
+ import.run!
46
+ import.report.success? # => true
47
+ import.report.message # => "Import completed. 4 created, 2 updated, 1 failed to update" end
48
+
49
+ end
50
+ end
51
+
@@ -0,0 +1,93 @@
1
+ # lib/sqa/datastore/csv.rb
2
+
3
+ require 'csv'
4
+ require 'forwardable'
5
+
6
+ module SQA::Datastore
7
+ class CSV
8
+ extend Forwardable
9
+ def_delegators :@data, :first, :last, :size, :empty?, :[], :map, :select, :reject
10
+
11
+ SOURCE_DOMAIN = "https://query1.finance.yahoo.com/v7/finance/download/"
12
+ # curl -o AAPL.csv -L --url "https://query1.finance.yahoo.com/v7/finance/download/AAPL?period1=345427200&period2=1691712000&interval=1d&events=history&includeAdjustedClose=true"
13
+
14
+
15
+ attr_accessor :ticker
16
+ attr_accessor :data
17
+
18
+ def initialize(ticker, adapter = YahooFinance)
19
+ @ticker = ticker
20
+ @data_path = Pathname.pwd + "#{ticker.downcase}.csv"
21
+ @adapter = adapter
22
+ @data = adapter.load(ticker)
23
+ end
24
+
25
+
26
+ #######################################################################
27
+ # Read the CSV file associated with the give ticker symbol
28
+ #
29
+ # def read_csv_data
30
+ # download_historical_prices unless @data_path.exist?
31
+
32
+ # csv_data = []
33
+
34
+ # ::CSV.foreach(@data_path, headers: true) do |row|
35
+ # csv_data << row.to_h
36
+ # end
37
+
38
+ # csv_data
39
+ # end
40
+
41
+ #######################################################################
42
+ # download a CSV file from https://query1.finance.yahoo.com
43
+ # given a stock ticker symbol as a String
44
+ # start and end dates
45
+ #
46
+ # For ticker "aapl" the downloaded file will be named "aapl.csv"
47
+ # That filename will be renamed to "aapl_YYYYmmdd.csv" where the
48
+ # date suffix is the end_date of the historical data.
49
+ #
50
+ # def download_historical_prices(
51
+ # start_date: Date.new(2019, 1, 1),
52
+ # end_date: previous_dow(:friday, Date.today)
53
+ # )
54
+
55
+ # start_timestamp = start_date.to_time.to_i # Convert to unix timestamp
56
+ # end_timestamp = end_date.to_time.to_i
57
+
58
+ # user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0"
59
+
60
+ # # TODO: replace curl with Faraday
61
+
62
+ # `curl -A "#{user_agent}" --cookie-jar cookies.txt -o #{@data_path} -L --url "#{SOURCE_DOMAIN}/#{ticker.upcase}?period1=#{start_timestamp}&period2=#{end_timestamp}&interval=1d&events=history&includeAdjustedClose=true"`
63
+
64
+ # check_csv_file
65
+ # end
66
+
67
+
68
+ # def check_csv_file
69
+ # f = File.open(@data_path, 'r')
70
+ # c1 = f.read(1)
71
+
72
+ # if '{' == c1
73
+ # error_msg = JSON.parse("#{c1}#{f.read}")
74
+ # raise "Not OK: #{error_msg}"
75
+ # end
76
+ # end
77
+ end
78
+ end
79
+
80
+ __END__
81
+
82
+ {
83
+ "finance": {
84
+ "error": {
85
+ "code": "Unauthorized",
86
+ "description": "Invalid cookie"
87
+ }
88
+ }
89
+ }
90
+
91
+
92
+
93
+
@@ -0,0 +1,7 @@
1
+ # lib/sqa/datastore/sqlite.rb
2
+
3
+ module Datastore
4
+ class Sqlite
5
+ def initialize(ticker); end
6
+ end
7
+ end