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