quantitative 0.1.10 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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