quantitative 0.1.5 → 0.1.7

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: c9fa0c537499409fa9faa3fbca76beca44d37205c04c8de1c01b3e681985c2dc
4
- data.tar.gz: 3916820207b47d2d19b79332e41329c35080ce1456c4a2ed751863ea220b5289
3
+ metadata.gz: 8c2f8345b245cd469b234752f2fb757a1006b6a04322412acbb3fc10bd759827
4
+ data.tar.gz: fa5388e69c9f82fe686424dcfd81d8795ea6814a4b2b98b0edb6a52598ffbdaf
5
5
  SHA512:
6
- metadata.gz: e3bead0b7fc23208c62dd18bb8f701b684db20780ae793fab973ab07cea0740897026a66755a07b9857228e815bfc3679801171f2b5875f510a7c98781fd30cb
7
- data.tar.gz: 8b07dbcbe1a86c507a31df950aef293ae018913964848a9298310fe71a7929924a5a7ced417cf5ef86e9282feb58bc9d16531bf6b77e0fa23cea28de7a761740
6
+ metadata.gz: bb3c3cc8db589f5051d9dcd921fc1c01292897c440cfdb72c45df568051213ddd3fad5aa27e40652aff452619eefa835dcc4675120d050c9a46bbf34e83be3cc
7
+ data.tar.gz: cfcc8acf5989103a9a59b84965b3d1d3eb1f3288737afa3636f684ea5f378e10d82f4bda5e487f9a1ddcb867abd0fb82cef4048219acac6ee98bcfd412124db5
data/.rubocop.yml CHANGED
@@ -3,6 +3,8 @@ inherit_gem:
3
3
 
4
4
  AllCops:
5
5
  TargetRubyVersion: 3.0
6
+ Exclude:
7
+ - 'spec/performance/*.rb'
6
8
 
7
9
  Style/AccessorGrouping:
8
10
  Enabled: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.1.5)
4
+ quantitative (0.1.7)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
@@ -1,10 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
+ # {Quant::Attributes} is similar to an +attr_accessor+ definition. It provides a simple DSL
5
+ # for defining attributes or properies on an {Quant::Indicators::IndicatorPoint} class.
6
+ #
7
+ # {Quant::Attributes} tracks all defined attributes from child to parent classes,
8
+ # allowing child classes to inherit their parent's attributes as well as redefine them.
9
+ #
10
+ # The exception on redefining is that a serialized key cannot be redefined. Experience
11
+ # has proven that this leads to serialization surprises where what was written to a specific
12
+ # key is not what was expected!
13
+ #
14
+ # NOTE: The above design constraint could be improved with a force or overwrite option.
15
+ #
16
+ # If :default is an immediate value (Integer, Float, Boolean, etc.), it will be used as the
17
+ # initial value for the attribute. If :default is a Symbol, it will send a message on
18
+ # current instance of the class get the default value.
19
+ #
20
+ # @example
21
+ # class FooPoint < IndicatorPoint
22
+ # # will not serialize to a key
23
+ # attribute :bar
24
+ # # serializes to "bzz" key
25
+ # attribute :baz, key: "bzz"
26
+ # # calls the random method on the instance for the default value
27
+ # attribute :foobar, default: :random
28
+ # # delegated to the tick's high_price method
29
+ # attribute :high, default: :high_price
30
+ # # calls the lambda bound to instance for default
31
+ # attribute :low, default: -> { high_price - 5 }
32
+ #
33
+ # def random
34
+ # rand(100)
35
+ # end
36
+ # end
37
+ #
38
+ # class BarPoint < FooPoint
39
+ # attribute :bar, key: "brr" # redefines and sets the key for bar
40
+ # attribute :qux, key: "qxx", default: 5.0 # serializes to "qxx" and defaults to 5.0
41
+ # end
42
+ #
43
+ # FooPoint.attributes
44
+ # # => { bar: { key: nil, default: nil },
45
+ # baz: { key: "bzz", default: nil } }
46
+ #
47
+ # BarPoint.attributes
48
+ # # => { bar: { key: "brr", default: nil },
49
+ # # baz: { key: "bzz", default: nil },
50
+ # # qux: { key: "qxx", default: nil } }
51
+ #
52
+ # BarPoint.new.bar # => nil
53
+ # BarPoint.new.qux # => 5.0
54
+ # BarPoint.new.bar = 2.0 => 2.0
4
55
  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.
56
+ # The +registry+ key is the class registering an attrbritute and is itself
57
+ # a hash of the attribute name and the attribute's key and default value.
8
58
  # Internal use only.
9
59
  #
10
60
  # @example
@@ -47,9 +97,18 @@ module Quant
47
97
  end
48
98
 
49
99
  module InstanceMethods
