quantitative 0.1.10 → 0.2.1

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.
@@ -0,0 +1,59 @@
1
+ require_relative "dominant_cycle"
2
+
3
+ module Quant
4
+ class Indicators
5
+ class DominantCycles
6
+ # The phase accumulation method of computing the dominant cycle is perhaps
7
+ # the easiest to comprehend. In this technique, we measure the phase
8
+ # at each sample by taking the arctangent of the ratio of the quadrature
9
+ # component to the in-phase component. A delta phase is generated by
10
+ # taking the difference of the phase between successive samples.
11
+ # At each sample we can then look backwards, adding up the delta
12
+ # phases. When the sum of the delta phases reaches 360 degrees,
13
+ # we must have passed through one full cycle, on average. The process
14
+ # is repeated for each new sample.
15
+ #
16
+ # The phase accumulation method of cycle measurement always uses one
17
+ # full cycle’s worth of historical data. This is both an advantage
18
+ # and a disadvantage. The advantage is the lag in obtaining the answer
19
+ # scales directly with the cycle period. That is, the measurement of
20
+ # a short cycle period has less lag than the measurement of a longer
21
+ # cycle period. However, the number of samples used in making the
22
+ # measurement means the averaging period is variable with cycle period.
23
+ # Longer averaging reduces the noise level compared to the signal.
24
+ # Therefore, shorter cycle periods necessarily have a higher output
25
+ # signal-to-noise ratio.
26
+ class PhaseAccumulator < DominantCycle
27
+ def compute_period
28
+ p0.i1 = 0.15 * p0.i1 + 0.85 * p1.i1
29
+ p0.q1 = 0.15 * p0.q1 + 0.85 * p1.q1
30
+
31
+ p0.accumulator_phase = Math.atan(p0.q1 / p0.i1) unless p0.i1.zero?
32
+
33
+ case
34
+ when p0.i1 < 0 && p0.q1 > 0 then p0.accumulator_phase = 180.0 - p0.accumulator_phase
35
+ when p0.i1 < 0 && p0.q1 < 0 then p0.accumulator_phase = 180.0 + p0.accumulator_phase
36
+ when p0.i1 > 0 && p0.q1 < 0 then p0.accumulator_phase = 360.0 - p0.accumulator_phase
37
+ end
38
+
39
+ p0.delta_phase = p1.accumulator_phase - p0.accumulator_phase
40
+ if p1.accumulator_phase < 90.0 && p0.accumulator_phase > 270.0
41
+ p0.delta_phase = 360.0 + p1.accumulator_phase - p0.accumulator_phase
42
+ end
43
+
44
+ p0.delta_phase = p0.delta_phase.clamp(min_period, max_period)
45
+
46
+ p0.inst_period = p1.inst_period
47
+ period_points(max_period).each_with_index do |prev, index|
48
+ p0.phase_sum += prev.delta_phase
49
+ if p0.phase_sum > 360.0
50
+ p0.inst_period = index
51
+ break
52
+ end
53
+ end
54
+ p0.period = (0.25 * p0.inst_period + 0.75 * p1.inst_period).round(0)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -7,13 +7,11 @@ module Quant
7
7
  include Mixins::Functions
8
8
  include Mixins::Filters
9
9
  include Mixins::MovingAverages
10
- # include Mixins::HilbertTransform
11
- # include Mixins::SuperSmoother
12
- # include Mixins::Stochastic
13
- # include Mixins::FisherTransform
14
- # include Mixins::HighPassFilter
10
+ include Mixins::HilbertTransform
11
+ include Mixins::SuperSmoother
12
+ include Mixins::Stochastic
13
+ include Mixins::FisherTransform
15
14
  # include Mixins::Direction
16
- # include Mixins::Filters
17
15
 
18
16
  attr_reader :source, :series
19
17
 
@@ -21,9 +19,42 @@ module Quant
21
19
  @series = series
22
20
  @source = source
23
21
  @points = {}
22
+ series.new_indicator(self)
24
23
  series.each { |tick| self << tick }
25
24
  end
26
25
 
