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.
- checksums.yaml +4 -4
- data/.config/tocer/configuration.yml +1 -0
- data/CHANGELOG.md +4 -0
- data/README.md +141 -4
- data/checksums/sqa-0.0.21.gem.sha512 +1 -1
- data/checksums/sqa-0.0.22.gem.sha512 +1 -0
- data/checksums/sqa-0.0.23.gem.sha512 +1 -0
- data/checksums/sqa-0.0.24.gem.sha512 +1 -0
- data/docs/ta_lib.md +160 -0
- data/lib/patches/dry-cli.rb +228 -0
- data/lib/sqa/cli.rb +16 -127
- data/lib/sqa/{analysis.rb → commands/analysis.rb} +17 -14
- data/lib/sqa/commands/base.rb +139 -0
- data/lib/sqa/{web.rb → commands/web.rb} +78 -38
- data/lib/sqa/commands.rb +22 -0
- data/lib/sqa/config.rb +22 -9
- data/lib/sqa/data_frame/yahoo_finance.rb +7 -0
- data/lib/sqa/data_frame.rb +23 -12
- data/lib/sqa/indicator/average_true_range.rb +0 -10
- data/lib/sqa/init.rb +1 -1
- data/lib/sqa/plugin_manager.rb +20 -0
- data/lib/sqa/stock.rb +30 -1
- data/lib/sqa/strategy/common.rb +0 -2
- data/lib/sqa/version.rb +1 -1
- data/lib/sqa.rb +22 -2
- metadata +35 -13
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
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
71
|
-
|
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/
|
1
|
+
# sqa/lib/sqa/commands/analysis.rb
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
include TTY::Option
|
3
|
+
class Commands::Analysis < Commands::Base
|
4
|
+
VERSION = "0.0.1-analysis"
|
6
5
|
|
7
|
-
|
6
|
+
Commands.register "analysis", self
|
8
7
|
|
9
|
-
|
8
|
+
desc "Provide an Analysis of a Portfolio"
|
10
9
|
|
11
10
|
|
12
|
-
|
13
|
-
|
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/
|
1
|
+
# sqa/lib/sqa/commands/web.rb
|
2
2
|
|
3
|
-
|
3
|
+
class Commands::Web < Commands::Base
|
4
|
+
VERSION = "0.0.1-web"
|
4
5
|
|
6
|
+
Commands.register "web", self
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
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
|
-
|
15
|
-
|
41
|
+
def initialize
|
42
|
+
# TODO: make it happen
|
43
|
+
end
|
44
|
+
|
16
45
|
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
data/lib/sqa/commands.rb
ADDED
@@ -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 :
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
+
|
data/lib/sqa/data_frame.rb
CHANGED
@@ -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
|
-
|
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
|
35
|
-
initialize_hofa(
|
34
|
+
if raw_data.is_a? Hash
|
35
|
+
initialize_hofa(raw_data, mapping: mapping)
|
36
36
|
|
37
|
-
elsif
|
38
|
-
|
39
|
-
initialize_aofh(
|
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)
|
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
|
-
|
57
|
+
klass = self.class
|
58
|
+
|
59
|
+
hofa = klass.aofh_to_hofa(
|
51
60
|
aofh,
|
52
|
-
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
|
-
|
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
|