50
- def initialize(...)
51
- initialize_attributes
52
- super(...)
100
+ # Makes some assumptions about the class's initialization having a +tick+ keyword argument.
101
+ #
102
+ # The challenge here is that we prepend this module to the class, and we are
103
+ # initializing attributes before the owning class gets the opportunity to initialize
104
+ # variables that we wanted to depend on with being able to define a default
105
+ # value that could set default values from a +tick+ method.
106
+ #
107
+ # Ok for now. May need to be more flexible in the future. Alternative strategy could be
108
+ # to lazy eval the default value the first time it is accessed.
109
+ def initialize(*args, **kwargs)
110
+ initialize_attributes(tick: kwargs[:tick])
111
+ super(*args, **kwargs)
53
112
  end
54
113
 
55
114
  # Iterates over all defined attributes in a child => parent hierarchy,
@@ -65,17 +124,42 @@ module Quant
65
124
  end
66
125
  end
67
126
 
127
+ # The default value can be one of the following:
128
+ # - A symbol that is a method on the instance responds to
129
+ # - A symbol that is a method that the instance's tick responds to
130
+ # - A Proc that is bound to the instance
131
+ # - An immediate value (Integer, Float, Boolean, etc.)
132
+ def default_value_for(entry, new_tick)
133
+ # let's not assume tick is always available/implemented
134
+ # can get from instance or from initializer passed here as `new_tick`
135
+ current_tick = new_tick
136
+ current_tick ||= tick if respond_to?(:tick)
137
+
138
+ if entry[:default].is_a?(Symbol) && respond_to?(entry[:default])
139
+ send(entry[:default])
140
+
141
+ elsif entry[:default].is_a?(Symbol) && current_tick&.respond_to?(entry[:default])
142
+ current_tick.send(entry[:default])
143
+
144
+ elsif entry[:default].is_a?(Proc)
145
+ instance_exec(&entry[:default])
146
+
147
+ else
148
+ entry[:default]
149
+ end
150
+ end
151
+
68
152
  # Initializes the defined attributes with default values and
69
153
  # defines accessor methods for each attribute.
70
154
  # If a child class redefines a parent's attribute, the child's
71
155
  # definition will be used.
72
- def initialize_attributes
156
+ def initialize_attributes(tick:)
73
157
  each_attribute do |name, entry|
74
158
  # use the child's definition, skipping the parent's
75
159
  next if respond_to?(name)
76
160
 
77
161
  ivar_name = "@#{name}"
78
- instance_variable_set(ivar_name, entry[:default])
162
+ instance_variable_set(ivar_name, default_value_for(entry, tick))
79
163
  define_singleton_method(name) { instance_variable_get(ivar_name) }
80
164
  define_singleton_method("#{name}=") { |value| instance_variable_set(ivar_name, value) }
81
165
  end
@@ -1,11 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  class Indicators
3
5
  class Indicator
4
6
  include Enumerable
5
-
6
- # # include Mixins::TrendMethods
7
- # include Mixins::Trig
8
- # include Mixins::WeightedAverage
7
+ include Mixins::Functions
8
+ include Mixins::MovingAverages
9
9
  # include Mixins::HilbertTransform
10
10
  # include Mixins::SuperSmoother
11
11
  # include Mixins::Stochastic
@@ -27,6 +27,10 @@ module Quant
27
27
  @points.keys
28
28
  end
29
29
 
30
+ def [](index)
31
+ values[index]
32
+ end
33
+
30
34
  def values
31
35
  @points.values
32
36
  end
@@ -59,7 +63,7 @@ module Quant
59
63
  end
60
64
 
61
65
  def inspect
62
- "#<#{self.class.name} symbol=#{series.symbol} source=#{source} #{points.size} ticks>"
66
+ "#<#{self.class.name} symbol=#{series.symbol} source=#{source} #{ticks.size} ticks>"
63
67
  end
64
68
 
65
69
  def compute
@@ -168,4 +172,4 @@ module Quant
168
172
  # end
169
173
  end
170
174
  end
171
- end
175
+ end
@@ -5,7 +5,7 @@ module Quant
5
5
  class IndicatorPoint
6
6
  include Quant::Attributes
7
7
 
8
- attribute :tick
8
+ attr_reader :tick
9
9
  attribute :source, key: "src"
10
10
  attribute :input, key: "in"
11
11
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  class Indicators
3
5
  class MaPoint < IndicatorPoint
@@ -1,5 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  class Indicators
5
+ # A simple point used primarily to test the indicator system in unit tests.
6
+ # It has a simple computation that just sets the pong value to the input value
7
+ # and increments the compute_count by 1 each time compute is called.
8
+ # Sometimes you just gotta play ping pong to win.
3
9
  class PingPoint < IndicatorPoint
4
10
  attribute :pong
5
11
  attribute :compute_count, default: 0
@@ -1,6 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
+ # The {Quant::IndicatorsProxy} class is responsible for lazily loading indicators
5
+ # so that not all indicators are always engaged and computing their values.
6
+ # If the indicator is never accessed, it's never computed, saving valuable
7
+ # processing CPU cycles.
8
+ #
9
+ # Indicators are generally built around the concept of a source input value and
10
+ # that source is designated by the source parameter when instantiating the
11
+ # {Quant::IndicatorsProxy} class.
12
+ #
13
+ # By design, the {Quant::Indicator} class holds the {Quant::Ticks::Tick} instance
14
+ # alongside the indicator's computed values for that tick.
4
15
  class IndicatorsProxy