26
+ def min_period
27
+ Quant.config.indicators.min_period
28
+ end
29
+
30
+ def max_period
31
+ Quant.config.indicators.max_period
32
+ end
33
+
34
+ def half_period
35
+ Quant.config.indicators.half_period
36
+ end
37
+
38
+ def micro_period
39
+ Quant.config.indicators.micro_period
40
+ end
41
+
42
+ def dominant_cycle_kind
43
+ Quant.config.indicators.dominant_cycle_kind
44
+ end
45
+
46
+ def pivot_kind
47
+ Quant.config.indicators.pivot_kind
48
+ end
49
+
50
+ def dominant_cycle
51
+ series.indicators[source].dominant_cycle
52
+ end
53
+
54
+ def dc_period
55
+ dominant_cycle.points[t0].period
56
+ end
57
+
27
58
  def ticks
28
59
  @points.keys
29
60
  end
@@ -45,7 +76,7 @@ module Quant
45
76
 
46
77
  def <<(tick)
47
78
  @t0 = tick
48
- @p0 = points_class.new(tick:, source:)
79
+ @p0 = points_class.new(indicator: self, tick:, source:)
49
80
  @points[tick] = @p0
50
81
 
51
82
  @p1 = values[-2] || @p0
@@ -4,19 +4,29 @@ module Quant
4
4
  class Indicators
5
5
  class IndicatorPoint
6
6
  include Quant::Attributes
7
+ extend Forwardable
7
8
 
8
- attr_reader :tick
9
+ attr_reader :indicator, :tick
9
10
 
10
11
  attribute :source, key: "src"
11
12
  attribute :input, key: "in"
12
13
 
13
- def initialize(tick:, source:)
14
+ def initialize(indicator:, tick:, source:)
15
+ @indicator = indicator
14
16
  @tick = tick
15
17
  @source = source
16
18
  @input = @tick.send(source)
17
19
  initialize_data_points
18
20
  end
19
21
 
22
+ def_delegator :indicator, :series
23
+ def_delegator :indicator, :min_period
24
+ def_delegator :indicator, :max_period
25
+ def_delegator :indicator, :half_period
26
+ def_delegator :indicator, :micro_period
27
+ def_delegator :indicator, :dominant_cycle_kind
28
+ def_delegator :indicator, :pivot_kind
29
+
20
30
  def initialize_data_points
21
31
  # No-Op - Override in subclass if needed.
22
32
  end
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "indicators_proxy"
3
4
  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
5
+ # TODO: build an Indicator registry so new indicators can be added and
6
+ # used outside those shipped with the library.
7
+ class Indicators < IndicatorsProxy
8
+ def ping; indicator(Indicators::Ping) end
9
+
10
+ def dominant_cycles
11
+ @dominant_cycles ||= Indicators::DominantCycleIndicators.new(series:, source:)
12
+ end
6
13
  end
7
14
  end
@@ -13,12 +13,21 @@ module Quant
13
13
  # By design, the {Quant::Indicator} class holds the {Quant::Ticks::Tick} instance
14
14
  # alongside the indicator's computed values for that tick.
15
15
  class IndicatorsProxy
16
- attr_reader :series, :source, :indicators
16
+ attr_reader :series, :source, :dominant_cycle, :indicators
17
17
 
18
18
  def initialize(series:, source:)
19
19
  @series = series
20
20
  @source = source
21
21
  @indicators = {}
22
+ @dominant_cycle = dominant_cycle_indicator
23
+ end
24
+
25
+ def dominant_cycle_indicator
26
+ kind = Quant.config.indicators.dominant_cycle_kind.to_s
27
+ base_class_name = kind.split("_").map(&:capitalize).join
28
+ class_name = "Quant::Indicators::DominantCycles::#{base_class_name}"
29
+ indicator_class = Object.const_get(class_name)
30
+ indicator_class.new(series:, source:)
22
31
  end
23
32
 
24
33
  # Instantiates the indicator class and stores it in the indicators hash. Once
@@ -36,6 +45,7 @@ module Quant
36
45
  # The IndicatorsProxy class is not responsible for enforcing
37
46
  # this order of events.
38
47
  def <<(tick)
48
+ dominant_cycle << tick
39
49
  indicators.each_value { |indicator| indicator << tick }
