quantitative 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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