quantitative 0.1.3 → 0.1.5

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: 5bfe61d98618c7cfb83052426689976be38c45e83fce9217a74a4f0392897b60
4
- data.tar.gz: 061d9a0179eb913c5f5f5d9f1016998c2cbc0d65da9a774620a3b591f18a1f49
3
+ metadata.gz: c9fa0c537499409fa9faa3fbca76beca44d37205c04c8de1c01b3e681985c2dc
4
+ data.tar.gz: 3916820207b47d2d19b79332e41329c35080ce1456c4a2ed751863ea220b5289
5
5
  SHA512:
6
- metadata.gz: 4154a9e589bcffd15b3415ace9c1ccc6b8d29ba5ecc0180e48259c649a746a157f58bd89aeabad4ffc4ac5edefe6f1892c94adcd001ba5d1f039a5bb46a8a9d0
7
- data.tar.gz: 7ac2b4661e6c4b0e02fda9389f73b8d007b92e754e8b4fa821bb55dca1d085dfd7e2e3eb6608d520a2e2e6008eb837aa75e7c07c05addc28af577b92fe98f8c3
6
+ metadata.gz: e3bead0b7fc23208c62dd18bb8f701b684db20780ae793fab973ab07cea0740897026a66755a07b9857228e815bfc3679801171f2b5875f510a7c98781fd30cb
7
+ data.tar.gz: 8b07dbcbe1a86c507a31df950aef293ae018913964848a9298310fe71a7929924a5a7ced417cf5ef86e9282feb58bc9d16531bf6b77e0fa23cea28de7a761740
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.1.3)
4
+ quantitative (0.1.5)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "asset_class"
4
+
5
+ module Quant
6
+ # A {Quant::Asset} is a representation of a financial instrument such as a stock, option, future, or currency.
7
+ # It is used to represent the instrument that is being traded, analyzed, or managed.
8
+ #
9
+ # Not all data sources have a rich set of attributes for their assets or securities. The {Quant::Asset} is designed
10
+ # to be flexible and to support a wide variety of data sources and use-cases. The most common use-cases are supported
11
+ # while allowing for additional attributes to be added via the +meta+ attribute, which is tyipically just a Hash,
12
+ # but can be any object that can hold useful information about the asset such as currency formatting, precision, etc.
13
+ # @example
14
+ # asset = Quant::Asset.new(symbol: "AAPL", name: "Apple Inc.", asset_class: :stock, exchange: "NASDAQ")
15
+ # asset.symbol # => "AAPL"
16
+ # asset.name # => "Apple Inc."
17
+ # asset.stock? # => true
18
+ # asset.option? # => false
19
+ # asset.future? # => false
20
+ # asset.currency? # => false
21
+ # asset.exchange # => "NASDAQ"
22
+ #
23
+ # # Can serialize two ways:
24
+ # asset.to_h # => { "s" => "AAPL" }
25
+ # asset.to_h(full: true) # => { "s" => "AAPL", "n" => "Apple Inc.", "sc" => "stock", "x" => "NASDAQ" }
26
+ class Asset
27
+ attr_reader :symbol, :name, :asset_class, :id, :exchange, :source, :meta, :created_at, :updated_at
28
+
29
+ def initialize(
30
+ symbol:,
31
+ name: nil,
32
+ id: nil,
33
+ active: true,
34
+ tradeable: true,
35
+ exchange: nil,
36
+ source: nil,
37
+ asset_class: nil,
38
+ created_at: Quant.current_time,
39
+ updated_at: Quant.current_time,
40
+ meta: {}
41
+ )
42
+ raise ArgumentError, "symbol is required" unless symbol
43
+
44
+ @symbol = symbol.to_s.upcase
45
+ @name = name
46
+ @id = id
47
+ @tradeable = tradeable
48
+ @active = active
49
+ @exchange = exchange
50
+ @source = source
51
+ @asset_class = AssetClass.new(asset_class)
52
+ @created_at = created_at
53
+ @updated_at = updated_at
54
+ @meta = meta
55
+ end
56
+
57
+ def active?
58
+ !!@active
59
+ end
60
+
61
+ def tradeable?
62
+ !!@tradeable
63
+ end
64
+
65
+ AssetClass::CLASSES.each do |class_name|
66
+ define_method("#{class_name}?") do
67
+ asset_class == class_name
68
+ end
69
+ end
70
+
71
+ def to_h(full: false)
72
+ return { "s" => symbol } unless full
73
+
74
+ { "s" => symbol,
75
+ "n" => name,
76
+ "id" => id,
77
+ "t" => tradeable?,
78
+ "a" => active?,
79
+ "x" => exchange,
80
+ "sc" => asset_class.to_s,
81
+ "src" => source.to_s }
82
+ end
83
+
84
+ def to_json(*args, full: false)
85
+ Oj.dump(to_h(full: full), *args)
86
+ end
87
+ end
88
+ end
@@ -24,7 +24,7 @@ module Quant
24
24
  # real estate. Investors can buy shares in a REIT, which provides them with a share of the