40
50
  end
41
51
 
@@ -54,8 +64,5 @@ module Quant
54
64
  def attach(name:, indicator_class:)
55
65
  define_singleton_method(name) { indicator(indicator_class) }
56
66
  end
57
-
58
- def ma; indicator(Indicators::Ma) end
59
- def ping; indicator(Indicators::Ping) end
60
67
  end
61
68
  end
@@ -7,12 +7,22 @@ module Quant
7
7
  @indicator_sources = {}
8
8
  end
9
9
 
10
+ def new_indicator(indicator)
11
+ @indicator_sources[indicator.source] ||= Indicators.new(series: @series, source: indicator.source)
12
+ end
13
+
14
+ def [](source)
15
+ return @indicator_sources[source] if @indicator_sources.key?(source)
16
+
17
+ raise Quant::Errors::InvalidIndicatorSource, "Invalid source, #{source.inspect}."
18
+ end
19
+
10
20
  def <<(tick)
11
21
  @indicator_sources.each_value { |indicator| indicator << tick }
12
22
  end
13
23
 
14
24
  def oc2
15
- @indicator_sources[:oc2] ||= IndicatorsProxy.new(series: @series, source: :oc2)
25
+ @indicator_sources[:oc2] ||= Indicators.new(series: @series, source: :oc2)
16
26
  end
17
27
  end
18
28
  end
@@ -2,44 +2,89 @@
2
2
 
3
3
  module Quant
4
4
  module Mixins
5
+ # The following are high pass filters that are used to remove low frequency
6
+ # components from a time series. In simple terms, a high pass filter
7
+ # allows signals above a certain frequency (the cutoff frequency) to
8
+ # pass through relatively unaffected, while attenuating or blocking
9
+ # signals below that frequency.
10
+ #
11
+ # HighPass Filters are “detrenders” because they attenuate low frequency components
12
+ # One pole HighPass and SuperSmoother does not produce a zero mean because low
13
+ # frequency spectral dilation components are "leaking" through The one pole
14
+ # HighPass Filter response
15
+ #
16
+ # == Experimental
17
+ # Across the various texts and papers, Ehlers presents varying implementations
18
+ # of high-pass filters. I believe the two pole high-pass filter is the most
19
+ # consistently presented while the one pole high-pass filter has been presented
20
+ # in a few different ways. In some implementations, alpha is based on simple
21
+ # bars/lag while others use alpha based on phase/trigonometry. I have not been
22
+ # able to reconcile the differences and have not been able to find a definitive
23
+ # source for the correct implementation and do not know enough math to reason
24
+ # these out mathematically nor do I possess an advanced understanding of the
25
+ # fundamentals around digital signal processing. As such, the single-pole
26
+ # high-pass filters in this module are marked as experimental and may be incorrect.
5
27
  module HighPassFilters
6
- # HighPass Filters are “detrenders” because they attenuate low frequency components
7
- # One pole HighPass and SuperSmoother does not produce a zero mean because low
8
- # frequency spectral dilation components are “leaking” through The one pole
9
- # HighPass Filter response
10
- def two_pole_high_pass_filter(source, prev_source, min_period, max_period = nil)
11
- raise "source must be a symbol" unless source.is_a?(Symbol)
12
- return p0.send(source) if p0 == p2
13
-
14
- max_period ||= min_period * 2
15
- (min_period * Math.sqrt(2))
16
- max_radians = 2.0 * Math::PI / (max_period * Math.sqrt(2))
28
+ # A two-pole high-pass filter is a more advanced filtering technique
29
+ # used to remove low-frequency components from financial time series
30
+ # data, such as stock prices or market indices.
31
+ #
32
+ # Similar to a single-pole high-pass filter, a two-pole high-pass filter
33
+ # is designed to attenuate or eliminate slow-moving trends or macroeconomic
34
+ # effects from the data while preserving higher-frequency fluctuations.
35
+ # However, compared to the single-pole filter, the two-pole filter
36
+ # typically offers a steeper roll-off and better attenuation of lower
37
+ # frequencies, resulting in a more pronounced emphasis on short-term fluctuations.
38
+ def two_pole_high_pass_filter(source, period:, previous: :hp)
39
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
40
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
17
41
 
