stockcruncher 1.0.1 → 1.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82cc9e3595d2f91bded92cbbf001fc8fb76ff3bd0d4604361c24f78c3ef6da01
4
- data.tar.gz: 510435224f9e7362f51f692c3b6634d7372adae0db724d9d9545f3d869b53bfa
3
+ metadata.gz: e4729948dfb9635323240f73d947c2257b1ca4b8a6c2a065a812a8726f502599
4
+ data.tar.gz: 3686ad0bbb61ef2a3801a2a2c1b3258b543da3015c49b2c959570c9d7b0e8a3a
5
5
  SHA512:
6
- metadata.gz: 4a4e42a9068eabbbd9b5da44212d39c98aebce85c2b36add25f5da51853bbb00117cd9d6783d5f3cb2b35789eba7c917fe9020198de54e9bd9c45eff49221a0c
7
- data.tar.gz: 191d02a244a4fe2b575a7252824c2c510140072b56f1ea9791ddd62b94da5fda784046f246a812c4feb141e66196c711bfdfa71c951f0c8cac33409c4dacea31
6
+ metadata.gz: 24120c82f3e7cd9d622bcf1636ee528ea69d170fe0f60951b33b34754870a1b92837d5f2cbcd3f59ab4be85264195e7c2058036a557e8b486c8fe55cf81e5c18
7
+ data.tar.gz: e849aea61ae75175742d97611cffdc889aa3c95394aec2cc8ccbe93ed35bafc6958984fb0fc92b08190a41003c183b753ad287a51676990eee855d32f43bfceb
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,9 +10,9 @@ module StockCruncher
10
10
 
11
11
  def start(args = ARGV)
12
12
  StockCruncher::CLI.start(args)
13
- rescue NotImplementedError => e
13
+ rescue StandardError => e
14
14
  warn e.message
15
- exit(3)
15
+ exit 1
16
16
  end
17
17
  end
18
18
  end
@@ -7,35 +7,98 @@ module StockCruncher
7
7
  # This is an data cruncher class for AlphaVantage API.
8
8
  class AlphaVantage < Cruncher
9
9
  API_URL = 'https://www.alphavantage.co/query?'
10
- SERIE = {
11
- 'daily' => 'TIME_SERIES_DAILY',
12
- 'quote_endpoint' => 'GLOBAL_QUOTE'
13
- }.freeze
10
+
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
37
+ end
14
38
 
15
39
  # Main method to crunch data.
16
- def crunch(symbol, serie, opts)
17
- err_msg = "#{serie} does not exist."
18
- raise NotImplementedError, err_msg unless SERIE.key?(serie)
40
+ def crunch_daily(symbol, fullsize)
41
+ url = API_URL + parameters(symbol, 'TIME_SERIES_DAILY')
42
+ url += '&datatype=csv&outputsize=' + (fullsize ? 'full' : 'compact')
43
+ res = request(url)
44
+ transform_daily(res.body)
45
+ end
19
46
 
20
- url = API_URL + parameters(symbol, serie) + options(serie, opts)
47
+ # Main method to crunch data.
48
+ def crunch_quote(symbol)
49
+ url = API_URL + parameters(symbol, 'GLOBAL_QUOTE')
50
+ url += '&datatype=csv'
21
51
  res = request(url)
22
- puts res.body
52
+ transform_quote(res.body)
23
53
  end
24
54
 
25
- def options(serie, opts)
26
- o = '&datatype=' + (opts['json'] ? 'json' : 'csv')
27
- if serie == 'daily'
28
- o += '&outputsize='
29
- o += opts['full'] ? 'full' : 'compact'
30
- end
31
- 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
+ }
32
62
  end
33
63
 
64
+ # Set parameters of api call
34
65
  def parameters(symbol, serie)
35
- p = 'function=' + SERIE[serie]
66
+ p = 'function=' + serie
36
67
  p += '&symbol=' + symbol
37
68
  p += '&apikey=' + @config[self.class.name.split('::').last]['apikey']
38
69
  p
39
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
40
103
  end
41
104
  end
