sqa 0.0.21 → 0.0.24

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,135 +1,40 @@
1
1
  # lib/sqa/cli.rb
2
2
 
3
+ require 'dry/cli'
3
4
 
4
5
  require_relative '../sqa'
6
+ require_relative 'commands'
5
7
 
6
- # SMELL: Architectyre has become confused between CLI and Command
7
-
8
- # TODO: Fix the mess between CLI and Command
9
-
10
-
11
- module SQA
12
- class CLI
13
- include TTY::Option
14
-
15
- header "Stock Quantitative Analysis (SQA)"
16
- footer "WARNING: This is a toy, a play thing, not intended for serious use."
17
-
18
- program "sqa"
19
- desc "A collection of things"
20
-
21
- example "sqa -c ~/.sqa.yml -p portfolio.csv -t trades.csv --data-dir ~/sqa_data"
22
-
23
-
24
- option :config_file do
25
- short "-c string"
26
- long "--config string"
27
- desc "Path to the config file"
8
+ module SQA::CLI
9
+ class << self
10
+ def run!
11
+ Dry::CLI.new(SQA::Commands).call
28
12
  end
13
+ end
14
+ end
29
15
 
30
- option :log_level do
31
- short "-l string"
32
- long "--log_level string"
33
- # default SQA.config.log_level
34
- desc "Set the log level (debug, info, warn, error, fatal)"
35
- end
36
16
 
37
- option :portfolio do
38
- short "-p string"
39
- long "--portfolio string"
40
- # default SQA.config.portfolio_filename
41
- desc "Set the filename of the portfolio"
42
- end
43
17
 
44
18
 
45
- option :trades do
46
- short "-t string"
47
- long "--trades string"
48
- # default SQA.config.trades_filename
49
- desc "Set the filename into which trades are stored"
50
- end
19
+ __END__
51
20
 
52
21
 
53
- option :data_dir do
54
- long "--data-dir string"
55
- # default SQA.config.data_dir
56
- desc "Set the directory for the SQA data"
57
- end
58
22
 
59
- option :dump_config do
60
- long "--dump-config path_to_file"
61
- desc "Dump the current configuration"
62
- end
63
23
 
64
- flag :help do
65
- short "-h"
66
- long "--help"
67
- desc "Print usage"
68
- end
24
+ # header "Stock Quantitative Analysis (SQA)"
25
+ # footer "WARNING: This is a toy, a play thing, not intended for serious use."
69
26
 
70
- flag :version do
71
- long "--version"
72
- desc "Print version"
73
- end
27
+ # program "sqa"
28
+ # desc "A collection of things"
74
29
 
75
- flag :debug do
76
- short "-d"
77
- long "--debug"
78
- # default SQA.config.debug
79
- desc "Turn on debugging output"
80
- end
81
30
 
82
- flag :verbose do
83
- short "-v"
84
- long "--verbose"
85
- # default SQA.config.debug
86
- desc "Print verbosely"
87
- end
88
31
 
89
- class << self
90
- @@subclasses = []
91
- @@commands_available = []
92
-
93
- def names
94
- '['+ @@commands_available.join('|')+']'
95
- end
96
-
97
- def inherited(subclass)
98
- super
99
- @@subclasses << subclass
100
- @@commands_available << subclass.command.join
101
- end
102
-
103
- def command_descriptions
104
- help_block = "Optional Command Available:"
105
-
106
- @@commands_available.size.times do |x|
107
- klass = @@subclasses[x]
108
- help_block << "\n " + @@commands_available[x] + " - "
109
- help_block << klass.desc.join
110
- end
111
-
112
- help_block
113
- end
114
32
 
33
+ class << self
115
34
 
116
35
  ##################################################
117
36
  def run(argv = ARGV)
118
- cli = new
119
- parser = cli.parse(argv)
120
- params = parser.params
121
37
 
122
- if params[:help]
123
- print parser.help
124
- exit(0)
125
-
126
- elsif params.errors.any?
127
- puts params.errors.summary
128
- exit(1)
129
-
130
- elsif params[:version]
131
- puts SQA.version
132
- exit(0)
133
38
 
134
39
  elsif params[:dump_config]
135
40
  SQA.config.config_file = params[:dump_config]
@@ -144,30 +49,14 @@ module SQA
144
49
  SQA.config.from_file
145
50
  end
146
51
 
52
+
53
+
147
54
  # Override the defaults <- envars <- config file <- cli parameters
