quantitative 0.2.0 → 0.2.2

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: e3db79bcbb841511c94568e330f60dfb6b68178ae874c56b867c17c538510a01
4
- data.tar.gz: c5da8745035d84023886dcc8fba53f0df473c404b979e084385f6d9a7e19d536
3
+ metadata.gz: 6b5786b75635cde82e7919fce6b8fcd568724b27fc2810efe6db36a7e6dd09cc
4
+ data.tar.gz: 3a6fb86c25bb4f2b1e4994710539bfb528377ef57531e6845a4417c87fc86e29
5
5
  SHA512:
6
- metadata.gz: 23c01e976533a62eddd80d5a774c55d23c644f6b9902392277f8a3450849f46a8dd8ead5588d8cc798cbaa36cce6c7ca7404ed332d4b41d6707982fb8e85c7bc
7
- data.tar.gz: 769c9951b5af3cef2f9a4b93c4a49bd13f33876daa18ba90cf78a00503bfea1e87d8fa4ece964e72f0e15dbc712ef857bf3fa102d438e982d308e00a4c376b9d
6
+ metadata.gz: 5ae30c5b971d6c7bd41e3c2b764e7d95438931790271133dc0e3a476a371affddfdcf06355ffa933240c0c9952fd8fa5e4212c496c95c80f028de7399d3b129f
7
+ data.tar.gz: 9ff8aeaf24800a7208e51f836dd7c7c3621b2e2e3a7f1e8820d89f6fc5cd56dd3c4b762c478d221c6ef7279f62c0df75ae1a19f6a4d34e7ba82e116e6a10a790
data/Gemfile CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- # Specify your gem's dependencies in quantitative.gemspec
5
+ # Specify your gem"s dependencies in quantitative.gemspec
6
6
  gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
@@ -18,7 +18,12 @@ gem "guard-rspec", "~> 4.7"
18
18
  gem "yard", "~> 0.9"
19
19
  gem "benchmark-ips", "~> 2.9"
20
20
 
21
- gem 'rspec-github'
22
- gem 'simplecov'
23
- gem 'simplecov-cobertura'
21
+ gem "rspec-github"
24
22
 
23
+ # Test coverage and profiling
24
+ gem "simplecov"
25
+ gem "simplecov-cobertura"
26
+ gem "stackprof", require: false
27
+ gem "test-prof", require: false
28
+ gem "vernier", require: false
29
+ gem "ruby-prof", require: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.2.0)
4
+ quantitative (0.2.2)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
@@ -107,6 +107,7 @@ GEM
107
107
  rubocop (~> 1.40)
108
108
  rubocop-capybara (~> 2.17)
109
109
  rubocop-factory_bot (~> 2.22)
110
+ ruby-prof (1.7.0)
110
111
  ruby-progressbar (1.13.0)
111
112
  shellany (0.0.1)
112
113
  simplecov (0.22.0)
@@ -118,9 +119,12 @@ GEM
118
119
  simplecov (~> 0.19)
119
120
  simplecov-html (0.12.3)
120
121
  simplecov_json_formatter (0.1.4)
122
+ stackprof (0.2.26)
121
123
  stringio (3.1.0)
124
+ test-prof (1.3.2)
122
125
  thor (1.3.0)
123
126
  unicode-display_width (2.5.0)
127
+ vernier (0.5.1)
124
128
  yard (0.9.34)
125
129
 
126
130
  PLATFORMS
@@ -138,8 +142,12 @@ DEPENDENCIES
138
142
  rspec-github
139
143
  rubocop (~> 1.21)
140
144
  rubocop-rspec
145
+ ruby-prof
141
146
  simplecov
142
147
  simplecov-cobertura
148
+ stackprof
149
+ test-prof
150
+ vernier
143
151
  yard (~> 0.9)
144
152
 
145
153
  BUNDLED WITH
@@ -0,0 +1,52 @@
1
+
2
+ require_relative 'indicators_proxy'
3
+
4
+ module Quant
5
+ # Dominant Cycles measure the primary cycle within a given range. By default, the library
6
+ # is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
7
+ # the `min_period` and `max_period` configuration values in {Quant::Config}.
8
+ #
9
+ # Quant.configure_indicators(min_period: 8, max_period: 32)
10
+ #
11
+ # The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
12
+ # the `dominant_cycle_kind` configuration value in {Quant::Config}.
13
+ #
14
+ # Quant.configure_indicators(dominant_cycle_kind: :band_pass)
15
+ #
16
+ # The purpose of these indicators is to compute the dominant cycle and underpin the various
17
+ # indicators that would otherwise be setting an arbitrary lookback period. This makes the
18
+ # indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
19
+ class DominantCycleIndicators < IndicatorsProxy
20
+ # Auto-Correlation Reversals is a method of computing the dominant cycle
21
+ # by correlating the data stream with itself delayed by a lag.
22
+ def acr; indicator(Indicators::DominantCycles::Acr) end
23
+
24
+ # The band-pass dominant cycle passes signals within a certain frequency
25
+ # range, and attenuates signals outside that range.
26
+ # The trend component of the signal is removed, leaving only the cyclical
27
+ # component. Then we count number of iterations between zero crossings
28
+ # and this is the `period` of the dominant cycle.
29
+ def band_pass; indicator(Indicators::DominantCycles::BandPass) end
30
+
31
+ # Homodyne means the signal is multiplied by itself. More precisely,
32
+ # we want to multiply the signal of the current bar with the complex
33
+ # value of the signal one bar ago
34
+ def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
35
+
36
+ # The Dual Differentiator algorithm computes the phase angle from the
37
+ # analytic signal as the arctangent of the ratio of the imaginary
38
+ # component to the real component. Further, the angular frequency
39
+ # is defined as the rate change of phase. We can use these facts to
40
+ # derive the cycle period.
41
+ def differential; indicator(Indicators::DominantCycles::Differential) end
42
+
43
+ # The phase accumulation method of computing the dominant cycle measures
44
+ # the phase at each sample by taking the arctangent of the ratio of the
45
+ # quadrature component to the in-phase component. The phase is then
46
+ # accumulated and the period is derived from the phase.
47
+ def phase_accumulator; indicator(Indicators::DominantCycles::PhaseAccumulator) end
48
+
49
+ # Static, arbitrarily set period.
50
+ def half_period; indicator(Indicators::DominantCycles::HalfPeriod) end
51
+ end
52
+ end
data/lib/quant/errors.rb CHANGED
@@ -10,6 +10,10 @@ module Quant
10
10
  # {Quant::Interval} with an invalid value.
