quantitative 0.1.10 → 0.2.0

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: e3db79bcbb841511c94568e330f60dfb6b68178ae874c56b867c17c538510a01
4
+ data.tar.gz: c5da8745035d84023886dcc8fba53f0df473c404b979e084385f6d9a7e19d536
5
5
  SHA512:
6
- metadata.gz: d48777cee5009e48b5d3ef6c531029fdaa8c9629b37223ad919aacd53c29caf13258d7cddc28413788b61ec086af0abde606996cdac3c7012565ec2cea5b6e77
7
- data.tar.gz: 8c9fa61a0f4b4c21b890c6895d4e74ba870fb92336cd30bb822e13bad5e49e9ee8d4b45e639171a73ab8c04e1756088e79979231a557a41ad9effa8ee1660e6b
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.10)
4
+ quantitative (0.2.0)
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?
@@ -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
@@ -7,13 +7,11 @@ module Quant
7
7
  include Mixins::Functions
8
8
  include Mixins::Filters
9
9
  include Mixins::MovingAverages
10
- # include Mixins::HilbertTransform
11
- # include Mixins::SuperSmoother
12
- # include Mixins::Stochastic
13
- # include Mixins::FisherTransform
14
- # include Mixins::HighPassFilter
10
+ include Mixins::HilbertTransform
11
+ include Mixins::SuperSmoother
12
+ include Mixins::Stochastic
13
+ include Mixins::FisherTransform
15
14
  # include Mixins::Direction
16
- # include Mixins::Filters
17
15
 
18
16
  attr_reader :source, :series
19
17
 
@@ -24,6 +22,30 @@ module Quant
24
22
  series.each { |tick| self << tick }
25
23
  end
26
24
 
25
+ def min_period
26
+ Quant.config.indicators.min_period
27
+ end
28
+
29
+ def max_period
30
+ Quant.config.indicators.max_period
31
+ end
32
+
33
+ def half_period
34
+ Quant.config.indicators.half_period
35
+ end
36
+
37
+ def micro_period
38
+ Quant.config.indicators.micro_period
39
+ end
40
+
41
+ def dominant_cycle_kind
42
+ Quant.config.indicators.dominant_cycle_kind
43
+ end
44
+
45
+ def pivot_kind
46
+ Quant.config.indicators.pivot_kind
47
+ end
48
+
27
49
  def ticks
28
50
  @points.keys
29
51
  end
@@ -45,7 +67,7 @@ module Quant
45
67
 
46
68
  def <<(tick)
47
69
  @t0 = tick
48
- @p0 = points_class.new(tick:, source:)
70
+ @p0 = points_class.new(indicator: self, tick:, source:)
49
71
  @points[tick] = @p0
50
72
 
51
73
  @p1 = values[-2] || @p0
@@ -4,19 +4,29 @@ module Quant
4
4
  class Indicators
5
5
  class IndicatorPoint
6
6
  include Quant::Attributes
7
+ extend Forwardable
7
8
 
8
- attr_reader :tick
9
+ attr_reader :indicator, :tick
9
10
 
10
11
  attribute :source, key: "src"
11
12
  attribute :input, key: "in"
12
13
 
13
- def initialize(tick:, source:)
14
+ def initialize(indicator:, tick:, source:)
15
+ @indicator = indicator
14
16
  @tick = tick
15
17
  @source = source
16
18
  @input = @tick.send(source)
17
19
  initialize_data_points
18
20
  end
19
21
 
22
+ def_delegator :indicator, :series
23
+ def_delegator :indicator, :min_period
24
+ def_delegator :indicator, :max_period
25
+ def_delegator :indicator, :half_period
26
+ def_delegator :indicator, :micro_period
27
+ def_delegator :indicator, :dominant_cycle_kind
28
+ def_delegator :indicator, :pivot_kind
29
+
20
30
  def initialize_data_points
21
31
  # No-Op - Override in subclass if needed.
22
32
  end
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "indicators_proxy"
3
4
  module Quant
4
- # TODO: build an Indicator registry so new indicators can be added and used outside those shipped with the library.
5
- class Indicators
5
+ # TODO: build an Indicator registry so new indicators can be added and
6
+ # used outside those shipped with the library.
7
+ class Indicators < IndicatorsProxy
8
+ def ping; indicator(Indicators::Ping) end
9
+
10
+ def dominant_cycles
11
+ @dominant_cycles ||= Indicators::DominantCycleIndicators.new(series:, source:)
12
+ end
6
13
  end