18
- v1 = p0.send(source) - (2.0 * p1.send(source)) + p2.send(source)
19
- v2 = p1.send(prev_source)
20
- v3 = p2.send(prev_source)
42
+ alpha = period_to_alpha(period, k: 0.707)
21
43
 
22
- alpha = period_to_alpha(max_radians)
44
+ v1 = p0.send(source) - (2.0 * p1.send(source)) + p2.send(source)
45
+ v2 = p1.send(previous)
46
+ v3 = p2.send(previous)
23
47
 
24
- a = (1 - (alpha * 0.5))**2 * v1
25
- b = 2 * (1 - alpha) * v2
26
- c = (1 - alpha)**2 * v3
48
+ a = v1 * (1 - (alpha * 0.5))**2
49
+ b = v2 * 2 * (1 - alpha)
50
+ c = v3 * (1 - alpha)**2
27
51
 
28
52
  a + b - c
29
53
  end
30
54
 
55
+ # A single-pole high-pass filter is used to filter out low-frequency
56
+ # components from financial time series data. This type of filter is
57
+ # commonly applied in signal processing techniques to remove noise or
58
+ # unwanted trends from the data while preserving higher-frequency fluctuations.
59
+ #
60
+ # A single-pole high-pass filter can be used to remove slow-moving trends
61
+ # or macroeconomic effects from the data, focusing instead on short-term
62
+ # fluctuations or high-frequency trading signals. By filtering out
63
+ # low-frequency components, traders aim to identify and exploit more
64
+ # immediate market opportunities, such as short-term price movements
65
+ # or momentum signals.
66
+ #
67
+ # The implementation of a single-pole high-pass filter in algorithmic
68
+ # trading typically involves applying a mathematical formula or algorithm
69
+ # to the historical price data of a financial instrument. This algorithm
70
+ # selectively attenuates or removes the low-frequency components of the
71
+ # data, leaving behind the higher-frequency fluctuations that traders
72
+ # are interested in analyzing for potential trading signals.
73
+ #
74
+ # Overall, single-pole high-pass filters in algorithmic trading are
75
+ # used as preprocessing steps to enhance the signal-to-noise ratio in
76
+ # financial data and to extract actionable trading signals from noisy
77
+ # or cluttered market data.
78
+ #
79
+ # == NOTES
31
80
  # alpha = (Cosine(.707* 2 * PI / 48) + Sine (.707*360 / 48) - 1) / Cosine(.707*360 / 48);
32
81
  # is the same as the following:
33
82
  # radians = Math.sqrt(2) * Math::PI / period
34
83
  # alpha = (Math.cos(radians) + Math.sin(radians) - 1) / Math.cos(radians)
35
84
  def high_pass_filter(source, period:, previous: :hp)
85
+ Quant.experimental("This method is unproven and may be incorrect.")
36
86
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
37
-
38
- v0 = p0.send(source)
39
- return v0 if p3 == p0
40
-
41
- v1 = p1.send(source)
42
- v2 = p2.send(source)
87
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
43
88
 
44
89
  radians = Math.sqrt(2) * Math::PI / period
45
90
  a = Math.exp(-radians)
@@ -49,7 +94,35 @@ module Quant
49
94
  c3 = -a**2
50
95
  c1 = (1 + c2 - c3) / 4
51
96
 