11
11
  class InvalidInterval < Error; end
12
12
 
13
+ # {InvalidIndicatorSource} is raised when attempting to reference
14
+ # an indicator through a source that has not been prepared, yet.
15
+ class InvalidIndicatorSource < Error; end
16
+
13
17
  # {InvalidResolution} is raised when attempting to instantiate
14
18
  # an {Quant::Resolution} with a resolution value that has not been defined.
15
19
  class InvalidResolution < Error; end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "indicator_point"
4
+ require_relative "indicator"
5
+
6
+ module Quant
7
+ class Indicators
8
+ class AdxPoint < IndicatorPoint
9
+ attribute :dmu, default: 0.0
10
+ attribute :dmd, default: 0.0
11
+ attribute :dmu_ema, default: 0.0
12
+ attribute :dmd_ema, default: 0.0
13
+ attribute :diu, default: 0.0
14
+ attribute :did, default: 0.0
15
+ attribute :di, default: 0.0
16
+ attribute :di_ema, default: 0.0
17
+ attribute :value, default: 0.0
18
+ attribute :inst_stoch, default: 0.0
19
+ attribute :stoch, default: 0.0
20
+ attribute :stoch_up, default: false
21
+ attribute :stoch_turned, default: false
22
+ attribute :ssf, default: 0.0
23
+ attribute :hp, default: 0.0
24
+ end
25
+
26
+ class Adx < Indicator
27
+ def alpha
28
+ bars_to_alpha(dc_period)
29
+ end
30
+
31
+ def scale
32
+ 1.0
33
+ end
34
+
35
+ def period
36
+ dc_period
37
+ end
38
+
39
+ def atr_point
40
+ series.indicators[source].atr.points[t0]
41
+ end
42
+
43
+ def compute
44
+ # To calculate the ADX, first determine the + and - directional movement, or DM.
45
+ # The +DM and -DM are found by calculating the "up-move," or current high minus
46
+ # the previous high, and "down-move," or current low minus the previous low.
47
+ # If the up-move is greater than the down-move and greater than zero, the +DM equals the up-move;
48
+ # otherwise, it equals zero. If the down-move is greater than the up-move and greater than zero,
49
+ # the -DM equals the down-move; otherwise, it equals zero.
50
+ dm_highs = [t0.high_price - t1.high_price, 0.0].max
51
+ dm_lows = [t0.low_price - t1.low_price, 0.0].max
52
+
53
+ p0.dmu = dm_highs > dm_lows ? 0.0 : dm_highs
54
+ p0.dmd = dm_lows > dm_highs ? 0.0 : dm_lows
55
+
56
+ p0.dmu_ema = three_pole_super_smooth :dmu, period:, previous: :dmu_ema
57
+ p0.dmd_ema = three_pole_super_smooth :dmd, period:, previous: :dmd_ema
58
+
59
+ atr_value = atr_point.fast * scale
60
+ return if atr_value == 0.0 || @points.size < period
61
+
62
+ # The positive directional indicator, or +DI, equals 100 times the EMA of +DM divided by the ATR
63
+ # over a given number of time periods. Welles usually used 14 periods.
64
+ # The negative directional indicator, or -DI, equals 100 times the EMA of -DM divided by the ATR.
65
+ p0.diu = (100.0 * p0.dmu_ema) / atr_value
66
+ p0.did = (100.0 * p0.dmd_ema) / atr_value
67
+
68
+ # The ADX indicator itself equals 100 times the EMA of the absolute value of (+DI minus -DI)
69
+ # divided by (+DI plus -DI).
70
+ delta = p0.diu + p0.did
71
+ p0.di = (p0.diu - p1.did).abs / delta
72
+ p0.di_ema = three_pole_super_smooth(:di, period:, previous: :di_ema).clamp(-10.0, 10.0)
73
+
74
+ p0.value = p0.di_ema
75
+ p0.inst_stoch = stochastic :di, period: dc_period
76
+ p0.stoch = three_pole_super_smooth :inst_stoch, period:, previous: :stoch
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "indicator_point"
4
+ require_relative "indicator"
5
+
6
+ module Quant
7
+ class Indicators
8
+ class AtrPoint < IndicatorPoint
9
+ attribute :tr, default: 0.0
10
+ attribute :period, default: :min_period
11
+ attribute :value, default: 0.0
12
+ attribute :slow, default: 0.0
13
+ attribute :fast, default: 0.0
14
+ attribute :inst_stoch, default: 0.0
15
+ attribute :stoch, default: 0.0
16
+ attribute :stoch_up, default: false
17
+ attribute :stoch_turned, default: false
18
+ attribute :osc, default: 0.0
19
+ attribute :crossed, default: :unchanged
20
+
21
+ def crossed_up?
22
+ @crossed == :up
23
+ end
24
+
25
+ def crossed_down?
26
+ @crossed == :down
27
+ end
28
+ end
29
+
30
+ class Atr < Indicator
31
+ attr_reader :points
32
+
33
+ def period
34
+ dc_period / 2
35
+ end
36
+
37
+ def fast_alpha
38
+ period_to_alpha(period)
39
+ end
40
+
41
+ def slow_alpha
42
+ period_to_alpha(2 * period)
43
+ end
44
+
45
+ # Typically, the Average True Range (ATR) is based on 14 periods and can be calculated on an intraday, daily, weekly
46
+ # or monthly basis. For this example, the ATR will be based on daily data. Because there must be a beginning, the first
47
+ # TR value is simply the High minus the Low, and the first 14-day ATR is the average of the daily TR values for the
48
+ # last 14 days. After that, Wilder sought to smooth the data by incorporating the previous period's ATR value.
49
+
50
+ # Current ATR = [(Prior ATR x 13) + Current TR] / 14
51
+
52
+ # - Multiply the previous 14-day ATR by 13.
53
+ # - Add the most recent day's TR value.
54
+ # - Divide the total by 14
55
+
56
+ def compute
57
+ p0.period = period
58
+ p0.tr = (t1.high_price - t0.close_price).abs
59
+
60
+ p0.value = three_pole_super_smooth :tr, period:, previous: :value
61
+
62
+ p0.slow = (slow_alpha * p0.value) + ((1.0 - slow_alpha) * p1.slow)
63
+ p0.fast = (fast_alpha * p0.value) + ((1.0 - fast_alpha) * p1.fast)
64
+
65
+ p0.inst_stoch = stochastic :value, period:
66
+ p0.stoch = three_pole_super_smooth(:inst_stoch, previous: :stoch, period:).clamp(0, 100)
67
+ p0.stoch_up = p0.stoch >= 70
68
+ p0.stoch_turned = p0.stoch_up && !p1.stoch_up
69
+ compute_oscillator
70
+ end
71
+
72
+ def compute_oscillator
73
+ p0.osc = p0.value - wma(:value)
74
+ p0.crossed = :up if p0.osc >= 0 && p1.osc < 0
75
+ p0.crossed = :down if p0.osc <= 0 && p1.osc > 0
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class Indicators
5
+ class CciPoint < IndicatorPoint
6
+ attribute :hp, default: 0.0
7
+ attribute :real, default: 0.0
8
+ attribute :imag, default: 0.0
9
+ attribute :angle, default: 0.0
10
+ attribute :state, default: 0
11
+ end
12
+
13
+ # Correlation Cycle Index
14
+ # The very definition of a trend mode and a cycle mode makes it simple
15
+ # to create a state variable that identifies the market state. If the
16
+ # state is zero, the market is in a cycle mode. If the state is +1 the
17
+ # market is in a trend up. If the state is -1 the market is in a trend down.
18
+ #
19
+ # SOURCE: https://www.mesasoftware.com/papers/CORRELATION%20AS%20A%20CYCLE%20INDICATOR.pdf
20
+ class Cci < Indicator
21
+ def max_period
22
+ [min_period, dc_period].max
23
+ end
24
+
25
+ def compute_correlations
26
+ corr_real = Statistics::Correlation.new
27
+ corr_imag = Statistics::Correlation.new
28
+ arc = 2.0 * Math::PI / max_period.to_f
29
+ (0...max_period).each do |period|
30
+ radians = arc * period
31
+ prev_hp = p(period).hp
32
+ corr_real.add(prev_hp, Math.cos(radians))
33
+ corr_imag.add(prev_hp, -Math.sin(radians))
34
+ end
35
+ p0.real = corr_real.coefficient
36
+ p0.imag = corr_imag.coefficient
37
+ end
38
+
39
+ def compute_angle
40
+ # Compute the angle as an arctangent and resolve the quadrant
41
+ p0.angle = 90 + rad2deg(Math.atan(p0.real / p0.imag))
42
+ p0.angle -= 180 if p0.imag > 0
43
+
44
+ # Do not allow the rate change of angle to go negative
45
+ p0.angle = p1.angle if (p0.angle < p1.angle) && (p1.angle - p0.angle) < 270
46
+ end
47
+
48
+ def compute_state
49
+ return unless (p0.angle - p1.angle).abs < 9
50
+
51
+ p0.state = p0.angle < 0 ? -1 : 1
52
+ end
53
+
54
+ def compute
55
+ p0.hp = two_pole_butterworth :input, previous: :hp, period: min_period
56
+
57
+ compute_correlations
58
+ compute_angle
59
+ compute_state
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class Indicators
5
+ # The decycler oscillator can be useful for determining the transition be- tween uptrends and downtrends by the crossing of the zero
6
+ # line. Alternatively, the changes of slope of the decycler oscillator are easier to identify than the changes in slope of the
7
+ # original decycler. Optimum cutoff periods can easily be found by experimentation.
8
+ #
9
+ # 1. A decycler filter functions the same as a low-pass filter.
10
+ # 2. A decycler filter is created by subtracting the output of a high-pass filter from the input, thereby removing the
11
+ # high-frequency components by cancellation.
12
+ # 3. A decycler filter has very low lag.
13
+ # 4. A decycler oscillator is created by subtracting the output of a high-pass filter having a shorter cutoff period from the
14
+ # output of another high-pass filter having a longer cutoff period.
15
+ # 5. A decycler oscillator shows transitions between uptrends and down-trends at the zero crossings.
16
+ class DecyclerPoint < IndicatorPoint
17
+ attr_accessor :decycle, :hp1, :hp2, :osc, :peak, :agc, :ift
18
+
19
+ def to_h
20
+ {
21
+ "dc" => decycle,
22
+ "hp1" => hp1,
23
+ "hp2" => hp2,
24
+ "osc" => osc,
25
+ }
26
+ end
27
+
28
+ def initialize_data_points(indicator:)
29
+ @decycle = oc2
30
+ @hp1 = 0.0
31
+ @hp2 = 0.0
32
+ @osc = 0.0
33
+ @peak = 0.0
34
+ @agc = 0.0
35
+ end
36
+ end
37
+
38
+ class Decycler < Indicator
39
+ def max_period
40
+ dc_period
41
+ end
42
+
43
+ def min_period
44
+ settings.min_period
45
+ end
46
+
47
+ def compute_decycler
48
+ alpha = period_to_alpha(max_period)
49
+ p0.decycle = (alpha / 2) * (p0.oc2 + p1.oc2) + (1.0 - alpha) * p1.decycle
50
+ end
51
+
52
+ # alpha1 = (Cosine(.707*360 / HPPeriod1) + Sine (.707*360 / HPPeriod1) - 1) / Cosine(.707*360 / HPPeriod1);
53
+ # HP1 = (1 - alpha1 / 2)*(1 - alpha1 / 2)*(Close - 2*Close[1] + Close[2]) + 2*(1 - alpha1)*HP1[1] - (1 - alpha1)*(1 - alpha1)*HP1[2];
54
+ def compute_hp(period, hp)
55
+ radians = deg2rad(360)
56
+ c = Math.cos(0.707 * radians / period)
57
+ s = Math.sin(0.707 * radians / period)
58
+ alpha = (c + s - 1) / c
59
+ (1 - alpha / 2)**2 * (p0.oc2 - 2 * p1.oc2 + p2.oc2) + 2 * (1 - alpha) * p1.send(hp) - (1 - alpha) * (1 - alpha) * p2.send(hp)
60
+ end
61
+
62
+ def compute_oscillator
63
+ p0.hp1 = compute_hp(min_period, :hp1)
64
+ p0.hp2 = compute_hp(max_period, :hp2)
65
+ p0.osc = p0.hp2 - p0.hp1
66
+ end
67
+
68
+ def compute_agc
69
+ p0.peak = [p0.osc.abs, 0.991 * p1.peak].max
70
+ p0.agc = p0.peak.zero? ? p0.osc : p0.osc / p0.peak
71
+ end
72
+
73
+ def compute_ift
74
+ p0.ift = ift(p0.agc, 5.0)
75
+ end
76
+
77
+ def compute
78
+ compute_decycler
79
+ compute_oscillator
80
+ compute_agc
81
+ compute_ift
82
+ end
83
+ end
84
+ end
85
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../indicator_point"
2
4
  require_relative "dominant_cycle"