5
16
  attr_reader :series, :source, :indicators
6
17
 
@@ -10,14 +21,41 @@ module Quant
10
21
  @indicators = {}
11
22
  end
12
23
 
24
+ # Instantiates the indicator class and stores it in the indicators hash. Once
25
+ # prepared, the indicator becomes active and all ticks pushed into the series
26
+ # are sent to the indicator for processing.
13
27
  def indicator(indicator_class)
14
28
  indicators[indicator_class] ||= indicator_class.new(series: series, source: source)
15
29
  end
16
30
 
31
+ # Adds the tick to all active indicators, triggering them to compute
32
+ # new values against the latest tick.
33
+ #
34
+ # NOTE: Dominant cycle indicators must be computed first as many
35
+ # indicators are adaptive and require the dominant cycle period.
36
+ # The IndicatorsProxy class is not responsible for enforcing
37
+ # this order of events.
17
38
  def <<(tick)
18
39
  indicators.each_value { |indicator| indicator << tick }
19
40
  end
20
41
 
42
+ # Attaches a given Indicator class and defines the method for
43
+ # accessing it using the given name. Indicators take care of
44
+ # computing their values when first attached to a populated
45
+ # series.
46
+ #
47
+ # The indicators shipped with the library are all wired into the framework, thus
48
+ # this method should be used for custom indicators not shipped with the library.
49
+ #
50
+ # @param name [Symbol] The name of the method to define for accessing the indicator.
51
+ # @param indicator_class [Class] The class of the indicator to attach.
52
+ # @example
53
+ # series.indicators.oc2.attach(name: :foo, indicator_class: Indicators::Foo)
54
+ def attach(name:, indicator_class:)
55
+ define_singleton_method(name) { indicator(indicator_class) }
56
+ end
57
+
21
58
  def ma; indicator(Indicators::Ma) end
59
+ def ping; indicator(Indicators::Ping) end
22
60
  end
23
61
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Mixins
5
+ module ExponentialMovingAverage
6
+ # Computes the Exponential Moving Average (EMA) of the given period.
7
+ #
8
+ # The EMA computation is optimized to compute using just the last two
9
+ # indicator data points and is expected to be called in each indicator's
10
+ # `#compute` method for each iteration on the series.
11
+ #
12
+ # @param source [Symbol] the source of the data points to be used in the calculation.
13
+ # @param previous [Symbol] the previous EMA value.
14
+ # @param period [Integer] the number of elements to compute the EMA over.
15
+ # @return [Float] the exponential moving average of the period.
16
+ # @raise [ArgumentError] if the source is not a Symbol.
17
+ # @example
18
+ # def compute
19
+ # p0.ema = exponential_moving_average(:close_price, period: 3)
20
+ # end
21
+ #
22
+ # def compute
23
+ # p0.ema = exponential_moving_average(:close_price, previous: :ema, period: 3)
24
+ # end
25
+ def exponential_moving_average(source, period:, previous: :ema)
26
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
27
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
28
+
29
+ alpha = bars_to_alpha(period)
30
+ p0.send(source) * alpha + p1.send(previous) * (1.0 - alpha)
31
+ end
32
+ alias ema exponential_moving_average
33
+ end
34
+ end
35
+ end
@@ -1,66 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "trig"
3
+ require_relative "functions"
4
4
 
5
5
  module Quant
6
6
  module Mixins
7
- # 1. All the common filters useful for traders have a transfer response that can be written
8
- # as a ratio of two polynomials.
9
- # 2. Lag is very important to traders. More complex filters can be created using more input data,
10
- # but more input data increases lag. Sophisticated filters are not very useful for trading
11
- # because they incur too much lag.
12
- # 3. Filter transfer response can be viewed in the time domain and the frequency domain with equal validity.
13
- # 4. Nonrecursive filters can have zeros in the transfer response, enabling the complete cancellation of
14
- # some selected frequency components.
15
- # 5. Nonrecursive filters having coefficients symmetrical about the center of the filter will have a delay
16
- # of half the degree of the transfer response polynomial at all frequencies.
17
- # 6. Low-pass filters are smoothers because they attenuate the high-frequency components of the input data.
18
- # 7. High-pass filters are detrenders because they attenuate the low-frequency components of trends.
19
- # 8. Band-pass filters are both detrenders and smoothers because they attenuate all but the desired frequency components.
20
- # 9. Filters provide an output only through their transfer response. The transfer response is strictly a
21
- # mathematical function, and interpretations such as overbought, oversold, convergence, divergence,
22
- # and so on are not implied. The validity of such interpretations must be made on the basis of
23
- # statistics apart from the filter.
24
- # 10. The critical period of a filter output is the frequency at which the output power of the filter
25
- # is half the power of the input wave at that frequency.
7
+ # 1. All the common filters useful for traders have a transfer response
8
+ # that can be written as a ratio of two polynomials.
9
+ # 2. Lag is very important to traders. More complex filters can be
10
+ # created using more input data, but more input data increases lag.
11
+ # Sophisticated filters are not very useful for trading because they
12
+ # incur too much lag.
13
+ # 3. Filter transfer response can be viewed in the time domain and
14
+ # the frequency domain with equal validity.
15
+ # 4. Nonrecursive filters can have zeros in the transfer response, enabling
16
+ # the complete cancellation of some selected frequency components.
17
+ # 5. Nonrecursive filters having coefficients symmetrical about the
18
+ # center of the filter will have a delay of half the degree of the
19
+ # transfer response polynomial at all frequencies.
20
+ # 6. Low-pass filters are smoothers because they attenuate the high-frequency
21
+ # components of the input data.
22
+ # 7. High-pass filters are detrenders because they attenuate the
23
+ # low-frequency components of trends.
24
+ # 8. Band-pass filters are both detrenders and smoothers because they
25
+ # attenuate all but the desired frequency components.
26
+ # 9. Filters provide an output only through their transfer response.
27
+ # The transfer response is strictly a mathematical function, and
28
+ # interpretations such as overbought, oversold, convergence, divergence,
29
+ # and so on are not implied. The validity of such interpretations
30
+ # must be made on the basis of statistics apart from the filter.
31
+ # 10. The critical period of a filter output is the frequency at which
32
+ # the output power of the filter is half the power of the input
33
+ # wave at that frequency.
26
34
  # 11. A WMA has little or no redeeming virtue.