7
14
  end
@@ -54,8 +54,5 @@ module Quant
54
54
  def attach(name:, indicator_class:)
55
55
  define_singleton_method(name) { indicator(indicator_class) }
56
56
  end
57
-
58
- def ma; indicator(Indicators::Ma) end
59
- def ping; indicator(Indicators::Ping) end
60
57
  end
61
58
  end
@@ -12,7 +12,7 @@ module Quant
12
12
  end
13
13
 
14
14
  def oc2
15
- @indicator_sources[:oc2] ||= IndicatorsProxy.new(series: @series, source: :oc2)
15
+ @indicator_sources[:oc2] ||= Indicators.new(series: @series, source: :oc2)
16
16
  end
17
17
  end
18
18
  end
@@ -2,44 +2,89 @@
2
2
 
3
3
  module Quant
4
4
  module Mixins
5
+ # The following are high pass filters that are used to remove low frequency
6
+ # components from a time series. In simple terms, a high pass filter
7
+ # allows signals above a certain frequency (the cutoff frequency) to
8
+ # pass through relatively unaffected, while attenuating or blocking
9
+ # signals below that frequency.
10
+ #
11
+ # HighPass Filters are “detrenders” because they attenuate low frequency components
12
+ # One pole HighPass and SuperSmoother does not produce a zero mean because low
13
+ # frequency spectral dilation components are "leaking" through The one pole
14
+ # HighPass Filter response
15
+ #
16
+ # == Experimental
17
+ # Across the various texts and papers, Ehlers presents varying implementations
18
+ # of high-pass filters. I believe the two pole high-pass filter is the most
19
+ # consistently presented while the one pole high-pass filter has been presented
20
+ # in a few different ways. In some implementations, alpha is based on simple
21
+ # bars/lag while others use alpha based on phase/trigonometry. I have not been
22
+ # able to reconcile the differences and have not been able to find a definitive
23
+ # source for the correct implementation and do not know enough math to reason
24
+ # these out mathematically nor do I possess an advanced understanding of the
25
+ # fundamentals around digital signal processing. As such, the single-pole
26
+ # high-pass filters in this module are marked as experimental and may be incorrect.
5
27
  module HighPassFilters
6
- # HighPass Filters are “detrenders” because they attenuate low frequency components
7
- # One pole HighPass and SuperSmoother does not produce a zero mean because low
8
- # frequency spectral dilation components are “leaking” through The one pole
9
- # HighPass Filter response
10
- def two_pole_high_pass_filter(source, prev_source, min_period, max_period = nil)
11
- raise "source must be a symbol" unless source.is_a?(Symbol)
12
- return p0.send(source) if p0 == p2
13
-
14
- max_period ||= min_period * 2
15
- (min_period * Math.sqrt(2))
16
- max_radians = 2.0 * Math::PI / (max_period * Math.sqrt(2))
28
+ # A two-pole high-pass filter is a more advanced filtering technique
29
+ # used to remove low-frequency components from financial time series
30
+ # data, such as stock prices or market indices.
31
+ #
32
+ # Similar to a single-pole high-pass filter, a two-pole high-pass filter
33
+ # is designed to attenuate or eliminate slow-moving trends or macroeconomic
34
+ # effects from the data while preserving higher-frequency fluctuations.
35
+ # However, compared to the single-pole filter, the two-pole filter
36
+ # typically offers a steeper roll-off and better attenuation of lower
37
+ # frequencies, resulting in a more pronounced emphasis on short-term fluctuations.
38
+ def two_pole_high_pass_filter(source, period:, previous: :hp)
39
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
40
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
17
41
 
18
- v1 = p0.send(source) - (2.0 * p1.send(source)) + p2.send(source)
19
- v2 = p1.send(prev_source)
20
- v3 = p2.send(prev_source)
42
+ alpha = period_to_alpha(period, k: 0.707)
21
43
 
22
- alpha = period_to_alpha(max_radians)
44
+ v1 = p0.send(source) - (2.0 * p1.send(source)) + p2.send(source)
45
+ v2 = p1.send(previous)
46
+ v3 = p2.send(previous)
23
47
 
