quantitative 0.1.10 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50dde4546ee2c241a00695bf5928fb76c92a7323e8b1a2d5d0090db74bbebee5
4
- data.tar.gz: f6432d642ac5111cd4fe0cb6b041ada60af39ba41033195c949b0126184ae180
3
+ metadata.gz: bf0aa73684247efc7bc9750c27c856a094fa362d99c1eba73bd70457da8f3a97
4
+ data.tar.gz: 56dcaa82230328cb44149ceb957420484217e33c6f601ea135997391bfb95440
5
5
  SHA512:
6
- metadata.gz: d48777cee5009e48b5d3ef6c531029fdaa8c9629b37223ad919aacd53c29caf13258d7cddc28413788b61ec086af0abde606996cdac3c7012565ec2cea5b6e77
7
- data.tar.gz: 8c9fa61a0f4b4c21b890c6895d4e74ba870fb92336cd30bb822e13bad5e49e9ee8d4b45e639171a73ab8c04e1756088e79979231a557a41ad9effa8ee1660e6b
6
+ metadata.gz: b674a5e4408a69039cf2c7ad2378d065e3f88b409a7530108af1bfc5a186a02690cba8ffb1c8d40a87ad69368f302d9eec0211135beec7db6a9c51c4e1be46d3
7
+ data.tar.gz: e7d7693cd878ebe777f81ca0d90b4f305e645c8547aa9f420cccb4a4f9b53920211a2b7f49af146508756e6e38f5cc7e0c4b57f1c8abdd722f65bd3409abd02f
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.1.10)
4
+ quantitative (0.2.1)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
@@ -98,85 +98,73 @@ module Quant
98
98
 
99
99
  module InstanceMethods
100
100
  # Makes some assumptions about the class's initialization having a +tick+ keyword argument.
101
- #
102
- # The challenge here is that we prepend this module to the class, and we are
103
- # initializing attributes before the owning class gets the opportunity to initialize
104
- # variables that we wanted to depend on with being able to define a default
105
- # value that could set default values from a +tick+ method.
106
- #
107
- # Ok for now. May need to be more flexible in the future. Alternative strategy could be
108
- # to lazy eval the default value the first time it is accessed.
109
- def initialize(*args, **kwargs)
110
- initialize_attributes(tick: kwargs[:tick])
111
- super(*args, **kwargs)
101
+ # If one does exist, the +tick+ is considered as a potential source for the declared defaults
102
+ def initialize(...)
103
+ super(...)
104
+ initialize_attributes
105
+ end
106
+
107
+ # Returns an array of all classes in the hierarchy, starting with the current class
108
+ def self_and_ancestors
109
+ [this_class = self.class].tap do |classes|
110
+ classes << this_class = this_class.superclass while !this_class.nil?
111
+ end
112
112
  end
113
113
 
114
114
  # Iterates over all defined attributes in a child => parent hierarchy,
115
115
  # and yields the name and entry for each.
116
116
  def each_attribute(&block)
117
- klass = self.class
118
- loop do
119
- attributes = Attributes.registry[klass]
120
- break if attributes.nil?
121
-
122
- attributes.each{ |name, entry| block.call(name, entry) }
123
- klass = klass.superclass
117
+ self_and_ancestors.select{ |klass| Attributes.registry[klass] }.each do |klass|
118
+ Attributes.registry[klass].each{ |name, entry| block.call(name, entry) }
124
119
  end
125
120
  end
126
121
 
127
122
  # The default value can be one of the following:
128
- # - A symbol that is a method on the instance responds to
123
+ # - A symbol that is a method the instance responds to
129
124
  # - A symbol that is a method that the instance's tick responds to
130
125
  # - A Proc that is bound to the instance
131
126
  # - An immediate value (Integer, Float, Boolean, etc.)
132
- def default_value_for(entry, new_tick)
133
- # let's not assume tick is always available/implemented
134
- # can get from instance or from initializer passed here as `new_tick`
135
- current_tick = new_tick
136
- current_tick ||= tick if respond_to?(:tick)
137
-
138
- if entry[:default].is_a?(Symbol) && respond_to?(entry[:default])
139
- send(entry[:default])
127
+ def default_value_for(entry)
128
+ return instance_exec(&entry[:default]) if entry[:default].is_a?(Proc)
129
+ return entry[:default] unless entry[:default].is_a?(Symbol)
130
+ return send(entry[:default]) if respond_to?(entry[:default])
131
+ return tick.send(entry[:default]) if tick.respond_to?(entry[:default])
140
132
 
