dtn 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +21 -5
  3. data/.env.example +5 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +7 -0
  6. data/CHANGELOG.md +5 -1
  7. data/Gemfile +3 -1
  8. data/README.md +288 -9
  9. data/Rakefile +2 -0
  10. data/docker-compose.yml +34 -0
  11. data/dtn.gemspec +1 -1
  12. data/lib/dtn.rb +18 -1
  13. data/lib/dtn/concerns/id.rb +36 -0
  14. data/lib/dtn/concerns/validation.rb +71 -0
  15. data/lib/dtn/helpers/catalog.rb +32 -0
  16. data/lib/dtn/lookups/catalog/listed_markets.rb +12 -0
  17. data/lib/dtn/lookups/catalog/naic_codes.rb +12 -0
  18. data/lib/dtn/lookups/catalog/security_types.rb +12 -0
  19. data/lib/dtn/lookups/catalog/sic_codes.rb +12 -0
  20. data/lib/dtn/lookups/catalog/trade_conditions.rb +12 -0
  21. data/lib/dtn/lookups/historical/base.rb +43 -0
  22. data/lib/dtn/lookups/historical/daily_datapoint.rb +13 -0
  23. data/lib/dtn/lookups/historical/daily_timeframe.rb +34 -0
  24. data/lib/dtn/lookups/historical/datapoint.rb +27 -0
  25. data/lib/dtn/lookups/historical/interval.rb +14 -0
  26. data/lib/dtn/lookups/historical/interval_datapoint.rb +31 -0
  27. data/lib/dtn/lookups/historical/interval_day.rb +32 -0
  28. data/lib/dtn/lookups/historical/interval_timeframe.rb +37 -0
  29. data/lib/dtn/lookups/historical/monthly_datapoint.rb +13 -0
  30. data/lib/dtn/lookups/historical/tick.rb +14 -0
  31. data/lib/dtn/lookups/historical/tick_datapoint.rb +24 -0
  32. data/lib/dtn/lookups/historical/tick_day.rb +34 -0
  33. data/lib/dtn/lookups/historical/tick_timeframe.rb +31 -0
  34. data/lib/dtn/lookups/historical/weekly_datapoint.rb +13 -0
  35. data/lib/dtn/lookups/news/base.rb +85 -0
  36. data/lib/dtn/lookups/news/config.rb +25 -0
  37. data/lib/dtn/lookups/news/headline.rb +40 -0
  38. data/lib/dtn/lookups/news/story.rb +40 -0
  39. data/lib/dtn/lookups/news/story_count.rb +36 -0
  40. data/lib/dtn/lookups/request.rb +92 -0
  41. data/lib/dtn/lookups/symbol/base.rb +11 -0
  42. data/lib/dtn/lookups/symbol/by_filter.rb +58 -0
  43. data/lib/dtn/lookups/symbol/by_naic.rb +26 -0
  44. data/lib/dtn/lookups/symbol/by_sic.rb +26 -0
  45. data/lib/dtn/message.rb +29 -0
  46. data/lib/dtn/messages/bar/base.rb +30 -0
  47. data/lib/dtn/messages/bar/current_bar.rb +11 -0
  48. data/lib/dtn/messages/bar/historical_bar.rb +11 -0
  49. data/lib/dtn/messages/bar/update_bar.rb +11 -0
  50. data/lib/dtn/messages/catalog/code.rb +22 -0
  51. data/lib/dtn/messages/catalog/listed_markets.rb +20 -0
  52. data/lib/dtn/messages/catalog/naic_codes.rb +10 -0
  53. data/lib/dtn/messages/catalog/security_types.rb +20 -0
  54. data/lib/dtn/messages/catalog/sic_codes.rb +10 -0
  55. data/lib/dtn/messages/catalog/trade_conditions.rb +20 -0
  56. data/lib/dtn/messages/historical/daily_weekly_monthly.rb +25 -0
  57. data/lib/dtn/messages/historical/interval.rb +28 -0
  58. data/lib/dtn/messages/historical/tick.rb +31 -0
  59. data/lib/dtn/messages/level2/level2_update.rb +38 -0
  60. data/lib/dtn/messages/level2/market_maker_name.rb +20 -0
  61. data/lib/dtn/messages/message_with_simple_parser.rb +38 -0
  62. data/lib/dtn/messages/news/base.rb +34 -0
  63. data/lib/dtn/messages/news/config.rb +24 -0
  64. data/lib/dtn/messages/news/headline.rb +25 -0
  65. data/lib/dtn/messages/news/story.rb +20 -0
  66. data/lib/dtn/messages/news/story_count.rb +21 -0
  67. data/lib/dtn/messages/quote/level1.rb +150 -0
  68. data/lib/dtn/messages/quote/level1_fundamental.rb +17 -0
  69. data/lib/dtn/messages/quote/level1_news.rb +27 -0
  70. data/lib/dtn/messages/quote/level1_regional.rb +31 -0
  71. data/lib/dtn/messages/quote/level1_summary.rb +19 -0
  72. data/lib/dtn/messages/quote/level1_update.rb +21 -0
  73. data/lib/dtn/messages/symbol/base.rb +35 -0
  74. data/lib/dtn/messages/symbol/by_filter.rb +11 -0
  75. data/lib/dtn/messages/symbol/by_naic.rb +22 -0
  76. data/lib/dtn/messages/symbol/by_sic.rb +22 -0
  77. data/lib/dtn/messages/system/client_stats.rb +46 -0
  78. data/lib/dtn/messages/system/customer_info.rb +30 -0
  79. data/lib/dtn/messages/system/end_of_message_characters.rb +22 -0
  80. data/lib/dtn/messages/system/error.rb +20 -0
  81. data/lib/dtn/messages/system/generic.rb +98 -0
  82. data/lib/dtn/messages/system/no_data_characters.rb +22 -0
  83. data/lib/dtn/messages/system/stats.rb +38 -0
  84. data/lib/dtn/messages/system/symbol_not_found.rb +19 -0
  85. data/lib/dtn/messages/system/timestamp.rb +16 -0
  86. data/lib/dtn/messages/unknown.rb +15 -0
  87. data/lib/dtn/registry.rb +57 -0
  88. data/lib/dtn/streaming/client.rb +105 -0
  89. data/lib/dtn/streaming/clients/admin.rb +20 -0
  90. data/lib/dtn/streaming/clients/bar.rb +49 -0
  91. data/lib/dtn/streaming/clients/level2.rb +25 -0
  92. data/lib/dtn/streaming/clients/quote.rb +57 -0
  93. data/lib/dtn/streaming/messages_recorder_observer.rb +26 -0
  94. data/lib/dtn/streaming/request.rb +27 -0
  95. data/lib/dtn/streaming/request_builder.rb +57 -0
  96. data/lib/dtn/streaming/requests/admin/register_client_app.rb +24 -0
  97. data/lib/dtn/streaming/requests/admin/remove_client_app.rb +22 -0
  98. data/lib/dtn/streaming/requests/admin/save_login_info.rb +18 -0
  99. data/lib/dtn/streaming/requests/admin/set_autoconnect.rb +18 -0
  100. data/lib/dtn/streaming/requests/admin/set_client_stats.rb +20 -0
  101. data/lib/dtn/streaming/requests/admin/set_loginid.rb +21 -0
  102. data/lib/dtn/streaming/requests/admin/set_password.rb +21 -0
  103. data/lib/dtn/streaming/requests/bar/unwatch.rb +18 -0
  104. data/lib/dtn/streaming/requests/bar/unwatch_all.rb +16 -0
  105. data/lib/dtn/streaming/requests/bar/watch.rb +81 -0
  106. data/lib/dtn/streaming/requests/bar/watches.rb +21 -0
  107. data/lib/dtn/streaming/requests/level2/connect.rb +16 -0
  108. data/lib/dtn/streaming/requests/level2/disconnect.rb +16 -0
  109. data/lib/dtn/streaming/requests/level2/market_maker_by_id.rb +18 -0
  110. data/lib/dtn/streaming/requests/level2/unwatch.rb +18 -0
  111. data/lib/dtn/streaming/requests/level2/watch.rb +18 -0
  112. data/lib/dtn/streaming/requests/quote/all_update_fieldnames.rb +16 -0
  113. data/lib/dtn/streaming/requests/quote/connect.rb +16 -0
  114. data/lib/dtn/streaming/requests/quote/current_update_fieldnames.rb +16 -0
  115. data/lib/dtn/streaming/requests/quote/fundamental_fieldnames.rb +16 -0
  116. data/lib/dtn/streaming/requests/quote/news_switch.rb +18 -0
  117. data/lib/dtn/streaming/requests/quote/refresh.rb +29 -0
  118. data/lib/dtn/streaming/requests/quote/regional_switch.rb +26 -0
  119. data/lib/dtn/streaming/requests/quote/set_client_name.rb +16 -0
  120. data/lib/dtn/streaming/requests/quote/set_protocol.rb +16 -0
  121. data/lib/dtn/streaming/requests/quote/timestamp.rb +21 -0
  122. data/lib/dtn/streaming/requests/quote/timestamp_switch.rb +18 -0
  123. data/lib/dtn/streaming/requests/quote/trades.rb +21 -0
  124. data/lib/dtn/streaming/requests/quote/unwatch.rb +22 -0
  125. data/lib/dtn/streaming/requests/quote/unwatch_all.rb +16 -0
  126. data/lib/dtn/streaming/requests/quote/update_fields.rb +40 -0
  127. data/lib/dtn/streaming/requests/quote/watch.rb +22 -0
  128. data/lib/dtn/streaming/requests/quote/watches.rb +21 -0
  129. data/lib/dtn/version.rb +1 -1
  130. data/lib/ext/business_day.rb +15 -0
  131. data/lib/tasks/spec_date.rake +13 -0
  132. metadata +126 -6