148
55
  SQA.config.merge!(remove_temps params.to_h)
149
56
 
150
- if SQA.debug? || SQA.verbose?
151
- debug_me("config after CLI parameters"){[
152
- "SQA.config"
153
- ]}
154
- end
155
- end
156
57
 
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
58
  end
162
59
  end
163
60
  end
164
61
  end
165
62
 
166
- require_relative 'analysis'
167
- require_relative 'web'
168
-
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
173
-
@@ -1,18 +1,26 @@
1
- # lib/sqa/command/analysis.rb
1
+ # sqa/lib/sqa/commands/analysis.rb
2
2
 
3
- module SQA
4
- class Analysis < CLI
5
- include TTY::Option
3
+ class Commands::Analysis < Commands::Base
4
+ VERSION = "0.0.1-analysis"
6
5
 
7
- command "Analysis"
6
+ Commands.register "analysis", self
8
7
 
9
- desc "Provide an Analysis of a Portfolio"
8
+ desc "Provide an Analysis of a Portfolio"
10
9
 
11
10
 
12
- def initialize
13
- # TODO: something
14
- end
11
+ def initialize
12
+ # TODO: something
15
13
  end
14
+
15
+ def call(params)
16
+ config = super
17
+
18
+ puts <<~EOS
19
+ ##################################
20
+ ## Running the Analysis Command ##
21
+ ##################################
22
+ EOS
23
+ end
16
24
  end
17
25
 
18
26
  __END__
@@ -270,11 +278,6 @@ tickers.each do |ticker|
270
278
  values << row
271
279
  end
272
280
 
273
- # debug_me{[
274
- # :result
275
- # ]}
276
-
277
-
278
281
  the_table = TTY::Table.new(headers, values)
279
282
 
280
283
  puts
@@ -0,0 +1,139 @@
1
+ # .../sqa/cli/commands/base.rb
2
+
3
+ # SQA.config will be built with its defaults
4
+ # and envar over-rides BEFORE a command is
5
+ # process. This means that options do not
6
+ # need to have a "default" value.
7
+
8
+ # Establish a Base command class that has global options
9
+ # available to all commands.
10
+
11
+ class Commands::Base < Dry::CLI::Command
12
+ # keys from Dry::Cli options which we do not want in the
13
+ # config object.
14
+ IGNORE_OPTIONS = %i[ version ]
15
+
16
+ global_header <<~EOS
17
+
18
+ SQA - Stock Quantitative Analysis
19
+ by: MadBomber
20
+
21
+ This is a work in progress. It is not fit for anything
22
+ other than play time. ** Do not ** use it to make any
23
+ kind of serious trading decisions.
24
+
25
+ EOS
26
+
27
+ global_footer <<~EOS
28
+
29
+ SARNING: This product is a work in progress. DO NOT USE
30
+ for serious trading decisions.
31
+
32
+ Copyright (c) 2023 - MadBomber Software
33
+
34
+ EOS
35
+
36
+ option :debug,
37
+ required: false,
38
+ type: :boolean,
39
+ desc: 'Print debug information',
40
+ aliases: %w[-d --debug]
41
+
42
+ option :verbose,
43
+ required: false,
44
+ type: :boolean,
45
+ desc: 'Print verbose information',
46
+ aliases: %w[-v --verbose]
47
+
48
+
49
+ option :version,
50
+ required: false,
51
+ type: :boolean,
52
+ default: false,
53
+ desc: 'Print version(s) and exit',
54
+ aliases: %w[--version]
55
+
56
+
57
+ option :config_file,
58
+ required: false,
59
+ type: :string,
60
+ desc: "Path to the config file"
61
+
62
+
63
+ option :log_level,
64
+ required: false,
65
+ type: :string,
66
+ values: %w[debug info warn error fatal ],
67
+ desc: "Set the log level"
68
+
69
+
70
+ option :portfolio,
71
+ required: false,
72
+ aliases: %w[ --portfolio --folio --file -f ],
73
+ type: :string,
74
+ desc: "Set the filename of the portfolio"
75
+
76
+
77
+ option :trades,
78
+ required: false,
79
+ aliases: %w[ --trades ],
80
+ type: :string,
81
+ desc: "Set the filename into which trades are stored"
82
+
83
+
84
+ option :data_dir,
85
+ required: false,
86
+ aliases: %w[ --data-dir --data --dir ],
87
+ type: :string,
88
+ desc: "Set the directory for the SQA data"
89
+
90
+
91
+ option :dump_config,
92
+ required: false,
93
+ type: :string,
94
+ desc: "Dump the current configuration to a file"
95
+
96
+
97
+ # All command class call methods should start with
98
+ # super so that this method is invoked.
99
+ #
100
+ # params is a Hash from Dry::CLI where keys are Symbol
101
+
102
+ def call(params)
103
+ show_versions_and_exit if params[:version]
104
+
105
+ unless params[:config_file].nil? || params[:config_file].empty?
106
+ SQA.config.config_file = params[:config_file]
107
+ SQA.config.from_file
108
+ end
109
+
110
+ update_config(params)
111
+
112
+ unless params[:dump_config].nil? || params[:dump_config].empty?
113
+ SQA.config.config_file = params[:dump_config]
114
+ SQA.config.dump_file
115
+ end
116
+
117
+ SQA.config
118
+ end
119
+
120
+ ################################################
121
+ private
122
+
123
+ def show_versions_and_exit
124
+ self.class.ancestors.each do |ancestor|
125
+ next unless ancestor.const_defined?(:VERSION)
126
+ puts "#{ancestor}: #{ancestor::VERSION}"
127
+ end
128
+
129
+ puts "SQA: #{SQA::VERSION}" if SQA.const_defined?(:VERSION)
130
+
131
+ exit(0)
132
+ end
133
+
134
+ def update_config(params)
135
+ SQA.config.inject_additional_properties
136
+ my_hash = params.reject { |key, _| IGNORE_OPTIONS.include?(key) }
137
+ SQA.config.merge!(my_hash)
138
+ end
139
+ end
@@ -1,63 +1,103 @@
1
- # lib/sqa/command/web.rb
1
+ # sqa/lib/sqa/commands/web.rb
2
2
 