24
- a = (1 - (alpha * 0.5))**2 * v1
25
- b = 2 * (1 - alpha) * v2
26
- c = (1 - alpha)**2 * v3
48
+ a = v1 * (1 - (alpha * 0.5))**2
49
+ b = v2 * 2 * (1 - alpha)
50
+ c = v3 * (1 - alpha)**2
27
51
 
28
52
  a + b - c
29
53
  end
30
54
 
55
+ # A single-pole high-pass filter is used to filter out low-frequency
56
+ # components from financial time series data. This type of filter is
57
+ # commonly applied in signal processing techniques to remove noise or
58
+ # unwanted trends from the data while preserving higher-frequency fluctuations.
59
+ #
60
+ # A single-pole high-pass filter can be used to remove slow-moving trends
61
+ # or macroeconomic effects from the data, focusing instead on short-term
62
+ # fluctuations or high-frequency trading signals. By filtering out
63
+ # low-frequency components, traders aim to identify and exploit more
64
+ # immediate market opportunities, such as short-term price movements
65
+ # or momentum signals.
66
+ #
67
+ # The implementation of a single-pole high-pass filter in algorithmic
68
+ # trading typically involves applying a mathematical formula or algorithm
69
+ # to the historical price data of a financial instrument. This algorithm
70
+ # selectively attenuates or removes the low-frequency components of the
71
+ # data, leaving behind the higher-frequency fluctuations that traders
72
+ # are interested in analyzing for potential trading signals.
73
+ #
74
+ # Overall, single-pole high-pass filters in algorithmic trading are
75
+ # used as preprocessing steps to enhance the signal-to-noise ratio in
76
+ # financial data and to extract actionable trading signals from noisy
77
+ # or cluttered market data.
78
+ #
79
+ # == NOTES
31
80
  # alpha = (Cosine(.707* 2 * PI / 48) + Sine (.707*360 / 48) - 1) / Cosine(.707*360 / 48);
32
81
  # is the same as the following:
33
82
  # radians = Math.sqrt(2) * Math::PI / period
34
83
  # alpha = (Math.cos(radians) + Math.sin(radians) - 1) / Math.cos(radians)
35
84
  def high_pass_filter(source, period:, previous: :hp)
85
+ Quant.experimental("This method is unproven and may be incorrect.")
36
86
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
37
-
38
- v0 = p0.send(source)
39
- return v0 if p3 == p0
40
-
41
- v1 = p1.send(source)
42
- v2 = p2.send(source)
87
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
43
88
 
44
89
  radians = Math.sqrt(2) * Math::PI / period
45
90
  a = Math.exp(-radians)
@@ -49,7 +94,35 @@ module Quant
49
94
  c3 = -a**2
50
95
  c1 = (1 + c2 - c3) / 4
51
96
 
52
- (c1 * (v0 - (2 * v1) + v2)) + (c2 * p1.hp) + (c3 * p2.hp)
97
+ v0 = p0.send(source)
98
+ v1 = p1.send(source)
99
+ v2 = p2.send(source)
100
+ f1 = p1.send(previous)
101
+ f2 = p2.send(previous)
102
+
103
+ (c1 * (v0 - (2 * v1) + v2)) + (c2 * f1) + (c3 * f2)
104
+ end
105
+
106
+ # HPF = (1 − α/2)2 * (Price − 2 * Price[1] + Price[2]) + 2 * (1 − α) * HPF[1] − (1 − α)2 * HPF[2];
107
+ # High Pass Filter presented in Ehlers Cybernetic Analysis for Stocks and Futures Equation 2.7
108
+ def hpf2(source, period:, previous:)
109
+ Quant.experimental("This method is unproven and may be incorrect.")
110
+ raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
111
+ raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
112
+
113
+ alpha = period_to_alpha(period, k: 1.0)
114
+ v0 = p0.send(source)
115
+ v1 = p1.send(source)
116
+ v2 = p1.send(source)
117
+
118
+ f1 = p1.send(previous)
119
+ f2 = p2.send(previous)
120
+
121
+ c1 = (1 - alpha / 2)**2
122
+ c2 = 2 * (1 - alpha)
123
+ c3 = (1 - alpha)**2
124
+
125
+ (c1 * (v0 - (2 * v1) + v2)) + (c2 * f1) - (c3 * f2)
53
126
  end
54
127
  end