27
- # 12. A median filter is best used when the data contain impulsive noise or when there are wild
28
- # variations in the data. Smoothing volume data is one example of a good application for a
29
- # median filter.
35
+ # 12. A median filter is best used when the data contain impulsive noise
36
+ # or when there are wild variations in the data. Smoothing volume
37
+ # data is one example of a good application for a median filter.
38
+ #
39
+ # == Filter Coefficients forVariousTypes of Filters
40
+ #
41
+ # Filter Type b0 b1 b2 a0 a1 a2
42
+ # EMA α 0 0 1 −(1−α) 0
43
+ # Two-pole low-pass α**2 0 0 1 −2*(1-α) (1-α)**2
44
+ # High-pass (1-α/2) -(1-α/2) 0 1 −(1−α) 0
45
+ # Two-pole high-pass (1-α/2)**2 -2*(1-α/2)**2 (1-α/2)**2 1 -2*(1-α) (1-α)**2
46
+ # Band-pass (1-σ)/2 0 -(1-σ)/2 1 -λ*(1+σ) σ
47
+ # Band-stop (1+σ)/2 -2λ*(1+σ)/2 (1+σ)/2 1 -λ*(1+σ) σ
30
48
  #
31
- # Filter Coefficients forVariousTypes of Filters
32
- # Filter Type b0 b1 b2 a0 a1 a2
33
- # EMA α 0 0 1 −(1−α) 0
34
- # Two-pole low-pass α**2 0 0 1 −2*(1-α) (1-α)**2
35
- # High-pass (1-α/2) -(1-α/2) 0 1 −(1−α) 0
36
- # Two-pole high-pass (1-α/2)**2 -2*(1-α/2)**2 (1-α/2)**2 1 -2*(1-α) (1-α)**2
37
- # Band-pass (1-σ)/2 0 -(1-σ)/2 1 -λ*(1+σ) σ
38
- # Band-stop (1+σ)/2 -2λ*(1+σ)/2 (1+σ)/2 1 -λ*(1+σ) σ
39
49
  module Filters
40
- include Mixins::Trig
41
-
42
- # α = Cos(K*360/Period)+Sin(K*360/Period)−1 / Cos(K*360/Period)
43
- # k = 1.0 for single-pole filters
44
- # k = 0.707 for two-pole high-pass filters
45
- # k = 1.414 for two-pole low-pass filters
46
- def period_to_alpha(period, k: 1.0)
47
- radians = deg2rad(k * 360 / period)
48
- cos = Math.cos(radians)
49
- sin = Math.sin(radians)
50
- (cos + sin - 1) / cos
51
- end
52
-
53
- # 3 bars = 0.5
54
- # 4 bars = 0.4
55
- # 5 bars = 0.333
56
- # 6 bars = 0.285
57
- # 10 bars = 0.182
58
- # 20 bars = 0.0952
59
- # 40 bars = 0.0488
60
- # 50 bars = 0.0392
61
- def bars_to_alpha(bars)
62
- 2.0 / (bars + 1)
63
- end
50
+ include Mixins::Functions
64
51
 
65
52
  def ema(source, prev_source, period)
66
53
  alpha = bars_to_alpha(period)
@@ -2,7 +2,30 @@
2
2
 
3
3
  module Quant
4
4
  module Mixins