3
5
 
@@ -22,20 +24,20 @@ module Quant
22
24
  attribute :reversal, default: false
23
25
  end
24
26
 
25
- # Auto-Correlation Reversals
27
+ # Auto-Correlation Reversals is a method of computing the dominant cycle
28
+ # by correlating the data stream with itself delayed by a lag.
29
+ # Construction of the autocorrelation periodogram starts with the
30
+ # autocorrelation function using the minimum three bars of averaging.
31
+ # The cyclic information is extracted using a discrete Fourier transform
32
+ # (DFT) of the autocorrelation results.
26
33
  class Acr < DominantCycle
27
- def average_length
28
- 3 # AvgLength
29
- end
30
-
31
- def bandwidth
32
- deg2rad(370)
33
- end
34
+ BANDWIDTH_DEGREES = 370
35
+ BANDWIDTH_RADIANS = BANDWIDTH_DEGREES * Math::PI / 180.0
34
36
 
35
37
  def compute_auto_correlations
36
38
  (min_period..max_period).each do |period|
37
39
  corr = Statistics::Correlation.new
38
- average_length.times do |lookback_period|
40
+ micro_period.times do |lookback_period|
39
41
  corr.add(p(lookback_period).filter, p(period + lookback_period).filter)
40
42
  end
41
43
  p0.corr[period] = corr.coefficient