52
- (c1 * (v0 - (2 * v1) + v2)) + (c2 * p1.hp) + (c3 * p2.hp)
97
+ v0 = p0.send(source)
98
+ v1 = p1.send(source)
99
+ v2 = p2.send(source)
100
+ f1 = p1.send(previous)
101
+ f2 = p2.send(previous)
102
+
103
+ (c1 * (v0 - (2 * v1) + v2)) + (c2 * f1) + (c3 * f2)
104
+ end
105
+
106
+ # HPF = (1 − α/2)2 * (Price − 2 * Price[1] + Price[2]) + 2 * (1 − α) * HPF[1] − (1 − α)2 * HPF[2];
107
+ # High Pass Filter presented in Ehlers Cybernetic Analysis for Stocks and Futures Equation 2.7
108
+ def hpf2(source, period:, previous:)
109
+ Quant.experimental("This method is unproven and may be incorrect.")
110
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
111
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
112
+
113
+ alpha = period_to_alpha(period, k: 1.0)
114
+ v0 = p0.send(source)
115
+ v1 = p1.send(source)
116
+ v2 = p1.send(source)
117
+
118
+ f1 = p1.send(previous)
119
+ f2 = p2.send(previous)
120
+
121
+ c1 = (1 - alpha / 2)**2
122
+ c2 = 2 * (1 - alpha)
123
+ c3 = (1 - alpha)**2
124
+
125
+ (c1 * (v0 - (2 * v1) + v2)) + (c2 * f1) - (c3 * f2)
53
126
  end
54
127
  end
55
128
  end
@@ -6,39 +6,42 @@ module Quant
6
6
  def two_pole_super_smooth(source, period:, previous: :ss)
7
7
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
8
8
 
9
- radians = Math::PI * Math.sqrt(2) / period
9
+ radians = Math.sqrt(2) * Math::PI / period
10
10
  a1 = Math.exp(-radians)
11
11
 
12
- coef2 = 2.0 * a1 * Math.cos(radians)
13
- coef3 = -a1 * a1
14
- coef1 = 1.0 - coef2 - coef3
12
+ c3 = -a1**2
13
+ c2 = 2.0 * a1 * Math.cos(radians)
14
+ c1 = 1.0 - c2 - c3
15
15
 
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
16
+ v1 = (p0.send(source) + p1.send(source)) * 0.5
17
+ v2 = p2.send(previous)
18
+ v3 = p3.send(previous)
19
+
20
+ (c1 * v1) + (c2 * v2) + (c3 * v3)
20
21
  end
22
+
21
23
  alias super_smoother two_pole_super_smooth
22
24
  alias ss2p two_pole_super_smooth
23
25
 
24
26
  def three_pole_super_smooth(source, period:, previous: :ss)
25
27
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
26
28
 
27
- a1 = Math.exp(-Math::PI / period)
28
- b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / period)
29
+ radians = Math::PI / period
30
+ a1 = Math.exp(-radians)
31
+ b1 = 2 * a1 * Math.cos(Math.sqrt(3) * radians)
29
32
  c1 = a1**2
30
33
 
31
- coef2 = b1 + c1
32
- coef3 = -(c1 + b1 * c1)
33
- coef4 = c1**2
34
- coef1 = 1 - coef2 - coef3 - coef4
34
+ c4 = c1**2
35
+ c3 = -(c1 + b1 * c1)
36
+ c2 = b1 + c1
37
+ c1 = 1 - c2 - c3 - c4
35
38
 
36
39
  v0 = p0.send(source)
37
40
  v1 = p1.send(previous)
38
41
  v2 = p2.send(previous)
39
42
  v3 = p3.send(previous)
40
43
 
41
- (coef1 * v0) + (coef2 * v1) + (coef3 * v2) + (coef4 * v3)
44
+ (c1 * v0) + (c2 * v1) + (c3 * v2) + (c4 * v3)
42
45
  end
43
46
  alias ss3p three_pole_super_smooth
44
47
  end
@@ -16,6 +16,20 @@ module Quant
16
16
  # the others are still unproven and Ehlers' many papers over the year
17
17
  # tend to change implementation details, too.
18
18
  #
19
+ # == Experimental!
20
+ # The main goal with the universal filters is to provide a means to
21
+ # compare the optimized filters with the generalized filters and
22
+ # generally show correctness of the solutions. However, that also
23
+ # means validating the outputs of those computations, which is not my forté.
24
+ # My idea of validating is if I have two or more implementations that produce
25
+ # identical (or nearly identical) results, then I consider the implementation
26
+ # sound and doing what it is supposed to do.
27
+ #
28
+ # Several are marked "experimental" because I have not been able to
29
+ # prove their correctness. Those that are proven correct are not
30
+ # marked as experimental and you'll find their outputs show up in other
31
+ # specs where they're used alongside the optimized versions of those filters.
32
+ #
19
33
  # == Ehlers' Notes on Generalized Filters