25
25
  # income produced by the real estate.
26
26
 
27
- # Cryptocurrencies: Digital or virtual currencies that use cryptography for security and operate on
27
+ # Cryptocurrencies: Digital or virtual currencies that use cryptography for asset and operate on
28
28
  # decentralized networks, typically based on blockchain technology. Examples include Bitcoin, Ethereum, and Ripple.
29
29
 
30
30
  # Preferred Stock: A type of stock that has priority over common stock in terms of dividend
@@ -40,7 +40,7 @@ module Quant
40
40
 
41
41
  # Foreign Exchange (Forex): The market where currencies are traded. Investors can buy and sell currencies to
42
42
  # profit from changes in exchange rates.
43
- class SecurityClass
43
+ class AssetClass
44
44
  CLASSES = %i(
45
45
  bond
46
46
  commodity
@@ -57,31 +57,31 @@ module Quant
57
57
  treasury_note
58
58
  ).freeze
59
59
 
60
- attr_reader :security_class
60
+ attr_reader :asset_class
61
61
 
62
62
  def initialize(name)
63
- return if @security_class = from_standard(name)
63
+ return if @asset_class = from_standard(name)
64
64
 
65
- @security_class = from_alternate(name.to_s.downcase.to_sym) unless name.nil?
66
- raise_unknown_security_class_error(name) unless security_class
65
+ @asset_class = from_alternate(name.to_s.downcase.to_sym) unless name.nil?
66
+ raise_unknown_asset_class_error(name) unless asset_class
67
67
  end
68
68
 
69
69
  CLASSES.each do |class_name|
70
70
  define_method("#{class_name}?") do
71
- security_class == class_name
71
+ asset_class == class_name
72
72
  end
73
73
  end
74
74
 
75
- def raise_unknown_security_class_error(name)
76
- raise SecurityClassError, "Unknown security class: #{name.inspect}"
75
+ def raise_unknown_asset_class_error(name)
76
+ raise Errors::AssetClassError, "Unknown asset class: #{name.inspect}"
77
77
  end
78
78
 
79
79
  def to_s
80
- security_class.to_s
80
+ asset_class.to_s
81
81
  end
82
82
 
83
83
  def to_h
84
- { "sc" => security_class }
84
+ { "sc" => asset_class }
85
85
  end
86
86
 
87
87
  def to_json(*args)
@@ -90,9 +90,9 @@ module Quant
90
90
 
91
91
  def ==(other)
92
92
  case other
93
- when String then from_alternate(other.to_sym) == security_class
94
- when Symbol then from_alternate(other) == security_class
95
- when SecurityClass then other.security_class == security_class
93
+ when String then from_alternate(other.to_sym) == asset_class
94
+ when Symbol then from_alternate(other) == asset_class
95
+ when AssetClass then other.asset_class == asset_class
96
96
  else
97
97
  false
98
98
  end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Attributes
