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.
- checksums.yaml +4 -4
- data/bin/sqa +1 -1
- data/checksums/sqa-0.0.6.gem.sha512 +1 -1
- data/checksums/sqa-0.0.7.gem.sha512 +1 -0
- data/checksums/sqa-0.0.8.gem.sha512 +1 -0
- data/docs/.gitignore +1 -0
- data/docs/data_frame.md +6 -6
- data/docs/libsvm_file_format.md +47 -0
- data/docs/predict_next_value.md +15 -0
- data/lib/patches/daru/category.rb +19 -0
- data/lib/patches/daru/data_frame.rb +19 -0
- data/lib/patches/daru/plotting/svg-graph/category.rb +55 -0
- data/lib/patches/daru/plotting/svg-graph/dataframe.rb +105 -0
- data/lib/patches/daru/plotting/svg-graph/vector.rb +102 -0
- data/lib/patches/daru/plotting/svg-graph.rb +7 -0
- data/lib/patches/daru/vector.rb +19 -0
- data/lib/patches/daru.rb +19 -0
- data/lib/sqa/analysis.rb +306 -0
- data/lib/sqa/cli.rb +161 -298
- data/lib/sqa/config.rb +169 -0
- data/lib/sqa/constants.rb +23 -0
- data/lib/sqa/data_frame/yahoo_finance.rb +6 -2
- data/lib/sqa/data_frame.rb +8 -10
- data/lib/sqa/indicator/predict_next_value.rb +63 -0
- data/lib/sqa/portfolio.rb +3 -1
- data/lib/sqa/stock.rb +4 -2
- data/lib/sqa/trade.rb +3 -1
- data/lib/sqa/version.rb +9 -2
- data/lib/sqa/web.rb +159 -0
- data/lib/sqa.rb +51 -36
- metadata +108 -6
data/lib/sqa/cli.rb
CHANGED
@@ -1,310 +1,173 @@
|
|
1
1
|
# lib/sqa/cli.rb
|
2
2
|
|
3
|
-
|
3
|
+
require 'tty-option'
|
4
4
|
|
5
|
-
|
6
|
-
class CLI
|
7
|
-
def initialize
|
8
|
-
@args = $ARGV.dup
|
9
|
-
end
|
5
|
+
require_relative '../sqa'
|
10
6
|
|
11
|
-
|
12
|
-
stock = Stock.new('aapl')
|
7
|
+
# SMELL: Architectyre has become confused between CLI and Command
|
13
8
|
|
14
|
-
|
9
|
+
# TODO: Fix the mess between CLI and Command
|
15
10
|
|
16
|
-
TBD
|
17
|
-
@args => #{@args}
|
18
|
-
stock => #{stock}
|
19
11
|
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
-
#
|
279
|
-
#
|
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.
|
7
|
-
df = SQA::DataFrame.
|
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,
|
data/lib/sqa/data_frame.rb
CHANGED
@@ -8,22 +8,20 @@ class SQA::DataFrame < Daru::DataFrame
|
|
8
8
|
SQA::Config.data_dir + filename
|
9
9
|
end
|
10
10
|
|
11
|
-
def self.
|
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
|
-
|
16
|
+
from_csv(source, options={}, &block)
|
23
17
|
elsif ".json" == type
|
24
|
-
|
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, "
|
24
|
+
raise SQA::BadParamenterError, "un-suppod file type: #{type}"
|
27
25
|
end
|
28
26
|
end
|
29
27
|
end
|