20
34
  # 1. All the common filters useful for traders have a transfer response
21
35
  # that can be written as a ratio of two polynomials.
@@ -272,7 +286,6 @@ module Quant
272
286
  # not be suitable for all trading or analysis purposes, and its effects
273
287
  # should be evaluated in the context of specific goals and strategies.
274
288
  def universal_two_pole_high_pass(source, previous:, period:)
275
- Quant.experimental("This method is unproven and may be incorrect.")
276
289
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
277
290
  raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
278
291
 
data/lib/quant/series.rb CHANGED
@@ -101,6 +101,20 @@ module Quant
101
101
  "#<#{self.class.name} symbol=#{symbol} interval=#{interval} ticks=#{ticks.size}>"
102
102
  end
103
103
 
104
+ # When the first indicator is instantiated, it will also lead to instantiating
105
+ # the dominant cycle indicator. The `new_indicator_lock` prevents reentrant calls
106
+ # to the `new_indicator` method with infinite recursion.
107
+ def new_indicator(indicator)
108
+ return if @new_indicator_lock
109
+
110
+ begin
111
+ @new_indicator_lock = true
112
+ indicators.new_indicator(indicator)
113
+ ensure
114
+ @new_indicator_lock = false
115
+ end
116
+ end
117
+
104
118
  def <<(tick)
105
119
  tick = Ticks::Spot.new(price: tick) if tick.is_a?(Numeric)
106
120
  indicators << tick unless tick.series?
@@ -8,14 +8,16 @@ module Quant
8
8
  # papers and books on the subject of technical analysis by John Ehlers where he variously suggests
9
9
  # a minimum period of 8 or 10 and a max period of 48.
10
10
  #
11
- # The half period is the average of the max_period and min_period.
11
+ # The half period is the average of the max_period and min_period. It is read-only and always computed
12
+ # relative to `min_period` and `max_period`.
13
+ #
12
14
  # The micro period comes from Ehler's writings on Swami charts and auto-correlation computations, which
13
15
  # is a period of 3 bars. It is useful enough in various indicators to be its own setting.
14
16
  #
15
17
  # The dominant cycle kind is the kind of dominant cycle to use in the indicator. The default is +:settings+
16
18
  # which means the dominant cycle is whatever the +max_period+ is set to. It is not adaptive when configured
17
19
  # this way. The other kinds are adaptive and are computed from the series data. The choices are:
18
- # * +:settings+ - the max_period is the dominant cycle and is not adaptive
20
+ # * +:half_period+ - the half_period is the dominant cycle and is not adaptive
19
21
  # * +:band_pass+ - The zero crossings of the band pass filter are used to compute the dominant cycle
20
22
  # * +:auto_correlation_reversal+ - The dominant cycle is computed from the auto-correlation of the series.
21
23
  # * +:homodyne+ - The dominant cycle is computed from the homodyne discriminator.
@@ -48,8 +50,8 @@ module Quant
48
50
  new
49
51
  end
50
52
 
51
- attr_accessor :max_period, :min_period, :half_period, :micro_period
52
- attr_accessor :dominant_cycle_kind, :pivot_kind
53
+ attr_reader :max_period, :min_period, :half_period
54
+ attr_accessor :micro_period, :dominant_cycle_kind, :pivot_kind
53
55
 
54
56
  def initialize(**settings)
55
57
  @max_period = settings[:max_period] || Settings::MAX_PERIOD
@@ -64,14 +66,22 @@ module Quant
64
66
  def apply_settings(**settings)
65
67
  @max_period = settings.fetch(:max_period, @max_period)
66
68
  @min_period = settings.fetch(:min_period, @min_period)
67
- @half_period = settings.fetch(:half_period, @half_period || compute_half_period)
69
+ compute_half_period
68
70
  @micro_period = settings.fetch(:micro_period, @micro_period)
69
71
  @dominant_cycle_kind = settings.fetch(:dominant_cycle_kind, @dominant_cycle_kind)
70
72
  @pivot_kind = settings.fetch(:pivot_kind, @pivot_kind)
