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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/quant/attributes.rb +31 -43
- data/lib/quant/config.rb +8 -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 +29 -7
- 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/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/statistics/correlation.rb +37 -0
- data/lib/quant/version.rb +1 -1
- data/lib/quantitative.rb +1 -1
- metadata +10 -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: 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/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,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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
data/lib/quant/indicators.rb
CHANGED
@@ -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
|
5
|
-
|
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
|
@@ -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
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
-
v2 = p1.send(prev_source)
|
20
|
-
v3 = p2.send(prev_source)
|
42
|
+
alpha = period_to_alpha(period, k: 0.707)
|
21
43
|
|
22
|
-
|
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
|
25
|
-
b = 2 * (1 - alpha)
|
26
|
-
c = (1 - alpha)**2
|
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
|
-
|
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
|
9
|
+
radians = Math.sqrt(2) * Math::PI / period
|
10
10
|
a1 = Math.exp(-radians)
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
c3 = -a1**2
|
13
|
+
c2 = 2.0 * a1 * Math.cos(radians)
|
14
|
+
c1 = 1.0 - c2 - c3
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
(
|
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
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.
|
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-
|
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
|
data/lib/quant/indicators/ma.rb
DELETED
@@ -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
|