quantitative 0.1.9 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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