5
- module Trig
5
+ module Functions
6
+ # α = Cos(K*360/Period)+Sin(K*360/Period)−1 / Cos(K*360/Period)
7
+ # k = 1.0 for single-pole filters
8
+ # k = 0.707 for two-pole high-pass filters
9
+ # k = 1.414 for two-pole low-pass filters
10
+ def period_to_alpha(period, k: 1.0)
11
+ radians = deg2rad(k * 360 / period)
12
+ cos = Math.cos(radians)
13
+ sin = Math.sin(radians)
14
+ (cos + sin - 1) / cos
15
+ end
16
+
17
+ # 3 bars = 0.5
18
+ # 4 bars = 0.4
19
+ # 5 bars = 0.333
20
+ # 6 bars = 0.285
21
+ # 10 bars = 0.182
22
+ # 20 bars = 0.0952
23
+ # 40 bars = 0.0488
24
+ # 50 bars = 0.0392
25
+ def bars_to_alpha(bars)
26
+ 2.0 / (bars + 1)
27
+ end
28
+
6
29
  def deg2rad(degrees)
7
30
  degrees * Math::PI / 180.0
8
31
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "weighted_moving_average"
4
+ require_relative "simple_moving_average"
5
+ require_relative "exponential_moving_average"
6
+ module Quant
7
+ module Mixins
8
+ module MovingAverages
9
+ include WeightedMovingAverage
10
+ include SimpleMovingAverage
11
+ include ExponentialMovingAverage
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Mixins
5
+ module SimpleMovingAverage
6
+ using Quant
7
+
8
+ # Computes the Simple Moving Average (SMA) of the given period.
9
+ #
10
+ # @param source [Symbol] the source of the data points to be used in the calculation.
11
+ # @param period [Integer] the number of elements to compute the SMA over.
12
+ # @return [Float] the simple moving average of the period.
13
+ def simple_moving_average(source, period:)
14
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
15
+
16
+ values.last(period).map { |value| value.send(source) }.mean
17
+ end
18
+ alias sma simple_moving_average
19
+ end
20
+ end
21
+ end
@@ -3,74 +3,29 @@
3
3
  module Quant
4
4
  module Mixins
5
5
  module SuperSmoother
6
- def super_smoother(source, prev_source, period)
7
- v0 = (source.is_a?(Symbol) ? p0.send(source) : source).to_d
8
- return v0.to_f if points.size < 4
6
+ def two_pole_super_smooth(source, period:, previous: :ss)
7
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
9
8
 
10
- k = Math.exp(-Math.sqrt(2) * Math::PI / period)
11
- coef3 = -k**2
12
- coef2 = 2.0 * k * Math.cos(Math.sqrt(2) * (Math::PI / 2) / period)
13
- coef1 = 1.0 - coef2 - coef3
14
-
15
- v1 = p1.send(prev_source).to_d
16
- v2 = p2.send(prev_source).to_d
17
- p3.send(prev_source).to_d
18
- ((coef1 * (v0 + v1)) / 2.0 + (coef2 * v1) + (coef3 * v2)).to_f
19
- end
20
-
21
- def two_pole_super_smooth(source, prev_source, ssperiod)
22
- return p1.send(source) if [p1 == p3]
23
-
24
- radians = Math::PI * Math.sqrt(2) / ssperiod
9
+ radians = Math::PI * Math.sqrt(2) / period
25
10
  a1 = Math.exp(-radians)
26
11
 
27
- coef2 = 2.0 * a1 * Math.cos(radians)
12
+ coef2 = 2.0r * a1 * Math.cos(radians)
28
13
  coef3 = -a1 * a1
29
14
  coef1 = 1.0 - coef2 - coef3
30
15
 
31
- v0 = (p1.send(source) + p2.send(source)) / 2.0
32
- v1 = p2.send(prev_source)
33
- v2 = p3.send(prev_source)
34
- (coef1 * v0) + (coef2 * v1) + (coef3 * v2)
16
+ v0 = (p0.send(source) + p1.send(source))/2.0
17
+ v1 = p2.send(previous)
18
+ v2 = p3.send(previous)
19
+ ((coef1 * v0) + (coef2 * v1) + (coef3 * v2)).to_f
35
20
  end
21
+ alias super_smoother two_pole_super_smooth
22
+ alias ss2p two_pole_super_smooth
36
23
 
37
- def three_pole_super_smooth(source, prev_source, ssperiod)
38
- a1 = Math.exp(-Math::PI / ssperiod)
39
- b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / ssperiod)
40
- c1 = a1**2
24
+ def three_pole_super_smooth(source, period:, previous: :ss)
25
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
41
26
 
42
- coef2 = b1 + c1
43
- coef3 = -(c1 + b1 * c1)
44
- coef4 = c1**2
45
- coef1 = 1 - coef2 - coef3 - coef4
46
-
47
- p0 = prev(0)
48
- p1 = prev(1)
49
- p2 = prev(2)
50
- p3 = prev(3)
51
-
52
- v0 = source.is_a?(Symbol) ? p0.send(source) : source
53
- return v0 if [p0, p1, p2].include?(p3)
54
-
55
- v1 = p1.send(prev_source)
56
- v2 = p2.send(prev_source)
57
- v3 = p3.send(prev_source)
58
- (coef1 * v0) + (coef2 * v1) + (coef3 * v2) + (coef4 * v3)
59
- end
60
-
61
- # super smoother 3 pole
62
- def ss3p(source, prev_source, ssperiod)
63
- p0 = points[-1]
64
- p1 = points[-2] || p0
65
- p2 = points[-3] || p1
66
- p3 = points[-4] || p2
67
-
68
- v0 = source.is_a?(Symbol) ? p0.send(source) : source
69
- return v0 if [p0 == p3]
70
-
71
- debugger if points.size > 4
72
- a1 = Math.exp(-Math::PI / ssperiod)
73
- b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / ssperiod)
27
+ a1 = Math.exp(-Math::PI / period)
28
+ b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / period)
74
29
  c1 = a1**2