@@ -46,8 +48,8 @@ module Quant
46
48
  p0.maxpwr = 0.995 * p1.maxpwr
47
49
 
48
50
  (min_period..max_period).each do |period|
49
- (average_length..max_period).each do |n|
50
- radians = bandwidth * n / period
51
+ (micro_period..max_period).each do |n|
52
+ radians = BANDWIDTH_RADIANS * n / period
51
53
  p0.cospart[period] += p0.corr[n] * Math.cos(radians)
52
54
  p0.sinpart[period] += p0.corr[n] * Math.sin(radians)
53
55
  end
@@ -98,4 +100,4 @@ module Quant
98
100
  end
99
101
  end
100
102
  end
101
- end
103
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "dominant_cycle"
2
4
 
3
5
  module Quant
@@ -14,6 +16,11 @@ module Quant
14
16
  attribute :direction, default: :flat
15
17
  end
16
18
 
19
+ # The band-pass dominant cycle passes signals within a certain frequency
20
+ # range, and attenuates signals outside that range.
21
+ # The trend component of the signal is revoved, leaving only the cyclical
22
+ # component. Then we count number of iterations between zero crossings
23
+ # and this is the `period` of the dominant cycle.
17
24
  class BandPass < DominantCycle
18
25
  def bandwidth
19
26
  0.75
@@ -1,9 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quant
2
4
  class Indicators
3
5
  class DominantCycles
4
- # The Dual Differentiator algorithm computes the phase angle from the analytic signal as the arctangent of
5
- # the ratio of the imaginary component to the real compo- nent. Further, the angular frequency is defined
6
- # as the rate change of phase. We can use these facts to derive the cycle period.
6
+ # The Dual Differentiator algorithm computes the phase angle from the
7
+ # analytic signal as the arctangent of the ratio of the imaginary
8
+ # component to the real component. Further, the angular frequency
9
+ # is defined as the rate change of phase. We can use these facts to
10
+ # derive the cycle period.
7
11
  class Differential < DominantCycle