3
- # require 'tty-option'
3
+ class Commands::Web < Commands::Base
4
+ VERSION = "0.0.1-web"
4
5
 
6
+ Commands.register "web", self
5
7
 
6
- module SQA
7
- class Web < CLI
8
- include TTY::Option
8
+ desc "Start a web application"
9
+
10
+ option :image,
11
+ required: true,
12
+ type: :string,
13
+ desc: "The name of the image to use"
14
+
15
+ SQA::PluginManager.new_property(:restart, default: 'no', coerce: String)
16
+
17
+ option :restart,
18
+ aliases: %w[ --restart ],
19
+ type: :string,
20
+ default: "no",
21
+ values: %w[ no on-failure always unless-stopped ],
22
+ desc: "Restart policy to apply when a container exits"
23
+
24
+ SQA::PluginManager.new_property(:detach, default: 'no', coerce: String)
25
+
26
+ option :detach,
27
+ aliases: %w[ --detach ],
28
+ type: :boolean,
29
+ default: false,
30
+ desc: "Run container in background and print container ID"
31
+
32
+ SQA::PluginManager.new_property(:port, default: 4567, coerce: Integer)
9
33
 
10
- command "web"
34
+ option :port,
35
+ aliases: %w[ -p --port ],
36
+ type: :integer,
37
+ default: 4567,
38
+ desc: "The port where the web app will run"
11
39
 
12
- desc "Run a web server"
13
40
 
14
- example "Set working directory (-w)",
15
- " sqa web --port 4567 --data-dir /path/to/dir/ ubuntu pwd"
41
+ def initialize
42
+ # TODO: make it happen
43
+ end
44
+
16
45
 
17
- example <<~EOS
18
- Do Something
19
- sqa web
46
+ # params is Object from the ARGV parser
47
+ def call(params)
48
+ config = super
49
+
50
+ puts <<~EOS
51
+ ###############################
52
+ ## Running the Web Interface ##
53
+ ###############################
20
54
  EOS
55
+ end
56
+ end
21
57
 
22
- argument :image do
23
- required
24
- desc "The name of the image to use"
25
- end
26
58
 
27
- keyword :restart do
28
- default "no"
29
- permit %w[no on-failure always unless-stopped]
30
- desc "Restart policy to apply when a container exits"
31
- end
32
59
 
33
- flag :detach do
34
- long "--detach"
35
- desc "Run container in background and print container ID"
60
+ __END__
61
+
62
+ require 'sinatra/base'
63
+
64
+ module SQA
65
+ class Web < Sinatra::Base
66
+ set :port, SQA.config.port || 4567
67
+
68
+ get '/' do
69
+ "Welcome to SQA Web Interface!"
36
70
  end
