stockcruncher 1.0.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1fe544c0ab3e0bb05c2cca11cb4363bce698cf88b565d897098420b3b8206ac
4
- data.tar.gz: bc0de76838cf286f9c5400ddc368da33ee2bd748f0a5a29ebed653010e183d8a
3
+ metadata.gz: '00734941064bb0a639312c8908fb983698df14f5ccfe50b5957c0631998d0da7'
4
+ data.tar.gz: 5e27da219578442b05ceebc79863d7e02048d1b97a13b5bd5fa6c8939e1fe2a4
5
5
  SHA512:
6
- metadata.gz: b69c6d91c2160e1e70a339ea68616b27012839b105dcfd0cef5ccf7a9643e3a08fb88738d794cf98e3d4de03e505c32db4a827d45a296d734f7e10d2334eb287
7
- data.tar.gz: 44ecc54323e85117c04bb0aa8b9c2ab54421020a2614b01356988700f610264e1e82a07e88d5fd62f62945c1469c508699b3a78c2dad52a73b988e508ce2264d
6
+ metadata.gz: 27ed506233c74a8690cb75e1e994f5ae639d55e281cd9adde3f2af9b96a675f7043d87395dfd42898af50b6759da7471352716bdd844f3a743ff3f1482ab4dae
7
+ data.tar.gz: 9780064a2b3928040b7acbababb3ba47f2b4ca115c750e8765a8d5bce48b7584c3542c87f2f996c31d81a62eb01676324254fd40c3db6778b9913b03599adbd6
data/CHANGELOG CHANGED
@@ -5,3 +5,26 @@ Changelog
5
5
  -----
6
6
 
7
7
  - Initial version
8
+
9
+ 1.1.0
10
+ -----
11
+
12
+ - Add InfluxDB as database.
13
+ - WIP for daily time serie.
14
+
15
+ 1.1.1
16
+ -----
17
+
18
+ - Rescue error on no data
19
+
20
+ 1.2.0
21
+ -----
22
+
23
+ - daily subcommand now recalculates missing data
24
+ - daily subcommand also writes data to database
25
+
26
+ 1.2.1
27
+ -----
28
+
29
+ - Refactor to preprocess data in source class
30
+ - Refactor to print pretty json instead of comma separated striing
data/README.md CHANGED
@@ -25,29 +25,38 @@ to write the data in a database for later calculation and use.
25
25
 
26
26
  $ gem install stockcruncher
27
27
  $ mkdir /etc/stockcruncher && cd /etc/stockcruncher
28
- $ echo $'AlphaVantage:\n apikey: CHANGEME' > stockcruncher.yml
28
+ $ echo 'AlphaVantage:' > stockcruncher.yml
29
+ $ echo ' apikey: CHANGEME' >> stockcruncher.yml
30
+ $ echo 'InfluxDB:' >> stockcruncher.yml
31
+ $ echo ' scheme: http' >> stockcruncher.yml
32
+ $ echo ' host: localhost' >> stockcruncher.yml
33
+ $ echo ' port: 8086' >> stockcruncher.yml
34
+ $ echo ' user: CHANGEMEAGAIN' >> stockcruncher.yml
35
+ $ echo ' password: CHANGEMETOO' >> stockcruncher.yml
36
+ $ echo ' dbname: stock' >> stockcruncher.yml
29
37
 
30
38
  ## Usage
31
39
 
32
40
  An interactive help is available with:
33
41
 
34
- $ stockcruncher help
42
+ $ stockcruncher help [subcommand]
35
43
 
36
44
  ## Examples
37
45
 
38
46
  To get daily time serie data of a symbol:
39
47
 
40
- $ stockcruncher crunch AAPL daily
48
+ $ stockcruncher daily AAPL
41
49
 
42
50
  To get last day endpoint data of a symbol:
43
51
 
44
- $ stockcruncher crunch AAPL quote_endpoint
52
+ $ stockcruncher quote AAPL
45
53
 
46
54
  ## Limitations
47
55
 
48
56
  Data are currently scraped from AlphaVantage API.
49
57
  More source could be great especially because AlphaVantage doesn't provide EU
50
58
  intraday data.
59
+ InfluxDB is used as database to keep time series data.
51
60
 
52
61
  ## Development
53
62
 
@@ -10,6 +10,9 @@ module StockCruncher
10
10
 