5
+ # Tracks all defined attributes, allowing child classes to inherit their parent's attributes.
6
+ # The registry key is the class registering an attrbritute and is itself a hash of the attribute name
7
+ # and the attribute's key and default value.
8
+ # Internal use only.
9
+ #
10
+ # @example
11
+ # { Quant::Indicators::IndicatorPoint => {
12
+ # tick: { key: nil, default: nil },
13
+ # source: { key: "src", default: nil },
14
+ # input: { key: "in", default: nil }
15
+ # },
16
+ # Quant::Indicators::PingPoint => {
17
+ # pong: { key: nil, default: nil },
18
+ # compute_count: { key: nil, default: 0 }
19
+ # }
20
+ # }
21
+ # @return [Hash] The registry of all defined attributes.
22
+ def self.registry
23
+ @registry ||= {}
24
+ end
25
+
26
+ # Removes the given class from the registry. Useful for testing.
27
+ def self.deregister(klass)
28
+ registry.delete(klass)
29
+ end
30
+
31
+ # Registers an attribute for a class in the registry.
32
+ # Internal use only.
33
+ #
34
+ # @param klass [Class] The class registering the attribute
35
+ # @param name [Symbol] The name of the attribute
36
+ # @param key [String] The key to use when serializing the attribute
37
+ # @param default [Object] The default value for the attribute
38
+ def self.register(klass, name, key, default)
39
+ # Disallow redefining or replacing a key as it is easy to miss the overwrite
40
+ # and leads to serialization surprises.
41
+ if key && registry.values.flat_map(&:values).map{ |entry| entry[:key] }.include?(key)
42
+ raise Errors::DuplicateAttributesKeyError, "Attribute Key #{key} already defined!"
43
+ end
44
+
45
+ registry[klass] ||= {}
46
+ registry[klass][name] = { key: key, default: default }
47
+ end
48
+
49
+ module InstanceMethods
50
+ def initialize(...)
51
+ initialize_attributes
52
+ super(...)
53
+ end
54
+
55
+ # Iterates over all defined attributes in a child => parent hierarchy,
56
+ # and yields the name and entry for each.
57
+ def each_attribute(&block)
58
+ klass = self.class
59
+ loop do
60
+ attributes = Attributes.registry[klass]
61
+ break if attributes.nil?
62
+
63
+ attributes.each{ |name, entry| block.call(name, entry) }
64
+ klass = klass.superclass
65
+ end
66
+ end
67
+
68
+ # Initializes the defined attributes with default values and
69
+ # defines accessor methods for each attribute.
70
+ # If a child class redefines a parent's attribute, the child's
71
+ # definition will be used.
72
+ def initialize_attributes
73
+ each_attribute do |name, entry|
74
+ # use the child's definition, skipping the parent's
75
+ next if respond_to?(name)
76
+
77
+ ivar_name = "@#{name}"
78
+ instance_variable_set(ivar_name, entry[:default])
79
+ define_singleton_method(name) { instance_variable_get(ivar_name) }
80
+ define_singleton_method("#{name}=") { |value| instance_variable_set(ivar_name, value) }
81
+ end
82
+ end
83
+
84
+ # Serializes keys that have been defined as serializeable attributes
85
+ # Key values that are nil are removed from the hash
86
+ # @return [Hash] The serialized attributes as a Ruby Hash.
87
+ def to_h
88
+ {}.tap do |key_values|
89
+ each_attribute do |name, entry|
90
+ next unless entry[:key]
91
+
92
+ ivar_name = "@#{name}"
93
+ value = instance_variable_get(ivar_name)
94
+
95
+ key_values[entry[:key]] = value if value
96
+ end
97
+ end
98
+ end
99
+
100
+ # Serializes keys that have been defined as serializeable attributes
101
+ # Key values that are nil are removed from the hash
102
+ # @return [String] The serialized attributes as a JSON string.
103
+ def to_json(*args)
104
+ Oj.dump(to_h, *args)
105
+ end
106
+ end
107
+
108
+ module ClassMethods
109
+ # Define an +attribute+ for the class that can optionally be serialized.
110
+ # Works much like an attr_accessor does, but also manages serialization for
111
+ # #to_h and #to_json methods.
112
+ #
113
+ # An +attribute+ will result in a same-named instance on the class when
114
+ # it is instantiated and it will set a default value if one is provided.
115
+ #
116
+ # @param name [Symbol] The name of the attribute and it's accessor methods
117
+ # @param key [String] The key to use when serializing the attribute
118
+ # @param default [Object] The default value for the attribute
119
+ #
120
+ # @examples
121
+ # attribute :tick # will not serialize to a key
122
+ # attribute :source, key: "src" # serializes to "src" key
123
+ # attribute :input, key: "in" # serializes to "in" key
124
+ def attribute(name, key: nil, default: nil)
125
+ Attributes.register(self, name, key, default)
126
+ end
127
+ end
128
+
129
+ def self.included(base) # :nodoc:
130
+ base.extend(ClassMethods)
131
+ base.prepend(InstanceMethods)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Config
5
+ class Config
6
+ attr_reader :indicators
7
+
8
+ def initialize
9
+ @indicators = Settings::Indicators.defaults
10
+ end
11
+
12
+ def apply_indicator_settings(**settings)
13
+ @indicators.apply_settings(**settings)
14
+ end
15
+ end
16
+
17
+ def self.config
18
+ @config ||= Config.new
19
+ end
20
+ end
21
+
22
+ module_function
23
+
24
+ def config
25
+ Config.config
26
+ end
27
+
28
+ def configure_indicators(**settings)
29
+ config.apply_indicator_settings(**settings)
30
+ yield config.indicators if block_given?
31
+ end
32
+ end
data/lib/quant/errors.rb CHANGED
@@ -1,9 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
- class Error < StandardError; end
5
- class InvalidInterval < Error; end
6
- class InvalidResolution < Error; end
7
- class ArrayMaxSizeError < Error; end
8
- class SecurityClassError < Error; end
4
+ module Errors
5
+ # {Error} is the base class for all errors in the Quant gem.
6
+ # It is a subclass of the Ruby {StandardError}.
7
+ class Error < StandardError; end
8
+
9
+ # {InvalidInterval} is raised when attempting to instantiate an
10
+ # {Quant::Interval} with an invalid value.
11
+ class InvalidInterval < Error; end
12
+
13
+ # {InvalidResolution} is raised when attempting to instantiate
14
+ # an {Quant::Resolution} with a resolution value that has not been defined.
15
+ class InvalidResolution < Error; end
16
+
17
+ # {ArrayMaxSizeError} is raised when attempting to set the +max_size+ on
18
+ # the refined {Array} class to an invalid value or when attempting to
19
+ # redefine the +max_size+ on the refined {Array} class.
20
+ class ArrayMaxSizeError < Error; end
21
+
22
+ # {AssetClassError} is raised when attempting to instantiate a
23
+ # {Quant::Asset} with an attribute that is not a valid {Quant::Asset} attribute.
24
+ class AssetClassError < Error; end
25
+
26
+ # {DuplicateAttributesKeyError} is raised when attempting to define an
27
+ # attribute with a key that has already been defined.
28
+ class DuplicateAttributesKeyError < Error; end
29
+
30
+ # {DuplicateAttributesNameError} is raised when attempting to define an
31
+ # attribute with a name that has already been defined.
32
+ class DuplicateAttributesNameError < Error; end
33
+ end
9
34
  end
