sqa 0.0.6 → 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,310 +1,173 @@
1
1
  # lib/sqa/cli.rb
2
2
 
3
- require_relative '../sqa'
3
+ require 'tty-option'
4
4
 
5
- module SQA
6
- class CLI
7
- def initialize
8
- @args = $ARGV.dup
9
- end
5
+ require_relative '../sqa'
10
6
 
11
- def run
12
- stock = Stock.new('aapl')
7
+ # SMELL: Architectyre has become confused between CLI and Command
13
8
 
14
- puts <<~OUTPUT
9
+ # TODO: Fix the mess between CLI and Command
15
10
 
16
- TBD
17
- @args => #{@args}
18
- stock => #{stock}
19
11
 
20
- OUTPUT
21
- end
12
+ module SQA
13
+ class CLI
14
+ include TTY::Option
15
+
16
+ header "Stock Quantitative Analysis (SQA)"
17
+ footer "WARNING: This is a toy, a play thing, not intended for serious use."
18
+
19
+ program "sqa"
20
+ desc "A collection of things"
21
+
22
+ example "sqa -c ~/.sqa.yml -p portfolio.csv -t trades.csv --data-dir ~/sqa_data"
23
+
24
+
25
+ option :config_file do
26
+ short "-c string"
27
+ long "--config string"
28
+ desc "Path to the config file"
29
+ end
30
+
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
37
+
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
44
+
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
52
+
53
+
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
59
+
60
+ flag :dump_config do
61
+ long "--dump-config path_to_file"
62
+ desc "Dump the current configuration"
63
+ end
64
+
65
+ flag :help do
66
+ short "-h"
67
+ long "--help"
68
+ desc "Print usage"
69
+ end
70
+
71
+ flag :version do
72
+ long "--version"
73
+ desc "Print version"
74
+ end
75
+
76
+ flag :debug do
77
+ short "-d"
78
+ long "--debug"
79
+ # default SQA.config.debug
80
+ desc "Turn on debugging output"
81
+ end
82
+
83
+ flag :verbose do
84
+ short "-v"
85
+ long "--verbose"
86
+ # default SQA.config.debug
87
+ desc "Print verbosely"
88
+ end
89
+
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
22
163
  end
23
164
  end
24
165
 