11
11
  def start(args = ARGV)
12
12
  StockCruncher::CLI.start(args)
13
+ rescue StandardError => e
14
+ warn e.message
15
+ exit 1
13
16
  end
14
17
  end
15
18
  end
@@ -8,36 +8,97 @@ module StockCruncher
8
8
  class AlphaVantage < Cruncher
9
9
  API_URL = 'https://www.alphavantage.co/query?'
10
10
 
11
- # Main method to crunch data.
12
- def crunch_daily(symbol, opts)
13
- url = API_URL + parameters(symbol, 'TIME_SERIES_DAILY') + options_d(opts)
14
- res = request(url)
15
- puts res.body
11
+ # Method to calculate missing data (previousClose, change, changePercent)
12
+ def calculate_missing_data(hash)
13
+ keys = hash.keys
14
+ hash.each_with_index do |(date, v), index|
15
+ prevday = keys[index + 1]
16
+ next if prevday.nil?
17
+
18
+ prevclose = hash[prevday]['close']
19
+ hash[date] = v.merge(generate_missing_data(v['close'], prevclose))
20
+ end
21
+ hash
22
+ end
23
+
24
+ # Method to calculate change difference
25
+ def change(value, base)
26
+ (value - base).round(4).to_s
27
+ end
28
+
29
+ # Method to calculate percentage of change
30
+ def change_percent(value, base)
31
+ ((value / base - 1) * 100).round(4).to_s
32
+ end
33
+
34
+ # Method to create a new hash from two arrays of keys and values
35
+ def create_hash(descriptions, values)
36
+ descriptions.split(',').zip(values.split(',')).to_h
16
37
  end
17
38
 
18
39
  # Main method to crunch data.
19
- def crunch_quote(symbol, opts)
20
- url = API_URL + parameters(symbol, 'GLOBAL_QUOTE') + options_q(opts)
40
+ def crunch_daily(symbol, fullsize)
41
+ url = API_URL + parameters(symbol, 'TIME_SERIES_DAILY')
42
+ url += '&datatype=csv&outputsize=' + (fullsize ? 'full' : 'compact')
21
43
  res = request(url)
22
- puts res.body
44
+ transform_daily(res.body)
23
45
  end
24
46
 
25
- def options_d(opts)
26
- o = '&datatype=' + (opts['json'] ? 'json' : 'csv')
27
- o += '&outputsize=' + (opts['full'] ? 'full' : 'compact')
28
- o
47
+ # Main method to crunch data.
48
+ def crunch_quote(symbol)
49
+ url = API_URL + parameters(symbol, 'GLOBAL_QUOTE')
50
+ url += '&datatype=csv'
51
+ res = request(url)
52
+ transform_quote(res.body)
29
53
  end
30
54
 
31
- def options_q(opts)
32
- o = '&datatype=' + (opts['json'] ? 'json' : 'csv')
33
- o
55
+ # Method to generate missing data
56
+ def generate_missing_data(current, previous)
57
+ {
58
+ 'previousClose' => previous,
59
+ 'change' => change(current.to_f, previous.to_f),
60
+ 'changePercent' => change_percent(current.to_f, previous.to_f)
61
+ }
34
62
  end
35
63
 
64
+ # Set parameters of api call
36
65
  def parameters(symbol, serie)
37
66
  p = 'function=' + serie
38
67
  p += '&symbol=' + symbol
39
68
  p += '&apikey=' + @config[self.class.name.split('::').last]['apikey']
40
69
  p
41
70
  end
71
+
72
+ # Method to transform raw data to constructed hash
73
+ def prepare_daily_timeserie(data)
74
+ lines = data.split("\r\n")
75
+ desc = lines.shift.split(',').drop(1)
76
+ hash = {}
77
+ lines.each do |line|
78
+ values = line.split(',')
79
+ date = values.shift
80
+ hash[date] = desc.zip(values).to_h
81
+ end
82
+ hash
83
+ end
84
+
85
+ # Method to transform daily result to nested hash
86
+ def transform_daily(rawdata)
87
+ raise StandardError, 'No data' if rawdata.match?(/Error Message/)
88
+
89
+ values = prepare_daily_timeserie(rawdata)
90
+ values = calculate_missing_data(values)
91
+ values
92
+ end
93
+
94
+ # Method to transform quote result to hash
95
+ def transform_quote(rawdata)
96
+ raise StandardError, 'No data' if rawdata.match?(/{}/)
97
+
98
+ values = create_hash(*rawdata.split("\r\n"))
99
+ values['close'] = values.delete('price')
100
+ values['changePercent'] = values['changePercent'].delete('%')
101
+ values
102
+ end
42
103
  end