@@ -1,20 +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('crunch SYMBOL TIMESERIES [options]',
15
- 'Crunch SYMBOL stock market data for time series.' \
16
- 'Possible timeseries: daily, quote_endpoint.')
17
- option(
11
+ class_option(
18
12
  :config,
19
13
  aliases: ['-c'],
20
14
  type: :string,
@@ -22,13 +16,28 @@ module StockCruncher
22
16
  desc: 'Yaml formatted config file to load ' \
23
17
  '(default to /etc/stockcruncher/stockcruncher.yml).'
24
18
  )
25
- option(
26
- :json,
27
- aliases: ['-j'],
19
+ class_option(
20
+ :insecure,
21
+ aliases: ['-k'],
22
+ type: :boolean,
23
+ default: false,
24
+ desc: 'Ignore SSL certificate (default to false).'
25
+ )
26
+ class_option(
27
+ :quiet,
28
+ aliases: ['-q'],
28
29
  type: :boolean,
29
30
  default: false,
30
- desc: 'Json format data (default to csv).'
31
+ desc: 'Run silently (default to false).'
31
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.')
32
41
  option(
33
42
  :full,
34
43
  aliases: ['-f'],
@@ -36,10 +45,24 @@ module StockCruncher
36
45
  default: false,
37
46
  desc: 'Full data size.'
38
47
  )
39
- def crunch(symbol, timeserie)
48
+ def daily(symbol)
49
+ opts = options.dup
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']
55
+ end
56
+
57
+ desc('quote SYMBOL [options]',
58
+ 'Crunch SYMBOL stock market data for last day quote.')
59
+ def quote(symbol)
40
60
  opts = options.dup
41
- cruncher = StockCruncher::AlphaVantage.new(opts['config'])
42
- cruncher.crunch(symbol, timeserie, opts)
61
+ config = YAML.load_file(opts['config'])
62
+ cruncher = StockCruncher::AlphaVantage.new(config, opts['insecure'])
63
+ data = cruncher.crunch_quote(symbol)
64
+ StockCruncher::InfluxDB.new(config).export_last_day(data)
65
+ puts JSON.pretty_generate(data) unless opts['quiet']
43
66
  end
44
67
  end
45
68
  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
- # Class abstract 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)
9
+ # Class constructor method
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,63 @@
1
+ #!/usr/bin/ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'date'
5
+ require 'net/http'
6
+
7
+ module StockCruncher
8
+ # this is a class to write time series to database
9
+ class InfluxDB
10
+ # Class constructor method
11
+ def initialize(config, insecure = false)
12
+ @cfg = config[self.class.name.split('::').last]
13
+ @insecure = insecure
14
+ end
15
+
16
+ # Method to export latest data to database
17
+ def export_last_day(values)
18
+ tags = { 'symbol' => values.delete('symbol') }
19
+ date = values.delete('latestDay')
20
+ write('daily', tags, values, date)
21
+ end
22
+
23
+ # Method to export historical data to database
24
+ def export_history(symbol, timeseries)
25
+ tags = { 'symbol' => symbol }
26
+ timeseries.each_pair do |date, values|
27
+ write('daily', tags, values, date)
28
+ end
29
+ end
30
+
31
+ # Method to format and array of values into comma separated string
32
+ def format_values(values)
33
+ string = ''
34
+ values.each_pair do |k, v|
35
+ string += "#{k}=#{v}"
36
+ string += ',' unless k == values.keys.last
37
+ end
38
+ string
39
+ end
40
+
41
+ # Method to send http post request
42
+ def request(url, body)
43
+ uri = URI.parse(url)
44
+ http = Net::HTTP.new(uri.host, uri.port)
45
+ http.use_ssl = uri.scheme.eql?('https')
46
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @insecure
47
+ req = Net::HTTP::Post.new(uri.request_uri)
48
+ req.basic_auth(@cfg['user'], @cfg['password'])
49
+ req.body = body
50
+ http.request(req)
51
+ end
52
+
53
+ # Method to write data in bucket
54
+ def write(name, tags, values, date)
55
+ url = "#{@cfg['scheme']}://#{@cfg['host']}:#{@cfg['port']}/write?" \
56
+ "db=#{@cfg['dbname']}"
57
+ timestamp = DateTime.parse(date + 'T18:00:00').strftime('%s%N')
58
+ body = "#{name},#{format_values(tags)} #{format_values(values)} " \
59
+ "#{timestamp}"
60
+ request(url, body)
61
+ end
62
+ end
63
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module StockCruncher
5
- VERSION = '1.0.1'
5
+ VERSION = '1.2.1'
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,9 +14,27 @@ describe StockCruncher::CLI do
10
14
  end
11
15
  end
12
16
 
13
- context 'crunch SYM quote_endpoint -c spec/files/stockcruncher.yml' do
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
+
23
+ context 'daily SYM -c spec/files/stockcruncher.yml' do
24
+ it 'Get the daily time serie for SYM.' do
25
+ expect { start(self) }.to output(daily).to_stdout
26
+ end
27
+ end
28
+
29
+ context 'quote NODATA -c spec/files/stockcruncher.yml' do
30
+ it 'Should not get any data and should fail.' do
31
+ expect { start(self) }.to raise_error(SystemExit)
32
+ end
33
+ end
34
+
35
+ context 'quote SYM -c spec/files/stockcruncher.yml' do
14
36
  it 'Get the quote for SYM.' do
15
- expect { start(self) }.to output("{}\n").to_stdout
37
+ expect { start(self) }.to output(quote).to_stdout
16
38
  end
17
39
  end
18
40
 
@@ -2,15 +2,35 @@
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
+
5
16
  RSpec.configure do |config|
6
17
  config.before(:each) do
7
18
  # requests an API without extra arguments
19
+ stub_request(:get, 'https://www.alphavantage.co/query?' \
20
+ 'function=GLOBAL_QUOTE&symbol=NODATA&apikey=demo&datatype=csv')
21
+ .to_return('status' => 200, 'body' => q_err, 'headers' => {})
8
22
  stub_request(:get, 'https://www.alphavantage.co/query?' \
9
23
  'function=GLOBAL_QUOTE&symbol=SYM&apikey=demo&datatype=csv')
10
- .to_return('status' => 200, 'body' => '{}', 'headers' => {})
24
+ .to_return('status' => 200, 'body' => quote, 'headers' => {})
25
+ stub_request(:get, 'https://www.alphavantage.co/query?' \
26
+ 'function=TIME_SERIES_DAILY&symbol=NODATA&apikey=demo' \
27
+ '&datatype=csv&outputsize=compact')
28
+ .to_return('status' => 200, 'body' => d_err, 'headers' => {})
11
29
  stub_request(:get, 'https://www.alphavantage.co/query?' \
12
30
  'function=TIME_SERIES_DAILY&symbol=SYM&apikey=demo' \
13
31
  '&datatype=csv&outputsize=compact')
14
- .to_return('status' => 200, 'body' => '{}', 'headers' => {})
32
+ .to_return('status' => 200, 'body' => daily, 'headers' => {})
33
+ stub_request(:post, 'http://localhost:8086/write?db=test')
34
+ .to_return('status' => 204, 'body' => '', 'headers' => {})
15
35
  end
16
36
  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.1
4
+ version: 1.2.1
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-27 00:00:00.000000000 Z
11
+ date: 2020-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -130,10 +130,12 @@ files:
130
130
  - lib/stockcruncher/alphavantage.rb
131
131
  - lib/stockcruncher/cli.rb
132
132
  - lib/stockcruncher/cruncher.rb
133
+ - lib/stockcruncher/influxdb.rb
133
134
  - lib/stockcruncher/version.rb
135
+ - spec/files/SYM.daily
136
+ - spec/files/SYM.quote
134
137
  - spec/files/stockcruncher.yml
135
138
  - spec/spec_helper.rb
136
- - spec/stockcruncher/alphavantage_spec.rb
137
139
  - spec/stockcruncher/cli_spec.rb
138
140
  - spec/stockcruncher/stubs/servers_stubs.rb
139
141
  - 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 'crunch SYM daily -c spec/files/stockcruncher.yml' do
7
- it 'requests a source and get the result.' do
8
- expect { start(self) }.to output("{}\n").to_stdout
9
- end
10
- end
11
- context 'crunch SYM weekly -c spec/files/stockcruncher.yml' do
12
- it 'requests a not implemented time serie.' do
13
- expect { start(self) }.to raise_error(SystemExit)
14
- end
15
- end
16
- end