71
73
  end
72
74
 
75
+ def max_period=(value)
76
+ (@max_period = value).tap { compute_half_period }
77
+ end
78
+
79
+ def min_period=(value)
80
+ (@min_period = value).tap { compute_half_period }
81
+ end
82
+
73
83
  def compute_half_period
74
- (max_period + min_period) / 2
84
+ @half_period = (max_period + min_period) / 2
75
85
  end
76
86
  end
77
87
  end
@@ -23,7 +23,7 @@ module Quant
23
23
  ).freeze
24
24
 
25
25
  DOMINANT_CYCLE_KINDS = %i(
26
- settings
26
+ half_period
27
27
  band_pass
28
28
  auto_correlation_reversal
29
29
  homodyne
@@ -0,0 +1,37 @@
1
+ module Quant
2
+ module Statistics
3
+ class Correlation
4
+ attr_accessor :length, :sx, :sy, :sxx, :sxy, :syy
5
+
6
+ def initialize
7
+ @length = 0.0
8
+ @sx = 0.0
9
+ @sy = 0.0
10
+ @sxx = 0.0
11
+ @sxy = 0.0
12
+ @syy = 0.0
13
+ end
14
+
15
+ def add(x, y)
16
+ @length += 1
17
+ @sx += x
18
+ @sy += y
19
+ @sxx += x * x
20
+ @sxy += x * y
21
+ @syy += y * y
22
+ end
23
+
24
+ def devisor
25
+ value = (length * sxx - sx**2) * (length * syy - sy**2)
26
+ value.zero? ? 1.0 : value
27
+ end
28
+
29
+ def coefficient
30
+ (length * sxy - sx * sy) / Math.sqrt(devisor)
31
+ rescue Math::DomainError
32
+ 0.0
33
+ end
34
+ end
35
+ end
36
+ end
37
+
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.10"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/quantitative.rb CHANGED
@@ -12,6 +12,6 @@ quant_folder = File.join(lib_folder, "quant")
12
12
  Dir.glob(File.join(quant_folder, "*.rb")).each { |fn| require fn }
13
13
 
14
14
  # require sub-folders and their sub-folders
15
- %w(refinements mixins settings ticks indicators).each do |sub_folder|
15
+ %w(refinements mixins statistics settings ticks indicators).each do |sub_folder|
16
16
  Dir.glob(File.join(quant_folder, sub_folder, "**/*.rb")).each { |fn| require fn }
17
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.10
4
+ version: 0.2.1
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-03-03 00:00:00.000000000 Z
11
+ date: 2024-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -51,9 +51,16 @@ files:
51
51
  - lib/quant/errors.rb
52
52
  - lib/quant/experimental.rb
53
53
  - lib/quant/indicators.rb
54
+ - lib/quant/indicators/dominant_cycle_indicators.rb
55
+ - lib/quant/indicators/dominant_cycles/acr.rb
56
+ - lib/quant/indicators/dominant_cycles/band_pass.rb
57
+ - lib/quant/indicators/dominant_cycles/differential.rb
58
+ - lib/quant/indicators/dominant_cycles/dominant_cycle.rb
59
+ - lib/quant/indicators/dominant_cycles/half_period.rb
60
+ - lib/quant/indicators/dominant_cycles/homodyne.rb
61
+ - lib/quant/indicators/dominant_cycles/phase_accumulator.rb
54
62
  - lib/quant/indicators/indicator.rb
55
63
  - lib/quant/indicators/indicator_point.rb
56
- - lib/quant/indicators/ma.rb
57
64
  - lib/quant/indicators/ping.rb
58
65
  - lib/quant/indicators_proxy.rb
59
66
  - lib/quant/indicators_sources.rb
@@ -76,6 +83,7 @@ files:
76
83
  - lib/quant/series.rb
77
84
  - lib/quant/settings.rb
78
85
  - lib/quant/settings/indicators.rb
86
+ - lib/quant/statistics/correlation.rb
79
87
  - lib/quant/ticks/ohlc.rb
80
88
  - lib/quant/ticks/serializers/ohlc.rb
81
89
  - lib/quant/ticks/serializers/spot.rb