sqa 0.0.1

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