sqa 0.0.7 → 0.0.8

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
+ 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