sqa 0.0.7 → 0.0.9

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.
data/lib/sqa/cli.rb CHANGED
@@ -1,324 +1,173 @@
1
1
  # lib/sqa/cli.rb
2
2
 
3
- require_relative '../sqa'
4
-
5
- module SQA
6
- class CLI
7
- include Mixlib::CLI
8
-
9
- option :config_file,
10
- short: "-c CONFIG",
11
- long: "--config CONFIG",
12
- default: "~/.sqa.rb",
13
- description: "The configuration file to use"
14
-
15
- option :log_level,
16
- short: "-l LEVEL",
17
- long: "--log_level LEVEL",
18
- description: "Set the log level (debug, info, warn, error, fatal)",
19
- required: true,
20
- in: [:debug, :info, :warn, :error, :fatal],
21
- proc: Proc.new { |l| l.to_sym }
22
-
23
- option :help,
24
- short: "-h",
25
- long: "--help",
26
- description: "Show this message",
27
- on: :tail,
28
- boolean: true,
29
- show_options: true,
30
- exit: 0
31
-
32
- def run(argv = ARGV)
33
- parse_options(argv)
34
- SQA::Config.merge!(config)
35
- end
36
- end
37
- end
38
-
39
- __END__
40
-
41
- ###################################################
42
- ## This is the old thing that got me started ...
43
-
44
- #!/usr/bin/env ruby
45
- # experiments/stocks/analysis.rb
46
- #
47
- # Some technical indicators from FinTech gem
48
- #
49
- # optional date CLI option in format YYYY-mm-dd
50
- # if not present uses Date.today
51
- #
52
-
53
-
54
- INVEST = 1000.00
55
-
56
- require 'pathname'
57
-
58
- require_relative 'stock'
59
- require_relative 'datastore'
60
-
61
-
62
- STOCKS = Pathname.pwd + "stocks.txt"
63
- TRADES = Pathname.pwd + "trades.csv"
64
-
65
- TRADES_FILE = File.open(TRADES, 'a')
66
-
67
- unless STOCKS.exist?
68
- puts
69
- puts "ERROR: The 'stocks.txt' file does not exist."
70
- puts
71
- exot(-1)
72
- end
73
-
74
- require 'debug_me'
75
- include DebugMe
76
-
77
- require 'csv'
78
- require 'date'
79
- require 'tty-table'
80
-
81
- require 'fin_tech'
82
- require 'previous_dow'
83
-
84
- class NilClass
85
- def blank?() = true
86
- end
87
-
88
- class String
89
- def blank?() = strip().empty?
90
- end
91
-
92
- class Array
93
- def blank?() = empty?
94
- end
95
-
3
+ require 'tty-option'
96
4
 
97
- def tickers
98
- return @tickers unless @tickers.blank?
5
+ require_relative '../sqa'
99
6
 
100
- @tickers = []
7
+ # SMELL: Architectyre has become confused between CLI and Command
101
8
 
102
- STOCKS.readlines.each do |a_line|
103
- ticker_symbol = a_line.chomp.strip.split()&.first&.downcase
104
- next if ticker_symbol.blank? || '#' == ticker_symbol
105
- @tickers << ticker_symbol unless @tickers.include?(ticker_symbol)
106
- end
9
+ # TODO: Fix the mess between CLI and Command
107
10
 
108
- @tickers.sort!
109
- end
110
11
 
111
- given_date = ARGV.first ? Date.parse(ARGV.first) : Date.today
12
+ module SQA
13
+ class CLI
14
+ include TTY::Option
112
15
 
113
- start_date = Date.new(2019, 1, 1)
114
- end_date = previous_dow(:friday, given_date)
16
+ header "Stock Quantitative Analysis (SQA)"
17
+ footer "WARNING: This is a toy, a play thing, not intended for serious use."
115
18
 
116
- ASOF = end_date.to_s.tr('-','')
19
+ program "sqa"
20
+ desc "A collection of things"
117
21
 
22
+ example "sqa -c ~/.sqa.yml -p portfolio.csv -t trades.csv --data-dir ~/sqa_data"
118
23
 