37
71
 
38
- option :name do
39
- required
40
- long "--name string"
41
- desc "Assign a name to the container"
72
+
73
+ get '/stocks/:ticker' do
74
+ ticker = params[:ticker]
75
+ stock = SQA::Stock.new(ticker: ticker, source: :alpha_vantage)
76
+
77
+ "Stock: #{stock.data.name}, Ticker: #{stock.data.ticker}"
42
78
  end
43
79
 
44
- option :port do
45
- arity one_or_more
46
- long "--port integer"
47
- default 4567
48
- desc "The port where the web app will run"
80
+
81
+ get '/stocks/:ticker/indicators/:indicator' do
82
+ ticker = params[:ticker]
83
+ indicator = params[:indicator]
84
+ stock = SQA::Stock.new(ticker: ticker, source: :alpha_vantage)
85
+
86
+ indicator_value = SQA::Indicator.send(indicator, stock.df.adj_close_price, 14)
87
+
88
+ "Indicator #{indicator} for Stock #{ticker} is #{indicator_value}"
49
89
  end
50
90
 
91
+ # TODO: Add more routes as needed to expose more functionality
51
92
 
52
- def initialize
53
- # TODO: make it happen
54
- end
55
- end
93
+ # start the server if ruby file executed directly
94
+ run! if app_file == $0
95
+ end
56
96
  end
57
97
 
58
- __END__
59
98
 
60
99
 
100
+ ###################################################
61
101
  #!/usr/bin/env ruby
62
102
  # experiments/sinatra_examples/svg_viewer.rb
63
103
  # builds on md_viewer.rb
@@ -0,0 +1,22 @@
1
+ # sqa/lib/sqa/commands.rb
2
+
3
+ # Adds command options to SQA.config
4
+ require_relative "plugin_manager"
5
+
6
+ module SQA::Commands
7
+ # Establish the command registry
8
+ extend Dry::CLI::Registry
9
+ end
10
+
11
+ Commands = SQA::Commands
12
+
13
+
14
+ load_these_first = [
15
+ "#{__dir__}/commands/base.rb",
16
+ ].each { |file| require_relative file }
17
+
18
+ Dir.glob("#{__dir__}/commands/*.rb")
19
+ .reject{|file| load_these_first.include? file}
20
+ .each do |file|
21
+ require_relative file
22
+ end
data/lib/sqa/config.rb CHANGED
@@ -6,10 +6,18 @@
6
6
  # config file ..... overrides envar
7
7
  # command line parameters ...... overrides config file
8
8
 
9
+ require 'yaml'
10
+ require 'toml-rb'
9
11
 
10
12
  module SQA
13
+ # class Config < Hashie::Trash
14
+ # include Hashie::Extensions::IgnoreUndeclared
15
+ # include Hashie::Extensions::Coercion
16
+
17
+
11
18
  class Config < Hashie::Dash
12
19
  include Hashie::Extensions::Dash::PropertyTranslation
20
+ include Hashie::Extensions::MethodAccess
13
21
  include Hashie::Extensions::Coercion
14
22
 
15
23
  # FIXME: Getting undefined error PredefinedValues
@@ -19,7 +27,8 @@ module SQA
19
27
  #
20
28
  # include Hashie::Extensions::Dash::PredefinedValues
21
29
 
22
- property :config_file #,a String filepath for the current config overriden by cli options
30
+ property :command # a String currently, nil, analysis or web
31
+ property :config_file # a String filepath for the current config overriden by cli options
23
32
  property :dump_config # a String filepath into which to dump the current config
24
33
 
25
34
  property :data_dir, default: Nenv.home + "/sqa_data"
@@ -111,13 +120,10 @@ module SQA
111
120
  raise BadParameterError, "No config file given"
112
121
  end
113
122
 
114
- if File.exist?(config_file) &&
115
- File.file?(config_file) &&
116
- File.writable?(config_file)
117
- type = File.extname(config_file).downcase
118
- else
119
- type = "invalid"
120
- end
123
+ `touch #{config_file}`
124
+ # unless File.exist?(config_file)
125
+
126
+ type = File.extname(config_file).downcase
121
127
 
122
128
  if ".json" == type
123
129
  dump_json
@@ -129,7 +135,14 @@ module SQA
129
135
  dump_toml
130
136
 
131
137
  else
