sqa 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
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
+ flag :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,169 @@
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 #, default: Nenv.home + "/.sqa.yml"
22
+ property :data_dir, default: Nenv.home + "/sqa_data"
23
+
24
+ # TODO: If no path is given, these files will be in
25
+ # data directory, otherwise, use the given path
26
+ property :portfolio_filename, from: :portfolio, default: "portfolio.csv"
27
+ property :trades_filename, from: :trades, default: "trades.csv"
28
+
29
+ property :log_level, default: :info, coerce: Symbol, values: %i[debug info warn error fatal]
30
+
31
+ # TODO: need a custom proc since there is no Boolean class in Ruby
32
+ property :debug, default: false #, coerce: Boolean
33
+ property :verbose, default: false #, coerce: Boolean
34
+
35
+ # TODO: use svggraph
36
+ property :plotting_library, from: :plot_lib, default: :gruff, coerce: Symbol
37
+ property :lazy_update, from: :lazy, default: false
38
+
39
+
40
+ coerce_key :debug, ->(v) do
41
+ case v
42
+ when String
43
+ !!(v =~ /\A(true|t|yes|y|1)\z/i)
44
+ when Numeric
45
+ !v.to_i.zero?
46
+ else
47
+ v == true
48
+ end
49
+ end
50
+
51
+ coerce_key :verbose, ->(v) do
52
+ case v
53
+ when String
54
+ !!(v =~ /\A(true|t|yes|y|1)\z/i)
55
+ when Numeric
56
+ !v.to_i.zero?
57
+ else
58
+ v == true
59
+ end
60
+ end
61
+
62
+ ########################################################
63
+ def initialize(a_hash={})
64
+ super(a_hash)
65
+ override_with_envars
66
+ end
67
+
68
+ def debug? = debug
69
+ def verbose? = verbose
70
+
71
+
72
+ ########################################################
73
+ def from_file
74
+ return if config_file.nil?
75
+
76
+ if File.exist?(config_file) &&
77
+ File.file?(config_file) &&
78
+ File.readable?(config_file)
79
+ type = File.extname(config_file).downcase
80
+ else
81
+ type = "invalid"
82
+ end
83
+
84
+ # TODO: arrange order in mostly often used
85
+
86
+ if ".json" == type
87
+ form_json
88
+
89
+ elsif %w[.yml .yaml].include?(type)
90
+ from_yaml
91
+
92
+ elsif ".toml" == type
93
+ from_toml
94
+
95
+ else
96
+ raise BadParameterError, "Invalid Config File: #{config_file}"
97
+ end
98
+ end
99
+
100
+ def dump_file
101
+ if config_file.nil?
102
+ raise BadParameterError, "No config file given"
103
+ end
104
+
105
+ if File.exist?(config_file) &&
106
+ File.file?(config_file) &&
107
+ File.writable?(config_file)
108
+ type = File.extname(config_file).downcase
109
+ else
110
+ type = "invalid"
111
+ end
112
+
113
+ if ".json" == type
114
+ dump_json
115
+
116
+ elsif %w[.yml .yaml].include?(type)
117
+ dump_yaml
118
+
119
+ elsif ".toml" == type
120
+ dump_toml
121
+
122
+ else
123
+ raise BadParameterError, "Invalid Config File: #{config_file}"
124
+ end
125
+ end
126
+
127
+
128
+ ########################################################
129
+ private
130
+
131
+ def override_with_envars(prefix = "SQA_")
132
+ keys.each do |key|
133
+ envar = ENV["#{prefix}#{key.to_s.upcase}"]
134
+ send("#{key}=", envar) unless envar.nil?
135
+ end
136
+ end
137
+
138
+
139
+ #####################################
140
+ ## override values from a config file
141
+
142
+ def from_json
143
+ incoming = ::JSON.load(File.open(config_file).read)
144
+ debug_me{[ :incoming ]}
145
+ end
146
+
147
+ def from_toml
148
+ incoming = TomlRB.load_file(config_file)
149
+ debug_me{[ :incoming ]}
150
+ end
151
+
152
+ def from_yaml
153
+ incoming = ::YAML.load_file(config_file)
154
+ debug_me{[ :incoming ]}
155
+ merge! incoming
156
+ end
157
+
158
+
159
+ #####################################
160
+ ## dump values to a config file
161
+
162
+ def as_hash = to_h.reject{|k, _| :config_file == k}
163
+ def dump_json = File.open(config_file, "w") { |f| f.write JSON.pretty_generate(as_hash)}
164
+ def dump_toml = File.open(config_file, "w") { |f| f.write TomlRB.dump(as_hash)}
165
+ def dump_yaml = File.open(config_file, "w") { |f| f.write as_hash.to_yaml}
166
+ end
167
+ end
168
+
169
+ SQA.config = SQA::Config.new
@@ -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