55
128
  end
@@ -6,39 +6,42 @@ module Quant
6
6
  def two_pole_super_smooth(source, period:, previous: :ss)
7
7
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
8
8
 
9
- radians = Math::PI * Math.sqrt(2) / period
9
+ radians = Math.sqrt(2) * Math::PI / period
10
10
  a1 = Math.exp(-radians)
11
11
 
12
- coef2 = 2.0 * a1 * Math.cos(radians)
13
- coef3 = -a1 * a1
14
- coef1 = 1.0 - coef2 - coef3
12
+ c3 = -a1**2
13
+ c2 = 2.0 * a1 * Math.cos(radians)
14
+ c1 = 1.0 - c2 - c3
15
15
 
16
- v0 = (p0.send(source) + p1.send(source)) / 2.0
17
- v1 = p2.send(previous)
18
- v2 = p3.send(previous)
19
- ((coef1 * v0) + (coef2 * v1) + (coef3 * v2)).to_f
16
+ v1 = (p0.send(source) + p1.send(source)) * 0.5
17
+ v2 = p2.send(previous)
18
+ v3 = p3.send(previous)
19
+
20
+ (c1 * v1) + (c2 * v2) + (c3 * v3)
20
21
  end
22
+
21
23
  alias super_smoother two_pole_super_smooth
22
24
  alias ss2p two_pole_super_smooth
23
25
 
24
26
  def three_pole_super_smooth(source, period:, previous: :ss)
25
27
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
26
28
 
27
- a1 = Math.exp(-Math::PI / period)
28
- b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / period)
29
+ radians = Math::PI / period
30
+ a1 = Math.exp(-radians)
31
+ b1 = 2 * a1 * Math.cos(Math.sqrt(3) * radians)
29
32
  c1 = a1**2
30
33
 
31
- coef2 = b1 + c1
32
- coef3 = -(c1 + b1 * c1)
33
- coef4 = c1**2
34
- coef1 = 1 - coef2 - coef3 - coef4
34
+ c4 = c1**2
35
+ c3 = -(c1 + b1 * c1)
36
+ c2 = b1 + c1
37
+ c1 = 1 - c2 - c3 - c4
35
38
 
36
39
  v0 = p0.send(source)
37
40
  v1 = p1.send(previous)
38
41
  v2 = p2.send(previous)
39
42
  v3 = p3.send(previous)
40
43
 
41
- (coef1 * v0) + (coef2 * v1) + (coef3 * v2) + (coef4 * v3)
44
+ (c1 * v0) + (c2 * v1) + (c3 * v2) + (c4 * v3)
42
45
  end
43
46
  alias ss3p three_pole_super_smooth
44
47
  end
@@ -16,6 +16,20 @@ module Quant
16
16
  # the others are still unproven and Ehlers' many papers over the year
17
17
  # tend to change implementation details, too.
18
18
  #
19
+ # == Experimental!
20
+ # The main goal with the universal filters is to provide a means to
21
+ # compare the optimized filters with the generalized filters and
22
+ # generally show correctness of the solutions. However, that also
23
+ # means validating the outputs of those computations, which is not my forté.
24
+ # My idea of validating is if I have two or more implementations that produce
25
+ # identical (or nearly identical) results, then I consider the implementation
26
+ # sound and doing what it is supposed to do.
27
+ #
28
+ # Several are marked "experimental" because I have not been able to
29
+ # prove their correctness. Those that are proven correct are not
30
+ # marked as experimental and you'll find their outputs show up in other
31
+ # specs where they're used alongside the optimized versions of those filters.
32
+ #
19
33
  # == Ehlers' Notes on Generalized Filters
20
34
  # 1. All the common filters useful for traders have a transfer response
21
35
  # that can be written as a ratio of two polynomials.
@@ -272,7 +286,6 @@ module Quant
272
286
  # not be suitable for all trading or analysis purposes, and its effects
273
287
  # should be evaluated in the context of specific goals and strategies.
274
288
  def universal_two_pole_high_pass(source, previous:, period:)
275
- Quant.experimental("This method is unproven and may be incorrect.")
276
289
  raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
277
290
  raise ArgumentError, ":previous must be a Symbol" unless previous.is_a?(Symbol)
278
291
 