43
104
  end
@@ -1,19 +1,14 @@
1
1
  #!/usr/bin/ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'json'
4
5
  require 'thor'
6
+ require 'yaml'
5
7
 
6
8
  module StockCruncher
7
9
  # Simple CLI for StockCruncher
8
10
  class CLI < Thor
9
- desc 'version', 'Print stockcruncher current version'
10
- def version
11
- puts "StockCruncher version #{StockCruncher::VERSION}"
12
- end
13
-
14
- desc('daily SYMBOL [options]',
15
- 'Crunch SYMBOL stock market data for daily time series.')
16
- option(
11
+ class_option(
17
12
  :config,
18
13
  aliases: ['-c'],
19
14
  type: :string,
@@ -21,13 +16,28 @@ module StockCruncher
21
16
  desc: 'Yaml formatted config file to load ' \
22
17
  '(default to /etc/stockcruncher/stockcruncher.yml).'
23
18
  )
24
- option(
25
- :json,
26
- aliases: ['-j'],
19
+ class_option(
20
+ :insecure,
21
+ aliases: ['-k'],
27
22
  type: :boolean,
28
23
  default: false,
29
- desc: 'Json format data (default to csv).'
24
+ desc: 'Ignore SSL certificate (default to false).'
30
25
  )
26
+ class_option(
27
+ :quiet,
28
+ aliases: ['-q'],
29
+ type: :boolean,
30
+ default: false,
31
+ desc: 'Run silently (default to false).'
32
+ )
33
+
34
+ desc 'version', 'Print stockcruncher current version'
35
+ def version
36
+ puts "StockCruncher version #{StockCruncher::VERSION}"
37
+ end
38
+
39
+ desc('daily SYMBOL [options]',
40
+ 'Crunch SYMBOL stock market data for daily time series.')
31
41
  option(
32
42
  :full,
33
43
  aliases: ['-f'],
@@ -37,31 +47,37 @@ module StockCruncher
37
47
  )
38
48
  def daily(symbol)
39
49
  opts = options.dup
40
- cruncher = StockCruncher::AlphaVantage.new(opts['config'])
41
- cruncher.crunch_daily(symbol, opts)
50
+ config = YAML.load_file(opts['config'])
51
+ cruncher = StockCruncher::AlphaVantage.new(config, opts['insecure'])
52
+ data = cruncher.crunch_daily(symbol, opts['full'])
53
+ StockCruncher::InfluxDB.new(config).export_history(symbol, data)
54
+ puts JSON.pretty_generate(data) unless opts['quiet']
42
55
  end
43
56
 
44
- desc('quote SYMBOL [options]',
45
- 'Crunch SYMBOL stock market data for last day quote.')
57
+ desc('movingaverages SYMBOL [options]',
58
+ 'Calculate and export moving averages for requested symbol.')
46
59
  option(
47
- :config,
48
- aliases: ['-c'],
49
- type: :string,
50
- default: '/etc/stockcruncher/stockcruncher.yml',
51
- desc: 'Yaml formatted config file to load ' \
52
- '(default to /etc/stockcruncher/stockcruncher.yml).'
53
- )
54
- option(
55
- :json,
56
- aliases: ['-j'],
60
+ :all,
61
+ aliases: ['-a'],
57
62
  type: :boolean,
58
63
  default: false,
59
- desc: 'Json format data (default to csv).'
64
+ desc: 'Recalculate all MA historical values.'
60
65
  )
66
+ def movingaverages(symbol)
67
+ opts = options.dup
68
+ config = YAML.load_file(opts['config'])
69
+ StockCruncher::InfluxDB.new(config).moving_averages(symbol, opts['all'])
70
+ end
71
+
72
+ desc('quote SYMBOL [options]',
73
+ 'Crunch SYMBOL stock market data for last day quote.')
61
74
  def quote(symbol)
62
75
  opts = options.dup
63
- cruncher = StockCruncher::AlphaVantage.new(opts['config'])
64
- cruncher.crunch_quote(symbol, opts)
76
+ config = YAML.load_file(opts['config'])
77
+ cruncher = StockCruncher::AlphaVantage.new(config, opts['insecure'])
78
+ data = cruncher.crunch_quote(symbol)
79
+ StockCruncher::InfluxDB.new(config).export_last_day(data)
80
+ puts JSON.pretty_generate(data) unless opts['quiet']
65
81
  end
66
82
  end
67
83
  end
@@ -2,27 +2,22 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'net/http'
5
- require 'yaml'
6
5
 
7
6
  module StockCruncher
8
7
  # This is an data cruncher abstract class.
9
8
  class Cruncher
10
9
  # Class constructor method
11
- def initialize(file)
12
- @config = load_conf(file)
13
- end
14
-
15
- # Method to load configurations described in config_file.
16
- def load_conf(file)
17
- YAML.load_file(file)
10
+ def initialize(config, insecure = false)
11
+ @config = config
12
+ @insecure = insecure
18
13
  end
19
14
 
20
15
  # Method to send http get request
21
- def request(url, insecure = false)
16
+ def request(url)
22
17
  uri = URI.parse(url)
23
18
  http = Net::HTTP.new(uri.host, uri.port)
24
19
  http.use_ssl = uri.scheme.eql?('https')
25
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
20
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @insecure
26
21
  req = Net::HTTP::Get.new(uri.request_uri)
27
22
  http.request(req)
28
23
  end
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'date'
5
+ require 'json'
6
+ require 'net/http'
7
+
8
+ module StockCruncher
9
+ # this is a class to write time series to database
10
+ class InfluxDB
11
+ # Class constructor method
12
+ def initialize(config, insecure = false)
13
+ @cfg = config[self.class.name.split('::').last]
14
+ @insecure = insecure
15
+ end
16
+
17
+ def get_daily_values(symbol, fullsize)
18
+ values = %w[close change changePercent volume]
19
+ data = query('daily', symbol, values, fullsize)
20
+ data['columns'].zip(data['values'].transpose).to_h
21
+ end
22
+
23
+ # Method to calculate moving averages based on last day values
24
+ def moving_averages(symbol, fullsize)
25
+ series = get_daily_values(symbol, fullsize)
26
+ tags = create_tags(symbol)
27
+ series['close'].each_index do |i|
28
+ date = series['time'][i]
29
+ serie = series['close'][i, 201]
30
+ weights = series['volume'][i, 201]
31
+ write_moving_averages(tags, serie, weights, date)
32
+ break unless fullsize
33
+ end
34
+ end
35
+
36
+ # Method to create tags hash containing only symbol
37
+ def create_tags(symbol)
38
+ { 'symbol' => symbol }
39
+ end
40
+
41
+ # Method to export historical data to database
42
+ def export_history(symbol, timeseries)
43
+ tags = create_tags(symbol)
44
+ timeseries.each_pair do |date, values|
45
+ write('daily', tags, values, date)
46
+ end
47
+ end
48
+
49
+ # Method to export latest data to database
50
+ def export_last_day(values)
51
+ tags = create_tags(values.delete('symbol'))
52
+ date = values.delete('latestDay')
53
+ write('daily', tags, values, date)
54
+ end
55
+
56
+ # Method to format and array of values into comma separated string
57
+ def format_values(values)
58
+ values.map { |k, v| "#{k}=#{v}" }.join(',')
59
+ end
60
+
61
+ # Method to calculate all statistics
62
+ def write_moving_averages(tags, serie, weights, date)
63
+ write('ema', tags, StockCruncher::Stats.list_ema(serie), date)
64
+ write('lwma', tags, StockCruncher::Stats.list_lwma(serie), date)
65
+ write('sma', tags, StockCruncher::Stats.list_sma(serie), date)
66
+ write('vwma', tags, StockCruncher::Stats.list_vwma(serie, weights), date)
67
+ end
68
+
69
+ # Method to query data in bucket
70
+ def query(name, symbol, values, full)
71
+ url = "#{@cfg['scheme']}://#{@cfg['host']}:#{@cfg['port']}/query?" \
72
+ "db=#{@cfg['dbname']}"
73
+ size = full ? '' : 'LIMIT 201'
74
+ body = "q=SELECT #{values.join(',')} FROM #{name} " \
75
+ "WHERE symbol = '#{symbol}' ORDER BY time DESC #{size}"
76
+ data = JSON.parse(request(url, body).body)['results'][0]['series']
77
+ raise StandardError, 'No data' if data.nil?
78
+
79
+ data[0]
80
+ end
81
+
82
+ # Method to send http post request
83
+ def request(url, body)
84
+ uri = URI.parse(url)
85
+ http = Net::HTTP.new(uri.host, uri.port)
86
+ http.use_ssl = uri.scheme.eql?('https')
87
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @insecure
88
+ req = Net::HTTP::Post.new(uri.request_uri)
89
+ req.basic_auth(@cfg['user'], @cfg['password'])
90
+ req.body = body
91
+ http.request(req)
92
+ end
93
+
94
+ # Method to write data in bucket
95
+ def write(name, tags, values, date)
96
+ url = "#{@cfg['scheme']}://#{@cfg['host']}:#{@cfg['port']}/write?" \
97
+ "db=#{@cfg['dbname']}"
98
+ timestamp = DateTime.parse(date + 'T18:00:00').strftime('%s%N')
99
+ body = "#{name},#{format_values(tags)} #{format_values(values)} " \
100
+ "#{timestamp}"
101
+ request(url, body)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/ruby
2
+ # frozen_string_literal: true
3
+
4
+ module StockCruncher
5
+ # this is a module with various statistic calculation methods
6
+ module Stats
7
+ extend self
8
+
9
+ RANGES = [5, 10, 20, 30, 50, 100, 200].freeze
10
+
11
+ # Calculate multiple range of exponential moving average
12
+ def list_ema(values)
13
+ h = {}
14
+ RANGES.each do |n|
15
+ next if values.size < n + 1
16
+
17
+ h["ema#{n}"] = ema(values[0, n + 1])
18
+ end
19
+ h
20
+ end
21
+
22
+ # Calculate multiple range of linearly weighted moving average
23
+ def list_lwma(values)
24
+ h = {}
25
+ RANGES.each do |n|
26
+ next if values.size < n
27
+
28
+ weights = (1..n).to_a.reverse
29
+ h["lwma#{n}"] = sma(values[0, n], weights)
30
+ end
31
+ h
32
+ end
33
+
34
+ # Calculate multiple range of simple moving average
35
+ def list_sma(values)
36
+ h = {}
37
+ RANGES.each do |n|
38
+ next if values.size < n
39
+
40
+ h["sma#{n}"] = sma(values[0, n])
41
+ end
42
+ h
43
+ end
44
+
45
+ # Calculate multiple range of volume weighted moving average
46
+ def list_vwma(values, volumes)
47
+ h = {}
48
+ RANGES.each do |n|
49
+ next if values.size < n
50
+
51
+ h["lwma#{n}"] = sma(values[0, n], volumes[0, n])
52
+ end
53
+ h
54
+ end
55
+
56
+ private
57
+
58
+ # Calculate exponential moving average
59
+ def ema(array, factor = 2, weights = nil)
60
+ f = factor.to_f / array.size
61
+ n = array.size - 1
62
+ tsma = sma(array[0, n], weights)
63
+ ysma = sma(array[1, n], weights)
64
+ (tsma * f + ysma * (1 - f)).round(4)
65
+ end
66
+
67
+ # Calculate simple moving average
68
+ def sma(array, weights = nil)
69
+ factor = weights.nil? ? Array.new(array.size, 1) : weights
70
+ dividend = array.each_with_index.map { |v, i| v * factor[i] }
71
+ (dividend.sum.to_f / factor.sum).round(4)
72
+ end
73
+ end
74
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module StockCruncher
5
- VERSION = '1.0.2'
5
+ VERSION = '1.3.0'
6
6
  end
@@ -0,0 +1,49 @@
1
+ {
2
+ "2020-08-03": {
3
+ "open": "23.8500",
4
+ "high": "24.6500",
5
+ "low": "23.7400",
6
+ "close": "24.5500",
7
+ "volume": "3112972",
8
+ "previousClose": "23.8100",
9
+ "change": "0.74",
10
+ "changePercent": "3.1079"
11
+ },
12
+ "2020-07-31": {
13
+ "open": "24.0100",
14
+ "high": "24.5100",
15
+ "low": "23.5900",
16
+ "close": "23.8100",
17
+ "volume": "5482485",
18
+ "previousClose": "23.6600",
19
+ "change": "0.15",
20
+ "changePercent": "0.634"
21
+ },
22
+ "2020-07-30": {
23
+ "open": "24.4700",
24
+ "high": "24.6600",
25
+ "low": "23.5200",
26
+ "close": "23.6600",
27
+ "volume": "6466738",
28
+ "previousClose": "24.5600",
29
+ "change": "-0.9",
30
+ "changePercent": "-3.6645"
31
+ },
32
+ "2020-07-29": {
33
+ "open": "24.7500",
34
+ "high": "25.0300",
35
+ "low": "24.0400",
36
+ "close": "24.5600",
37
+ "volume": "4605804",
38
+ "previousClose": "24.7500",
39
+ "change": "-0.19",
40
+ "changePercent": "-0.7677"
41
+ },
42
+ "2020-07-28": {
43
+ "open": "25.9900",
44
+ "high": "26.0700",
45
+ "low": "24.5100",
46
+ "close": "24.7500",
47
+ "volume": "6904261"
48
+ }
49
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "open": "100.0000",
3
+ "high": "100.1000",
4
+ "low": "99.9000",
5
+ "volume": "4",
6
+ "previousClose": "100.0000",
7
+ "change": "0.0000",
8
+ "changePercent": "0.0000",
9
+ "close": "100.0000"
10
+ }
@@ -1,2 +1,9 @@
1
1
  AlphaVantage:
2
2
  apikey: demo
3
+ InfluxDB:
4
+ scheme: http
5
+ host: localhost
6
+ port: 8086
7
+ user: testuser
8
+ password: testpassword
9
+ dbname: test
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require 'spec_helper'
4
5
 
5
- describe StockCruncher::CLI do
6
+ daily = File.read('spec/files/SYM.daily')
7
+ quote = File.read('spec/files/SYM.quote')
8
+
9
+ describe StockCruncher::CLI do # rubocop:disable Metrics/BlockLength
6
10
  context 'version' do
7
11
  it 'prints the version.' do
8
12
  out = "StockCruncher version #{StockCruncher::VERSION}\n"
@@ -10,15 +14,33 @@ describe StockCruncher::CLI do
10
14
  end
11
15
  end
12
16
 
17
+ context 'daily NODATA -c spec/files/stockcruncher.yml' do
18
+ it 'Should not get any data and should fail.' do
19
+ expect { start(self) }.to raise_error(SystemExit)
20
+ end
21
+ end
22
+
13
23
  context 'daily SYM -c spec/files/stockcruncher.yml' do
14
24
  it 'Get the daily time serie for SYM.' do
15
- expect { start(self) }.to output("{}\n").to_stdout
25
+ expect { start(self) }.to output(daily).to_stdout
26
+ end
27
+ end
28
+
29
+ context 'movingaverages SYM -c spec/files/stockcruncher.yml' do
30
+ it 'Get the daily time serie for SYM.' do
31
+ expect { start(self) }.to output('').to_stdout
32
+ end
33
+ end
34
+
35
+ context 'quote NODATA -c spec/files/stockcruncher.yml' do
36
+ it 'Should not get any data and should fail.' do
37
+ expect { start(self) }.to raise_error(SystemExit)
16
38
  end
17
39
  end
18
40
 
19
41
  context 'quote SYM -c spec/files/stockcruncher.yml' do
20
42
  it 'Get the quote for SYM.' do
21
- expect { start(self) }.to output("{}\n").to_stdout
43
+ expect { start(self) }.to output(quote).to_stdout
22
44
  end
23
45
  end
24
46
 
@@ -2,15 +2,42 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
+ quote = 'symbol,open,high,low,price,volume,latestDay,previousClose,change,ch' \
6
+ "angePercent\r\nSYM,100.0000,100.1000,99.9000,100.0000,4,2020-07-30," \
7
+ "100.0000,0.0000,0.0000%\r\n"
8
+ q_err = '{}'
9
+ daily = "timestamp,open,high,low,close,volume\r\n2020-08-03,23.8500,24.6500," \
10
+ "23.7400,24.5500,3112972\r\n2020-07-31,24.0100,24.5100,23.5900,23.81" \
11
+ "00,5482485\r\n2020-07-30,24.4700,24.6600,23.5200,23.6600,6466738\r" \
12
+ "\n2020-07-29,24.7500,25.0300,24.0400,24.5600,4605804\r\n2020-07-28," \
13
+ "25.9900,26.0700,24.5100,24.7500,6904261\r\n"
14
+ d_err = "{\n \"Error Message\": \"Invalid API call.\"\n}"
15
+ mvavg = '{"results":[{"statement_id":0,"series":[{"name":"daily","columns":[' \
16
+ '"time","close","change","changePercent","volume"],"values":[["2017-' \
17
+ '03-01T18:00:00Z",1,0,0,1],["2017-03-02T18:00:00Z",1,0,0,1],["2017-0' \
18
+ '3-03T18:00:00Z",1,0,0,1],["2017-03-04T18:00:00Z",1,0,0,1],["2017-03' \
19
+ '-05T18:00:00Z",1,0,0,1],["2017-03-06T18:00:00Z",1,0,0,1]]}]}]}'
20
+
5
21
  RSpec.configure do |config|
6
22
  config.before(:each) do
7
23
  # requests an API without extra arguments
24
+ stub_request(:get, 'https://www.alphavantage.co/query?' \
25
+ 'function=GLOBAL_QUOTE&symbol=NODATA&apikey=demo&datatype=csv')
26
+ .to_return('status' => 200, 'body' => q_err, 'headers' => {})
8
27
  stub_request(:get, 'https://www.alphavantage.co/query?' \
9
28
  'function=GLOBAL_QUOTE&symbol=SYM&apikey=demo&datatype=csv')
10
- .to_return('status' => 200, 'body' => '{}', 'headers' => {})
29
+ .to_return('status' => 200, 'body' => quote, 'headers' => {})
30
+ stub_request(:get, 'https://www.alphavantage.co/query?' \
31
+ 'function=TIME_SERIES_DAILY&symbol=NODATA&apikey=demo' \
32
+ '&datatype=csv&outputsize=compact')
33
+ .to_return('status' => 200, 'body' => d_err, 'headers' => {})
11
34
  stub_request(:get, 'https://www.alphavantage.co/query?' \
12
35
  'function=TIME_SERIES_DAILY&symbol=SYM&apikey=demo' \
13
36
  '&datatype=csv&outputsize=compact')
14
- .to_return('status' => 200, 'body' => '{}', 'headers' => {})
37
+ .to_return('status' => 200, 'body' => daily, 'headers' => {})
38
+ stub_request(:post, 'http://localhost:8086/query?db=test')
39
+ .to_return('status' => 204, 'body' => mvavg, 'headers' => {})
40
+ stub_request(:post, 'http://localhost:8086/write?db=test')
41
+ .to_return('status' => 204, 'body' => '', 'headers' => {})
15
42
  end
16
43
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stockcruncher
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Delaplace
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-30 00:00:00.000000000 Z
11
+ date: 2020-09-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -130,10 +130,13 @@ files:
130
130
  - lib/stockcruncher/alphavantage.rb
131
131
  - lib/stockcruncher/cli.rb
132
132
  - lib/stockcruncher/cruncher.rb
133
+ - lib/stockcruncher/influxdb.rb
134
+ - lib/stockcruncher/stats.rb
133
135
  - lib/stockcruncher/version.rb
136
+ - spec/files/SYM.daily
137
+ - spec/files/SYM.quote
134
138
  - spec/files/stockcruncher.yml
135
139
  - spec/spec_helper.rb
136
- - spec/stockcruncher/alphavantage_spec.rb
137
140
  - spec/stockcruncher/cli_spec.rb
138
141
  - spec/stockcruncher/stubs/servers_stubs.rb
139
142
  - spec/stockcruncher_spec.rb
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- describe StockCruncher::AlphaVantage do
6
- context 'daily SYM -c spec/files/stockcruncher.yml' do
7
- it 'requests a daily time serie.' do
8
- expect { start(self) }.to output("{}\n").to_stdout
9
- end
10
- end
11
- context 'quote SYM -c spec/files/stockcruncher.yml' do
12
- it 'requests a quote endpoint.' do
13
- expect { start(self) }.to output("{}\n").to_stdout
14
- end
15
- end
16
- end