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
@@ -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 sample 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
|
|
@@ -21,9 +19,42 @@ module Quant
|
|
21
19
|
@series = series
|
22
20
|
@source = source
|
23
21
|
@points = {}
|
22
|
+
series.new_indicator(self)
|
24
23
|
series.each { |tick| self << tick }
|
25
24
|
end
|
26
25
|
|
26
|
+
def min_period
|
27
|
+
Quant.config.indicators.min_period
|
28
|
+
end
|
29
|
+
|
30
|
+
def max_period
|
31
|
+
Quant.config.indicators.max_period
|
32
|
+
end
|
33
|
+
|
34
|
+
def half_period
|
35
|
+
Quant.config.indicators.half_period
|
36
|
+
end
|
37
|
+
|
38
|
+
def micro_period
|
39
|
+
Quant.config.indicators.micro_period
|
40
|
+
end
|
41
|
+
|
42
|
+
def dominant_cycle_kind
|
43
|
+
Quant.config.indicators.dominant_cycle_kind
|
44
|
+
end
|
45
|
+
|
46
|
+
def pivot_kind
|
47
|
+
Quant.config.indicators.pivot_kind
|
48
|
+
end
|
49
|
+
|
50
|
+
def dominant_cycle
|
51
|
+
series.indicators[source].dominant_cycle
|
52
|
+
end
|
53
|
+
|
54
|
+
def dc_period
|
55
|
+
dominant_cycle.points[t0].period
|
56
|
+
end
|
57
|
+
|
27
58
|
def ticks
|
28
59
|
@points.keys
|
29
60
|
end
|
@@ -45,7 +76,7 @@ module Quant
|
|
45
76
|
|
46
77
|
def <<(tick)
|
47
78
|
@t0 = tick
|
48
|
-
@p0 = points_class.new(tick:, source:)
|
79
|
+
@p0 = points_class.new(indicator: self, tick:, source:)
|
49
80
|
@points[tick] = @p0
|
50
81
|
|
51
82
|
@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
|
@@ -13,12 +13,21 @@ module Quant
|
|
13
13
|
# By design, the {Quant::Indicator} class holds the {Quant::Ticks::Tick} instance
|
14
14
|
# alongside the indicator's computed values for that tick.
|
15
15
|
class IndicatorsProxy
|
16
|
-
attr_reader :series, :source, :indicators
|
16
|
+
attr_reader :series, :source, :dominant_cycle, :indicators
|
17
17
|
|
18
18
|
def initialize(series:, source:)
|
19
19
|
@series = series
|
20
20
|
@source = source
|
21
21
|
@indicators = {}
|
22
|
+
@dominant_cycle = dominant_cycle_indicator
|
23
|
+
end
|
24
|
+
|
25
|
+
def dominant_cycle_indicator
|
26
|
+
kind = Quant.config.indicators.dominant_cycle_kind.to_s
|
27
|
+
base_class_name = kind.split("_").map(&:capitalize).join
|
28
|
+
class_name = "Quant::Indicators::DominantCycles::#{base_class_name}"
|
29
|
+
indicator_class = Object.const_get(class_name)
|
30
|
+
indicator_class.new(series:, source:)
|
22
31
|
end
|
23
32
|
|
24
33
|
# Instantiates the indicator class and stores it in the indicators hash. Once
|
@@ -36,6 +45,7 @@ module Quant
|
|
36
45
|
# The IndicatorsProxy class is not responsible for enforcing
|
37
46
|
# this order of events.
|
38
47
|
def <<(tick)
|
48
|
+
dominant_cycle << tick
|
39
49
|
indicators.each_value { |indicator| indicator << tick }
|
40
50
|
end
|
41
51
|
|
@@ -54,8 +64,5 @@ module Quant
|
|
54
64
|
def attach(name:, indicator_class:)
|
55
65
|
define_singleton_method(name) { indicator(indicator_class) }
|
56
66
|
end
|
57
|
-
|
58
|
-
def ma; indicator(Indicators::Ma) end
|
59
|
-
def ping; indicator(Indicators::Ping) end
|
60
67
|
end
|
61
68
|
end
|
@@ -7,12 +7,22 @@ module Quant
|
|
7
7
|
@indicator_sources = {}
|
8
8
|
end
|
9
9
|
|
10
|
+
def new_indicator(indicator)
|
11
|
+
@indicator_sources[indicator.source] ||= Indicators.new(series: @series, source: indicator.source)
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](source)
|
15
|
+
return @indicator_sources[source] if @indicator_sources.key?(source)
|
16
|
+
|
17
|
+
raise Quant::Errors::InvalidIndicatorSource, "Invalid source, #{source.inspect}."
|
18
|
+
end
|
19
|
+
|
10
20
|
def <<(tick)
|
11
21
|
@indicator_sources.each_value { |indicator| indicator << tick }
|
12
22
|
end
|
13
23
|
|
14
24
|
def oc2
|
15
|
-
@indicator_sources[:oc2] ||=
|
25
|
+
@indicator_sources[:oc2] ||= Indicators.new(series: @series, source: :oc2)
|
16
26
|
end
|
17
27
|
end
|
18
28
|
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
|
|
data/lib/quant/series.rb
CHANGED
@@ -101,6 +101,20 @@ module Quant
|
|
101
101
|
"#<#{self.class.name} symbol=#{symbol} interval=#{interval} ticks=#{ticks.size}>"
|
102
102
|
end
|
103
103
|
|
104
|
+
# When the first indicator is instantiated, it will also lead to instantiating
|
105
|
+
# the dominant cycle indicator. The `new_indicator_lock` prevents reentrant calls
|
106
|
+
# to the `new_indicator` method with infinite recursion.
|
107
|
+
def new_indicator(indicator)
|
108
|
+
return if @new_indicator_lock
|
109
|
+
|
110
|
+
begin
|
111
|
+
@new_indicator_lock = true
|
112
|
+
indicators.new_indicator(indicator)
|
113
|
+
ensure
|
114
|
+
@new_indicator_lock = false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
104
118
|
def <<(tick)
|
105
119
|
tick = Ticks::Spot.new(price: tick) if tick.is_a?(Numeric)
|
106
120
|
indicators << tick unless tick.series?
|
@@ -8,14 +8,16 @@ module Quant
|
|
8
8
|
# papers and books on the subject of technical analysis by John Ehlers where he variously suggests
|
9
9
|
# a minimum period of 8 or 10 and a max period of 48.
|
10
10
|
#
|
11
|
-
# The half period is the average of the max_period and min_period.
|
11
|
+
# The half period is the average of the max_period and min_period. It is read-only and always computed
|
12
|
+
# relative to `min_period` and `max_period`.
|
13
|
+
#
|
12
14
|
# The micro period comes from Ehler's writings on Swami charts and auto-correlation computations, which
|
13
15
|
# is a period of 3 bars. It is useful enough in various indicators to be its own setting.
|
14
16
|
#
|
15
17
|
# The dominant cycle kind is the kind of dominant cycle to use in the indicator. The default is +:settings+
|
16
18
|
# which means the dominant cycle is whatever the +max_period+ is set to. It is not adaptive when configured
|
17
19
|
# this way. The other kinds are adaptive and are computed from the series data. The choices are:
|
18
|
-
# * +:
|
20
|
+
# * +:half_period+ - the half_period is the dominant cycle and is not adaptive
|
19
21
|
# * +:band_pass+ - The zero crossings of the band pass filter are used to compute the dominant cycle
|
20
22
|
# * +:auto_correlation_reversal+ - The dominant cycle is computed from the auto-correlation of the series.
|
21
23
|
# * +:homodyne+ - The dominant cycle is computed from the homodyne discriminator.
|
@@ -48,8 +50,8 @@ module Quant
|
|
48
50
|
new
|
49
51
|
end
|
50
52
|
|
51
|
-
|
52
|
-
attr_accessor :dominant_cycle_kind, :pivot_kind
|
53
|
+
attr_reader :max_period, :min_period, :half_period
|
54
|
+
attr_accessor :micro_period, :dominant_cycle_kind, :pivot_kind
|
53
55
|
|
54
56
|
def initialize(**settings)
|
55
57
|
@max_period = settings[:max_period] || Settings::MAX_PERIOD
|
@@ -64,14 +66,22 @@ module Quant
|
|
64
66
|
def apply_settings(**settings)
|
65
67
|
@max_period = settings.fetch(:max_period, @max_period)
|
66
68
|
@min_period = settings.fetch(:min_period, @min_period)
|
67
|
-
|
69
|
+
compute_half_period
|
68
70
|
@micro_period = settings.fetch(:micro_period, @micro_period)
|
69
71
|
@dominant_cycle_kind = settings.fetch(:dominant_cycle_kind, @dominant_cycle_kind)
|
70
72
|
@pivot_kind = settings.fetch(:pivot_kind, @pivot_kind)
|
71
73
|
end
|
72
74
|
|
75
|
+
def max_period=(value)
|
76
|
+
(@max_period = value).tap { compute_half_period }
|
77
|
+
end
|
78
|
+
|
79
|
+
def min_period=(value)
|
80
|
+
(@min_period = value).tap { compute_half_period }
|
81
|
+
end
|
82
|
+
|
73
83
|
def compute_half_period
|
74
|
-
(max_period + min_period) / 2
|
84
|
+
@half_period = (max_period + min_period) / 2
|
75
85
|
end
|
76
86
|
end
|
77
87
|
end
|
data/lib/quant/settings.rb
CHANGED
@@ -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.1
|
4
|
+
version: 0.2.1
|
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-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -51,9 +51,16 @@ 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/half_period.rb
|
60
|
+
- lib/quant/indicators/dominant_cycles/homodyne.rb
|
61
|
+
- lib/quant/indicators/dominant_cycles/phase_accumulator.rb
|
54
62
|
- lib/quant/indicators/indicator.rb
|
55
63
|
- lib/quant/indicators/indicator_point.rb
|
56
|
-
- lib/quant/indicators/ma.rb
|
57
64
|
- lib/quant/indicators/ping.rb
|
58
65
|
- lib/quant/indicators_proxy.rb
|
59
66
|
- lib/quant/indicators_sources.rb
|
@@ -76,6 +83,7 @@ files:
|
|
76
83
|
- lib/quant/series.rb
|
77
84
|
- lib/quant/settings.rb
|
78
85
|
- lib/quant/settings/indicators.rb
|
86
|
+
- lib/quant/statistics/correlation.rb
|
79
87
|
- lib/quant/ticks/ohlc.rb
|
80
88
|
- lib/quant/ticks/serializers/ohlc.rb
|
81
89
|
- lib/quant/ticks/serializers/spot.rb
|