119
- #######################################################################
120
- # download a CSV file from https://query1.finance.yahoo.com
121
- # given a stock ticker symbol as a String
122
- # start and end dates
123
- #
124
- # For ticker "aapl" the downloaded file will be named "aapl.csv"
125
- # That filename will be renamed to "aapl_YYYYmmdd.csv" where the
126
- # date suffix is the end_date of the historical data.
127
- #
128
- def download_historical_prices(ticker, start_date, end_date)
129
- data_path = Pathname.pwd + "#{ticker}_#{ASOF}.csv"
130
- return if data_path.exist?
131
24
 
132
- mew_path = Pathname.pwd + "#{ticker}.csv"
25
+ option :config_file do
26
+ short "-c string"
27
+ long "--config string"
28
+ desc "Path to the config file"
29
+ end
133
30
 
134
- start_timestamp = start_date.to_time.to_i
135
- end_timestamp = end_date.to_time.to_i
136
- ticker_upcase = ticker.upcase
137
- filename = "#{ticker.downcase}.csv"
31
+ option :log_level do
32
+ short "-l string"
33
+ long "--log_level string"
34
+ # default SQA.config.log_level
35
+ desc "Set the log level (debug, info, warn, error, fatal)"
36
+ end
138
37
 
139
- `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"`
38
+ option :portfolio do
39
+ short "-p string"
40
+ long "--portfolio string"
41
+ # default SQA.config.portfolio_filename
42
+ desc "Set the filename of the portfolio"
43
+ end
140
44
 
141
- mew_path.rename data_path
142
- end
143
45
 
46
+ option :trades do
47
+ short "-t string"
48
+ long "--trades string"
49
+ # default SQA.config.trades_filename
50
+ desc "Set the filename into which trades are stored"
51
+ end
144
52
 
145
- #######################################################################
146
- # Read the CSV file associated with the give ticker symbol
147
- # and the ASOF date.
148
- #
149
- def read_csv(ticker)
150
- filename = "#{ticker.downcase}_#{ASOF}.csv"
151
- data = []
152
53
 
153
- CSV.foreach(filename, headers: true) do |row|
154
- data << row.to_h
155
- end
54
+ option :data_dir do
55
+ long "--data-dir string"
56
+ # default SQA.config.data_dir
57
+ desc "Set the directory for the SQA data"
58
+ end
156
59
 
157
- data
158
- end
60
+ option :dump_config do
61
+ long "--dump-config path_to_file"
62
+ desc "Dump the current configuration"
63
+ end
159
64
 
160
- ##########################
161
- # record a recommend trade
65
+ flag :help do
66
+ short "-h"
67
+ long "--help"
68
+ desc "Print usage"
69
+ end
162
70
 
163
- def trade(ticker, signal, shares, price)
164
- TRADES_FILE.puts "#{ticker},#{ASOF},#{signal},#{shares},#{price}"
165
- end
71
+ flag :version do
72
+ long "--version"
73
+ desc "Print version"
74
+ end
166
75
 
167
- #######################################################################
168
- ###
169
- ## Main
170
- #
76
+ flag :debug do
77
+ short "-d"
78
+ long "--debug"
79
+ # default SQA.config.debug
80
+ desc "Turn on debugging output"
81
+ end
171
82
 
83
+ flag :verbose do
84
+ short "-v"
85
+ long "--verbose"
86
+ # default SQA.config.debug
87
+ desc "Print verbosely"
88
+ end
172
89
 
173
- tickers.each do |ticker|
174
- download_historical_prices(ticker, start_date, end_date)
90
+ class << self
91
+ @@subclasses = []
92
+ @@commands_available = []
93
+
94
+ def names
95
+ '['+ @@commands_available.join('|')+']'
96
+ end
97
+
98
+ def inherited(subclass)
99
+ super
100
+ @@subclasses << subclass
101
+ @@commands_available << subclass.command.join
102
+ end
103
+
104
+ def command_descriptions
105
+ help_block = "Optional Command Available:"
106
+
107
+ @@commands_available.size.times do |x|
108
+ klass = @@subclasses[x]
109
+ help_block << "\n " + @@commands_available[x] + " - "
110
+ help_block << klass.desc.join
111
+ end
112
+
113
+ help_block
114
+ end
115
+
116
+
117
+ ##################################################
118
+ def run(argv = ARGV)
119
+ cli = new
120
+ parser = cli.parse(argv)
121
+ params = parser.params
122
+
123
+ if params[:help]
124
+ print parser.help
125
+ exit(0)
126
+
127
+ elsif params.errors.any?
128
+ puts params.errors.summary
129
+ exit(1)
130
+
131
+ elsif params[:version]
132
+ puts SQA.version
133
+ exit(0)
134
+
135
+ elsif params[:dump_config]
136
+ SQA.config.config_file = params[:dump_config]
137
+ `touch #{SQA.config.config_file}`
138
+ SQA.config.dump_file
139
+ exit(0)
140
+
141
+ elsif params[:config_file]
142
+ # Override the defaults <- envars <- config file content
143
+ SQA.config.config_file = params[:config_file]
144
+ SQA.config.from_file
145
+ end
146
+
147
+ # Override the defaults <- envars <- config file <- cli parameters
148
+ SQA.config.merge!(remove_temps params.to_h)
149
+
150
+ if SQA.debug? || SQA.verbose?
151
+ debug_me("config after CLI parameters"){[
152
+ "SQA.config"
153
+ ]}
154
+ end
155
+ end
156
+
157
+ def remove_temps(a_hash)
158
+ temps = %i[ help version dump ]
159
+ # debug_me{[ :a_hash ]}
160
+ a_hash.reject{|k, _| temps.include? k}
161
+ end
162
+ end
163
+ end
175
164
  end