75
30
 
76
31
  coef2 = b1 + c1
@@ -78,56 +33,14 @@ module Quant
78
33
  coef4 = c1**2
79
34
  coef1 = 1 - coef2 - coef3 - coef4
80
35
 
81
- v1 = p1.send(prev_source)
82
- v2 = p2.send(prev_source)
83
- v3 = p3.send(prev_source)
36
+ v0 = p0.send(source)
37
+ v1 = p1.send(previous)
38
+ v2 = p2.send(previous)
39
+ v3 = p3.send(previous)
40
+
84
41
  (coef1 * v0) + (coef2 * v1) + (coef3 * v2) + (coef4 * v3)
85
42
  end
86
-
87
- # attr_reader :hpfs, :value1s, :hpf_psns
88
-
89
- # def hpf
90
- # @hpfs[-1]
91
- # end
92
-
93
- # def hpf_psn
94
- # @hpf_psns[-1]
95
- # end
96
-
97
- # def prev offset, source
98
- # idx = offset + 1
99
- # source[[-idx, -source.size].max]
100
- # end
101
-
102
- # def weighted_average source
103
- # [ 4.0 * prev(0, source),
104
- # 3.0 * prev(1, source),
105
- # 2.0 * prev(2, source),
106
- # prev(3, source),
107
- # ].sum / 10.0
108
- # end
109
-
110
- # def compute_hpf
111
- # @hpfs ||= []
112
- # @value1s ||= []
113
- # @hpf_psns ||= []
114
- # max_cycle = period * 10
115
-
116
- # r = (360.0 / max_cycle) * (Math::PI / 180)
117
- # alpha = (1 - Math::sin(r)) / Math::cos(r)
118
- # hpf = @hpfs.empty? ? 0.0 : (0.5 * (1.0 + alpha) * (current_value - prev_value(1))) + (alpha * (@hpfs[-1]))
119
-
120
- # @hpfs << hpf
121
- # @hpfs.shift if @hpfs.size > max_cycle
122
-
123
- # hh = @hpfs.max
124
- # ll = @hpfs.min
125
- # @value1s << value1 = (hh == ll ? 0.0 : 100 * (hpf - ll) / (hh - ll))
126
- # @value1s.shift if @value1s.size > max_cycle
127
-
128
- # @hpf_psns << weighted_average(@value1s)
129
- # @hpf_psns.shift if @hpf_psns.size > max_cycle
130
- # end
43
+ alias ss3p three_pole_super_smooth
131
44
  end
132
45
  end
133
46
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Mixins
5
+ module WeightedMovingAverage
6
+ using Quant
7
+ # Computes the Weighted Moving Average (WMA) of the series, using the four most recent data points.
8
+ #
9
+ # @param source [Symbol] the source of the data points to be used in the calculation.
10
+ # @return [Float] the weighted average of the series.
11
+ # @raise [ArgumentError] if the source is not a Symbol.
12
+ # @example
13
+ # p0.wma = weighted_average(:close_price)
14
+ def weighted_moving_average(source)
15
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
16
+
17
+ [4.0 * p0.send(source),
18
+ 3.0 * p1.send(source),
19
+ 2.0 * p2.send(source),
20
+ p3.send(source)].sum / 10.0
21
+ end
22
+ alias wma weighted_moving_average
23
+
24
+ # Computes the Weighted Moving Average (WMA) of the series, using the seven most recent data points.
25
+ #
26
+ # @param source [Symbol] the source of the data points to be used in the calculation.
27
+ # @return [Float] the weighted average of the series.
28
+ # @raise [ArgumentError] if the source is not a Symbol.
29
+ # @example
30
+ # p0.wma = weighted_average(:close_price)
31
+ def extended_weighted_moving_average(source)
32
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
33
+
34
+ [7.0 * p0.send(source),
35
+ 6.0 * p1.send(source),
36
+ 5.0 * p2.send(source),
37
+ 4.0 * p3.send(source),
38
+ 3.0 * p(4).send(source),
39
+ 2.0 * p(5).send(source),
40
+ p(6).send(source)].sum / 28.0
41
+ end
42
+ alias ewma extended_weighted_moving_average
43
+ end
44
+ end
45
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  module Refinements
3
5
  # Refinements for the standard Ruby {Quant::Array} class.
@@ -32,7 +34,6 @@ module Quant
32
34
  # The refined behavior generally only exists within the library's scope, but if you call `using Quant` in your
33
35
  # own code, you may encounter the changed behavior unexpectedly.
