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.
- checksums.yaml +7 -0
- data/.envrc +4 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +29 -0
- data/Rakefile +4 -0
- data/bin/sqa +6 -0
- data/checksums/sqa-0.0.1.gem.sha512 +1 -0
- data/docs/requirements.md +40 -0
- data/lib/sqa/activity.rb +10 -0
- data/lib/sqa/cli.rb +310 -0
- data/lib/sqa/datastore/active_record.rb +89 -0
- data/lib/sqa/datastore/csv/yahoo_finance.rb +51 -0
- data/lib/sqa/datastore/csv.rb +93 -0
- data/lib/sqa/datastore/sqlite.rb +7 -0
- data/lib/sqa/datastore.rb +6 -0
- data/lib/sqa/errors.rb +5 -0
- data/lib/sqa/indicator/README.md +33 -0
- data/lib/sqa/indicator/average_true_range.md +9 -0
- data/lib/sqa/indicator/average_true_range.rb +30 -0
- data/lib/sqa/indicator/bollinger_bands.md +15 -0
- data/lib/sqa/indicator/bollinger_bands.rb +28 -0
- data/lib/sqa/indicator/candlestick_pattern_recognizer.md +4 -0
- data/lib/sqa/indicator/candlestick_pattern_recognizer.rb +60 -0
- data/lib/sqa/indicator/classify_market_profile.md +4 -0
- data/lib/sqa/indicator/classify_market_profile.rb +33 -0
- data/lib/sqa/indicator/donchian_channel.md +5 -0
- data/lib/sqa/indicator/donchian_channel.rb +29 -0
- data/lib/sqa/indicator/double_top_bottom_pattern.md +3 -0
- data/lib/sqa/indicator/double_top_bottom_pattern.rb +34 -0
- data/lib/sqa/indicator/ema_analysis.md +19 -0
- data/lib/sqa/indicator/ema_analysis.rb +70 -0
- data/lib/sqa/indicator/fibonacci_retracement.md +3 -0
- data/lib/sqa/indicator/fibonacci_retracement.rb +25 -0
- data/lib/sqa/indicator/head_and_shoulders_pattern.md +3 -0
- data/lib/sqa/indicator/head_and_shoulders_pattern.rb +26 -0
- data/lib/sqa/indicator/identify_wave_condition.md +6 -0
- data/lib/sqa/indicator/identify_wave_condition.rb +40 -0
- data/lib/sqa/indicator/mean_reversion.md +8 -0
- data/lib/sqa/indicator/mean_reversion.rb +37 -0
- data/lib/sqa/indicator/momentum.md +19 -0
- data/lib/sqa/indicator/momentum.rb +26 -0
- data/lib/sqa/indicator/moving_average_convergence_divergence.md +23 -0
- data/lib/sqa/indicator/moving_average_convergence_divergence.rb +25 -0
- data/lib/sqa/indicator/relative_strength_index.md +6 -0
- data/lib/sqa/indicator/relative_strength_index.md.rb +47 -0
- data/lib/sqa/indicator/simple_moving_average.md +8 -0
- data/lib/sqa/indicator/simple_moving_average.rb +21 -0
- data/lib/sqa/indicator/simple_moving_average_trend.rb +31 -0
- data/lib/sqa/indicator/stochastic_oscillator.md +5 -0
- data/lib/sqa/indicator/stochastic_oscillator.rb +39 -0
- data/lib/sqa/indicator/true_range.md +12 -0
- data/lib/sqa/indicator/true_range.rb +37 -0
- data/lib/sqa/indicator.rb +11 -0
- data/lib/sqa/protfolio.rb +8 -0
- data/lib/sqa/stock.rb +29 -0
- data/lib/sqa/version.rb +5 -0
- data/lib/sqa.rb +12 -0
- 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
data/CHANGELOG.md
ADDED
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
data/bin/sqa
ADDED
@@ -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.
|
data/lib/sqa/activity.rb
ADDED
@@ -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
|
+
|