quantitative 0.1.10 → 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.
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