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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fcc7dbda5b62549fa1fb9691bf7ef9af1a0ba5dfcff63494ce2cdf6ca7e578a
4
- data.tar.gz: 9527508fa1573a09536ef4bf735671761d722d1611494d3aeb8615ba25385f2c
3
+ metadata.gz: b7733f5cb84c4ccbad5e68105e818b35f1aca450fd97eeb3186cfb9a1962d44e
4
+ data.tar.gz: a9b2bf2a0fa6a91de1367fd73beac6a28d263024ded37c216520120cc3181cc9
5
5
  SHA512:
6
- metadata.gz: c5ce2164cd95cf91c62e054dad4fef84ed105d7713207bf5f5e39fbad282785184217ed303fe73b24801b871e9ca0db371c0f514949803aace37b788ebec6140
7
- data.tar.gz: f7336d71e365388d571267e87fbdd82a66dbd532da8e386e9670e1a9f7034d2279202e6df16ce30d14410b2ab39923ecf4bb78e0941b554bf11ee3101fdb9a41
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 are part of a game. **DO NOT USE** when real money is at stake.
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'm making use of lots of gems which may not be part of the gemspec at this time. I will be adding them as they make the final cut as to fitness for the intended function. Some gems are configurable. For example the default for the plotting library is `gruff`. There are several available that the `daru` gem can use.
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
- ## ShoutOut To `daru`
22
+ ### Semantic Versioning
28
23
 
29
- **D**ata **A**nalysis in **RU**by
24
+ ```ruby
25
+ SQA.version # returns SemVersion object
26
+ exit(1) unless SQA.version >= SemVersion("1.0.0")
27
+ ```
30
28
 
31
- http://github.com/v0dro/daru
29
+ ## Usage
32
30
 
33
- Its `DataFrame` class is a very interesting in memory data structure.
31
+ **Do not use!** but its okay to play with.
34
32
 
35
- ## Usage
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 in two different areas. First is the stocks and indicators. The second is with trading strategies.
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 in to the `sqa_data` directory in your HOME directory.
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: AAPL.csv
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
- # TODO: See the documentation on configurable items
58
- # Omit to use defaults
59
- SQA::Config.from_file(path_to_config_file)
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
- # initialize framework from configuration values
62
- SQA.init
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
- `aapl.df` is the Daru::DataFrame
68
- see the `daru` gem for how to manipulate the DataFrame
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.to_a
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
- ### With Strategies
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. Build on the previous Ruby snippet ...
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
- ### See my **experiments** Repository in the **stocks** Directory
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
- I have a program `analysis.rb` that I'm writing along with this `sqa` gem. Its intended as a practical example/test case for how the gem can be used to analyze a complete portfolio one stock at a time.
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 < Daru::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=#{API_KEY}&" +
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
- data = raw.map do |e|
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
- # What oldest data first in the data frame
123
- normalize_vector_names Daru::DataFrame.new(data.reverse)
124
- end
125
-
92
+ if from_date
93
+ aofh.reject!{|e| Date.parse(e['date']) < from_date}
94
+ end
126
95
 
127
- # Append update_df rows to the base_df
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
- last_inx = filtered_df.size - 1
98
+ # ensure tha the data frame is
99
+ # always sorted oldest to newest.
145
100
 
146
- (0..last_inx).each do |x|
147
- base_df.add_row filtered_df.row[last_inx-x]
101
+ if aofh.first['date'] > aofh.last['date']
102
+ aofh.reverse!
148
103
  end
149
104
 
150
- base_df
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 < Daru::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
- data = []
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
- data << HEADERS.zip(cols).to_h
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
- base_df
71
+ aofh
114
72
  end
115
73
  end
116
74
  end