quantitative 0.1.3 → 0.1.5

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: 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