quantitative 0.1.10 → 0.2.1

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: 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