sqa 0.0.15 → 0.0.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +170 -32
- data/checksums/sqa-0.0.17.gem.sha512 +1 -0
- data/checksums/sqa-0.0.18.gem.sha512 +1 -0
- data/lib/sqa/data_frame/alpha_vantage.rb +24 -69
- data/lib/sqa/data_frame/yahoo_finance.rb +4 -46
- data/lib/sqa/data_frame.rb +282 -32
- data/lib/sqa/errors.rb +27 -3
- data/lib/sqa/init.rb +9 -4
- data/lib/sqa/stock.rb +96 -36
- data/lib/sqa/strategy.rb +1 -1
- data/lib/sqa/version.rb +1 -1
- data/lib/sqa.rb +4 -4
- metadata +23 -29
- data/checksums/sqa-0.0.14.gem.sha512 +0 -1
- data/lib/patches/daru/category.rb +0 -19
- data/lib/patches/daru/data_frame.rb +0 -19
- data/lib/patches/daru/plotting/svg-graph/category.rb +0 -55
- data/lib/patches/daru/plotting/svg-graph/dataframe.rb +0 -105
- data/lib/patches/daru/plotting/svg-graph/vector.rb +0 -102
- data/lib/patches/daru/plotting/svg-graph.rb +0 -6
- data/lib/patches/daru/vector.rb +0 -19
- data/lib/patches/daru.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7733f5cb84c4ccbad5e68105e818b35f1aca450fd97eeb3186cfb9a1962d44e
|
4
|
+
data.tar.gz: a9b2bf2a0fa6a91de1367fd73beac6a28d263024ded37c216520120cc3181cc9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 27280c675c37a92f450d5108c40848cc65a3975e8e783ca25cbbee0bfa18b1c570d697c4c33d4fd73d84c57d717df3e6cbb8667c51fb3ed8fe48df509f9d1f7e
|
7
|
+
data.tar.gz: 475333a102814f5bef21dbea61126bf6b0b61836cccb6a333acc2dbcaa2ba5bc14c8c9191b31065777ca984fcec883fb37da36b07a9121f2afeb0a0004bc841b
|
data/README.md
CHANGED
@@ -1,18 +1,13 @@
|
|
1
1
|
# SQA - Simple Qualitative Analysis
|
2
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.
|
3
|
+
This is a very simplistic set of tools for running technical analysis (quantitative and qualitative) on a stock portfolio. Simplistic means it is not reliable nor intended for any kind of mission-critical 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
4
|
|
5
|
-
The BUY/SELL signals that it generates
|
5
|
+
The BUY/SELL signals that it generates should not be taken seriously. **DO NOT USE** this library when real money is at stake. If you lose your shirt playing in the stock market don't come crying to me. I think playing in the market is like playing in the street. You are going to get run over.
|
6
6
|
|
7
7
|
## This is a Work in Progress
|
8
8
|
|
9
|
-
I
|
9
|
+
I am experimenting with different gems to support various functionality. Sometimes they do not work out well. For example I've gone through two different gems to implement the data frame capability. Neither did what I wanted so I ended up creating my own data frame class based upon the old tried and true Hashie library.
|
10
10
|
|
11
|
-
### DARU or RedAmber
|
12
|
-
|
13
|
-
I'm just really using `daru` for its data frame object; However, I just learned about the RedAmber data frame object in Ruby based off of Apache Arrow. I'm going to look at that product since it is actively maintained.
|
14
|
-
|
15
|
-
https://github.com/red-data-tools/red_amber
|
16
11
|
|
17
12
|
## Installation
|
18
13
|
|
@@ -24,62 +19,200 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
24
19
|
|
25
20
|
gem install sqa
|
26
21
|
|
27
|
-
|
22
|
+
### Semantic Versioning
|
28
23
|
|
29
|
-
|
24
|
+
```ruby
|
25
|
+
SQA.version # returns SemVersion object
|
26
|
+
exit(1) unless SQA.version >= SemVersion("1.0.0")
|
27
|
+
```
|
30
28
|
|
31
|
-
|
29
|
+
## Usage
|
32
30
|
|
33
|
-
|
31
|
+
**Do not use!** but its okay to play with.
|
34
32
|
|
35
|
-
|
33
|
+
`SQA` can be used from the command line or as a library in your own application.
|
34
|
+
|
35
|
+
`SQA` has a command line component.
|
36
|
+
|
37
|
+
```plaintext
|
38
|
+
$ sqa --help
|
39
|
+
Stock Quantitative Analysis (SQA)
|
40
|
+
|
41
|
+
Usage: sqa [analysis|web] [OPTIONS]
|
42
|
+
|
43
|
+
A collection of things
|
44
|
+
|
45
|
+
Options:
|
46
|
+
-c, --config string Path to the config file
|
47
|
+
--data-dir string Set the directory for the SQA data
|
48
|
+
-d, --debug Turn on debugging output
|
49
|
+
--dump-config path_to_file Dump the current configuration
|
50
|
+
-h, --help Print usage
|
51
|
+
-l, --log_level string Set the log level (debug, info, warn, error,
|
52
|
+
fatal)
|
53
|
+
-p, --portfolio string Set the filename of the portfolio
|
54
|
+
-t, --trades string Set the filename into which trades are
|
55
|
+
stored
|
56
|
+
-v, --verbose Print verbosely
|
57
|
+
--version Print version
|
58
|
+
|
59
|
+
Examples:
|
60
|
+
sqa -c ~/.sqa.yml -p portfolio.csv -t trades.csv --data-dir ~/sqa_data
|
61
|
+
|
62
|
+
Optional Command Available:
|
63
|
+
analysis - Provide an Analysis of a Portfolio
|
64
|
+
web - Run a web server
|
65
|
+
|
66
|
+
WARNING: This is a toy, a play thing, not intended for serious use.
|
67
|
+
```
|
68
|
+
### Setup a Config File
|
69
|
+
|
70
|
+
You will need to create a directory to store the `sqa` data and a configuration file. You can start by doing this:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
gem install sqa
|
74
|
+
mkdir ~/Documents/sqa_data
|
75
|
+
sqa --data-dir ~/Documents/sqa_data --dump-config ~/.sqa.yml
|
76
|
+
```
|
77
|
+
|
78
|
+
By default `SQA` looks for a configuration file named `.sqa.yml` in the current directory. If it does not find one there it looks in the home directory. You can use the `--config` CLI option to specify a path to your custom config file name.
|
79
|
+
|
80
|
+
|
81
|
+
### AlphaVantage
|
82
|
+
|
83
|
+
`SQA` makes use of the `AlphaVantage` API to get some stock related information. You will need an API key in order to use this functionality. They have a free rate limited API key which allows 5 accesses per second; total of 100 accesses in a day. If you are doing more than that you are not playing an ought to purchase one of there serious API key plans.
|
84
|
+
|
85
|
+
[https://www.alphavantage.co/](https://www.alphavantage.co/)
|
36
86
|
|
37
|
-
**Do not use!**
|
38
87
|
|
39
88
|
## Playing in IRB
|
40
89
|
|
41
|
-
You can play around in IRB with the SQA framework
|
90
|
+
You can play around in IRB with the SQA framework.
|
91
|
+
|
42
92
|
|
43
93
|
### With Stocks and Indicators
|
44
94
|
|
45
|
-
You will need some CSV files.
|
95
|
+
You will need some CSV files. If you ask for a stock to which you have not existing historical price data in a CSV file, `SQA` can use either Alpha Vantage or Yahoo Finance to get some data. I like Alpha Vantage better because it has a well defined and documented API. Yahoo Finance on the other hand does not. You can manually download historical stock price data from Yahoo Finance into you `sqa data directory`
|
96
|
+
|
97
|
+
Historical price data is kept in the `SQA.data_dir` in a CSV file whose name is all lowercase. If you download the CSV file for the stock symbol "AAPL" it should be saved in you `SQA.data_dir` as `aapl.csv`
|
46
98
|
|
47
99
|
#### Get Historical Prices
|
48
100
|
|
49
|
-
Go to https::/finance.yahoo.com and down some historical price data for your favorite stocks. Put those CSV files
|
101
|
+
Go to https::/finance.yahoo.com and down some historical price data for your favorite stocks. Put those CSV files into the `SQA.data_dir`.
|
50
102
|
|
51
103
|
You may need to create a `portfolio.csv` file or you may not. TODO
|
52
104
|
|
53
|
-
The CSV files will be named by the stock's ticker symbol. For example:
|
105
|
+
The CSV files will be named by the stock's ticker symbol. For example: `aapl.csv`
|
106
|
+
|
107
|
+
### Playing in the IRB - Setup
|
54
108
|
|
55
109
|
```ruby
|
56
110
|
require 'sqa'
|
57
|
-
|
58
|
-
|
59
|
-
|
111
|
+
require 'sqa/cli'
|
112
|
+
|
113
|
+
# You can pass a set of CLI options in a String
|
114
|
+
SQA.init "-c ~/.sqa.yml"
|
115
|
+
|
116
|
+
aapl = SQA::Stock.new(ticker: 'aapl', source: :alpha_vantage)
|
117
|
+
#=> aapl with 1207 data points from 2019-01-02 to 2023-10-17
|
118
|
+
```
|
119
|
+
|
120
|
+
`aapl.df` is the data frame. It is implemented as a Hashie::Mash obect -- a Hash or Arrays.
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
aapl.df.keys
|
124
|
+
#=> [:timestamp, :open_price, :high_price, :low_price, :close_price, :adj_close_price, :volume]
|
125
|
+
|
126
|
+
aapl.df.adj_close_price.last(5)
|
127
|
+
#=> [179.8, 180.71, 178.85, 178.72, 177.15]
|
128
|
+
```
|
129
|
+
|
130
|
+
`aapl.data` is basic static data, company name, industry etc. It is also implemented as a Hassie::Mash object but is primary treated as a plain hash object.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
aapl.data.keys
|
134
|
+
#=> [:ticker, :source, :indicators, :overview]
|
135
|
+
|
136
|
+
aapl.data.source
|
137
|
+
=> "alpha_vantage"
|
60
138
|
|
61
|
-
|
62
|
-
|
139
|
+
aapl.data.overciew.keys
|
140
|
+
=> [:symbol, :asset_type, :name, :description, :cik, :exchange, :currency, :country, :sector, :industry, :address, :fiscal_year_end, :latest_quarter, :market_capitalization, :ebitda, :pe_ratio, :peg_ratio, :book_value, :dividend_per_share, :dividend_yield, :eps, :revenue_per_share_ttm, :profit_margin, :operating_margin_ttm, :return_on_assets_ttm, :return_on_equity_ttm, :revenue_ttm, :gross_profit_ttm, :diluted_epsttm, :quarterly_earnings_growth_yoy, :quarterly_revenue_growth_yoy, :analyst_target_price, :trailing_pe, :forward_pe, :price_to_sales_ratio_ttm, :price_to_book_ratio, :ev_to_revenue, :ev_to_ebitda, :beta, :"52_week_high", :"52_week_low", :"50_day_moving_average", :"200_day_moving_average", :shares_outstanding, :dividend_date, :ex_dividend_date]
|
63
141
|
|
64
|
-
aapl = SQA::Stock.new('aapl')
|
65
142
|
```
|
66
143
|
|
67
|
-
|
68
|
-
|
144
|
+
### Playing in the IRB - Statistics
|
145
|
+
|
146
|
+
Basic statistics are available on all of the SQA::DataFrame arrays.
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
require 'amazing_print' # to get the ap command
|
150
|
+
|
151
|
+
# Look at some summary stats on the last 5 days of
|
152
|
+
# adjusted closing pricess of AAPL
|
153
|
+
ap aapl.df.adj_close_price.last(5).summary
|
154
|
+
{
|
155
|
+
:frequencies => {
|
156
|
+
179.8 => 1,
|
157
|
+
180.71 => 1,
|
158
|
+
178.85 => 1,
|
159
|
+
178.72 => 1,
|
160
|
+
177.15 => 1
|
161
|
+
},
|
162
|
+
:max => 180.71,
|
163
|
+
:mean => 179.046,
|
164
|
+
:median => 178.85,
|
165
|
+
:midrange => 178.93,
|
166
|
+
:min => 177.15,
|
167
|
+
:mode => nil,
|
168
|
+
:proportions => {
|
169
|
+
179.8 => 0.2,
|
170
|
+
180.71 => 0.2,
|
171
|
+
178.85 => 0.2,
|
172
|
+
178.72 => 0.2,
|
173
|
+
177.15 => 0.2
|
174
|
+
},
|
175
|
+
:quartile1 => 178.85,
|
176
|
+
:quartile2 => 179.8,
|
177
|
+
:quartile3 => 180.71,
|
178
|
+
:range => 3.5600000000000023,
|
179
|
+
:size => 5,
|
180
|
+
:sum => 895.23,
|
181
|
+
:sample_coefficient_of_variation => 0.006644656242680533,
|
182
|
+
:sample_kurtosis => 2.089087404921432,
|
183
|
+
:sample_size => 5,
|
184
|
+
:sample_skewness => -0.2163861377512453,
|
185
|
+
:sample_standard_deviation => 1.1896991216269788,
|
186
|
+
:sample_standard_error => 0.532049621745943,
|
187
|
+
:sample_variance => 1.415384000000005,
|
188
|
+
:sample_zscores => {
|
189
|
+
179.8 => 0.6337736880639895,
|
190
|
+
180.71 => 1.3986729667618856,
|
191
|
+
178.85 => -0.16474753695031497,
|
192
|
+
178.72 => -0.2740188624785824,
|
193
|
+
177.15 => -1.5936802553969298
|
194
|
+
}
|
195
|
+
}
|
196
|
+
#=> nil
|
197
|
+
```
|
198
|
+
|
199
|
+
### Playing in the IRB - Indicators
|
200
|
+
|
69
201
|
The SQA::Indicator class methods use Arrays not the DataFrame
|
70
202
|
Here is an example:
|
71
203
|
|
72
204
|
|
73
205
|
```ruby
|
74
|
-
prices = aapl.df.adj_close_price
|
206
|
+
prices = aapl.df.adj_close_price
|
75
207
|
period = 14 # size of the window in prices to analyze
|
76
208
|
|
77
209
|
rsi = SQA::Indicator.rsi(prices, period)
|
210
|
+
#=> {:rsi=>63.46652828230407, :trend=>:normal}
|
78
211
|
```
|
79
212
|
|
80
|
-
###
|
213
|
+
### Playing in the IRB - Strategies
|
81
214
|
|
82
|
-
The strategies work off of an Object that contains the information required to make its recommendation.
|
215
|
+
The strategies work off of an Object that contains the information required to make its recommendation. Building on the previous Ruby snippet ...
|
83
216
|
|
84
217
|
```ruby
|
85
218
|
require 'ostruct'
|
@@ -96,19 +229,24 @@ ss.add SQA::Strategy::RSI
|
|
96
229
|
|
97
230
|
# This is an Array with each "trade" method
|
98
231
|
# that is defined in each strategy added
|
99
|
-
ss.strategies
|
232
|
+
ap ss.strategies
|
233
|
+
[
|
234
|
+
[0] SQA::Strategy::Random#trade(vector),
|
235
|
+
[1] SQA::Strategy::RSI#trade(vector)
|
236
|
+
]
|
100
237
|
|
101
238
|
# Execute those strategies
|
102
239
|
results = ss.execute(vector)
|
240
|
+
#=> [:hold, :hold]
|
103
241
|
```
|
104
242
|
|
105
243
|
`results` is an Array with an entry for each strategy executed. The entries are either :buy, :sell or :hold.
|
106
244
|
|
107
245
|
Currently the strategies are executed sequentially so the results can easily be mapped back to which strategy produced which result. In the future that will change so that the strategies are executed concurrently. When that change is introduced the entries in the `results` object will change -- most likely to an Array of Hashes.
|
108
246
|
|
109
|
-
|
247
|
+
Any specific strategy may not work on every stock. Using the historical data, it is possible to see which strategy works better for a specific stock. **Of course the statistical motto is that historical performance is not a fail-proof indicator for future performance.**
|
110
248
|
|
111
|
-
|
249
|
+
The strategies that come with the `SQA::Strategy` class are examples only. Its expected that you will come up with your own. If you do, consider sharing them.
|
112
250
|
|
113
251
|
## Contributing
|
114
252
|
|
@@ -0,0 +1 @@
|
|
1
|
+
2ee94a54d6ac3d13685dc9b91a2bae0fe75feab6148e1aa9a9d4096961b9b7b577b7ce9d1264f0cce260640515ddd86d5fd5fd2b66f49175844c903581ff6fd9
|
@@ -0,0 +1 @@
|
|
1
|
+
0b3d327017ae67b0ce46082acff8bebdb4a981575e9818418c7279de286a233dde003877f3907b37f3f7e0de236015b4ee4404818d692509c805641862025bc6
|
@@ -5,9 +5,8 @@
|
|
5
5
|
#
|
6
6
|
|
7
7
|
|
8
|
-
class SQA::DataFrame
|
8
|
+
class SQA::DataFrame
|
9
9
|
class AlphaVantage
|
10
|
-
API_KEY = Nenv.av_api_key
|
11
10
|
CONNECTION = Faraday.new(url: 'https://www.alphavantage.co')
|
12
11
|
HEADERS = YahooFinance::HEADERS
|
13
12
|
|
@@ -24,47 +23,16 @@ class SQA::DataFrame < Daru::DataFrame
|
|
24
23
|
"volume" => HEADERS[6]
|
25
24
|
}
|
26
25
|
|
26
|
+
TRANSFORMERS = {
|
27
|
+
HEADERS[1] => -> (v) { v.to_f.round(3) },
|
28
|
+
HEADERS[2] => -> (v) { v.to_f.round(3) },
|
29
|
+
HEADERS[3] => -> (v) { v.to_f.round(3) },
|
30
|
+
HEADERS[4] => -> (v) { v.to_f.round(3) },
|
31
|
+
HEADERS[5] => -> (v) { v.to_f.round(3) },
|
32
|
+
HEADERS[6] => -> (v) { v.to_i }
|
33
|
+
}
|
27
34
|
|
28
35
|
################################################################
|
29
|
-
# Load a Dataframe from a csv file
|
30
|
-
def self.load(ticker, type="csv")
|
31
|
-
filepath = SQA.data_dir + "#{ticker}.#{type}"
|
32
|
-
|
33
|
-
if filepath.exist?
|
34
|
-
df = normalize_vector_names SQA::DataFrame.load(ticker, type)
|
35
|
-
else
|
36
|
-
df = recent(ticker, full: true)
|
37
|
-
df.send("to_#{type}",filepath)
|
38
|
-
end
|
39
|
-
|
40
|
-
df
|
41
|
-
end
|
42
|
-
|
43
|
-
|
44
|
-
# Normalize the vector (aka column) names as
|
45
|
-
# symbols using the standard names set by
|
46
|
-
# Yahoo Finance ... since it was the first one
|
47
|
-
# not because its anything special.
|
48
|
-
#
|
49
|
-
def self.normalize_vector_names(df)
|
50
|
-
headers = df.vectors.to_a
|
51
|
-
|
52
|
-
# convert vector names to symbols
|
53
|
-
# when they are strings. They become stings
|
54
|
-
# when the data frame is saved to a CSV file
|
55
|
-
# and then loaded back in.
|
56
|
-
|
57
|
-
if headers.first == HEADERS.first.to_s
|
58
|
-
a_hash = {}
|
59
|
-
HEADERS.each {|k| a_hash[k.to_s] = k}
|
60
|
-
df.rename_vectors(a_hash) # renames from String to Symbol
|
61
|
-
else
|
62
|
-
df.rename_vectors(HEADER_MAPPING)
|
63
|
-
end
|
64
|
-
|
65
|
-
df
|
66
|
-
end
|
67
|
-
|
68
36
|
|
69
37
|
# Get recent data from JSON API
|
70
38
|
#
|
@@ -82,7 +50,8 @@ class SQA::DataFrame < Daru::DataFrame
|
|
82
50
|
# and adding that to the data frame as if it were
|
83
51
|
# adjusted.
|
84
52
|
#
|
85
|
-
def self.recent(ticker, full: false)
|
53
|
+
def self.recent(ticker, full: false, from_date: nil)
|
54
|
+
|
86
55
|
# NOTE: Using the CSV format because the JSON format has
|
87
56
|
# really silly key values. The column names for the
|
88
57
|
# CSV format are much better.
|
@@ -90,7 +59,7 @@ class SQA::DataFrame < Daru::DataFrame
|
|
90
59
|
"/query?" +
|
91
60
|
"function=TIME_SERIES_DAILY&" +
|
92
61
|
"symbol=#{ticker.upcase}&" +
|
93
|
-
"apikey=#{
|
62
|
+
"apikey=#{SQA.av.key}&" +
|
94
63
|
"datatype=csv&" +
|
95
64
|
"outputsize=#{full ? 'full' : 'compact'}"
|
96
65
|
).to_hash
|
@@ -100,18 +69,19 @@ class SQA::DataFrame < Daru::DataFrame
|
|
100
69
|
end
|
101
70
|
|
102
71
|
raw = response[:body].split
|
103
|
-
|
104
72
|
headers = raw.shift.split(',')
|
73
|
+
|
105
74
|
headers[0] = 'date' # website returns "timestamp" but that
|
106
75
|
# has an unintended side-effect when
|
107
76
|
# the names are normalized.
|
77
|
+
# SMELL: IS THIS STILL TRUE?
|
108
78
|
|
109
79
|
close_inx = headers.size - 2
|
110
80
|
adj_close_inx = close_inx + 1
|
111
81
|
|
112
82
|
headers.insert(adj_close_inx, 'adjusted_close')
|
113
83
|
|
114
|
-
|
84
|
+
aofh = raw.map do |e|
|
115
85
|
e2 = e.split(',')
|
116
86
|
e2[1..-2] = e2[1..-2].map(&:to_f) # converting open, high, low, close
|
117
87
|
e2[-1] = e2[-1].to_i # converting volumn
|
@@ -119,35 +89,20 @@ class SQA::DataFrame < Daru::DataFrame
|
|
119
89
|
headers.zip(e2).to_h
|
120
90
|
end
|
121
91
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
92
|
+
if from_date
|
93
|
+
aofh.reject!{|e| Date.parse(e['date']) < from_date}
|
94
|
+
end
|
126
95
|
|
127
|
-
|
128
|
-
#
|
129
|
-
# base_df is ascending on timestamp
|
130
|
-
# update_df is descending on timestamp
|
131
|
-
#
|
132
|
-
# base_df content came from CSV file downloaded
|
133
|
-
# from Yahoo Finance.
|
134
|
-
#
|
135
|
-
# update_df came from scraping the webpage
|
136
|
-
# at Yahoo Finance for the recent history.
|
137
|
-
#
|
138
|
-
# Returns a combined DataFrame.
|
139
|
-
#
|
140
|
-
def self.append(base_df, updates_df)
|
141
|
-
last_timestamp = Date.parse base_df.timestamp.last
|
142
|
-
filtered_df = updates_df.filter_rows { |row| Date.parse(row[:timestamp]) > last_timestamp }
|
96
|
+
return nil if aofh.empty?
|
143
97
|
|
144
|
-
|
98
|
+
# ensure tha the data frame is
|
99
|
+
# always sorted oldest to newest.
|
145
100
|
|
146
|
-
|
147
|
-
|
101
|
+
if aofh.first['date'] > aofh.last['date']
|
102
|
+
aofh.reverse!
|
148
103
|
end
|
149
104
|
|
150
|
-
|
105
|
+
SQA::DataFrame.from_aofh(aofh, mapping: HEADER_MAPPING, transformers: TRANSFORMERS)
|
151
106
|
end
|
152
107
|
end
|
153
108
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
|
5
|
-
class SQA::DataFrame
|
5
|
+
class SQA::DataFrame
|
6
6
|
class YahooFinance
|
7
7
|
CONNECTION = Faraday.new(url: 'https://finance.yahoo.com')
|
8
8
|
HEADERS = [
|
@@ -30,21 +30,6 @@ class SQA::DataFrame < Daru::DataFrame
|
|
30
30
|
}
|
31
31
|
|
32
32
|
################################################################
|
33
|
-
def self.load(filename, options={}, &block)
|
34
|
-
df = SQA::DataFrame.load(filename, options={}, &block)
|
35
|
-
|
36
|
-
headers = df.vectors
|
37
|
-
|
38
|
-
if headers.first == HEADERS.first.to_s
|
39
|
-
a_hash = {}
|
40
|
-
HEADERS.each {|k| a_hash[k.to_s] = k}
|
41
|
-
df.rename_vectors(a_hash)
|
42
|
-
else
|
43
|
-
df.rename_vectors(HEADER_MAPPING)
|
44
|
-
end
|
45
|
-
|
46
|
-
df
|
47
|
-
end
|
48
33
|
|
49
34
|
|
50
35
|
# Scrape the Yahoo Finance website to get recent
|
@@ -62,7 +47,7 @@ class SQA::DataFrame < Daru::DataFrame
|
|
62
47
|
|
63
48
|
rows = table.css('tbody tr')
|
64
49
|
|
65
|
-
|
50
|
+
aofh = []
|
66
51
|
|
67
52
|
rows.each do |row|
|
68
53
|
cols = row.css('td').map{|c| c.children[0].text}
|
@@ -80,37 +65,10 @@ class SQA::DataFrame < Daru::DataFrame
|
|
80
65
|
cols[0] = Date.parse(cols[0]).to_s
|
81
66
|
cols[6] = cols[6].tr(',','').to_i
|
82
67
|
(1..5).each {|x| cols[x] = cols[x].to_f}
|
83
|
-
|
84
|
-
end
|
85
|
-
|
86
|
-
Daru::DataFrame.new(data)
|
87
|
-
end
|
88
|
-
|
89
|
-
|
90
|
-
# Append update_df rows to the base_df
|
91
|
-
#
|
92
|
-
# base_df is ascending on timestamp
|
93
|
-
# update_df is descending on timestamp
|
94
|
-
#
|
95
|
-
# base_df content came from CSV file downloaded
|
96
|
-
# from Yahoo Finance.
|
97
|
-
#
|
98
|
-
# update_df came from scraping the webpage
|
99
|
-
# at Yahoo Finance for the recent history.
|
100
|
-
#
|
101
|
-
# Returns a combined DataFrame.
|
102
|
-
#
|
103
|
-
def self.append(base_df, updates_df)
|
104
|
-
last_timestamp = Date.parse base_df.timestamp.last
|
105
|
-
filtered_df = updates_df.filter_rows { |row| Date.parse(row[:timestamp]) > last_timestamp }
|
106
|
-
|
107
|
-
last_inx = filtered_df.size - 1
|
108
|
-
|
109
|
-
(0..last_inx).each do |x|
|
110
|
-
base_df.add_row filtered_df.row[last_inx-x]
|
68
|
+
aofh << HEADERS.zip(cols).to_h
|
111
69
|
end
|
112
70
|
|
113
|
-
|
71
|
+
aofh
|
114
72
|
end
|
115
73
|
end
|
116
74
|
end
|