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