141
- elsif entry[:default].is_a?(Symbol) && current_tick.respond_to?(entry[:default])
142
- current_tick.send(entry[:default])
143
-
144
- elsif entry[:default].is_a?(Proc)
145
- instance_exec(&entry[:default])
146
-
147
- else
148
- entry[:default]
149
- end
133
+ entry[:default]
150
134
  end
151
135
 
152
136
  # Initializes the defined attributes with default values and
153
137
  # defines accessor methods for each attribute.
154
138
  # If a child class redefines a parent's attribute, the child's
155
139
  # definition will be used.
156
- def initialize_attributes(tick:)
140
+ def initialize_attributes
157
141
  each_attribute do |name, entry|
158
142
  # use the child's definition, skipping the parent's
159
143
  next if respond_to?(name)
160
144
 
161
145
  ivar_name = "@#{name}"
162
- instance_variable_set(ivar_name, default_value_for(entry, tick))
163
- define_singleton_method(name) { instance_variable_get(ivar_name) }
146
+ define_singleton_method(name) do
147
+ return instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
148
+
149
+ # Sets the default value when accessed and ivar is not already set
150
+ default_value_for(entry).tap { |value| instance_variable_set(ivar_name, value) }
151
+ end
164
152
  define_singleton_method("#{name}=") { |value| instance_variable_set(ivar_name, value) }
165
153
  end
166
154
  end
167
155
 
168
156
  # Serializes keys that have been defined as serializeable attributes
169
- # Key values that are nil are removed from the hash
157
+ # Key values that are nil are omitted from the hash
170
158
  # @return [Hash] The serialized attributes as a Ruby Hash.
171
159
  def to_h
172
160
  {}.tap do |key_values|
173
161
  each_attribute do |name, entry|
174
162
  next unless entry[:key]
175
163
 
176
- ivar_name = "@#{name}"
177
- value = instance_variable_get(ivar_name)
164
+ value = send(name)
165
+ next unless value
178
166
 
179
- key_values[entry[:key]] = value if value
167
+ key_values[entry[:key]] = value
180
168
  end
181
169
  end
182
170
  end
data/lib/quant/config.rb CHANGED
@@ -14,6 +14,10 @@ module Quant
14
14
  end
15
15
  end
16
16
 
17
+ def self.default!
18
+ @config = Config.new
19
+ end
20
+
17
21
  def self.config
18
22
  @config ||= Config.new
19
23
  end
@@ -25,6 +29,10 @@ module Quant
25
29
  Config.config
26
30
  end
27
31
 
32
+ def default_configuration!
33
+ Config.default!
34
+ end
35
+
28
36
  def configure_indicators(**settings)
29
37
  config.apply_indicator_settings(**settings)