8
12
  def compute_period
9
13
  p0.ddd = (p0.q2 * (p0.i2 - p1.i2)) - (p0.i2 * (p0.q2 - p1.q2))
@@ -16,4 +20,4 @@ module Quant
16
20
  end
17
21
  end
18
22
  end
19
- end
23
+ end
@@ -1,7 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../indicator"
2
4
 
3
5
  module Quant
4
6
  class Indicators
7
+ # Dominant Cycles measure the primary cycle within a given range. By default, the library
8
+ # is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
9
+ # the `min_period` and `max_period` configuration values in {Quant::Config}.
10
+ #
11
+ # Quant.configure_indicators(min_period: 8, max_period: 32)
12
+ #
13
+ # The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
14
+ # the `dominant_cycle_kind` configuration value in {Quant::Config}.
15
+ #
16
+ # Quant.configure_indicators(dominant_cycle_kind: :band_pass)
17
+ #
18
+ # The purpose of these indicators is to compute the dominant cycle and underpin the various
19
+ # indicators that would otherwise be setting an arbitrary lookback period. This makes the
20
+ # indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
5
21
  class DominantCycles
6
22
  class DominantCyclePoint < Quant::Indicators::IndicatorPoint
7
23
  attribute :smooth, default: 0.0
@@ -39,6 +55,8 @@ module Quant
39
55
  p0.inst_period = p0.inst_period.clamp(min_period, max_period)
40
56
  end
41
57
 
58
+ attr_reader :points
59
+
42
60
  # constrain magnitude of change in phase
43
61
  def constrain_period_magnitude_change
44
62
  p0.inst_period = [1.5 * p1.inst_period, p0.inst_period].min
@@ -60,11 +78,6 @@ module Quant
60
78
  [p0.period.to_i, min_period].max
61
79
  end
62
80
 
63
- def period_points(max_period)
64
- extent = [values.size, max_period].min
65
- values[-extent, extent]
66
- end
67
-
68
81
  def compute
69
82
  compute_input_data_points
70
83
  compute_quadrature_components
@@ -125,4 +138,4 @@ module Quant
125
138
  end
126
139
  end
127
140
  end
128
- end
141
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dominant_cycle"
4
+
5
+ module Quant
6
+ class Indicators
7
+ class DominantCycles
8
+ # This dominant cycle indicator is based on the half period
9
+ # that is the midpoint of the `min_period` and `max_period`
10
+ # configured in the `Quant.config.indicators` object.
11
+ # Effectively providing a static, arbitrarily set period.
12
+ class HalfPeriodPoint < Quant::Indicators::IndicatorPoint
13
+ attribute :period, default: :half_period
14
+ end
15
+
16
+ class HalfPeriod < DominantCycle
17
+ def compute
18
+ # No-Op
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,11 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../indicator_point"
2
4
  require_relative "dominant_cycle"
3
5
 
4
6
  module Quant
5
7
  class Indicators
6
8
  class DominantCycles
7
- # Homodyne means the signal is multiplied by itself. More precisely, we want to multiply the signal
8
- # of the current bar with the complex value of the signal one bar ago
9
+ # Homodyne means the signal is multiplied by itself. More precisely,
10
+ # we want to multiply the signal of the current bar with the complex
11
+ # value of the signal one bar ago
9
12
  class Homodyne < DominantCycle
10
13
  def compute_period
11
14
  p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
@@ -14,7 +17,7 @@ module Quant
14
17
  p0.re = (0.2 * p0.re) + (0.8 * p1.re)
15
18
  p0.im = (0.2 * p0.im) + (0.8 * p1.im)
16
19
 
17
- p0.inst_period = 360.0 / rad2deg(Math.atan(p0.im/p0.re)) if (p0.im != 0) && (p0.re != 0)
20
+ p0.inst_period = 360.0 / rad2deg(Math.atan(p0.im / p0.re)) if (p0.im != 0) && (p0.re != 0)
18
21
 
19
22
  constrain_period_magnitude_change
20
23
  constrain_period_bars
@@ -24,4 +27,4 @@ module Quant
24
27
  end
25
28
  end
26
29
  end
27
- end
30
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "dominant_cycle"
2
4
 
3
5
  module Quant
@@ -8,7 +10,7 @@ module Quant
8
10
  # at each sample by taking the arctangent of the ratio of the quadrature
9
11
  # component to the in-phase component. A delta phase is generated by
10
12
  # taking the difference of the phase between successive samples.
11
- # At each sam- ple we can then look backwards, adding up the delta
13
+ # At each sample we can then look backwards, adding up the delta
12
14
  # phases. When the sum of the delta phases reaches 360 degrees,
13
15
  # we must have passed through one full cycle, on average. The process
14
16
  # is repeated for each new sample.
@@ -30,10 +32,12 @@ module Quant
30
32
 
31
33
  p0.accumulator_phase = Math.atan(p0.q1 / p0.i1) unless p0.i1.zero?
32
34
 
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
35
+ if p0.i1 < 0 && p0.q1 > 0
36
+ p0.accumulator_phase = 180.0 - p0.accumulator_phase
37
+ elsif p0.i1 < 0 && p0.q1 < 0
38
+ p0.accumulator_phase = 180.0 + p0.accumulator_phase
39
+ elsif p0.i1 > 0 && p0.q1 < 0
40
+ p0.accumulator_phase = 360.0 - p0.accumulator_phase
37
41
  end
38
42
 