data/Rakefile CHANGED
@@ -5,6 +5,8 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
+ Dir.glob("lib/tasks/*.rake").each { |r| load r }
9
+
8
10
  require "rubocop/rake_task"
9
11
 
10
12
  RuboCop::RakeTask.new
@@ -0,0 +1,34 @@
1
+ version: "3.9"
2
+ services:
3
+ iqfeed:
4
+ image: kvokka/iqfeed
5
+ container_name: iqfeed
6
+ ports:
7
+ - "5009:5009"
8
+ - "9100:9100"
9
+ - "9200:9200"
10
+ - "9300:9300"
11
+ - "9400:9400"
12
+ - "8088:8080"
13
+ - "5901:5900"
14
+ volumes:
15
+ - ./logs/iqfeed:/root/DTN/IQFeed
16
+ - ./logs/distdnnd:/var/log/distdnnd
17
+ healthcheck:
18
+ test: ["CMD", "python3", "/root/is_iqfeed_running.py"]
19
+ interval: 30s
20
+ timeout: 15s
21
+ retries: 3
22
+ start_period: 1m
23
+ env_file:
24
+ - .env
25
+
26
+ autoheal:
27
+ image: willfarrell/autoheal:latest
28
+ container_name: autoheal
29
+ restart: always
30
+ environment:
31
+ AUTOHEAL_CONTAINER_LABEL: all
32
+ volumes:
33
+ - /var/run/docker.sock:/var/run/docker.sock
34
+ hostname: autoheal
data/dtn.gemspec CHANGED
@@ -27,6 +27,6 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
29
 
