quantitative 0.1.9 → 0.2.0

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/Guardfile +1 -1
  4. data/Rakefile +6 -1
  5. data/lib/quant/attributes.rb +31 -43
  6. data/lib/quant/config.rb +8 -0
  7. data/lib/quant/experimental.rb +20 -0
  8. data/lib/quant/indicators/dominant_cycle_indicators.rb +10 -0
  9. data/lib/quant/indicators/dominant_cycles/acr.rb +101 -0
  10. data/lib/quant/indicators/dominant_cycles/band_pass.rb +80 -0
  11. data/lib/quant/indicators/dominant_cycles/differential.rb +19 -0
  12. data/lib/quant/indicators/dominant_cycles/dominant_cycle.rb +128 -0
  13. data/lib/quant/indicators/dominant_cycles/homodyne.rb +27 -0
  14. data/lib/quant/indicators/dominant_cycles/phase_accumulator.rb +59 -0
  15. data/lib/quant/indicators/indicator.rb +30 -8
  16. data/lib/quant/indicators/indicator_point.rb +12 -2
  17. data/lib/quant/indicators.rb +9 -2
  18. data/lib/quant/indicators_proxy.rb +0 -3
  19. data/lib/quant/indicators_sources.rb +1 -1
  20. data/lib/quant/interval.rb +6 -9
  21. data/lib/quant/mixins/filters.rb +5 -42
  22. data/lib/quant/mixins/functions.rb +7 -3
  23. data/lib/quant/mixins/high_pass_filters.rb +129 -0
  24. data/lib/quant/mixins/super_smoother.rb +18 -15
  25. data/lib/quant/mixins/universal_filters.rb +326 -0
  26. data/lib/quant/series.rb +1 -1
  27. data/lib/quant/statistics/correlation.rb +37 -0
  28. data/lib/quant/ticks/ohlc.rb +5 -4
  29. data/lib/quant/time_methods.rb +4 -0
  30. data/lib/quant/time_period.rb +13 -14
  31. data/lib/quant/version.rb +1 -1
  32. data/lib/quantitative.rb +1 -1
  33. metadata +13 -4
  34. data/lib/quant/indicators/ma.rb +0 -40
  35. data/lib/quant/mixins/high_pass_filter.rb +0 -54
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7ddfcfd200564c3ce9764a7ba635635c56649c6681ac6d76b93390734da4fc9
4
- data.tar.gz: 04d93778a91c74ee4c3459009f4990644121974b77079d79a3816c5f63a63f6c
3
+ metadata.gz: e3db79bcbb841511c94568e330f60dfb6b68178ae874c56b867c17c538510a01
4
+ data.tar.gz: c5da8745035d84023886dcc8fba53f0df473c404b979e084385f6d9a7e19d536
5
5
  SHA512:
6
- metadata.gz: 8facaa3efaef73c00f98f03b4482b876cf9a6155f46fb56f1c0d3534c996b6c33fefa067ee07ef8f326e76dfbdd41b0fa7d04f349eef49add179444bbe0d9db6
7
- data.tar.gz: 3fa7bb3d2e34cd3f2ebd718105ab20ffb60e044c45fb5c3909a14cdd1d2876288ae23d4eb7c4211f35890ddd250772068f1c0e0463db71d1c84c490a06edc2d8
6
+ metadata.gz: 23c01e976533a62eddd80d5a774c55d23c644f6b9902392277f8a3450849f46a8dd8ead5588d8cc798cbaa36cce6c7ca7404ed332d4b41d6707982fb8e85c7bc
7
+ data.tar.gz: 769c9951b5af3cef2f9a4b93c4a49bd13f33876daa18ba90cf78a00503bfea1e87d8fa4ece964e72f0e15dbc712ef857bf3fa102d438e982d308e00a4c376b9d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.1.9)
4
+ quantitative (0.2.0)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
data/Guardfile CHANGED
@@ -1,6 +1,6 @@
1
1
  guard :rspec, cmd: "rspec" do
2
2
  watch(%r{^spec/.+_spec\.rb$})
3
- watch(%r{^spec/lib/**/.+_spec\.rb$})
3
+ watch(%r{^spec/lib/.+_spec\.rb$})
4
4
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
5
5
  watch("spec/spec_helper.rb") { "spec" }
6
6
  end
data/Rakefile CHANGED
@@ -38,4 +38,9 @@ namespace :gem do
38
38
  task release: [:build, :tag] do
39
39
  sh "gem push quantitative-#{Quant::VERSION}.gem"
40
40
  end