39
43
  p0.delta_phase = p1.accumulator_phase - p0.accumulator_phase
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class Indicators
5
+ class FramaPoint < IndicatorPoint
6
+ attribute :frama, default: :input
7
+ end
8
+
9
+ # FRAMA (FRactal Adaptive Moving Average). A nonlinear moving average
10
+ # is derived using the Hurst exponent. It rapidly follows significant
11
+ # changes in price but becomes very flat in congestion zones so that
12
+ # bad whipsaw trades can be eliminated.
13
+ #
14
+ # SOURCE: http://www.mesasoftware.com/papers/FRAMA.pdf
15
+ class Frama < Indicator
16
+ using Quant
17
+
18
+ # The max_period is divided into two smaller, equal periods, so must be even
19
+ def max_period
20
+ @max_period ||= begin
21
+ mp = super
22
+ mp.even? ? mp : mp + 1
23
+ end
24
+ end
25
+
26
+ # def max_period
27
+ # mp = dc_period
28
+ # mp.even? ? mp : mp + 1
29
+ # end
30
+
31
+ def half_period
32
+ max_period / 2
33
+ end
34
+
35
+ def compute
36
+ pp = period_points(max_period).map(&:input)
37
+ return if pp.size < max_period
38
+
39
+ n3 = (pp.maximum - pp.minimum) / max_period
40
+
41
+ ppn2 = pp.first(half_period)
42
+ n2 = (ppn2.maximum - ppn2.minimum) / half_period
43
+
44
+ ppn1 = pp.last(half_period)
45
+ n1 = (ppn1.maximum - ppn1.minimum) / half_period
46
+
47
+ dimension = (Math.log(n1 + n2) - Math.log(n3)) / Math.log(2)
48
+ alpha = Math.exp(-4.6 * (dimension - 1.0)).clamp(0.01, 1.0)
49
+ p0.frama = (alpha * p0.input) + ((1 - alpha) * p1.frama)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -19,6 +19,7 @@ module Quant
19
19
  @series = series
20
20
  @source = source
21
21
  @points = {}
22
+ series.new_indicator(self)
22
23
  series.each { |tick| self << tick }
23
24
  end
24
25
 
@@ -46,6 +47,14 @@ module Quant
46
47
  Quant.config.indicators.pivot_kind
47
48
  end
48
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
+
49
58
  def ticks
50
59
  @points.keys
51
60
  end
@@ -62,6 +71,11 @@ module Quant
62
71
  @points.size
63
72
  end
64
73
 
74
+ def period_points(max_period)
75
+ extent = [values.size, max_period].min
76
+ values[-extent, extent]
77
+ end
78
+
65
79
  attr_reader :p0, :p1, :p2, :p3
66
80
  attr_reader :t0, :t1, :t2, :t3