176
165
 
177
- result = {}
178
-
179
- mwfd = 14 # moving_window_forcast_days
180
-
181
- headers = %w[ Ticker AdjClose Trend Slope M'tum RSI Analysis MACD Target Signal $]
182
- values = []
183
-
184
- tickers.each do |ticker|
185
-
186
- data = read_csv ticker
187
- prices = data.map{|r| r["Adj Close"].to_f}
188
- volumes = data.map{|r| r["volume"].to_f}
189
-
190
- if data.blank?
191
- puts
192
- puts "ERROR: cannot get data for #{ticker}"
193
- puts
194
- next
195
- end
196
-
197
-
198
- result[ticker] = {
199
- date: data.last["Date"],
200
- adj_close: data.last["Adj Close"].to_f
201
- }
202
-
203
- result[ticker][:market] = FinTech.classify_market_profile(
204
- volumes.last(mwfd),
205
- prices.last(mwfd),
206
- prices.last(mwfd).first,
207
- prices.last
208
- )
209
-
210
- fr = FinTech.fibonacci_retracement( prices.last(mwfd).first,
211
- prices.last).map{|x| x.round(3)}
212
-
213
-
214
- puts "\n#{result[ticker][:market]} .. #{ticker}\t#{fr}"
215
- print "\t"
216
- print FinTech.head_and_shoulders_pattern?(prices.last(mwfd))
217
- print "\t"
218
- print FinTech.double_top_bottom_pattern?(prices.last(mwfd))
219
- print "\t"
220
- mr = FinTech.mean_reversion?(prices, mwfd, 0.5)
221
- print mr
222
-
223
- if mr
224
- print "\t"
225
- print FinTech.mr_mean(prices, mwfd).round(3)
226
- end
227
-
228
- print "\t"
229
- print FinTech.identify_wave_condition?(prices, 2*mwfd, 1.0)
230
- puts
231
-
232
- print "\t"
233
- print FinTech.ema_analysis(prices, mwfd).except(:ema_values)
234
-
235
- puts
236
-
237
- row = [ ticker ]
238
-
239
- # result[ticker][:moving_averages] = FinTech.sma(data, mwfd)
240
- result[ticker][:trend] = FinTech.sma_trend(data, mwfd)
241
- result[ticker][:momentum] = FinTech.momentum(prices, mwfd)
242
- result[ticker][:rsi] = FinTech.rsi(data, mwfd)
243
- result[ticker][:bollinger_bands] = FinTech.bollinger_bands(data, mwfd, 2)
244
- result[ticker][:macd] = FinTech.macd(data, mwfd, 2*mwfd, mwfd/2)
245
-
246
- price = result[ticker][:adj_close].round(3)
247
-
248
- row << price
249
- row << result[ticker][:trend][:trend]
250
- row << result[ticker][:trend][:angle].round(3)
251
- row << result[ticker][:momentum].round(3)
252
- row << result[ticker][:rsi][:rsi].round(3)
253
- row << result[ticker][:rsi][:meaning]
254
- row << result[ticker][:macd].first.round(3)
255
- row << result[ticker][:macd].last.round(3)
256
-
257
- analysis = result[ticker][:rsi][:meaning]
258
-
259
- signal = ""
260
- macd_diff = result[ticker][:macd].first
261
- target = result[ticker][:macd].last
262
- current = result[ticker][:adj_close]
263
-
264
- trend_down = "down" == result[ticker][:trend][:trend]
265
-
266
- if current < target
267
- signal = "buy" unless "Over Bought" == analysis
268
- elsif (current > target) && trend_down
269
- signal = "sell" unless "Over Sold" == analysis
270
- end
271
-
272
- if "buy" == signal
273
- pps = target - price
274
- shares = INVEST.to_i / price.to_i
275
- upside = (shares * pps).round(2)
276
- trade(ticker, signal, shares, price)
277
- elsif "sell" == signal
278
- pps = target - price
279
- shares = INVEST.to_i / price.to_i
280
- upside = (shares * pps).round(2)
281
- trade(ticker, signal, shares, price)
282
- else
283
- upside = ""
284
- end
285
-
286
- row << signal
287
- row << upside
288
-
289
- values << row
290
- end
166
+ require_relative 'analysis'
167
+ require_relative 'web'
291
168
 
292
- # debug_me{[
293
- # :result
294
- # ]}
295
-
296
-
297
- the_table = TTY::Table.new(headers, values)
298
-
299
- puts
300
- puts "Analysis as of Friday Close: #{end_date}"
301
-
302
- puts the_table.render(
303
- :unicode,
304
- {
305
- padding: [0, 0, 0, 0],
306
- alignments: [
307
- :left, # ticker
308
- :right, # adj close
309
- :center, # trend
310
- :right, # slope
311
- :right, # momentum
312
- :right, # rsi
313
- :center, # meaning / analysis
314
- :right, # macd
315
- :right, # target
316
- :center, # signal
317
- :right # upside
318
- ],
319
- }
320
- )
321
- puts
322
-
323
- TRADES_FILE.close
169
+ # First Load TTY-Option's command content with all available commands
170
+ # then these have access to the entire ObjectSpace ...
171
+ SQA::CLI.command SQA::CLI.names
172
+ SQA::CLI.example SQA::CLI.command_descriptions
324
173
 
data/lib/sqa/config.rb ADDED
@@ -0,0 +1,179 @@
1
+ # lib/sqa/config.rb
2
+
3
+ # The hierarchies of values should be:
4
+ # default
5
+ # envar ..... overrides default
6
+ # config file ..... overrides envar
7
+ # command line parameters ...... overrides config file
8
+
9
+ require 'hashie'
10
+ require 'yaml'
11
+ require 'json'
12
+ require 'toml-rb'
13
+
14
+
15
+ module SQA
16
+ class Config < Hashie::Dash
17
+ include Hashie::Extensions::Dash::PropertyTranslation
18
+ include Hashie::Extensions::Coercion
19
+ include Hashie::Extensions::Dash::PredefinedValues
20
+
21
+ property :config_file #,a String filepath for the current config overriden by cli options
22
+ property :dump_config # a String filepath into which to dump the current config
23
+
24
+ property :data_dir, default: Nenv.home + "/sqa_data"
25
+
26
+ # TODO: If no path is given, these files will be in
27
+ # data directory, otherwise, use the given path
28
+ property :portfolio_filename, from: :portfolio, default: "portfolio.csv"
29
+ property :trades_filename, from: :trades, default: "trades.csv"
30
+
31
+ property :log_level, default: :info, coerce: Symbol, values: %i[debug info warn error fatal]
32
+
33
+ # TODO: need a custom proc since there is no Boolean class in Ruby
34
+ property :debug, default: false #, coerce: Boolean
35
+ property :verbose, default: false #, coerce: Boolean
36
+
37
+ # TODO: use svggraph
38
+ property :plotting_library, from: :plot_lib, default: :gruff, coerce: Symbol
39
+ property :lazy_update, from: :lazy, default: false
40
+
41
+
42
+ coerce_key :debug, ->(v) do
43
+ case v
44
+ when String
45
+ !!(v =~ /\A(true|t|yes|y|1)\z/i)
46
+ when Numeric
47
+ !v.to_i.zero?
48
+ else
49
+ v == true
50
+ end
51
+ end
52
+
53
+ coerce_key :verbose, ->(v) do
54
+ case v
55
+ when String
56
+ !!(v =~ /\A(true|t|yes|y|1)\z/i)
57
+ when Numeric
58
+ !v.to_i.zero?
59
+ else
60
+ v == true
61
+ end
62
+ end
63
+
64
+ ########################################################
65
+ def initialize(a_hash={})
66
+ super(a_hash)
67
+ override_with_envars
68
+ end
69
+
70
+ def debug? = debug
71
+ def verbose? = verbose
72
+
73
+
74
+ ########################################################
75
+ def from_file
76
+ return if config_file.nil?
77
+
78
+ if File.exist?(config_file) &&
79
+ File.file?(config_file) &&
80
+ File.readable?(config_file)
81
+ type = File.extname(config_file).downcase
82
+ else
83
+ type = "invalid"
84
+ end
85
+
86
+ # TODO: arrange order in mostly often used
87
+
88
+ if ".json" == type
89
+ form_json
90
+
91
+ elsif %w[.yml .yaml].include?(type)
92
+ from_yaml
93
+
94
+ elsif ".toml" == type
95
+ from_toml
96
+
97
+ else
98
+ raise BadParameterError, "Invalid Config File: #{config_file}"
99
+ end
100
+ end
101
+
102
+ def dump_file
103
+ if config_file.nil?
104
+ raise BadParameterError, "No config file given"
105
+ end
106
+
107
+ if File.exist?(config_file) &&
108
+ File.file?(config_file) &&
109
+ File.writable?(config_file)
110
+ type = File.extname(config_file).downcase
111
+ else
112
+ type = "invalid"
113
+ end
114
+
115
+ if ".json" == type
116
+ dump_json
117
+
118
+ elsif %w[.yml .yaml].include?(type)
119
+ dump_yaml
120
+
121
+ elsif ".toml" == type
122
+ dump_toml
123
+
124
+ else
125
+ raise BadParameterError, "Invalid Config File: #{config_file}"
126
+ end
127
+ end
128
+
129
+
130
+ ########################################################
131
+ private
132
+
133
+ def override_with_envars(prefix = "SQA_")
134
+ keys.each do |key|
135
+ envar = ENV["#{prefix}#{key.to_s.upcase}"]
136
+ send("#{key}=", envar) unless envar.nil?
137
+ end
138
+ end
139
+
140
+
141
+ #####################################
142
+ ## override values from a config file
143
+
144
+ def from_json
145
+ incoming = ::JSON.load(File.open(config_file).read)
146
+ debug_me{[ :incoming ]}
147
+ end
148
+
149
+ def from_toml
150
+ incoming = TomlRB.load_file(config_file)
151
+ debug_me{[ :incoming ]}
152
+ end
153
+
154
+ def from_yaml
155
+ incoming = ::YAML.load_file(config_file)
156
+ debug_me{[ :incoming ]}
157
+ merge! incoming
158
+ end
159
+
160
+
161
+ #####################################
162
+ ## dump values to a config file
163
+
164
+ def as_hash = to_h.reject{|k, _| :config_file == k}
165
+ def dump_json = File.open(config_file, "w") { |f| f.write JSON.pretty_generate(as_hash)}
166
+ def dump_toml = File.open(config_file, "w") { |f| f.write TomlRB.dump(as_hash)}
167
+ def dump_yaml = File.open(config_file, "w") { |f| f.write as_hash.to_yaml}
168
+
169
+
170
+ #####################################
171
+ class << self
172
+ def reset
173
+ SQA.config = new
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ SQA::Config.reset
@@ -0,0 +1,23 @@
1
+ # lib/sqa/constants.rb
2
+
3
+ module SQA
4
+ module Constants
5
+ Signal = {
6
+ hold: 0,
7
+ buy: 1,
8
+ sell: 2
9
+ }.freeze
10
+
11
+ Trend = {
12
+ up: 0,
13
+ down: 1
14
+ }.freeze
15
+
16
+ Swing = {
17
+ valley: 0,
18
+ peak: 1,
19
+ }.freeze
20
+ end
21
+
22
+ include Constants
23
+ end
@@ -5,7 +5,7 @@ require_relative 'data_frame/yahoo_finance'
5
5
 
6
6
  class SQA::DataFrame < Daru::DataFrame
7
7
  def self.path(filename)
8
- SQA::Config.data_dir + filename
8
+ Pathname.new SQA.config.data_dir + filename
9
9
  end
10
10
 
11
11
  def self.load(filename, options={}, &block)