quantitative 0.1.10 → 0.2.1
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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/quant/attributes.rb +31 -43
- data/lib/quant/config.rb +8 -0
- data/lib/quant/errors.rb +4 -0
- data/lib/quant/indicators/dominant_cycle_indicators.rb +49 -0
- data/lib/quant/indicators/dominant_cycles/acr.rb +101 -0
- data/lib/quant/indicators/dominant_cycles/band_pass.rb +85 -0
- data/lib/quant/indicators/dominant_cycles/differential.rb +21 -0
- data/lib/quant/indicators/dominant_cycles/dominant_cycle.rb +144 -0
- data/lib/quant/indicators/dominant_cycles/half_period.rb +21 -0
- data/lib/quant/indicators/dominant_cycles/homodyne.rb +28 -0
- data/lib/quant/indicators/dominant_cycles/phase_accumulator.rb +59 -0
- data/lib/quant/indicators/indicator.rb +38 -7
- data/lib/quant/indicators/indicator_point.rb +12 -2
- data/lib/quant/indicators.rb +9 -2
- data/lib/quant/indicators_proxy.rb +11 -4
- data/lib/quant/indicators_sources.rb +11 -1
- data/lib/quant/mixins/high_pass_filters.rb +98 -25
- data/lib/quant/mixins/super_smoother.rb +18 -15
- data/lib/quant/mixins/universal_filters.rb +14 -1
- data/lib/quant/series.rb +14 -0
- data/lib/quant/settings/indicators.rb +16 -6
- data/lib/quant/settings.rb +1 -1
- data/lib/quant/statistics/correlation.rb +37 -0
- data/lib/quant/version.rb +1 -1
- data/lib/quantitative.rb +1 -1
- metadata +11 -3
- data/lib/quant/indicators/ma.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf0aa73684247efc7bc9750c27c856a094fa362d99c1eba73bd70457da8f3a97
|
4
|
+
data.tar.gz: 56dcaa82230328cb44149ceb957420484217e33c6f601ea135997391bfb95440
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b674a5e4408a69039cf2c7ad2378d065e3f88b409a7530108af1bfc5a186a02690cba8ffb1c8d40a87ad69368f302d9eec0211135beec7db6a9c51c4e1be46d3
|
7
|
+
data.tar.gz: e7d7693cd878ebe777f81ca0d90b4f305e645c8547aa9f420cccb4a4f9b53920211a2b7f49af146508756e6e38f5cc7e0c4b57f1c8abdd722f65bd3409abd02f
|
data/Gemfile.lock
CHANGED
data/lib/quant/attributes.rb
CHANGED
@@ -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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
118
|
-
|
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
|
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
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
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
|
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
|
-
|
163
|
-
|
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
|
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
|
-
|
177
|
-
|
164
|
+
value = send(name)
|
165
|
+
next unless value
|
178
166
|
|
179
|
-
key_values[entry[:key]] = value
|
167
|
+
key_values[entry[:key]] = value
|
180
168
|
end
|
181
169
|
end
|
182
170
|
end
|
data/lib/quant/config.rb
CHANGED
@@ -14,6 +14,10 @@ module Quant
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
def self.default!
|
18
|
+
@config = Config.new
|
19
|
+
end
|
20
|
+
|
17
21
|
def self.config
|
18
22
|
@config ||= Config.new
|
19
23
|
end
|
@@ -25,6 +29,10 @@ module Quant
|
|
25
29
|
Config.config
|
26
30
|
end
|
27
31
|
|
32
|
+
def default_configuration!
|
33
|
+
Config.default!
|
34
|
+
end
|
35
|
+
|
28
36
|
def configure_indicators(**settings)
|
29
37
|
config.apply_indicator_settings(**settings)
|
30
38
|
yield config.indicators if block_given?
|
data/lib/quant/errors.rb
CHANGED
@@ -10,6 +10,10 @@ module Quant
|
|
10
10
|
# {Quant::Interval} with an invalid value.
|
11
11
|
class InvalidInterval < Error; end
|
12
12
|
|
13
|
+
# {InvalidIndicatorSource} is raised when attempting to reference
|
14
|
+
# an indicator through a source that has not been prepared, yet.
|
15
|
+
class InvalidIndicatorSource < Error; end
|
16
|
+
|
13
17
|
# {InvalidResolution} is raised when attempting to instantiate
|
14
18
|
# an {Quant::Resolution} with a resolution value that has not been defined.
|
15
19
|
class InvalidResolution < Error; end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Quant
|
2
|
+
# Dominant Cycles measure the primary cycle within a given range. By default, the library
|
3
|
+
# is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
|
4
|
+
# the `min_period` and `max_period` configuration values in {Quant::Config}.
|
5
|
+
#
|
6
|
+
# Quant.configure_indicators(min_period: 8, max_period: 32)
|
7
|
+
#
|
8
|
+
# The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
|
9
|
+
# the `dominant_cycle_kind` configuration value in {Quant::Config}.
|
10
|
+
#
|
11
|
+
# Quant.configure_indicators(dominant_cycle_kind: :band_pass)
|
12
|
+
#
|
13
|
+
# The purpose of these indicators is to compute the dominant cycle and underpin the various
|
14
|
+
# indicators that would otherwise be setting an arbitrary lookback period. This makes the
|
15
|
+
# indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
|
16
|
+
class DominantCycleIndicators < IndicatorsProxy
|
17
|
+
# Auto-Correlation Reversals is a method of computing the dominant cycle
|
18
|
+
# by correlating the data stream with itself delayed by a lag.
|
19
|
+
def acr; indicator(Indicators::DominantCycles::Acr) end
|
20
|
+
|
21
|
+
# The band-pass dominant cycle passes signals within a certain frequency
|
22
|
+
# range, and attenuates signals outside that range.
|
23
|
+
# The trend component of the signal is removed, leaving only the cyclical
|
24
|
+
# component. Then we count number of iterations between zero crossings
|
25
|
+
# and this is the `period` of the dominant cycle.
|
26
|
+
def band_pass; indicator(Indicators::DominantCycles::BandPass) end
|
27
|
+
|
28
|
+
# Homodyne means the signal is multiplied by itself. More precisely,
|
29
|
+
# we want to multiply the signal of the current bar with the complex
|
30
|
+
# value of the signal one bar ago
|
31
|
+
def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
|
32
|
+
|
33
|
+
# The Dual Differentiator algorithm computes the phase angle from the
|
34
|
+
# analytic signal as the arctangent of the ratio of the imaginary
|
35
|
+
# component to the real component. Further, the angular frequency
|
36
|
+
# is defined as the rate change of phase. We can use these facts to
|
37
|
+
# derive the cycle period.
|
38
|
+
def differential; indicator(Indicators::DominantCycles::Differential) end
|
39
|
+
|
40
|
+
# The phase accumulation method of computing the dominant cycle measures
|
41
|
+
# the phase at each sample by taking the arctangent of the ratio of the
|
42
|
+
# quadrature component to the in-phase component. The phase is then
|
43
|
+
# accumulated and the period is derived from the phase.
|
44
|
+
def phase_accumulator; indicator(Indicators::DominantCycles::PhaseAccumulator) end
|
45
|
+
|
46
|
+
# Static, arbitrarily set period.
|
47
|
+
def half_period; indicator(Indicators::DominantCycles::HalfPeriod) end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require_relative "../indicator_point"
|
2
|
+
require_relative "dominant_cycle"
|
3
|
+
|
4
|
+
module Quant
|
5
|
+
class Indicators
|
6
|
+
class DominantCycles
|
7
|
+
class AcrPoint < DominantCyclePoint
|
8
|
+
attribute :hp, default: 0.0
|
9
|
+
attribute :filter, default: 0.0
|
10
|
+
attribute :interim_period, default: 0.0
|
11
|
+
attribute :inst_period, default: :min_period
|
12
|
+
attribute :period, default: 0.0
|
13
|
+
attribute :sp, default: 0.0
|
14
|
+
attribute :spx, default: 0.0
|
15
|
+
attribute :maxpwr, default: 0.0
|
16
|
+
attribute :r1, default: -> { Hash.new(0.0) }
|
17
|
+
attribute :corr, default: -> { Hash.new(0.0) }
|
18
|
+
attribute :pwr, default: -> { Hash.new(0.0) }
|
19
|
+
attribute :cospart, default: -> { Hash.new(0.0) }
|
20
|
+
attribute :sinpart, default: -> { Hash.new(0.0) }
|
21
|
+
attribute :sqsum, default: -> { Hash.new(0.0) }
|
22
|
+
attribute :reversal, default: false
|
23
|
+
end
|
24
|
+
|
25
|
+
# Auto-Correlation Reversals is a method of computing the dominant cycle
|
26
|
+
# by correlating the data stream with itself delayed by a lag.
|
27
|
+
# Construction of the autocorrelation periodogram starts with the
|
28
|
+
# autocorrelation function using the minimum three bars of averaging.
|
29
|
+
# The cyclic information is extracted using a discrete Fourier transform
|
30
|
+
# (DFT) of the autocorrelation results.
|
31
|
+
class Acr < DominantCycle
|
32
|
+
BANDWIDTH_DEGREES = 370
|
33
|
+
BANDWIDTH_RADIANS = BANDWIDTH_DEGREES * Math::PI / 180.0
|
34
|
+
|
35
|
+
def compute_auto_correlations
|
36
|
+
(min_period..max_period).each do |period|
|
37
|
+
corr = Statistics::Correlation.new
|
38
|
+
micro_period.times do |lookback_period|
|
39
|
+
corr.add(p(lookback_period).filter, p(period + lookback_period).filter)
|
40
|
+
end
|
41
|
+
p0.corr[period] = corr.coefficient
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def compute_powers
|
46
|
+
p0.maxpwr = 0.995 * p1.maxpwr
|
47
|
+
|
48
|
+
(min_period..max_period).each do |period|
|
49
|
+
(micro_period..max_period).each do |n|
|
50
|
+
radians = BANDWIDTH_RADIANS * n / period
|
51
|
+
p0.cospart[period] += p0.corr[n] * Math.cos(radians)
|
52
|
+
p0.sinpart[period] += p0.corr[n] * Math.sin(radians)
|
53
|
+
end
|
54
|
+
p0.sqsum[period] = p0.cospart[period]**2 + p0.sinpart[period]**2
|
55
|
+
p0.r1[period] = (0.2 * p0.sqsum[period]**2) + (0.8 * p1.r1[period])
|
56
|
+
p0.pwr[period] = p0.r1[period]
|
57
|
+
p0.maxpwr = [p0.maxpwr, p0.r1[period]].max
|
58
|
+
end
|
59
|
+
return if p0.maxpwr.zero?
|
60
|
+
|
61
|
+
(min_period..max_period).each do |period|
|
62
|
+
p0.pwr[period] = p0.r1[period] / p0.maxpwr
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def compute_period
|
67
|
+
(min_period..max_period).each do |period|
|
68
|
+
if p0.pwr[period] >= 0.4
|
69
|
+
p0.spx += (period * p0.pwr[period])
|
70
|
+
p0.sp += p0.pwr[period]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
p0.interim_period = p0.sp.zero? ? p1.period : p0.spx / p0.sp
|
75
|
+
p0.inst_period = two_pole_butterworth(:interim_period, previous: :period, period: min_period)
|
76
|
+
p0.period = p0.inst_period.round(0)
|
77
|
+
end
|
78
|
+
|
79
|
+
def compute_reversal
|
80
|
+
sum_deltas = 0
|
81
|
+
(min_period..max_period).each do |period|
|
82
|
+
sc1 = (p0.corr[period] + 1) * 0.5
|
83
|
+
sc2 = (p1.corr[period] + 1) * 0.5
|
84
|
+
sum_deltas += 1 if (sc1 > 0.5 && sc2 < 0.5) || (sc1 < 0.5 && sc2 > 0.5)
|
85
|
+
end
|
86
|
+
p0.reversal = sum_deltas > 24
|
87
|
+
end
|
88
|
+
|
89
|
+
def compute
|
90
|
+
p0.hp = two_pole_high_pass_filter(:input, period: max_period)
|
91
|
+
p0.filter = two_pole_butterworth(:hp, previous: :filter, period: min_period)
|
92
|
+
|
93
|
+
compute_auto_correlations
|
94
|
+
compute_powers
|
95
|
+
compute_period
|
96
|
+
compute_reversal
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require_relative "dominant_cycle"
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
class DominantCycles
|
6
|
+
class BandPassPoint < Quant::Indicators::IndicatorPoint
|
7
|
+
attribute :hp, default: 0.0
|
8
|
+
attribute :bp, default: 0.0
|
9
|
+
attribute :counter, default: 0
|
10
|
+
attribute :period, default: :half_period
|
11
|
+
attribute :peak, default: :half_period
|
12
|
+
attribute :real, default: :half_period
|
13
|
+
attribute :crosses, default: false
|
14
|
+
attribute :direction, default: :flat
|
15
|
+
end
|
16
|
+
|
17
|
+
# The band-pass dominant cycle passes signals within a certain frequency
|
18
|
+
# range, and attenuates signals outside that range.
|
19
|
+
# The trend component of the signal is revoved, leaving only the cyclical
|
20
|
+
# component. Then we count number of iterations between zero crossings
|
21
|
+
# and this is the `period` of the dominant cycle.
|
22
|
+
class BandPass < DominantCycle
|
23
|
+
def bandwidth
|
24
|
+
0.75
|
25
|
+
end
|
26
|
+
|
27
|
+
# alpha2 = (Cosine(.25*Bandwidth*360 / Period) +
|
28
|
+
# Sine(.25*Bandwidth*360 / Period) - 1) / Cosine(.25*Bandwidth*360 / Period);
|
29
|
+
# HP = (1 + alpha2 / 2)*(Close - Close[1]) + (1- alpha2)*HP[1];
|
30
|
+
# beta = Cosine(360 / Period);
|
31
|
+
# gamma = 1 / Cosine(360*Bandwidth / Period);
|
32
|
+
# alpha = gamma - SquareRoot(gamma*gamma - 1);
|
33
|
+
# BP = .5*(1 - alpha)*(HP - HP[2]) + beta*(1 + alpha)*BP[1] - alpha*BP[2];
|
34
|
+
# If Currentbar = 1 or CurrentBar = 2 then BP = 0;
|
35
|
+
|
36
|
+
# Peak = .991*Peak;
|
37
|
+
# If AbsValue(BP) > If Peak <> 0 Then DC = DC[1];
|
38
|
+
# If DC < 6 Then DC counter = counter
|
39
|
+
# If Real Crosses Over 0 or Real Crosses Under 0 Then Begin
|
40
|
+
# DC = 2*counter;
|
41
|
+
# If 2*counter > 1.25*DC[1] Then DC = 1.25*DC[1];
|
42
|
+
# If 2*counter < .8*DC[1] Then DC = .8*DC[1];
|
43
|
+
# counter = 0;
|
44
|
+
# End;
|
45
|
+
|
46
|
+
def compute_high_pass
|
47
|
+
alpha = period_to_alpha(max_period, k: 0.25 * bandwidth)
|
48
|
+
p0.hp = (1 + alpha / 2) * (p0.input - p1.input) + (1 - alpha) * p1.hp
|
49
|
+
end
|
50
|
+
|
51
|
+
def compute_band_pass
|
52
|
+
radians = deg2rad(360.0 / max_period)
|
53
|
+
beta = Math.cos(radians)
|
54
|
+
gamma = 1.0 / Math.cos(bandwidth * radians)
|
55
|
+
alpha = gamma - Math.sqrt(gamma**2 - 1.0)
|
56
|
+
|
57
|
+
a = 0.5 * (1 - alpha) * (p0.hp - p2.hp)
|
58
|
+
b = beta * (1 + alpha) * p1.bp
|
59
|
+
c = alpha * p2.bp
|
60
|
+
p0.bp = a + b - c
|
61
|
+
end
|
62
|
+
|
63
|
+
def compute_period
|
64
|
+
p0.peak = [0.991 * p1.peak, p0.bp.abs].max
|
65
|
+
p0.real = p0.bp / p0.peak unless p0.peak.zero?
|
66
|
+
p0.counter = p1.counter + 1
|
67
|
+
p0.period = [p1.period, min_period].max.to_i
|
68
|
+
p0.crosses = (p0.real > 0.0 && p1.real < 0.0) || (p0.real < 0.0 && p1.real > 0.0)
|
69
|
+
if (p0.real >= 0.0 && p1.real < 0.0) || (p0.real <= 0.0 && p1.real > 0.0)
|
70
|
+
p0.period = [2 * p0.counter, 1.25 * p1.period].min.to_i
|
71
|
+
p0.period = [p0.period, 0.8 * p1.period].max.to_i
|
72
|
+
p0.counter = 0
|
73
|
+
end
|
74
|
+
p0.direction = p0.real > (p1.real + p2.real + p3.real) / 3.0 ? :up : :down
|
75
|
+
end
|
76
|
+
|
77
|
+
def compute
|
78
|
+
compute_high_pass
|
79
|
+
compute_band_pass
|
80
|
+
compute_period
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Quant
|
2
|
+
class Indicators
|
3
|
+
class DominantCycles
|
4
|
+
# The Dual Differentiator algorithm computes the phase angle from the
|
5
|
+
# analytic signal as the arctangent of the ratio of the imaginary
|
6
|
+
# component to the real component. Further, the angular frequency
|
7
|
+
# is defined as the rate change of phase. We can use these facts to
|
8
|
+
# derive the cycle period.
|
9
|
+
class Differential < DominantCycle
|
10
|
+
def compute_period
|
11
|
+
p0.ddd = (p0.q2 * (p0.i2 - p1.i2)) - (p0.i2 * (p0.q2 - p1.q2))
|
12
|
+
p0.inst_period = p0.ddd > 0.01 ? 6.2832 * (p0.i2**2 + p0.q2**2) / p0.ddd : 0.0
|
13
|
+
|
14
|
+
constrain_period_magnitude_change
|
15
|
+
constrain_period_bars
|
16
|
+
p0.period = p0.inst_period.round(0).to_i
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require_relative "../indicator"
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
# Dominant Cycles measure the primary cycle within a given range. By default, the library
|
6
|
+
# is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
|
7
|
+
# the `min_period` and `max_period` configuration values in {Quant::Config}.
|
8
|
+
#
|
9
|
+
# Quant.configure_indicators(min_period: 8, max_period: 32)
|
10
|
+
#
|
11
|
+
# The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
|
12
|
+
# the `dominant_cycle_kind` configuration value in {Quant::Config}.
|
13
|
+
#
|
14
|
+
# Quant.configure_indicators(dominant_cycle_kind: :band_pass)
|
15
|
+
#
|
16
|
+
# The purpose of these indicators is to compute the dominant cycle and underpin the various
|
17
|
+
# indicators that would otherwise be setting an arbitrary lookback period. This makes the
|
18
|
+
# indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
|
19
|
+
class DominantCycles
|
20
|
+
class DominantCyclePoint < Quant::Indicators::IndicatorPoint
|
21
|
+
attribute :smooth, default: 0.0
|
22
|
+
attribute :detrend, default: 0.0
|
23
|
+
attribute :inst_period, default: :min_period
|
24
|
+
attribute :period, key: "p", default: nil # intentially nil! (see: compute_period)
|
25
|
+
attribute :smooth_period, key: "sp", default: :min_period
|
26
|
+
attribute :mean_period, key: "mp", default: :min_period
|
27
|
+
attribute :ddd, default: 0.0
|
28
|
+
attribute :q1, default: 0.0
|
29
|
+
attribute :q2, default: 0.0
|
30
|
+
attribute :i1, default: 0.0
|
31
|
+
attribute :i2, default: 0.0
|
32
|
+
attribute :ji, default: 0.0
|
33
|
+
attribute :jq, default: 0.0
|
34
|
+
attribute :re, default: 0.0
|
35
|
+
attribute :im, default: 0.0
|
36
|
+
attribute :phase, default: 0.0
|
37
|
+
attribute :phase_sum, key: "ps", default: 0.0
|
38
|
+
attribute :delta_phase, default: 0.0
|
39
|
+
attribute :accumulator_phase, default: 0.0
|
40
|
+
attribute :real_part, default: 0.0
|
41
|
+
attribute :imag_part, default: 0.0
|
42
|
+
end
|
43
|
+
|
44
|
+
class DominantCycle < Indicators::Indicator
|
45
|
+
def points_class
|
46
|
+
Object.const_get "Quant::Indicators::DominantCycles::#{indicator_name}Point"
|
47
|
+
rescue NameError
|
48
|
+
DominantCyclePoint
|
49
|
+
end
|
50
|
+
|
51
|
+
# constrain between min_period and max_period bars
|
52
|
+
def constrain_period_bars
|
53
|
+
p0.inst_period = p0.inst_period.clamp(min_period, max_period)
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :points
|
57
|
+
|
58
|
+
# constrain magnitude of change in phase
|
59
|
+
def constrain_period_magnitude_change
|
60
|
+
p0.inst_period = [1.5 * p1.inst_period, p0.inst_period].min
|
61
|
+
p0.inst_period = [0.67 * p1.inst_period, p0.inst_period].max
|
62
|
+
end
|
63
|
+
|
64
|
+
# amplitude correction using previous period value
|
65
|
+
def compute_smooth_period
|
66
|
+
p0.inst_period = (0.2 * p0.inst_period) + (0.8 * p1.inst_period)
|
67
|
+
p0.smooth_period = (0.33333 * p0.inst_period) + (0.666667 * p1.smooth_period)
|
68
|
+
end
|
69
|
+
|
70
|
+
def compute_mean_period
|
71
|
+
ss_period = super_smoother(:smooth_period, previous: :mean_period, period: micro_period)
|
72
|
+
p0.mean_period = ss_period.clamp(min_period, max_period)
|
73
|
+
end
|
74
|
+
|
75
|
+
def dominant_cycle_period
|
76
|
+
[p0.period.to_i, min_period].max
|
77
|
+
end
|
78
|
+
|
79
|
+
def period_points(max_period)
|
80
|
+
extent = [values.size, max_period].min
|
81
|
+
values[-extent, extent]
|
82
|
+
end
|
83
|
+
|
84
|
+
def compute
|
85
|
+
compute_input_data_points
|
86
|
+
compute_quadrature_components
|
87
|
+
compute_period
|
88
|
+
compute_smooth_period
|
89
|
+
compute_mean_period
|
90
|
+
compute_phase
|
91
|
+
end
|
92
|
+
|
93
|
+
def compute_input_data_points
|
94
|
+
p0.smooth = wma :input
|
95
|
+
p0.detrend = hilbert_transform :smooth, period: p1.inst_period
|
96
|
+
end
|
97
|
+
|
98
|
+
# NOTE: The phase lag of q1 and `i1 is (360 * 7 / Period - 90)` degrees
|
99
|
+
# where Period is the dominant cycle period.
|
100
|
+
def compute_quadrature_components
|
101
|
+
# { Compute Inphase and Quadrature components }
|
102
|
+
p0.q1 = hilbert_transform :detrend, period: p1.inst_period
|
103
|
+
p0.i1 = p3.detrend
|
104
|
+
|
105
|
+
# { Advance the phase of I1 and Q1 by 90 degrees }
|
106
|
+
p0.ji = hilbert_transform :i1, period: p1.inst_period
|
107
|
+
p0.jq = hilbert_transform :q1, period: p1.inst_period
|
108
|
+
|
109
|
+
# { Smooth the I and Q components before applying the discriminator }
|
110
|
+
p0.i2 = (0.2 * (p0.i1 - p0.jq)) + 0.8 * (p1.i2 || (p0.i1 - p0.jq))
|
111
|
+
p0.q2 = (0.2 * (p0.q1 + p0.ji)) + 0.8 * (p1.q2 || (p0.q1 + p0.ji))
|
112
|
+
end
|
113
|
+
|
114
|
+
def compute_period
|
115
|
+
raise NotImplementedError
|
116
|
+
end
|
117
|
+
|
118
|
+
def compute_phase
|
119
|
+
raise "must compute period before calling!" unless p0.period
|
120
|
+
|
121
|
+
period_points(dominant_cycle_period).map(&:smooth).each_with_index do |smooth, index|
|
122
|
+
radians = deg2rad((1 + index) * 360.0 / dominant_cycle_period)
|
123
|
+
p0.real_part += smooth * Math.sin(radians)
|
124
|
+
p0.imag_part += smooth * Math.cos(radians)
|
125
|
+
end
|
126
|
+
|
127
|
+
if p0.imag_part.zero?
|
128
|
+
p0.phase = 90.0 * (p0.real_part.positive? ? 1 : 0)
|
129
|
+
else
|
130
|
+
radians = deg2rad(p0.real_part / p0.imag_part)
|
131
|
+
p0.phase = rad2deg(Math.atan(radians))
|
132
|
+
end
|
133
|
+
p0.phase += 90
|
134
|
+
# { Compensate for one bar lag of the Weighted Moving Average }
|
135
|
+
p0.phase += (360.0 / p0.inst_period)
|
136
|
+
|
137
|
+
p0.phase += 180.0 if p0.imag_part < 0.0
|
138
|
+
p0.phase -= 360.0 if p0.phase > 315.0
|
139
|
+
p0.delta_phase = [1.0, p1.phase - p0.phase].max
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative "dominant_cycle"
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
class DominantCycles
|
6
|
+
# This dominant cycle indicator is based on the half period
|
7
|
+
# that is the midpoint of the `min_period` and `max_period`
|
8
|
+
# configured in the `Quant.config.indicators` object.
|
9
|
+
# Effectively providing a static, arbitrarily set period.
|
10
|
+
class HalfPeriodPoint < Quant::Indicators::IndicatorPoint
|
11
|
+
attribute :period, default: :half_period
|
12
|
+
end
|
13
|
+
|
14
|
+
class HalfPeriod < DominantCycle
|
15
|
+
def compute
|
16
|
+
# No-Op
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative "../indicator_point"
|
2
|
+
require_relative "dominant_cycle"
|
3
|
+
|
4
|
+
module Quant
|
5
|
+
class Indicators
|
6
|
+
class DominantCycles
|
7
|
+
# Homodyne means the signal is multiplied by itself. More precisely,
|
8
|
+
# we want to multiply the signal of the current bar with the complex
|
9
|
+
# value of the signal one bar ago
|
10
|
+
class Homodyne < DominantCycle
|
11
|
+
def compute_period
|
12
|
+
p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
|
13
|
+
p0.im = (p0.i2 * p1.q2) - (p0.q2 * p1.i2)
|
14
|
+
|
15
|
+
p0.re = (0.2 * p0.re) + (0.8 * p1.re)
|
16
|
+
p0.im = (0.2 * p0.im) + (0.8 * p1.im)
|
17
|
+
|
18
|
+
p0.inst_period = 360.0 / rad2deg(Math.atan(p0.im/p0.re)) if (p0.im != 0) && (p0.re != 0)
|
19
|
+
|
20
|
+
constrain_period_magnitude_change
|
21
|
+
constrain_period_bars
|
22
|
+
p0.mean_period = super_smoother :inst_period, previous: :mean_period, period: max_period
|
23
|
+
p0.period = p0.mean_period.round(0).to_i
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|