quantitative 0.1.5 → 0.1.7

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