@@ -0,0 +1,171 @@
1
+ module Quant
2
+ class Indicators
3
+ class Indicator
4
+ include Enumerable
5
+
6
+ # # include Mixins::TrendMethods
7
+ # include Mixins::Trig
8
+ # include Mixins::WeightedAverage
9
+ # include Mixins::HilbertTransform
10
+ # include Mixins::SuperSmoother
11
+ # include Mixins::Stochastic
12
+ # include Mixins::FisherTransform
13
+ # include Mixins::HighPassFilter
14
+ # include Mixins::Direction
15
+ # include Mixins::Filters
16
+
17
+ attr_reader :source, :series
18
+
19
+ def initialize(series:, source:)
20
+ @series = series
21
+ @source = source
22
+ @points = {}
23
+ series.each { |tick| self << tick }
24
+ end
25
+
26
+ def ticks
27
+ @points.keys
28
+ end
29
+
30
+ def values
31
+ @points.values
32
+ end
33
+
34
+ def size
35
+ @points.size
36
+ end
37
+
38
+ attr_reader :p0, :p1, :p2, :p3
39
+ attr_reader :t0, :t1, :t2, :t3
40
+
41
+ def <<(tick)
42
+ @t0 = tick
43
+ @p0 = points_class.new(tick: tick, source: source)
44
+ @points[tick] = @p0
45
+
46
+ @p1 = values[-2] || @p0
47
+ @p2 = values[-3] || @p1
48
+ @p3 = values[-4] || @p2
49
+
50
+ @t1 = ticks[-2] || @t0
51
+ @t2 = ticks[-3] || @t1
52
+ @t3 = ticks[-4] || @t2
53
+
54
+ compute
55
+ end
56
+
57
+ def each(&block)
58
+ @points.each_value(&block)
59
+ end
60
+
61
+ def inspect
62
+ "#<#{self.class.name} symbol=#{series.symbol} source=#{source} #{points.size} ticks>"
63
+ end
64
+
65
+ def compute
66
+ raise NotImplementedError
67
+ end
68
+
69
+ def indicator_name
70
+ self.class.name.split("::").last
71
+ end
72
+
73
+ def points_class
74
+ Object.const_get "Quant::Indicators::#{indicator_name}Point"
75
+ end
76
+
77
+ # p(0) => values[-1]
78
+ # p(1) => values[-2]
79
+ # p(2) => values[-3]
80
+ # p(3) => values[-4]
81
+ def p(offset)
82
+ raise ArgumentError, "offset must be a positive value" if offset < 0
83
+
84
+ index = offset + 1
85
+ values[[-index, -size].max]
86
+ end
87
+
88
+ # t(0) => ticks[-1]
89
+ # t(1) => ticks[-2]
90
+ # t(2) => ticks[-3]
91
+ # t(3) => ticks[-4]
92
+ def t(offset)
93
+ raise ArgumentError, "offset must be a positive value" if offset < 0
94
+
95
+ index = offset + 1
96
+ ticks[[-index, -size].max]
97
+ end
98
+
99
+ # The input is the value derived from the source for the indicator
100
+ # for the current tick.
101
+ # For example, if the source is :oc2, then the input is the
102
+ # value of the current tick's (open + close) / 2
103
+ # @return [Numeric]
104
+ def input
105
+ t0.send(source)
106
+ end
107
+
108
+ # def warmed_up?
109
+ # true
110
+ # end
111
+
112
+ # attr_reader :dc_period
113
+
114
+ # def points_for(series:)
115
+ # @points_for_cache[series] ||= self.class.new(series:, settings:, cloning: true).tap do |indicator|
116
+ # series.ticks.each { |tick| indicator.points.push(tick.indicators[self]) }
117
+ # end
118
+ # end
119
+
120
+ # # Ticks belong to the first series they're associated with always
121
+ # # NOTE: No provisions for series merging their ticks to one series!
122
+ # def parent_series
123
+ # series.ticks.empty? ? series : series.ticks.first.series
124
+ # end
125
+
126
+ # # Returns the last point of the current indicator rather than the entire series
127
+ # # This is used for indicators that depend on dominant cycle or other indicators
128
+ # # to compute their data points.
129
+ # def current_point
130
+ # points.size - 1
131
+ # end
132
+
133
+ # def dominant_cycles
134
+ # parent_series.indicators.dominant_cycles
135
+ # end
136
+
137
+ # # Override this method to change source of dominant cycle computation for an indicator
138
+ # def dominant_cycle_indicator
139
+ # @dominant_cycle_indicator ||= dominant_cycles.band_pass
140
+ # end
141
+
142
+ # def ensure_not_dominant_cycler_indicator
143
+ # return unless is_a? Quant::Indicators::DominantCycles::DominantCycle
144
+
145
+ # raise 'Dominant Cycle Indicators cannot use the thing they compute!'
146
+ # end
147
+
148
+ # # Returns the dominant cycle point for the current indicator's point
149
+ # def current_dominant_cycle
150
+ # dominant_cycle_indicator[current_point]
151
+ # end
152
+
153
+ # # Returns the atr point for the current indicator's point
154
+ # def atr_point
155
+ # parent_series.indicators.atr[current_point]
156
+ # end
157
+
158
+ # # def dc_period
159
+ # # dominant_cycle.period.round(0).to_i
160
+ # # end
161
+
162
+ # def <<(ohlc)
163
+ # points.append(ohlc)
164
+ # end
165
+
166
+ # def append(ohlc)
167
+ # points.append(ohlc)
168
+ # end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class Indicators
5
+ class IndicatorPoint
6
+ include Quant::Attributes
7
+
8
+ attribute :tick
9
+ attribute :source, key: "src"
10
+ attribute :input, key: "in"
11
+
12
+ def initialize(tick:, source:)
13
+ @tick = tick
14
+ @source = source
15
+ @input = @tick.send(source)
16
+ initialize_data_points
17
+ end
18
+
19
+ def initialize_data_points
20
+ # No-Op - Override in subclass if needed.
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ module Quant
2
+ class Indicators
3
+ class MaPoint < IndicatorPoint
4
+ attribute :ss, key: "ss"
5
+ attribute :ema, key: "ema"
6
+ attr_accessor :ss, :ema, :osc
7
+
8
+ def initialize_data_points
9
+ @ss = input
10
+ @ema = input
11
+ @osc = nil
12
+ end
13
+ end
14
+
15
+ # Moving Averages
16
+ class Ma < Indicator
17
+ include Quant::Mixins::Filters
18
+
19
+ def alpha(period)
20
+ bars_to_alpha(period)
21
+ end
22
+
23
+ def min_period
24
+ 8 # Quant.config.indicators.min_period
25
+ end
26
+
27
+ def max_period
28
+ 48 # Quant.config.indicators.max_period
29
+ end
30
+
31
+ def compute
32
+ # p0.ss = super_smoother input, :ss, min_period
33
+ p0.ema = alpha(max_period) * input + (1 - alpha(max_period)) * p1.ema
34
+ p0.osc = p0.ss - p0.ema
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ module Quant
2
+ class Indicators
3
+ class PingPoint < IndicatorPoint
4
+ attribute :pong
5
+ attribute :compute_count, default: 0
6
+ end
7
+
8
+ # A simple idicator used primarily to test the indicator system
9
+ class Ping < Indicator
10
+ def compute
11
+ p0.pong = input
12
+ p0.compute_count += 1
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ # TODO: build an Indicator registry so new indicators can be added and used outside those shipped with the library.
5
+ class Indicators
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class IndicatorsProxy
5
+ attr_reader :series, :source, :indicators
6
+
7
+ def initialize(series:, source:)
8
+ @series = series
9
+ @source = source
10
+ @indicators = {}
11
+ end
12
+
13
+ def indicator(indicator_class)
14
+ indicators[indicator_class] ||= indicator_class.new(series: series, source: source)
15
+ end
16
+
17
+ def <<(tick)
18
+ indicators.each_value { |indicator| indicator << tick }
19
+ end
20
+
21
+ def ma; indicator(Indicators::Ma) end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class IndicatorsSources
5
+ def initialize(series:)
6
+ @series = series
7
+ @indicator_sources = {}
8
+ end
9
+
10
+ def <<(tick)
11
+ @indicator_sources.each_value { |indicator| indicator << tick }
12
+ end
13
+
14
+ def oc2
15
+ @indicator_sources[:oc2] ||= IndicatorsProxy.new(series: @series, source: :oc2)
16
+ end
17
+ end
18
+ end