132
- raise BadParameterError, "Invalid Config File: #{config_file}"
138
+ raise BadParameterError, "Invalid Config File Type: #{config_file}"
139
+ end
140
+ end
141
+
142
+ # Method to dynamically extend properties from external sources (e.g., plugins)
143
+ def inject_additional_properties
144
+ SQA::PluginManager.registered_properties.each do |prop, options|
145
+ self.class.property(prop, options)
133
146
  end
134
147
  end
135
148
 
@@ -1,6 +1,12 @@
1
1
  # lib/sqa/data_frame/yahoo_finance.rb
2
2
  # frozen_string_literal: true
3
3
 
4
+ =begin
5
+ The website financial.yahoo.com no longer supports an API.
6
+ To get recent stock historical price updates you have
7
+ to scrape the webpage.
8
+ =end
9
+
4
10
 
5
11
  class SQA::DataFrame
6
12
  class YahooFinance
@@ -72,3 +78,4 @@ class SQA::DataFrame
72
78
  end
73
79
  end
74
80
  end
81
+
@@ -26,30 +26,39 @@ class SQA::DataFrame
26
26
  # mapping is a Hash { old_key => new_key }
27
27
  # transformers is also a Hash { key => Proc}
28
28
  def initialize(
29
- aofh_or_hofa= {}, # Array of Hashes or hash of array or hash
29
+ raw_data= {}, # Array of Hashes or hash of array or hash
30
30
  mapping: {}, # { old_key => new_key }
31
31
  transformers: {} # { key => Proc }
32
32
  )
33
33
 
34
- if aofh_or_hofa.is_a? Hash
35
- initialize_hofa(aofh_or_hofa, mapping: mapping)
34
+ if raw_data.is_a? Hash
35
+ initialize_hofa(raw_data, mapping: mapping)
36
36
 
37
- elsif aofh_or_hofa.is_a?(Array) &&
38
- aofh_or_hofa.first.is_a?(Hash)
39
- initialize_aofh(aofh_or_hofa, mapping: mapping)
37
+ elsif raw_data.is_a?(Array) &&
38
+ raw_data.first.is_a?(Hash)
39
+ initialize_aofh(raw_data, mapping: mapping)
40
40
 
41
41
  else
42
42
  raise BadParameterError, "Expecting Hash or Array of Hashes got: #{aofh_or_hofa.class}"
43
43
  end
44
44
 
45
- coerce_vectors!(transformers) unless transformers.empty?
45
+ coerce_vectors!(transformers) if good_data? && !(transformers.nil? || transformers.empty?)
46
+ end
47
+
48
+
49
+ def good_data?
50
+ return false if @data.empty? || @data.values.all?{|v| v.nil? || v.empty?}
51
+
52
+ true
46
53
  end
47
54
 
48
55
 
49
56
  def initialize_aofh(aofh, mapping:)
50
- hofa = self.class.aofh_to_hofa(
57
+ klass = self.class
58
+
59
+ hofa = klass.aofh_to_hofa(
51
60
  aofh,
52
- mapping: mapping
61
+ mapping: mapping
53
62
  )
54
63
 
55
64
  initialize_hofa(hofa, mapping: mapping)
@@ -57,7 +66,8 @@ class SQA::DataFrame
57
66
 
58
67
 
59
68
  def initialize_hofa(hofa, mapping:)
60
- hofa = self.class.normalize_keys(
69
+ klass = self.class
70
+ hofa = klass.normalize_keys(
61
71
  hofa,
62
72
  adapter_mapping: mapping
63
73
  ) unless mapping.empty?
@@ -283,7 +293,7 @@ class SQA::DataFrame
283
293
  end
284
294
  end
285
295
 
286
- # SMELL: This might be necessary
296
+ # SMELL: This might not be necessary
287
297
  normalize_keys(hofa, adapter_mapping: mapping)
288
298
  end
289
299
 
@@ -291,13 +301,14 @@ class SQA::DataFrame
291
301
  def normalize_keys(hofa, adapter_mapping: {})
292
302
  hofa = rename(adapter_mapping, hofa)
293
303
  mapping = generate_mapping(hofa.keys)
304
+
294
305
  rename(mapping, hofa)
295
306
  end
296
307
 
297
308
 
298
309
  def rename(mapping, hofa)
299
310
  mapping.each_pair do |old_key, new_key|
300
- hofa[new_key] = hofa.delete(old_key)
311
+ hofa[new_key] = hofa.delete(old_key) if hofa.has_key?(old_key)
301
312
  end
302
313
 
303
314
  hofa