25
- __END__
26
-
27
- ###################################################
28
- ## This is the old thing that got me started ...
29
-
30
- #!/usr/bin/env ruby
31
- # experiments/stocks/analysis.rb
32
- #
33
- # Some technical indicators from FinTech gem
34
- #
35
- # optional date CLI option in format YYYY-mm-dd
36
- # if not present uses Date.today
37
- #
38
-
39
-
40
- INVEST = 1000.00
41
-
42
- require 'pathname'
43
-
44
- require_relative 'stock'
45
- require_relative 'datastore'
46
-
47
-
48
- STOCKS = Pathname.pwd + "stocks.txt"
49
- TRADES = Pathname.pwd + "trades.csv"
50
-
51
- TRADES_FILE = File.open(TRADES, 'a')
52
-
53
- unless STOCKS.exist?
54
- puts
55
- puts "ERROR: The 'stocks.txt' file does not exist."
56
- puts
57
- exot(-1)
58
- end
59
-
60
- require 'debug_me'
61
- include DebugMe
62
-
63
- require 'csv'
64
- require 'date'
65
- require 'tty-table'
66
-
67
- require 'fin_tech'
68
- require 'previous_dow'
69
-
70
- class NilClass
71
- def blank?() = true
72
- end
73
-
74
- class String
75
- def blank?() = strip().empty?
76
- end
77
-
78
- class Array
79
- def blank?() = empty?
80
- end
81
-
82
-
83
- def tickers
84
- return @tickers unless @tickers.blank?
85
-
86
- @tickers = []
87
-
88
- STOCKS.readlines.each do |a_line|
89
- ticker_symbol = a_line.chomp.strip.split()&.first&.downcase
90
- next if ticker_symbol.blank? || '#' == ticker_symbol
91
- @tickers << ticker_symbol unless @tickers.include?(ticker_symbol)
92
- end
93
-
94
- @tickers.sort!
95
- end
96
-
97
- given_date = ARGV.first ? Date.parse(ARGV.first) : Date.today
98
-
99
- start_date = Date.new(2019, 1, 1)
100
- end_date = previous_dow(:friday, given_date)
101
-
102
- ASOF = end_date.to_s.tr('-','')
103
-
104
-
105
- #######################################################################
106
- # download a CSV file from https://query1.finance.yahoo.com
107
- # given a stock ticker symbol as a String
108
- # start and end dates
109
- #
110
- # For ticker "aapl" the downloaded file will be named "aapl.csv"
111
- # That filename will be renamed to "aapl_YYYYmmdd.csv" where the
112
- # date suffix is the end_date of the historical data.
113
- #
114
- def download_historical_prices(ticker, start_date, end_date)
115
- data_path = Pathname.pwd + "#{ticker}_#{ASOF}.csv"
116
- return if data_path.exist?
117
-
118
- mew_path = Pathname.pwd + "#{ticker}.csv"
119
-
120
- start_timestamp = start_date.to_time.to_i
121
- end_timestamp = end_date.to_time.to_i
122
- ticker_upcase = ticker.upcase
123
- filename = "#{ticker.downcase}.csv"
124
-
125
- `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"`
126
-
127
- mew_path.rename data_path
128
- end
129
-
130
-
131
- #######################################################################
132
- # Read the CSV file associated with the give ticker symbol
133
- # and the ASOF date.
134
- #
135
- def read_csv(ticker)
136
- filename = "#{ticker.downcase}_#{ASOF}.csv"
137
- data = []
138
-
139
- CSV.foreach(filename, headers: true) do |row|
140
- data << row.to_h
141
- end
142
-
143
- data
144
- end
145
-
146
- ##########################
147
- # record a recommend trade
148
-
149
- def trade(ticker, signal, shares, price)
150
- TRADES_FILE.puts "#{ticker},#{ASOF},#{signal},#{shares},#{price}"
151
- end
152
-
153
- #######################################################################
154
- ###
155
- ## Main
156
- #
157
-
158
-
159
- tickers.each do |ticker|
160
- download_historical_prices(ticker, start_date, end_date)
161
- end
162
-
163
- result = {}
164
-
165
- mwfd = 14 # moving_window_forcast_days
166
-
167
- headers = %w[ Ticker AdjClose Trend Slope M'tum RSI Analysis MACD Target Signal $]
168
- values = []
169
-
170
- tickers.each do |ticker|
171
-
172
- data = read_csv ticker
173
- prices = data.map{|r| r["Adj Close"].to_f}
174
- volumes = data.map{|r| r["volume"].to_f}
175
-
176
- if data.blank?
177
- puts
178
- puts "ERROR: cannot get data for #{ticker}"
179
- puts
180
- next
181
- end
182
-
183
-
184
- result[ticker] = {
185
- date: data.last["Date"],
186
- adj_close: data.last["Adj Close"].to_f
187
- }
188
-
189
- result[ticker][:market] = FinTech.classify_market_profile(
190
- volumes.last(mwfd),
191
- prices.last(mwfd),
192
- prices.last(mwfd).first,
193
- prices.last
194
- )
195
-
196
- fr = FinTech.fibonacci_retracement( prices.last(mwfd).first,
197
- prices.last).map{|x| x.round(3)}
198
-
199
-
200
- puts "\n#{result[ticker][:market]} .. #{ticker}\t#{fr}"
201
- print "\t"
202
- print FinTech.head_and_shoulders_pattern?(prices.last(mwfd))
203
- print "\t"
204
- print FinTech.double_top_bottom_pattern?(prices.last(mwfd))
205
- print "\t"
206
- mr = FinTech.mean_reversion?(prices, mwfd, 0.5)
207
- print mr
208
-
209
- if mr
210
- print "\t"
211
- print FinTech.mr_mean(prices, mwfd).round(3)
212
- end
213
-
214
- print "\t"
215
- print FinTech.identify_wave_condition?(prices, 2*mwfd, 1.0)
216
- puts
217
-
218
- print "\t"
219
- print FinTech.ema_analysis(prices, mwfd).except(:ema_values)
220
-
221
- puts
222
-
223
- row = [ ticker ]
224
-
225
- # result[ticker][:moving_averages] = FinTech.sma(data, mwfd)
226
- result[ticker][:trend] = FinTech.sma_trend(data, mwfd)
227
- result[ticker][:momentum] = FinTech.momentum(prices, mwfd)
228
- result[ticker][:rsi] = FinTech.rsi(data, mwfd)
229
- result[ticker][:bollinger_bands] = FinTech.bollinger_bands(data, mwfd, 2)
230
- result[ticker][:macd] = FinTech.macd(data, mwfd, 2*mwfd, mwfd/2)
231
-
232
- price = result[ticker][:adj_close].round(3)
233
-
234
- row << price
235
- row << result[ticker][:trend][:trend]
236
- row << result[ticker][:trend][:angle].round(3)
237
- row << result[ticker][:momentum].round(3)
238
- row << result[ticker][:rsi][:rsi].round(3)
239
- row << result[ticker][:rsi][:meaning]
240
- row << result[ticker][:macd].first.round(3)
241
- row << result[ticker][:macd].last.round(3)
242
-
243
- analysis = result[ticker][:rsi][:meaning]
244
-
245
- signal = ""
246
- macd_diff = result[ticker][:macd].first
247
- target = result[ticker][:macd].last
248
- current = result[ticker][:adj_close]
249
-
250
- trend_down = "down" == result[ticker][:trend][:trend]
251
-
252
- if current < target
253
- signal = "buy" unless "Over Bought" == analysis
254
- elsif (current > target) && trend_down
255
- signal = "sell" unless "Over Sold" == analysis
256
- end
257
-
258
- if "buy" == signal
259
- pps = target - price
260
- shares = INVEST.to_i / price.to_i
261
- upside = (shares * pps).round(2)
262
- trade(ticker, signal, shares, price)
263
- elsif "sell" == signal
264
- pps = target - price
265
- shares = INVEST.to_i / price.to_i
266
- upside = (shares * pps).round(2)
267
- trade(ticker, signal, shares, price)
268
- else
269
- upside = ""
270
- end
271
-
272
- row << signal
273
- row << upside
274
-
275
- values << row
276
- end
166
+ require_relative 'analysis'
167
+ require_relative 'web'
277
168
 
278
- # debug_me{[
279
- # :result
280
- # ]}
281
-
282
-
283
- the_table = TTY::Table.new(headers, values)
284
-
285
- puts
286
- puts "Analysis as of Friday Close: #{end_date}"
287
-
288
- puts the_table.render(
289
- :unicode,
290
- {
291
- padding: [0, 0, 0, 0],
292
- alignments: [
293
- :left, # ticker
294
- :right, # adj close
295
- :center, # trend
296
- :right, # slope
297
- :right, # momentum
298
- :right, # rsi
299
- :center, # meaning / analysis
300
- :right, # macd
301
- :right, # target
302
- :center, # signal
303
- :right # upside
304
- ],
305
- }
306
- )
307
- puts
308
-
309
- 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
310
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
@@ -3,9 +3,13 @@
3
3
 
4
4
  class SQA::DataFrame < Daru::DataFrame
5
5
  class YahooFinance
6
- def self.from_csv(ticker)
7
- df = SQA::DataFrame.from_csv(ticker)
6
+ def self.load(filename, options={}, &block)
7
+ df = SQA::DataFrame.load(filename, options={}, &block)
8
8
 
9
+ # ASSUMPTION: This is the column headers from Yahoo Finance for
10
+ # CSV files. If the file type is something different from the
11
+ # same source, they may not be the same.
12
+ #
9
13
  new_names = {
10
14
  "Date" => :timestamp,
11
15
  "Open" => :open_price,
@@ -8,22 +8,20 @@ class SQA::DataFrame < Daru::DataFrame
8
8
  SQA::Config.data_dir + filename
9
9
  end
10
10
 
11
- def self.from_csv(ticker)
12
- df = super(path("#{ticker.downcase}.csv"))
13
- df[:ticker] = ticker
14
- df
15
- end
16
-
17
- def self.load(filename)
11
+ def self.load(filename, options={}, &block)
18
12
  source = path(filename)
19
13
  type = source.extname.downcase
20
14
 
21
15
  if ".csv" == type
22
- @df = Daru::DataFrame.from_csv(source)
16
+ from_csv(source, options={}, &block)
23
17
  elsif ".json" == type
24
- @df = Daru::DataFrame.from_json(source)
18
+ from_json(source, options={}, &block)
19
+ elsif %w[.txt .dat].include?(type)
20
+ from_plaintext(source, options={}, &block)
21
+ elsif ".xls" == type
22
+ from_excel(source, options={}, &block)
25
23
  else
26
- raise SQA::BadParamenterError, "supports csv or json only"
24
+ raise SQA::BadParamenterError, "un-suppod file type: #{type}"
27
25
  end
28
26
  end
29
27
  end