41
- end
41
+
42
+ desc "push #{Quant::VERSION} to rubygems.org"
43
+ task push: [:build] do
44
+ sh "gem push quantitative-#{Quant::VERSION}.gem"
45
+ end
46
+ end
@@ -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?
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Experimental
5
+ def self.tracker
6
+ @tracker ||= {}
7
+ end
8
+ end
9
+
10
+ def self.experimental(message)
11
+ return if defined?(RSpec)
12
+ return if Experimental.tracker[caller.first]
13
+
14
+ Experimental.tracker[caller.first] = message
15
+
16
+ calling_method = caller.first.scan(/`([^']*)/)[0][0]
17
+ full_message = "EXPERIMENTAL: #{calling_method.inspect}: #{message}\nsource location: #{caller.first}"
18
+ puts full_message
19
+ end
20
+ end
@@ -0,0 +1,10 @@
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
@@ -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
26
+ class Acr < DominantCycle
27
+ def average_length
28
+ 3 # AvgLength
29
+ end
30
+
31
+ def bandwidth
32
+ deg2rad(370)
33
+ end
34
+
35
+ def compute_auto_correlations
36
+ (min_period..max_period).each do |period|
37
+ corr = Statistics::Correlation.new
38
+ average_length.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
+ (average_length..max_period).each do |n|
50
+ radians = bandwidth * 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,80 @@
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
+ class BandPass < DominantCycle
18
+ def bandwidth
19
+ 0.75
20
+ end
21
+
22
+ # alpha2 = (Cosine(.25*Bandwidth*360 / Period) +
23
+ # Sine(.25*Bandwidth*360 / Period) - 1) / Cosine(.25*Bandwidth*360 / Period);
24
+ # HP = (1 + alpha2 / 2)*(Close - Close[1]) + (1- alpha2)*HP[1];
25
+ # beta = Cosine(360 / Period);
26
+ # gamma = 1 / Cosine(360*Bandwidth / Period);
27
+ # alpha = gamma - SquareRoot(gamma*gamma - 1);
28
+ # BP = .5*(1 - alpha)*(HP - HP[2]) + beta*(1 + alpha)*BP[1] - alpha*BP[2];
29
+ # If Currentbar = 1 or CurrentBar = 2 then BP = 0;
30
+
31
+ # Peak = .991*Peak;
32
+ # If AbsValue(BP) > If Peak <> 0 Then DC = DC[1];
33
+ # If DC < 6 Then DC counter = counter
34
+ # If Real Crosses Over 0 or Real Crosses Under 0 Then Begin
35
+ # DC = 2*counter;
36
+ # If 2*counter > 1.25*DC[1] Then DC = 1.25*DC[1];
37
+ # If 2*counter < .8*DC[1] Then DC = .8*DC[1];
38
+ # counter = 0;
39
+ # End;
40
+
41
+ def compute_high_pass
42
+ alpha = period_to_alpha(max_period, k: 0.25 * bandwidth)
43
+ p0.hp = (1 + alpha / 2) * (p0.input - p1.input) + (1 - alpha) * p1.hp
44
+ end
45
+
46
+ def compute_band_pass
47
+ radians = deg2rad(360.0 / max_period)
48
+ beta = Math.cos(radians)
49
+ gamma = 1.0 / Math.cos(bandwidth * radians)
50
+ alpha = gamma - Math.sqrt(gamma**2 - 1.0)
51
+
52
+ a = 0.5 * (1 - alpha) * (p0.hp - p2.hp)
53
+ b = beta * (1 + alpha) * p1.bp
54
+ c = alpha * p2.bp
55
+ p0.bp = a + b - c
56
+ end
57
+
58
+ def compute_period
59
+ p0.peak = [0.991 * p1.peak, p0.bp.abs].max
60
+ p0.real = p0.bp / p0.peak unless p0.peak.zero?
61
+ p0.counter = p1.counter + 1
62
+ p0.period = [p1.period, min_period].max.to_i
63
+ p0.crosses = (p0.real > 0.0 && p1.real < 0.0) || (p0.real < 0.0 && p1.real > 0.0)
64
+ if (p0.real >= 0.0 && p1.real < 0.0) || (p0.real <= 0.0 && p1.real > 0.0)
65
+ p0.period = [2 * p0.counter, 1.25 * p1.period].min.to_i
66
+ p0.period = [p0.period, 0.8 * p1.period].max.to_i
67
+ p0.counter = 0
68
+ end
69
+ p0.direction = p0.real > (p1.real + p2.real + p3.real) / 3.0 ? :up : :down
70
+ end
71
+
72
+ def compute
73
+ compute_high_pass
74
+ compute_band_pass
75
+ compute_period
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,19 @@
1
+ module Quant
2
+ class Indicators
3
+ 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.
7
+ class Differential < DominantCycle
8
+ def compute_period
9
+ p0.ddd = (p0.q2 * (p0.i2 - p1.i2)) - (p0.i2 * (p0.q2 - p1.q2))
10
+ p0.inst_period = p0.ddd > 0.01 ? 6.2832 * (p0.i2**2 + p0.q2**2) / p0.ddd : 0.0
11
+
12
+ constrain_period_magnitude_change
13
+ constrain_period_bars
14
+ p0.period = p0.inst_period.round(0).to_i
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,128 @@
1
+ require_relative "../indicator"
2
+
3
+ module Quant
4
+ class Indicators
5
+ class DominantCycles
6
+ class DominantCyclePoint < Quant::Indicators::IndicatorPoint
7
+ attribute :smooth, default: 0.0
8
+ attribute :detrend, default: 0.0
9
+ attribute :inst_period, default: :min_period
10
+ attribute :period, key: "p", default: nil # intentially nil! (see: compute_period)
11
+ attribute :smooth_period, key: "sp", default: :min_period
12
+ attribute :mean_period, key: "mp", default: :min_period
13
+ attribute :ddd, default: 0.0
14
+ attribute :q1, default: 0.0
15
+ attribute :q2, default: 0.0
16
+ attribute :i1, default: 0.0
17
+ attribute :i2, default: 0.0
18
+ attribute :ji, default: 0.0
19
+ attribute :jq, default: 0.0
20
+ attribute :re, default: 0.0
21
+ attribute :im, default: 0.0
22
+ attribute :phase, default: 0.0
23
+ attribute :phase_sum, key: "ps", default: 0.0
24
+ attribute :delta_phase, default: 0.0
25
+ attribute :accumulator_phase, default: 0.0
26
+ attribute :real_part, default: 0.0
27
+ attribute :imag_part, default: 0.0
28
+ end
29
+
30
+ class DominantCycle < Indicators::Indicator
31
+ def points_class
32
+ Object.const_get "Quant::Indicators::DominantCycles::#{indicator_name}Point"
33
+ rescue NameError
34
+ DominantCyclePoint
35
+ end
36
+
37
+ # constrain between min_period and max_period bars
38
+ def constrain_period_bars
39
+ p0.inst_period = p0.inst_period.clamp(min_period, max_period)
40
+ end
41
+
42
+ # constrain magnitude of change in phase
43
+ def constrain_period_magnitude_change
44
+ p0.inst_period = [1.5 * p1.inst_period, p0.inst_period].min
45
+ p0.inst_period = [0.67 * p1.inst_period, p0.inst_period].max
46
+ end
47
+
48
+ # amplitude correction using previous period value
49
+ def compute_smooth_period
50
+ p0.inst_period = (0.2 * p0.inst_period) + (0.8 * p1.inst_period)
51
+ p0.smooth_period = (0.33333 * p0.inst_period) + (0.666667 * p1.smooth_period)
52
+ end
53
+
54
+ def compute_mean_period
55
+ ss_period = super_smoother(:smooth_period, previous: :mean_period, period: micro_period)
56
+ p0.mean_period = ss_period.clamp(min_period, max_period)
57
+ end
58
+
59
+ def dominant_cycle_period
60
+ [p0.period.to_i, min_period].max
61
+ end
62
+
63
+ def period_points(max_period)
64
+ extent = [values.size, max_period].min
65
+ values[-extent, extent]
66
+ end
67
+
68
+ def compute
69
+ compute_input_data_points
70
+ compute_quadrature_components
71
+ compute_period
72
+ compute_smooth_period
73
+ compute_mean_period
74
+ compute_phase
75
+ end
76
+
77
+ def compute_input_data_points
78
+ p0.smooth = wma :input
79
+ p0.detrend = hilbert_transform :smooth, period: p1.inst_period
80
+ end
81
+
82
+ # NOTE: The phase lag of q1 and `i1 is (360 * 7 / Period - 90)` degrees
83
+ # where Period is the dominant cycle period.
84
+ def compute_quadrature_components
85
+ # { Compute Inphase and Quadrature components }
86
+ p0.q1 = hilbert_transform :detrend, period: p1.inst_period
87
+ p0.i1 = p3.detrend
88
+
89
+ # { Advance the phase of I1 and Q1 by 90 degrees }
90
+ p0.ji = hilbert_transform :i1, period: p1.inst_period
91
+ p0.jq = hilbert_transform :q1, period: p1.inst_period
92
+
93
+ # { Smooth the I and Q components before applying the discriminator }
94
+ p0.i2 = (0.2 * (p0.i1 - p0.jq)) + 0.8 * (p1.i2 || (p0.i1 - p0.jq))
95
+ p0.q2 = (0.2 * (p0.q1 + p0.ji)) + 0.8 * (p1.q2 || (p0.q1 + p0.ji))
96
+ end
97
+
98
+ def compute_period
99
+ raise NotImplementedError
100
+ end
101
+
102
+ def compute_phase
103
+ raise "must compute period before calling!" unless p0.period
104
+
105
+ period_points(dominant_cycle_period).map(&:smooth).each_with_index do |smooth, index|
106
+ radians = deg2rad((1 + index) * 360.0 / dominant_cycle_period)
107
+ p0.real_part += smooth * Math.sin(radians)
108
+ p0.imag_part += smooth * Math.cos(radians)
109
+ end
110
+
111
+ if p0.imag_part.zero?
112
+ p0.phase = 90.0 * (p0.real_part.positive? ? 1 : 0)
113
+ else
114
+ radians = deg2rad(p0.real_part / p0.imag_part)
115
+ p0.phase = rad2deg(Math.atan(radians))
116
+ end
117
+ p0.phase += 90
118
+ # { Compensate for one bar lag of the Weighted Moving Average }
119
+ p0.phase += (360.0 / p0.inst_period)
120
+
121
+ p0.phase += 180.0 if p0.imag_part < 0.0
122
+ p0.phase -= 360.0 if p0.phase > 315.0
123
+ p0.delta_phase = [1.0, p1.phase - p0.phase].max
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,27 @@
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, we want to multiply the signal
8
+ # of the current bar with the complex value of the signal one bar ago
9
+ class Homodyne < DominantCycle
10
+ def compute_period
11
+ p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
12
+ p0.im = (p0.i2 * p1.q2) - (p0.q2 * p1.i2)
13
+
14
+ p0.re = (0.2 * p0.re) + (0.8 * p1.re)
15
+ p0.im = (0.2 * p0.im) + (0.8 * p1.im)
16
+
17
+ p0.inst_period = 360.0 / rad2deg(Math.atan(p0.im/p0.re)) if (p0.im != 0) && (p0.re != 0)
18
+
19
+ constrain_period_magnitude_change
20
+ constrain_period_bars
21
+ p0.mean_period = super_smoother :inst_period, previous: :mean_period, period: max_period
22
+ p0.period = p0.mean_period.round(0).to_i
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ require_relative "dominant_cycle"
2
+
3
+ module Quant
4
+ class Indicators
5
+ class DominantCycles
6
+ # The phase accumulation method of computing the dominant cycle is perhaps
7
+ # the easiest to comprehend. In this technique, we measure the phase
8
+ # at each sample by taking the arctangent of the ratio of the quadrature
9
+ # component to the in-phase component. A delta phase is generated by
10
+ # taking the difference of the phase between successive samples.
11
+ # At each sam- ple we can then look backwards, adding up the delta
12
+ # phases. When the sum of the delta phases reaches 360 degrees,
13
+ # we must have passed through one full cycle, on average. The process
14
+ # is repeated for each new sample.
15
+ #
16
+ # The phase accumulation method of cycle measurement always uses one
17
+ # full cycle’s worth of historical data. This is both an advantage
18
+ # and a disadvantage. The advantage is the lag in obtaining the answer
19
+ # scales directly with the cycle period. That is, the measurement of
20
+ # a short cycle period has less lag than the measurement of a longer
21
+ # cycle period. However, the number of samples used in making the
22
+ # measurement means the averaging period is variable with cycle period.
23
+ # Longer averaging reduces the noise level compared to the signal.
24
+ # Therefore, shorter cycle periods necessarily have a higher output
25
+ # signal-to-noise ratio.
26
+ class PhaseAccumulator < DominantCycle
27
+ def compute_period
28
+ p0.i1 = 0.15 * p0.i1 + 0.85 * p1.i1
29
+ p0.q1 = 0.15 * p0.q1 + 0.85 * p1.q1
30
+
31
+ p0.accumulator_phase = Math.atan(p0.q1 / p0.i1) unless p0.i1.zero?
32
+
33
+ case
34
+ when p0.i1 < 0 && p0.q1 > 0 then p0.accumulator_phase = 180.0 - p0.accumulator_phase
35
+ when p0.i1 < 0 && p0.q1 < 0 then p0.accumulator_phase = 180.0 + p0.accumulator_phase
36
+ when p0.i1 > 0 && p0.q1 < 0 then p0.accumulator_phase = 360.0 - p0.accumulator_phase
37
+ end
38
+
39
+ p0.delta_phase = p1.accumulator_phase - p0.accumulator_phase
40
+ if p1.accumulator_phase < 90.0 && p0.accumulator_phase > 270.0
41
+ p0.delta_phase = 360.0 + p1.accumulator_phase - p0.accumulator_phase
42
+ end
43
+
44
+ p0.delta_phase = p0.delta_phase.clamp(min_period, max_period)
45
+
46
+ p0.inst_period = p1.inst_period
47
+ period_points(max_period).each_with_index do |prev, index|
48
+ p0.phase_sum += prev.delta_phase
49
+ if p0.phase_sum > 360.0
50
+ p0.inst_period = index
51
+ break
52
+ end
53
+ end
54
+ p0.period = (0.25 * p0.inst_period + 0.75 * p1.inst_period).round(0)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end