67
81
 
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class Indicators
5
+ class MamaPoint < IndicatorPoint
6
+ attribute :smooth, default: 0.0
7
+ attribute :detrend, default: 0.0
8
+ attribute :re, default: 0.0
9
+ attribute :im, default: 0.0
10
+ attribute :i1, default: 0.0
11
+ attribute :q1, default: 0.0
12
+ attribute :ji, default: 0.0
13
+ attribute :jq, default: 0.0
14
+ attribute :i2, default: 0.0
15
+ attribute :q2, default: 0.0
16
+ attribute :period, default: :min_period
17
+ attribute :smooth_period, default: :min_period
18
+ attribute :mama, default: :input
19
+ attribute :fama, default: :input
20
+ attribute :gama, default: :input
21
+ attribute :dama, default: :input
22
+ attribute :lama, default: :input
23
+ attribute :faga, default: :input
24
+ attribute :phase, default: 0.0
25
+ attribute :delta_phase, default: 0.0
26
+ attribute :osc, default: 0.0
27
+ attribute :crossed, default: :unchanged
28
+
29
+ def crossed_up?
30
+ @crossed == :up
31
+ end
32
+
33
+ def crossed_down?
34
+ @crossed == :down
35
+ end
36
+ end
37
+
38
+ # https://www.mesasoftware.com/papers/MAMA.pdf
39
+ # MESA Adaptive Moving Average (MAMA) adapts to price movement in an
40
+ # entirely new and unique way. The adapation is based on the rate change
41
+ # of phase as measured by the Hilbert Transform Discriminator.
42
+ #
43
+ # This version of Ehler's MAMA indicator duplicates the computations
44
+ # present in the homodyne version of the dominant cycle indicator.
45
+ # Use this version of the indicator when you're using a different
46
+ # dominant cycle indicator other than the homodyne for the rest
47
+ # of your indicators.
48
+ class Mama < Indicator
49
+ # constrain between 6 and 50 bars
50
+ def constrain_period_bars
51
+ p0.period = p0.period.clamp(min_period, max_period)
52
+ end
53
+
54
+ # constrain magnitude of change in phase
55
+ def constrain_period_magnitude_change
56
+ p0.period = [1.5 * p1.period, p0.period].min
57
+ p0.period = [0.67 * p1.period, p0.period].max
58
+ end
59
+
60
+ # amplitude correction using previous period value
61
+ def compute_smooth_period
62
+ p0.period = ((0.2 * p0.period) + (0.8 * p1.period)).round
63
+ p0.smooth_period = ((0.33333 * p0.period) + (0.666667 * p1.smooth_period)).round
64
+ end
65
+
66
+ def homodyne_discriminator
67
+ p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
68
+ p0.im = (p0.i2 * p1.q2) - (p0.q2 * p1.i2)
69
+
70
+ p0.re = (0.2 * p0.re) + (0.8 * p1.re)
71
+ p0.im = (0.2 * p0.im) + (0.8 * p1.im)
72
+
73
+ p0.period = 360.0 / rad2deg(Math.atan(p0.im / p0.re)) if (p0.im != 0) && (p0.re != 0)
74
+
75
+ constrain_period_magnitude_change
76
+ constrain_period_bars
77
+ compute_smooth_period
78
+ end
79
+
80
+ def compute_dominant_cycle
81
+ p0.smooth = wma :input
82
+ p0.detrend = hilbert_transform :smooth, period: p1.period
83
+
84
+ # { Compute Inphase and Quadrature components }
85
+ p0.q1 = hilbert_transform :detrend, period: p1.period
86
+ p0.i1 = p3.detrend
87
+
88
+ # { Advance the phase of I1 and Q1 by 90 degrees }
89
+ p0.ji = hilbert_transform :i1, period: p1.period
90
+ p0.jq = hilbert_transform :q1, period: p1.period
91
+
92
+ # { Smooth the I and Q components before applying the discriminator }
93
+ p0.i2 = (0.2 * (p0.i1 - p0.jq)) + 0.8 * (p1.i2 || (p0.i1 - p0.jq))
94
+ p0.q2 = (0.2 * (p0.q1 + p0.ji)) + 0.8 * (p1.q2 || (p0.q1 + p0.ji))
95
+
96
+ homodyne_discriminator
97
+ end
98
+
99
+ def fast_limit
100
+ @fast_limit ||= bars_to_alpha(min_period / 2)
101
+ end
102
+
103
+ def slow_limit
104
+ @slow_limit ||= bars_to_alpha(max_period)
105
+ end
106
+
107
+ def compute_dominant_cycle_phase
108
+ p0.delta_phase = p1.phase - p0.phase
109
+ p0.delta_phase = 1.0 if p0.delta_phase < 1.0
110
+ end
111
+
112
+ FAMA = 0.500
113
+ GAMA = 0.950
114
+ DAMA = 0.125
115
+ LAMA = 0.100
116
+ FAGA = 0.050
117
+
118
+ def compute_moving_averages
119
+ alpha = [fast_limit / p0.delta_phase, slow_limit].max
120
+ p0.mama = (alpha * p0.input) + ((1.0 - alpha) * p1.mama)
121
+
122
+ p0.fama = (FAMA * alpha * p0.mama) + ((1.0 - (FAMA * alpha)) * p1.fama)
123
+ p0.gama = (GAMA * alpha * p0.mama) + ((1.0 - (GAMA * alpha)) * p1.gama)
124
+ p0.dama = (DAMA * alpha * p0.mama) + ((1.0 - (DAMA * alpha)) * p1.dama)
125
+ p0.lama = (LAMA * alpha * p0.mama) + ((1.0 - (LAMA * alpha)) * p1.lama)
126
+ p0.faga = (FAGA * alpha * p0.fama) + ((1.0 - (FAGA * alpha)) * p1.faga)
127
+ end
128
+
129
+ def compute_oscillator
130
+ p0.osc = p0.mama - p0.fama
131
+ p0.crossed = :up if p0.osc >= 0 && p1.osc < 0
132
+ p0.crossed = :down if p0.osc <= 0 && p1.osc > 0
133
+ end
134
+
135
+ def compute
136
+ compute_dominant_cycle
137
+ compute_dominant_cycle_phase
138
+ compute_moving_averages
139
+ compute_oscillator
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ class Indicators
5
+ # The MESA inidicator
6
+ class MesaPoint < IndicatorPoint
7
+ attribute :mama, default: :input
8
+ attribute :fama, default: :input
9
+ attribute :dama, default: :input
10
+ attribute :gama, default: :input
11
+ attribute :lama, default: :input
12
+ attribute :faga, default: :input
13
+ attribute :osc, default: 0.0
14
+ attribute :crossed, default: :unchanged
15
+
16
+ def crossed_up?
17
+ @crossed == :up
18
+ end
19
+
20
+ def crossed_down?
21
+ @crossed == :down
22
+ end
23
+ end
24
+
25
+ # https://www.mesasoftware.com/papers/MAMA.pdf
26
+ # MESA Adaptive Moving Average (MAMA) adapts to price movement in an
27
+ # entirely new and unique way. The adapation is based on the rate change
28
+ # of phase as measured by the Hilbert Transform Discriminator.
29
+ #
30
+ # This version of Ehler's MAMA indicator ties into the homodyne
31
+ # dominant cycle indicator to provide a more efficient computation
32
+ # for this indicator. If you're using the homodyne in all your
33
+ # indicators for the dominant cycle, then this version is useful
34
+ # as it avoids extra computational steps.
35
+ class Mesa < Indicator
36
+ def period
37
+ dc_period
38
+ end
39
+
40
+ def fast_limit
41
+ @fast_limit ||= bars_to_alpha(min_period / 2)
42
+ end
43
+
44
+ def slow_limit
45
+ @slow_limit ||= bars_to_alpha(max_period)
46
+ end
47
+
48
+ def homodyne_dominant_cycle
49
+ series.indicators[source].dominant_cycles.homodyne
50
+ end
51
+
52
+ def current_dominant_cycle
53
+ homodyne_dominant_cycle.points[t0]
54
+ end
55
+
56
+ def delta_phase
57
+ current_dominant_cycle.delta_phase
58
+ end
59
+
60
+ FAMA = 0.500
61
+ GAMA = 0.950
62
+ DAMA = 0.125
63
+ LAMA = 0.100
64
+ FAGA = 0.050
65
+
66
+ def compute
67
+ alpha = [fast_limit / delta_phase, slow_limit].max
68
+
69
+ p0.mama = (alpha * p0.input) + ((1.0 - alpha) * p1.mama)
70
+ p0.fama = (FAMA * alpha * p0.mama) + ((1.0 - (FAMA * alpha)) * p1.fama)
71
+ p0.gama = (GAMA * alpha * p0.mama) + ((1.0 - (GAMA * alpha)) * p1.gama)
72
+ p0.dama = (DAMA * alpha * p0.mama) + ((1.0 - (DAMA * alpha)) * p1.dama)
73
+ p0.lama = (LAMA * alpha * p0.mama) + ((1.0 - (LAMA * alpha)) * p1.lama)
74
+ p0.faga = (FAGA * alpha * p0.fama) + ((1.0 - (FAGA * alpha)) * p1.faga)
75
+
76
+ compute_oscillator
77
+ end
78
+
79
+ def compute_oscillator
80
+ p0.osc = p0.mama - p0.fama
81
+ p0.crossed = :up if p0.osc >= 0 && p1.osc < 0
82
+ p0.crossed = :down if p0.osc <= 0 && p1.osc > 0
83
+ end
84
+ end
85
+ end
86
+ end
@@ -6,9 +6,13 @@ module Quant
6
6
  # used outside those shipped with the library.