34
36
  module Array
35
-
36
37
  # Overrides the standard +<<+ method to track the +maximum+ and +minimum+ values
37
38
  # while also respecting the +max_size+ setting.
38
39
  def <<(value)
@@ -109,7 +110,7 @@ module Quant
109
110
  subset = last(n)
110
111
  return 0.0 if subset.empty?
111
112
 
112
- sum = subset.sum / subset.size.to_f
113
+ subset.sum / subset.size.to_f
113
114
  end
114
115
 
115
116
  # Computes the Exponential Moving Average (EMA) of the array. When +n+ is specified,
@@ -175,7 +176,7 @@ module Quant
175
176
  # @param n [Integer] the number of elements to compute the Standard Deviation over.
176
177
  # @return [Float]
177
178
  def stddev(reference_value, n: size)
178
- variance(reference_value, n: n) ** 0.5
179
+ variance(reference_value, n: n)**0.5
179
180
  end
180
181
 
181
182
  def variance(reference_value, n: size)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  module Settings
3
5
  MAX_PERIOD = 48
@@ -4,11 +4,15 @@ require_relative "tick"
4
4
 
5
5
  module Quant
6
6
  module Ticks
7
- # An {Quant::Ticks::OHLC} is a bar or candle for a point in time that has an open, high, low, and close price.
8
- # It is the most common form of a {Quant::Ticks::Tick} and is usually used to representa time period such as a
9
- # minute, hour, day, week, or month. The {Quant::Ticks::OHLC} is used to represent the price action of an asset
10
- # The interval of the {Quant::Ticks::OHLC} is the time period that the {Quant::Ticks::OHLC} represents,
11
- # such has hourly, daily, weekly, etc.
7
+ # A {Quant::Ticks::OHLC} is a bar or candle for a point in time that
8
+ # has an open, high, low, and close price. It is the most common form
9
+ # of a {Quant::Ticks::Tick} and is usually used to representa time
10
+ # period such as a minute, hour, day, week, or month.
11
+ #
12
+ # The {Quant::Ticks::OHLC} is used to represent the price action of
13
+ # an asset The interval of the {Quant::Ticks::OHLC} is the time period
14
+ # that the {Quant::Ticks::OHLC} represents, such has hourly, daily,
15
+ # weekly, etc.
12
16
  class OHLC < Tick
13
17
  include TimeMethods
14
18
 
@@ -89,7 +93,7 @@ module Quant
89
93
  # This method is useful for comparing the volatility of different assets.
90
94
  # @return [Float]
91
95
  def daily_price_change_ratio
92
- @price_change ||= ((open_price - close_price) / oc2).abs
96
+ @daily_price_change_ratio ||= ((open_price - close_price) / oc2).abs
93
97
  end
94
98
 
95
99
  # Set the #green? property to true when the close_price is greater than or equal to the open_price.
@@ -2,22 +2,29 @@
2
2
 
3
3
  module Quant
4
4
  module Ticks
5
- # {Quant::Ticks::Tick} is the abstract ancestor for all Ticks and holds the logic for interacting with series and indicators.
6
- # The public interface is devoid of properties around price, volume, and timestamp, etc. Descendant classes
7
- # are responsible for defining the properties and how they are represented.
8
- #
9
- # The {Quant::Ticks::Tick} class is designed to be immutable and is intended to be used as a value object. This means that
10
- # once a {Quant::Ticks::Tick} is created, it cannot be changed. This is important for the integrity of the series and
11
- # indicators that depend on the ticks within the series.
12
- #
13
- # When a tick is added to a series, it is locked into the series and ownership cannot be changed. This is important
14
- # for the integrity of the series and indicators that depend on the ticks within the series. This is a key design
15
- # to being able to being able to not only compute indicators on the ticks just once, but also avoid recomputing
16
- # indicators when series are limited/sliced/filtered into subsets of the original series.
17
- #
5
+ # {Quant::Ticks::Tick} is the abstract ancestor for all Ticks and holds
6
+ # the logic for interacting with series and indicators. The public
7
+ # interface is devoid of properties around price, volume, and timestamp, etc.
8
+ # Descendant classes are responsible for defining the properties and
9
+ # how they are represented.
10
+
11
+ # The {Quant::Ticks::Tick} class is designed to be immutable and is
12
+ # intended to be used as a value object. This means that once a
13
+ # {Quant::Ticks::Tick} is created, it cannot be changed. This is important
14
+ # for the integrity of the series and indicators that depend on the
15
+ # ticks within the series.
16
+
17
+ # When a tick is added to a series, it is locked into the series and
18
+ # ownership cannot be changed. This is important for the integrity
19
+ # of the series and indicators that depend on the ticks within the series.
20
+ # This is a key design to being able to being able to not only compute
21
+ # indicators on the ticks just once, but also avoid recomputing indicators
22
+ # when series are limited/sliced/filtered into subsets of the original series.
23
+
18
24
  # Ticks can be serialized to and from Ruby Hash, JSON strings, and CSV strings.
19
25
  class Tick