@@ -0,0 +1,37 @@
1
+ module Quant
2
+ module Statistics
3
+ class Correlation
4
+ attr_accessor :length, :sx, :sy, :sxx, :sxy, :syy
5
+
6
+ def initialize
7
+ @length = 0.0
8
+ @sx = 0.0
9
+ @sy = 0.0
10
+ @sxx = 0.0
11
+ @sxy = 0.0
12
+ @syy = 0.0
13
+ end
14
+
15
+ def add(x, y)
16
+ @length += 1
17
+ @sx += x
18
+ @sy += y
19
+ @sxx += x * x
20
+ @sxy += x * y
21
+ @syy += y * y
22
+ end
23
+
24
+ def devisor
25
+ value = (length * sxx - sx**2) * (length * syy - sy**2)
26
+ value.zero? ? 1.0 : value
27
+ end
28
+
29
+ def coefficient
30
+ (length * sxy - sx * sy) / Math.sqrt(devisor)
31
+ rescue Math::DomainError
32
+ 0.0
33
+ end
34
+ end
35
+ end
36
+ end
37
+
data/lib/quant/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
- VERSION = "0.1.10"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/quantitative.rb CHANGED
@@ -12,6 +12,6 @@ quant_folder = File.join(lib_folder, "quant")
12
12
  Dir.glob(File.join(quant_folder, "*.rb")).each { |fn| require fn }
13
13
 
14
14
  # require sub-folders and their sub-folders
15
- %w(refinements mixins settings ticks indicators).each do |sub_folder|
15
+ %w(refinements mixins statistics settings ticks indicators).each do |sub_folder|
16
16
  Dir.glob(File.join(quant_folder, sub_folder, "**/*.rb")).each { |fn| require fn }
17
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quantitative
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Lang
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-03 00:00:00.000000000 Z
11
+ date: 2024-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -51,9 +51,15 @@ files:
51
51
  - lib/quant/errors.rb
52
52
  - lib/quant/experimental.rb
53
53
  - lib/quant/indicators.rb
54
+ - lib/quant/indicators/dominant_cycle_indicators.rb
55
+ - lib/quant/indicators/dominant_cycles/acr.rb
56
+ - lib/quant/indicators/dominant_cycles/band_pass.rb
57
+ - lib/quant/indicators/dominant_cycles/differential.rb
58
+ - lib/quant/indicators/dominant_cycles/dominant_cycle.rb
59
+ - lib/quant/indicators/dominant_cycles/homodyne.rb
60
+ - lib/quant/indicators/dominant_cycles/phase_accumulator.rb
54
61
  - lib/quant/indicators/indicator.rb
55
62
  - lib/quant/indicators/indicator_point.rb
56
- - lib/quant/indicators/ma.rb
57
63
  - lib/quant/indicators/ping.rb
58
64
  - lib/quant/indicators_proxy.rb
59
65
  - lib/quant/indicators_sources.rb
@@ -76,6 +82,7 @@ files:
76
82
  - lib/quant/series.rb
77
83
  - lib/quant/settings.rb
78
84
  - lib/quant/settings/indicators.rb
85
+ - lib/quant/statistics/correlation.rb
79
86
  - lib/quant/ticks/ohlc.rb
80
87
  - lib/quant/ticks/serializers/ohlc.rb
81
88
  - lib/quant/ticks/serializers/spot.rb
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quant
4
- class Indicators
5
- class MaPoint < IndicatorPoint
6
- attribute :ss, key: "ss"
7
- attribute :ema, key: "ema"
8
- attr_accessor :ss, :ema, :osc
9
-
10
- def initialize_data_points
11
- @ss = input
12
- @ema = input
13
- @osc = nil
14
- end
15
- end
16
-
17
- # Moving Averages
18
- class Ma < Indicator
19
- include Quant::Mixins::Filters
20
-
21
- def alpha(period)
22
- bars_to_alpha(period)
23
- end
24
-
25
- def min_period
26
- 8 # Quant.config.indicators.min_period
27
- end
28
-
29
- def max_period
30
- 48 # Quant.config.indicators.max_period
31
- end
32
-
33
- def compute
34
- # p0.ss = super_smoother input, :ss, min_period
35
- p0.ema = alpha(max_period) * input + (1 - alpha(max_period)) * p1.ema
36
- p0.osc = p0.ss - p0.ema
37
- end
38
- end
39
- end
40
- end