7
7
  class Indicators < IndicatorsProxy
8
8
  def ping; indicator(Indicators::Ping) end
9
+ def adx; indicator(Indicators::Adx) end
10
+ def atr; indicator(Indicators::Atr) end
11
+ def mesa; indicator(Indicators::Mesa) end
12
+ def mama; indicator(Indicators::MAMA) end
9
13
 
10
14
  def dominant_cycles
11
- @dominant_cycles ||= Indicators::DominantCycleIndicators.new(series:, source:)
15
+ @dominant_cycles ||= Quant::DominantCycleIndicators.new(series:, source:)
12
16
  end
13
17
  end
14
18
  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
 
@@ -7,6 +7,16 @@ 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
@@ -16,7 +16,7 @@ module Quant
16
16
  def stochastic(source, period:)
17
17
  subset = values.last(period).map{ |p| p.send(source) }
18
18
 
19
- lowest, highest = subset.minimum, subset.maximum
19
+ lowest, highest = subset.minimum.to_f, subset.maximum.to_f
20
20
  return 0.0 if (highest - lowest).zero?
21
21
 
22
22
  100.0 * (subset[-1] - lowest) / (highest - lowest)
@@ -2,8 +2,17 @@
2
2
 
3
3
  module Quant
4
4
  module Mixins
5
+ # Super Smoother Filters provide a way to smooth out the noise in a series
6
+ # without out introducing undesirable lag that you would other get with
7
+ # traditional moving averages.
8
+ #
9
+ # The EMA only reduces the amplitude at the Nyquist frequency by 13 dB.
10
+ # On the other hand, the SuperSmoother filter theoretically completely
11
+ # eliminates components at the Nyquist Frequency. The added benefit is
12
+ # that the SuperSmoother filter has significantly less lag than the EMA.
5
13
  module SuperSmoother
6
- def two_pole_super_smooth(source, period:, previous: :ss)
14
+ # https://www.mesasoftware.com/papers/PredictiveIndicators.pdf
15
+ def two_pole_super_smooth(source, period:, previous:)
7
16
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
8
17
 
9
18
  radians = Math.sqrt(2) * Math::PI / period
@@ -23,7 +32,7 @@ module Quant
23
32
  alias super_smoother two_pole_super_smooth
24
33
  alias ss2p two_pole_super_smooth
25
34
 
26
- def three_pole_super_smooth(source, period:, previous: :ss)
35
+ def three_pole_super_smooth(source, period:, previous:)
27
36
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
28
37
 
29
38
  radians = Math::PI / period
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
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.2.0"
4
+ VERSION = "0.2.2"
5
5
  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.2.0
4
+ version: 0.2.2
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-11 00:00:00.000000000 Z
11
+ date: 2024-03-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -48,18 +48,26 @@ files:
48
48
  - lib/quant/asset_class.rb
49
49
  - lib/quant/attributes.rb
50
50
  - lib/quant/config.rb
51
+ - lib/quant/dominant_cycle_indicators.rb
51
52
  - lib/quant/errors.rb
52
53
  - lib/quant/experimental.rb
53
54
  - lib/quant/indicators.rb
54
- - lib/quant/indicators/dominant_cycle_indicators.rb
55
+ - lib/quant/indicators/adx.rb
56
+ - lib/quant/indicators/atr.rb
57
+ - lib/quant/indicators/cci.rb
58
+ - lib/quant/indicators/decycler.rb
55
59
  - lib/quant/indicators/dominant_cycles/acr.rb
56
60
  - lib/quant/indicators/dominant_cycles/band_pass.rb
57
61
  - lib/quant/indicators/dominant_cycles/differential.rb
58
62
  - lib/quant/indicators/dominant_cycles/dominant_cycle.rb
63
+ - lib/quant/indicators/dominant_cycles/half_period.rb
59
64
  - lib/quant/indicators/dominant_cycles/homodyne.rb
60
65
  - lib/quant/indicators/dominant_cycles/phase_accumulator.rb
66
+ - lib/quant/indicators/frama.rb
61
67
  - lib/quant/indicators/indicator.rb
62
68
  - lib/quant/indicators/indicator_point.rb
69
+ - lib/quant/indicators/mama.rb
70
+ - lib/quant/indicators/mesa.rb
63
71
  - lib/quant/indicators/ping.rb
64
72
  - lib/quant/indicators_proxy.rb
65
73
  - lib/quant/indicators_sources.rb
@@ -1,10 +0,0 @@
1
- module Quant
2
- class DominantCycleIndicators < IndicatorsProxy
3
- def acr; indicator(Indicators::DominantCycles::Acr) end
4
- def band_pass; indicator(Indicators::DominantCycles::BandPass) end
5
- def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
6
-
7
- def differential; indicator(Indicators::DominantCycles::Differential) end
8
- def phase_accumulator; indicator(Indicators::DominantCycles::PhaseAccumulator) end
9
- end
10
- end