20
- # Returns a {Quant::Ticks::Tick} from a Ruby +Hash+. The default serializer is used to generate the {Quant::Ticks::Tick}.
26
+ # Returns a {Quant::Ticks::Tick} from a Ruby +Hash+. The default
27
+ # serializer is used to generate the {Quant::Ticks::Tick}.
21
28
  # @param hash [Hash]
22
29
  # @param serializer_class [Class] The serializer class to use for the conversion.
23
30
  # @return [Quant::Ticks::Tick]
@@ -30,7 +37,8 @@ module Quant
30
37
  serializer_class.from(hash, tick_class: self)
31
38
  end
32
39
 
33
- # Returns a {Quant::Ticks::Tick} from a JSON string. The default serializer is used to generate the {Quant::Ticks::Tick}.
40
+ # Returns a {Quant::Ticks::Tick} from a JSON string. The default
41
+ # serializer is used to generate the {Quant::Ticks::Tick}.
34
42
  # @param json [String]
35
43
  # @param serializer_class [Class] The serializer class to use for the conversion.
36
44
  # @return [Quant::Ticks::Tick]
@@ -80,7 +88,8 @@ module Quant
80
88
  self
81
89
  end
82
90
 
83
- # Returns a Ruby hash for the Tick. The default serializer is used to generate the hash.
91
+ # Returns a Ruby hash for the Tick. The default serializer is used
92
+ # to generate the hash.
84
93
  #
85
94
  # @param serializer_class [Class] the serializer class to use for the conversion.
86
95
  # @example
@@ -90,7 +99,8 @@ module Quant
90
99
  serializer_class.to_h(self)
91
100
  end
92
101
 
93
- # Returns a JSON string for the Tick. The default serializer is used to generate the JSON string.
102
+ # Returns a JSON string for the Tick. The default serializer is used
103
+ # to generate the JSON string.
94
104
  #
95
105
  # @param serializer_class [Class] the serializer class to use for the conversion.
96
106
  # @example
@@ -100,8 +110,9 @@ module Quant
100
110
  serializer_class.to_json(self)
101
111
  end
102
112
 
103
- # Returns a CSV row as a String for the Tick. The default serializer is used to generate the CSV string.
104
- # If headers is true, two lines returned separated by newline.
113
+ # Returns a CSV row as a String for the Tick. The default serializer
114
+ # is used to generate the CSV string. If headers is true, two lines
115
+ # returned separated by newline.
105
116
  # The first line is the header row and the second line is the data row.
106
117
  #
107
118
  # @param serializer_class [Class] the serializer class to use for the conversion.
data/lib/quant/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.7"
5
5
  end
data/lib/quantitative.rb CHANGED
@@ -14,4 +14,4 @@ Dir.glob(File.join(quant_folder, "*.rb")).each { |fn| require fn }
14
14
  # require sub-folders and their sub-folders
15
15
  %w(refinements mixins settings ticks indicators).each do |sub_folder|
16
16
  Dir.glob(File.join(quant_folder, sub_folder, "**/*.rb")).each { |fn| require fn }
17
- end
17
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quantitative
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Lang
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-25 00:00:00.000000000 Z
11
+ date: 2024-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -58,14 +58,17 @@ files:
58
58
  - lib/quant/indicators_sources.rb
59
59
  - lib/quant/interval.rb
60
60
  - lib/quant/mixins/direction.rb
61
+ - lib/quant/mixins/exponential_moving_average.rb
61
62
  - lib/quant/mixins/filters.rb
62
63
  - lib/quant/mixins/fisher_transform.rb
64
+ - lib/quant/mixins/functions.rb
63
65
  - lib/quant/mixins/high_pass_filter.rb
64
66
  - lib/quant/mixins/hilbert_transform.rb
67
+ - lib/quant/mixins/moving_averages.rb
68
+ - lib/quant/mixins/simple_moving_average.rb
65
69
  - lib/quant/mixins/stochastic.rb
66
70
  - lib/quant/mixins/super_smoother.rb
67
- - lib/quant/mixins/trig.rb
68
- - lib/quant/mixins/weighted_average.rb
71
+ - lib/quant/mixins/weighted_moving_average.rb
69
72
  - lib/quant/refinements/array.rb
70
73
  - lib/quant/series.rb
71
74
  - lib/quant/settings.rb
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quant
4
- module Mixins
5
- module WeightedAverage
6
- def weighted_average(source)
7
- value = source.is_a?(Symbol) ? p0.send(source) : source
8
- [4.0 * value,
9
- 3.0 * p1.send(source),
10
- 2.0 * p2.send(source),
11
- p3.send(source),].sum / 10.0
12
- end
13
-
14
- def extended_weighted_average(source)
15
- value = source.is_a?(Symbol) ? p0.send(source) : source
16
- [7.0 * value,
17
- 6.0 * p1.send(source),
18
- 5.0 * p2.send(source),
19
- 4.0 * p3.send(source),
20
- 3.0 * prev(4).send(source),
21
- 2.0 * prev(5).send(source),
22
- prev(6).send(source),].sum / 28.0
23
- end
24
- end
25
- end
26
- end