30
- spec.add_dependency "dry-configurable", "~> 0.12.0"
30
+ spec.add_dependency "concurrent-ruby", "~> 1.0"
31
31
  spec.add_dependency "zeitwerk", "~> 2.4.0"
32
32
  end
data/lib/dtn.rb CHANGED
@@ -4,8 +4,25 @@ require "zeitwerk"
4
4
  loader = Zeitwerk::Loader.for_gem
5
5
  loader.setup # ready!
6
6
 
7
- require "dry-configurable"
7
+ require "forwardable"
8
+ require "concurrent/atomic/atomic_fixnum"
9
+ require "concurrent/map"
10
+ require "active_support/concern"
11
+ require "active_support/inflector"
12
+ require "active_support/core_ext/string"
13
+ require "active_support/time_with_zone"
14
+ require "socket"
15
+ require "securerandom"
16
+ require "ostruct"
17
+ require "date"
8
18
 
19
+ # Top level API methods only
9
20
  module Dtn
10
21
  class Error < StandardError; end
22
+
23
+ class ValidationError < Error; end
24
+
25
+ extend Helpers::Catalog
26
+
27
+ mattr_accessor :host, instance_accessor: false, default: ENV.fetch("DTN_HOST", "localhost")
11
28
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Concerns
5
+ # Id creation and management methods
6
+ module Id
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ extend Forwardable
11
+ delegate next_id: :"self.class"
12
+ end
13
+
14
+ class_methods do
15
+ def next_id
16
+ _id_tvar.increment
17
+ last_id
18
+ end
19
+
20
+ def last_id
21
+ _id_tvar.value
22
+ end
23
+
24
+ private
25
+
26
+ def _id_tvar
27
+ @_id_tvar ||= Concurrent::AtomicFixnum.new
28
+ end
29
+ end
30
+
31
+ def id
32
+ @id ||= next_id
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Concerns
5
+ # Collection of common validations
6
+ # This gem too small for ActiveModel, so manual approach should be just ok
7
+ module Validation
8
+ # Ruby 2.6 support
9
+ class ::Date
10
+ class Error < ArgumentError; end
11
+ end
12
+
13
+ DATE_TIME_FORMAT = "%Y%m%d %H%M%S"
14
+ DATE_FORMAT = "%Y%m%d"
15
+ MAX_INT16 = 2**16 - 1
16
+
17
+ DEFAULT_INTERVAL_TYPE = "s"
18
+
19
+ private
20
+
21
+ def validate_interval_type(value)
22
+ return DEFAULT_INTERVAL_TYPE unless value
23
+
24
+ it = value.to_s.downcase
25
+ return it if %w[s v t].include?(it)
26
+
27
+ raise ValidationError,
28
+ "Got #{value}, but interval_type can be only 's' for seconds, 'v' for volume or 't' for ticks"
29
+ end
30
+
31
+ def validate_datetime(value)
32
+ _general_date_validation(value: value, converter: :to_datetime, format: DATE_TIME_FORMAT)
33
+ end
34
+
35
+ def validate_date(value)
36
+ _general_date_validation(value: value, converter: :to_date, format: DATE_FORMAT)
37
+ end
38
+
39
+ def validate_short_int(value)
40
+ validate_int(value).yield_self do |v|
41
+ v > MAX_INT16 ? MAX_INT16 : v
42
+ end
43
+ end
44
+
45
+ def validate_int(value)
46
+ Integer(value)
47
+ rescue TypeError, ArgumentError
48
+ raise ValidationError, "Value '#{value}' is not an integer"
49
+ end
50
+
51
+ def validate_float(value)
52
+ Float(value)
53
+ rescue TypeError, ArgumentError
54
+ raise ValidationError, "Value '#{value}' is not a float"
55
+ end
56
+
57
+ def _general_date_validation(value:, converter:, format:)
58
+ return if value.blank?
59
+
60
+ _convert_date(value: value, converter: converter, format: format)
61
+ rescue ArgumentError
62
+ raise ValidationError, "Value '#{value}' is not a date"
63
+ end
64
+
65
+ def _convert_date(value:, converter:, format:)
66
+ base = value.is_a?(Date) ? value : value.to_s.public_send(converter)
67
+ base.strftime(format)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Helpers
5
+ # Global catalog helpers
6
+ module Catalog
7
+ def method_missing(method_name, *, **)
8
+ return super unless method_name.to_s.end_with?("_catalog")
9
+
10
+ get_catalog(catalog: method_name.to_s.sub(/_catalog$/, ""))
11
+ end
12
+
13
+ def respond_to_missing?(method_name, include_private = false)
14
+ method_name.to_s.end_with?("_catalog") || super
15
+ end
16
+
17
+ private
18
+
19
+ def get_catalog(catalog:)
20
+ v = instance_variable_get("@#{catalog}")
21
+ return v if v
22
+
23
+ result = Registry.new(name: catalog).tap do |registry|
24
+ "Dtn::Lookups::Catalog::#{catalog.camelcase}".constantize.call.each do |message|
25
+ registry[message.id] = message
26
+ end
27
+ end
28
+ instance_variable_set("@#{catalog}", result)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Catalog
6
+ # Listed markets
7
+ class ListedMarkets < Request
8
+ TEMPLATE = "SLM"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Catalog
6
+ # Naic codes
7
+ class NaicCodes < Request
8
+ TEMPLATE = "SNC"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Catalog
6
+ # Security types
7
+ class SecurityTypes < Request
8
+ TEMPLATE = "SST"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Catalog
6
+ # Sic codes
7
+ class SicCodes < Request
8
+ TEMPLATE = "SSC"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Catalog
6
+ # Trade conditions
7
+ class TradeConditions < Request
8
+ TEMPLATE = "STC"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Historical
6
+ # User historical requests
7
+ class Base < Request
8
+ # Maximum data allowed per 1 request and per 1 batch
9
+ DEFAULT_MAX_DATAPOINTS = 1_000_000
10
+ DEFAULT_DATAPOINTS_PER_SEND = 500
11
+
12
+ # In case we are fetching the data for a few days we can filter all the days
13
+ # from and to. The pattern means %H%M%S
14
+ DEFAULT_BEGIN_FILTER_TIME = "093000"
15
+ DEFAULT_END_FILTER_TIME = "160000"
16
+
17
+ # Returned data order
18
+ DEFAULT_DATA_DIRECTION = 1
19
+
20
+ include Dtn::Concerns::Validation
21
+
22
+ private
23
+
24
+ def defaults(**options)
25
+ super.merge({
26
+ max_datapoints: DEFAULT_MAX_DATAPOINTS,
27
+ begin_filter_time: DEFAULT_BEGIN_FILTER_TIME,
28
+ data_direction: DEFAULT_DATA_DIRECTION,
29
+ end_filter_time: DEFAULT_END_FILTER_TIME,
30
+ datapoints_per_send: DEFAULT_DATAPOINTS_PER_SEND
31
+ }).merge(options)
32
+ end
33
+
34
+ def validate_symbol(value)
35
+ v = value.to_s
36
+ return v.upcase if v.length.positive?
37
+
38
+ raise ValidationError, "Symbol must be present"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Historical
6
+ # Daily datapoint requests
7
+ class DailyDatapoint < Datapoint
8
+ TEMPLATE =
9
+ "HDX,%<symbol>s,%<max_datapoints>d,%<data_direction>d,%<id>d,%<datapoints_per_send>d"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Historical
6
+ # Daily timeframe requests
7
+ class DailyTimeframe < Interval
8
+ TEMPLATE =
9
+ "HDT,%<symbol>s,%<begin_date>s,%<end_date>s,%<max_datapoints>d,%<data_direction>d,%<id>d"
10
+
11
+ # HDT - Retrieves [Days] days of interval data for the specified [Symbol].
12
+ #
13
+ # Example message
14
+ #
15
+ # HDT,[Symbol],[BeginDate],[EndDate],[MaxDatapoints],[DataDirection],[RequestID],\
16
+ # [DatapointsPerSend]<CR><LF>
17
+ def call(symbol:, begin_date:, end_date:, **options)
18
+ self.combined_options = defaults(**options).merge(
19
+ {
20
+ symbol: validate_symbol(symbol),
21
+ begin_date: validate_date(begin_date),
22
+ end_date: validate_date(end_date)
23
+ }
24
+ )
25
+ super
26
+ end
27
+
28
+ def expected_messages_class
29
+ Messages::Historical::DailyWeeklyMonthly
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Historical
6
+ # Weekly datapoint requests
7
+ class Datapoint < Base
8
+ # Retrieves up to [maxDatapoints] datapoints of composite daily/weekly/monthly datapoints
9
+ # for the specified [Symbol].
10
+ #
11
+ # Example messages
12
+ #
13
+ # HDX,[Symbol],[MaxDatapoints],[DataDirection],[RequestID],[DatapointsPerSend]<CR><LF>
14
+ # HWX,[Symbol],[MaxDatapoints],[DataDirection],[RequestID],[DatapointsPerSend]<CR><LF>
15
+ # HMX,[Symbol],[MaxDatapoints],[DataDirection],[RequestID],[DatapointsPerSend]<CR><LF>
16
+ def call(symbol:, **options)
17
+ self.combined_options = defaults(**options).merge(symbol: validate_symbol(symbol))
18
+ super
19
+ end
20
+
21
+ def expected_messages_class
22
+ Messages::Historical::DailyWeeklyMonthly
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Historical
6
+ # Interval requests
7
+ class Interval < Base
8
+ def expected_messages_class
9
+ Messages::Historical::Interval
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dtn
4
+ module Lookups
5
+ module Historical
6
+ # Interval datapoint requests
7
+ class IntervalDatapoint < Interval
8
+ TEMPLATE =
9
+ "HIX,%<symbol>s,%<interval>d,%<max_datapoints>d,%<data_direction>d,%<id>d,"\
10
+ "%<datapoints_per_send>d,%<interval_type>s"
11
+
12
+ # HIX - Retrieves [maxDatapoints] number of Intervals of data for the specified [Symbol].
13
+ #
14
+ # Example message
15
+ #
16
+ # HIX,[Symbol],[Interval],[MaxDatapoints],[DataDirection],[RequestID],[DatapointsPerSend],\
17
+ # [IntervalType]<CR><LF>
18
+ def call(symbol:, interval:, **options)
19
+ self.combined_options = defaults(**options).merge(
20
+ {
21
+ symbol: validate_symbol(symbol),
22
+ interval: validate_int(interval),
23
+ interval_type: validate_interval_type(options[:interval_type])
24
+ }
25
+ )
26
+ super
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end