30
38
  yield config.indicators if block_given?
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,49 @@
1
+ module Quant
2
+ # Dominant Cycles measure the primary cycle within a given range. By default, the library
3
+ # is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
4
+ # the `min_period` and `max_period` configuration values in {Quant::Config}.
5
+ #
6
+ # Quant.configure_indicators(min_period: 8, max_period: 32)
7
+ #
8
+ # The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
9
+ # the `dominant_cycle_kind` configuration value in {Quant::Config}.
10
+ #
11
+ # Quant.configure_indicators(dominant_cycle_kind: :band_pass)
12
+ #
13
+ # The purpose of these indicators is to compute the dominant cycle and underpin the various
14
+ # indicators that would otherwise be setting an arbitrary lookback period. This makes the
15
+ # indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
16
+ class DominantCycleIndicators < IndicatorsProxy
17
+ # Auto-Correlation Reversals is a method of computing the dominant cycle
18
+ # by correlating the data stream with itself delayed by a lag.
19
+ def acr; indicator(Indicators::DominantCycles::Acr) end
20
+
21
+ # The band-pass dominant cycle passes signals within a certain frequency
22
+ # range, and attenuates signals outside that range.
23
+ # The trend component of the signal is removed, leaving only the cyclical
24
+ # component. Then we count number of iterations between zero crossings
25
+ # and this is the `period` of the dominant cycle.
26
+ def band_pass; indicator(Indicators::DominantCycles::BandPass) end
27
+
28
+ # Homodyne means the signal is multiplied by itself. More precisely,
29
+ # we want to multiply the signal of the current bar with the complex
30
+ # value of the signal one bar ago
31
+ def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
32
+
33
+ # The Dual Differentiator algorithm computes the phase angle from the
34
+ # analytic signal as the arctangent of the ratio of the imaginary
35
+ # component to the real component. Further, the angular frequency
36
+ # is defined as the rate change of phase. We can use these facts to
37
+ # derive the cycle period.
38
+ def differential; indicator(Indicators::DominantCycles::Differential) end
39
+
40
+ # The phase accumulation method of computing the dominant cycle measures
41
+ # the phase at each sample by taking the arctangent of the ratio of the
42
+ # quadrature component to the in-phase component. The phase is then
43
+ # accumulated and the period is derived from the phase.
44
+ def phase_accumulator; indicator(Indicators::DominantCycles::PhaseAccumulator) end
45
+
46
+ # Static, arbitrarily set period.
47
+ def half_period; indicator(Indicators::DominantCycles::HalfPeriod) end
48
+ end
49
+ end
@@ -0,0 +1,101 @@
1
+ require_relative "../indicator_point"
2
+ require_relative "dominant_cycle"
3
+
4
+ module Quant
5
+ class Indicators
6
+ class DominantCycles
7
+ class AcrPoint < DominantCyclePoint
8
+ attribute :hp, default: 0.0
9
+ attribute :filter, default: 0.0
10
+ attribute :interim_period, default: 0.0
11
+ attribute :inst_period, default: :min_period
12
+ attribute :period, default: 0.0
13
+ attribute :sp, default: 0.0
14
+ attribute :spx, default: 0.0
15
+ attribute :maxpwr, default: 0.0
16
+ attribute :r1, default: -> { Hash.new(0.0) }
17
+ attribute :corr, default: -> { Hash.new(0.0) }
18
+ attribute :pwr, default: -> { Hash.new(0.0) }
19
+ attribute :cospart, default: -> { Hash.new(0.0) }
20
+ attribute :sinpart, default: -> { Hash.new(0.0) }
21
+ attribute :sqsum, default: -> { Hash.new(0.0) }
22
+ attribute :reversal, default: false
23
+ end
24
+
25
+ # Auto-Correlation Reversals is a method of computing the dominant cycle
26
+ # by correlating the data stream with itself delayed by a lag.
27
+ # Construction of the autocorrelation periodogram starts with the
28
+ # autocorrelation function using the minimum three bars of averaging.
29
+ # The cyclic information is extracted using a discrete Fourier transform
30
+ # (DFT) of the autocorrelation results.
31
+ class Acr < DominantCycle
32
+ BANDWIDTH_DEGREES = 370
33
+ BANDWIDTH_RADIANS = BANDWIDTH_DEGREES * Math::PI / 180.0
34
+
35
+ def compute_auto_correlations
36
+ (min_period..max_period).each do |period|
37
+ corr = Statistics::Correlation.new
38
+ micro_period.times do |lookback_period|
39
+ corr.add(p(lookback_period).filter, p(period + lookback_period).filter)
40
+ end
41
+ p0.corr[period] = corr.coefficient
42
+ end
43
+ end
44
+
45
+ def compute_powers
46
+ p0.maxpwr = 0.995 * p1.maxpwr
47
+
48
+ (min_period..max_period).each do |period|
49
+ (micro_period..max_period).each do |n|
50
+ radians = BANDWIDTH_RADIANS * n / period
51
+ p0.cospart[period] += p0.corr[n] * Math.cos(radians)
52
+ p0.sinpart[period] += p0.corr[n] * Math.sin(radians)
53
+ end
54
+ p0.sqsum[period] = p0.cospart[period]**2 + p0.sinpart[period]**2
55
+ p0.r1[period] = (0.2 * p0.sqsum[period]**2) + (0.8 * p1.r1[period])
56
+ p0.pwr[period] = p0.r1[period]
57
+ p0.maxpwr = [p0.maxpwr, p0.r1[period]].max
58
+ end
59
+ return if p0.maxpwr.zero?
60
+
61
+ (min_period..max_period).each do |period|
62
+ p0.pwr[period] = p0.r1[period] / p0.maxpwr
63
+ end
64
+ end
65
+
66
+ def compute_period
67
+ (min_period..max_period).each do |period|
68
+ if p0.pwr[period] >= 0.4
69
+ p0.spx += (period * p0.pwr[period])
70
+ p0.sp += p0.pwr[period]
71
+ end
72
+ end
73
+
74
+ p0.interim_period = p0.sp.zero? ? p1.period : p0.spx / p0.sp
75
+ p0.inst_period = two_pole_butterworth(:interim_period, previous: :period, period: min_period)
76
+ p0.period = p0.inst_period.round(0)
77
+ end
78
+
79
+ def compute_reversal
80
+ sum_deltas = 0
81
+ (min_period..max_period).each do |period|
82
+ sc1 = (p0.corr[period] + 1) * 0.5
83
+ sc2 = (p1.corr[period] + 1) * 0.5
84
+ sum_deltas += 1 if (sc1 > 0.5 && sc2 < 0.5) || (sc1 < 0.5 && sc2 > 0.5)
85
+ end
86
+ p0.reversal = sum_deltas > 24
87
+ end
88
+
89
+ def compute
90
+ p0.hp = two_pole_high_pass_filter(:input, period: max_period)
91
+ p0.filter = two_pole_butterworth(:hp, previous: :filter, period: min_period)
92
+
93
+ compute_auto_correlations
94
+ compute_powers
95
+ compute_period
96
+ compute_reversal
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,85 @@
1
+ require_relative "dominant_cycle"
2
+
3
+ module Quant
4
+ class Indicators
5
+ class DominantCycles
6
+ class BandPassPoint < Quant::Indicators::IndicatorPoint
7
+ attribute :hp, default: 0.0
8
+ attribute :bp, default: 0.0
9
+ attribute :counter, default: 0
10
+ attribute :period, default: :half_period
11
+ attribute :peak, default: :half_period
12
+ attribute :real, default: :half_period
13
+ attribute :crosses, default: false
14
+ attribute :direction, default: :flat
15
+ end
16
+
17
+ # The band-pass dominant cycle passes signals within a certain frequency
18
+ # range, and attenuates signals outside that range.
19
+ # The trend component of the signal is revoved, leaving only the cyclical
20
+ # component. Then we count number of iterations between zero crossings
21
+ # and this is the `period` of the dominant cycle.
22
+ class BandPass < DominantCycle
23
+ def bandwidth
24
+ 0.75
25
+ end
26
+
27
+ # alpha2 = (Cosine(.25*Bandwidth*360 / Period) +
28
+ # Sine(.25*Bandwidth*360 / Period) - 1) / Cosine(.25*Bandwidth*360 / Period);
29
+ # HP = (1 + alpha2 / 2)*(Close - Close[1]) + (1- alpha2)*HP[1];
30
+ # beta = Cosine(360 / Period);
31
+ # gamma = 1 / Cosine(360*Bandwidth / Period);
32
+ # alpha = gamma - SquareRoot(gamma*gamma - 1);
33
+ # BP = .5*(1 - alpha)*(HP - HP[2]) + beta*(1 + alpha)*BP[1] - alpha*BP[2];
34
+ # If Currentbar = 1 or CurrentBar = 2 then BP = 0;
35
+
36
+ # Peak = .991*Peak;
37
+ # If AbsValue(BP) > If Peak <> 0 Then DC = DC[1];
38
+ # If DC < 6 Then DC counter = counter
39
+ # If Real Crosses Over 0 or Real Crosses Under 0 Then Begin
40
+ # DC = 2*counter;
41
+ # If 2*counter > 1.25*DC[1] Then DC = 1.25*DC[1];
42
+ # If 2*counter < .8*DC[1] Then DC = .8*DC[1];
43
+ # counter = 0;
44
+ # End;
45
+
46
+ def compute_high_pass
47
+ alpha = period_to_alpha(max_period, k: 0.25 * bandwidth)
48
+ p0.hp = (1 + alpha / 2) * (p0.input - p1.input) + (1 - alpha) * p1.hp
49
+ end
50
+
51
+ def compute_band_pass
52
+ radians = deg2rad(360.0 / max_period)
53
+ beta = Math.cos(radians)
54
+ gamma = 1.0 / Math.cos(bandwidth * radians)
55
+ alpha = gamma - Math.sqrt(gamma**2 - 1.0)
56
+
57
+ a = 0.5 * (1 - alpha) * (p0.hp - p2.hp)
58
+ b = beta * (1 + alpha) * p1.bp
59
+ c = alpha * p2.bp
60
+ p0.bp = a + b - c
61
+ end
62
+
63
+ def compute_period
64
+ p0.peak = [0.991 * p1.peak, p0.bp.abs].max
65
+ p0.real = p0.bp / p0.peak unless p0.peak.zero?
66
+ p0.counter = p1.counter + 1
67
+ p0.period = [p1.period, min_period].max.to_i
68
+ p0.crosses = (p0.real > 0.0 && p1.real < 0.0) || (p0.real < 0.0 && p1.real > 0.0)
69
+ if (p0.real >= 0.0 && p1.real < 0.0) || (p0.real <= 0.0 && p1.real > 0.0)
70
+ p0.period = [2 * p0.counter, 1.25 * p1.period].min.to_i
71
+ p0.period = [p0.period, 0.8 * p1.period].max.to_i
72
+ p0.counter = 0
73
+ end
74
+ p0.direction = p0.real > (p1.real + p2.real + p3.real) / 3.0 ? :up : :down
75
+ end
76
+
77
+ def compute
78
+ compute_high_pass
79
+ compute_band_pass
80
+ compute_period
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,21 @@
1
+ module Quant
2
+ class Indicators
3
+ class DominantCycles
4
+ # The Dual Differentiator algorithm computes the phase angle from the
5
+ # analytic signal as the arctangent of the ratio of the imaginary
6
+ # component to the real component. Further, the angular frequency
7
+ # is defined as the rate change of phase. We can use these facts to
8
+ # derive the cycle period.
9
+ class Differential < DominantCycle
10
+ def compute_period
11
+ p0.ddd = (p0.q2 * (p0.i2 - p1.i2)) - (p0.i2 * (p0.q2 - p1.q2))
12
+ p0.inst_period = p0.ddd > 0.01 ? 6.2832 * (p0.i2**2 + p0.q2**2) / p0.ddd : 0.0
13
+
14
+ constrain_period_magnitude_change
15
+ constrain_period_bars
16
+ p0.period = p0.inst_period.round(0).to_i
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,144 @@
1
+ require_relative "../indicator"
2
+
3
+ module Quant
4
+ class Indicators
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 DominantCycles
20
+ class DominantCyclePoint < Quant::Indicators::IndicatorPoint
21
+ attribute :smooth, default: 0.0
22
+ attribute :detrend, default: 0.0
23
+ attribute :inst_period, default: :min_period
24
+ attribute :period, key: "p", default: nil # intentially nil! (see: compute_period)
25
+ attribute :smooth_period, key: "sp", default: :min_period
26
+ attribute :mean_period, key: "mp", default: :min_period
27
+ attribute :ddd, default: 0.0
28
+ attribute :q1, default: 0.0
29
+ attribute :q2, default: 0.0
30
+ attribute :i1, default: 0.0
31
+ attribute :i2, default: 0.0
32
+ attribute :ji, default: 0.0
33
+ attribute :jq, default: 0.0
34
+ attribute :re, default: 0.0
35
+ attribute :im, default: 0.0
36
+ attribute :phase, default: 0.0
37
+ attribute :phase_sum, key: "ps", default: 0.0
38
+ attribute :delta_phase, default: 0.0
39
+ attribute :accumulator_phase, default: 0.0
40
+ attribute :real_part, default: 0.0
41
+ attribute :imag_part, default: 0.0
42
+ end
43
+
44
+ class DominantCycle < Indicators::Indicator
45
+ def points_class
46
+ Object.const_get "Quant::Indicators::DominantCycles::#{indicator_name}Point"
47
+ rescue NameError
48
+ DominantCyclePoint
49
+ end
50
+
51
+ # constrain between min_period and max_period bars
52
+ def constrain_period_bars
53
+ p0.inst_period = p0.inst_period.clamp(min_period, max_period)
54
+ end
55
+
56
+ attr_reader :points
57
+
58
+ # constrain magnitude of change in phase
59
+ def constrain_period_magnitude_change
60
+ p0.inst_period = [1.5 * p1.inst_period, p0.inst_period].min
61
+ p0.inst_period = [0.67 * p1.inst_period, p0.inst_period].max
62
+ end
63
+
64
+ # amplitude correction using previous period value
65
+ def compute_smooth_period
66
+ p0.inst_period = (0.2 * p0.inst_period) + (0.8 * p1.inst_period)
67
+ p0.smooth_period = (0.33333 * p0.inst_period) + (0.666667 * p1.smooth_period)
68
+ end
69
+
70
+ def compute_mean_period
71
+ ss_period = super_smoother(:smooth_period, previous: :mean_period, period: micro_period)
72
+ p0.mean_period = ss_period.clamp(min_period, max_period)
73
+ end
74
+
75
+ def dominant_cycle_period
76
+ [p0.period.to_i, min_period].max
77
+ end
78
+
79
+ def period_points(max_period)
80
+ extent = [values.size, max_period].min
81
+ values[-extent, extent]
82
+ end
83
+
84
+ def compute
85
+ compute_input_data_points
86
+ compute_quadrature_components
87
+ compute_period
88
+ compute_smooth_period
89
+ compute_mean_period
90
+ compute_phase
91
+ end
92
+
93
+ def compute_input_data_points
94
+ p0.smooth = wma :input
95
+ p0.detrend = hilbert_transform :smooth, period: p1.inst_period
96
+ end
97
+
98
+ # NOTE: The phase lag of q1 and `i1 is (360 * 7 / Period - 90)` degrees
99
+ # where Period is the dominant cycle period.
100
+ def compute_quadrature_components
101
+ # { Compute Inphase and Quadrature components }
102
+ p0.q1 = hilbert_transform :detrend, period: p1.inst_period
103
+ p0.i1 = p3.detrend
104
+
105
+ # { Advance the phase of I1 and Q1 by 90 degrees }
106
+ p0.ji = hilbert_transform :i1, period: p1.inst_period
107
+ p0.jq = hilbert_transform :q1, period: p1.inst_period
108
+
109
+ # { Smooth the I and Q components before applying the discriminator }
110
+ p0.i2 = (0.2 * (p0.i1 - p0.jq)) + 0.8 * (p1.i2 || (p0.i1 - p0.jq))
111
+ p0.q2 = (0.2 * (p0.q1 + p0.ji)) + 0.8 * (p1.q2 || (p0.q1 + p0.ji))
112
+ end
113
+
114
+ def compute_period
115
+ raise NotImplementedError
116
+ end
117
+
118
+ def compute_phase
119
+ raise "must compute period before calling!" unless p0.period
120
+
121
+ period_points(dominant_cycle_period).map(&:smooth).each_with_index do |smooth, index|
122
+ radians = deg2rad((1 + index) * 360.0 / dominant_cycle_period)
123
+ p0.real_part += smooth * Math.sin(radians)
124
+ p0.imag_part += smooth * Math.cos(radians)
125
+ end
126
+
127
+ if p0.imag_part.zero?
128
+ p0.phase = 90.0 * (p0.real_part.positive? ? 1 : 0)
129
+ else
130
+ radians = deg2rad(p0.real_part / p0.imag_part)
131
+ p0.phase = rad2deg(Math.atan(radians))
132
+ end
133
+ p0.phase += 90
134
+ # { Compensate for one bar lag of the Weighted Moving Average }
135
+ p0.phase += (360.0 / p0.inst_period)
136
+
137
+ p0.phase += 180.0 if p0.imag_part < 0.0
138
+ p0.phase -= 360.0 if p0.phase > 315.0
139
+ p0.delta_phase = [1.0, p1.phase - p0.phase].max
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "dominant_cycle"
2
+
3
+ module Quant
4
+ class Indicators
5
+ class DominantCycles
6
+ # This dominant cycle indicator is based on the half period
7
+ # that is the midpoint of the `min_period` and `max_period`
8
+ # configured in the `Quant.config.indicators` object.
9
+ # Effectively providing a static, arbitrarily set period.
10
+ class HalfPeriodPoint < Quant::Indicators::IndicatorPoint
11
+ attribute :period, default: :half_period
12
+ end
13
+
14
+ class HalfPeriod < DominantCycle
15
+ def compute
16
+ # No-Op
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ require_relative "../indicator_point"
2
+ require_relative "dominant_cycle"
3
+
4
+ module Quant
5
+ class Indicators
6
+ class DominantCycles
7
+ # Homodyne means the signal is multiplied by itself. More precisely,
8
+ # we want to multiply the signal of the current bar with the complex
9
+ # value of the signal one bar ago
10
+ class Homodyne < DominantCycle
11
+ def compute_period
12
+ p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
13
+ p0.im = (p0.i2 * p1.q2) - (p0.q2 * p1.i2)
14
+
15
+ p0.re = (0.2 * p0.re) + (0.8 * p1.re)
16
+ p0.im = (0.2 * p0.im) + (0.8 * p1.im)
17
+
18
+ p0.inst_period = 360.0 / rad2deg(Math.atan(p0.im/p0.re)) if (p0.im != 0) && (p0.re != 0)
19
+
20
+ constrain_period_magnitude_change
21
+ constrain_period_bars
22
+ p0.mean_period = super_smoother :inst_period, previous: :mean_period, period: max_period
23
+ p0.period = p0.mean_period.round(0).to_i
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end