quantitative 0.2.0 → 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/errors.rb +4 -0
- data/lib/quant/indicators/dominant_cycle_indicators.rb +39 -0
- data/lib/quant/indicators/dominant_cycles/acr.rb +12 -12
- data/lib/quant/indicators/dominant_cycles/band_pass.rb +5 -0
- data/lib/quant/indicators/dominant_cycles/differential.rb +5 -3
- data/lib/quant/indicators/dominant_cycles/dominant_cycle.rb +16 -0
- data/lib/quant/indicators/dominant_cycles/half_period.rb +21 -0
- data/lib/quant/indicators/dominant_cycles/homodyne.rb +3 -2
- data/lib/quant/indicators/dominant_cycles/phase_accumulator.rb +1 -1
- data/lib/quant/indicators/indicator.rb +9 -0
- data/lib/quant/indicators_proxy.rb +11 -1
- data/lib/quant/indicators_sources.rb +10 -0
- 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/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf0aa73684247efc7bc9750c27c856a094fa362d99c1eba73bd70457da8f3a97
|
4
|
+
data.tar.gz: 56dcaa82230328cb44149ceb957420484217e33c6f601ea135997391bfb95440
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b674a5e4408a69039cf2c7ad2378d065e3f88b409a7530108af1bfc5a186a02690cba8ffb1c8d40a87ad69368f302d9eec0211135beec7db6a9c51c4e1be46d3
|
7
|
+
data.tar.gz: e7d7693cd878ebe777f81ca0d90b4f305e645c8547aa9f420cccb4a4f9b53920211a2b7f49af146508756e6e38f5cc7e0c4b57f1c8abdd722f65bd3409abd02f
|
data/Gemfile.lock
CHANGED
data/lib/quant/errors.rb
CHANGED
@@ -10,6 +10,10 @@ module Quant
|
|
10
10
|
# {Quant::Interval} with an invalid value.
|
11
11
|
class InvalidInterval < Error; end
|
12
12
|
|
13
|
+
# {InvalidIndicatorSource} is raised when attempting to reference
|
14
|
+
# an indicator through a source that has not been prepared, yet.
|
15
|
+
class InvalidIndicatorSource < Error; end
|
16
|
+
|
13
17
|
# {InvalidResolution} is raised when attempting to instantiate
|
14
18
|
# an {Quant::Resolution} with a resolution value that has not been defined.
|
15
19
|
class InvalidResolution < Error; end
|
@@ -1,10 +1,49 @@
|
|
1
1
|
module Quant
|
2
|
+
# Dominant Cycles measure the primary cycle within a given range. By default, the library
|
3
|
+
# is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
|
4
|
+
# the `min_period` and `max_period` configuration values in {Quant::Config}.
|
5
|
+
#
|
6
|
+
# Quant.configure_indicators(min_period: 8, max_period: 32)
|
7
|
+
#
|
8
|
+
# The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
|
9
|
+
# the `dominant_cycle_kind` configuration value in {Quant::Config}.
|
10
|
+
#
|
11
|
+
# Quant.configure_indicators(dominant_cycle_kind: :band_pass)
|
12
|
+
#
|
13
|
+
# The purpose of these indicators is to compute the dominant cycle and underpin the various
|
14
|
+
# indicators that would otherwise be setting an arbitrary lookback period. This makes the
|
15
|
+
# indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
|
2
16
|
class DominantCycleIndicators < IndicatorsProxy
|
17
|
+
# Auto-Correlation Reversals is a method of computing the dominant cycle
|
18
|
+
# by correlating the data stream with itself delayed by a lag.
|
3
19
|
def acr; indicator(Indicators::DominantCycles::Acr) end
|
20
|
+
|
21
|
+
# The band-pass dominant cycle passes signals within a certain frequency
|
22
|
+
# range, and attenuates signals outside that range.
|
23
|
+
# The trend component of the signal is removed, leaving only the cyclical
|
24
|
+
# component. Then we count number of iterations between zero crossings
|
25
|
+
# and this is the `period` of the dominant cycle.
|
4
26
|
def band_pass; indicator(Indicators::DominantCycles::BandPass) end
|
27
|
+
|
28
|
+
# Homodyne means the signal is multiplied by itself. More precisely,
|
29
|
+
# we want to multiply the signal of the current bar with the complex
|
30
|
+
# value of the signal one bar ago
|
5
31
|
def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
|
6
32
|
|
33
|
+
# The Dual Differentiator algorithm computes the phase angle from the
|
34
|
+
# analytic signal as the arctangent of the ratio of the imaginary
|
35
|
+
# component to the real component. Further, the angular frequency
|
36
|
+
# is defined as the rate change of phase. We can use these facts to
|
37
|
+
# derive the cycle period.
|
7
38
|
def differential; indicator(Indicators::DominantCycles::Differential) end
|
39
|
+
|
40
|
+
# The phase accumulation method of computing the dominant cycle measures
|
41
|
+
# the phase at each sample by taking the arctangent of the ratio of the
|
42
|
+
# quadrature component to the in-phase component. The phase is then
|
43
|
+
# accumulated and the period is derived from the phase.
|
8
44
|
def phase_accumulator; indicator(Indicators::DominantCycles::PhaseAccumulator) end
|
45
|
+
|
46
|
+
# Static, arbitrarily set period.
|
47
|
+
def half_period; indicator(Indicators::DominantCycles::HalfPeriod) end
|
9
48
|
end
|
10
49
|
end
|
@@ -22,20 +22,20 @@ module Quant
|
|
22
22
|
attribute :reversal, default: false
|
23
23
|
end
|
24
24
|
|
25
|
-
# Auto-Correlation Reversals
|
25
|
+
# Auto-Correlation Reversals is a method of computing the dominant cycle
|
26
|
+
# by correlating the data stream with itself delayed by a lag.
|
27
|
+
# Construction of the autocorrelation periodogram starts with the
|
28
|
+
# autocorrelation function using the minimum three bars of averaging.
|
29
|
+
# The cyclic information is extracted using a discrete Fourier transform
|
30
|
+
# (DFT) of the autocorrelation results.
|
26
31
|
class Acr < DominantCycle
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
def bandwidth
|
32
|
-
deg2rad(370)
|
33
|
-
end
|
32
|
+
BANDWIDTH_DEGREES = 370
|
33
|
+
BANDWIDTH_RADIANS = BANDWIDTH_DEGREES * Math::PI / 180.0
|
34
34
|
|
35
35
|
def compute_auto_correlations
|
36
36
|
(min_period..max_period).each do |period|
|
37
37
|
corr = Statistics::Correlation.new
|
38
|
-
|
38
|
+
micro_period.times do |lookback_period|
|
39
39
|
corr.add(p(lookback_period).filter, p(period + lookback_period).filter)
|
40
40
|
end
|
41
41
|
p0.corr[period] = corr.coefficient
|
@@ -46,8 +46,8 @@ module Quant
|
|
46
46
|
p0.maxpwr = 0.995 * p1.maxpwr
|
47
47
|
|
48
48
|
(min_period..max_period).each do |period|
|
49
|
-
(
|
50
|
-
radians =
|
49
|
+
(micro_period..max_period).each do |n|
|
50
|
+
radians = BANDWIDTH_RADIANS * n / period
|
51
51
|
p0.cospart[period] += p0.corr[n] * Math.cos(radians)
|
52
52
|
p0.sinpart[period] += p0.corr[n] * Math.sin(radians)
|
53
53
|
end
|
@@ -98,4 +98,4 @@ module Quant
|
|
98
98
|
end
|
99
99
|
end
|
100
100
|
end
|
101
|
-
end
|
101
|
+
end
|
@@ -14,6 +14,11 @@ module Quant
|
|
14
14
|
attribute :direction, default: :flat
|
15
15
|
end
|
16
16
|
|
17
|
+
# The band-pass dominant cycle passes signals within a certain frequency
|
18
|
+
# range, and attenuates signals outside that range.
|
19
|
+
# The trend component of the signal is revoved, leaving only the cyclical
|
20
|
+
# component. Then we count number of iterations between zero crossings
|
21
|
+
# and this is the `period` of the dominant cycle.
|
17
22
|
class BandPass < DominantCycle
|
18
23
|
def bandwidth
|
19
24
|
0.75
|
@@ -1,9 +1,11 @@
|
|
1
1
|
module Quant
|
2
2
|
class Indicators
|
3
3
|
class DominantCycles
|
4
|
-
# The Dual Differentiator algorithm computes the phase angle from the
|
5
|
-
#
|
6
|
-
#
|
4
|
+
# The Dual Differentiator algorithm computes the phase angle from the
|
5
|
+
# analytic signal as the arctangent of the ratio of the imaginary
|
6
|
+
# component to the real component. Further, the angular frequency
|
7
|
+
# is defined as the rate change of phase. We can use these facts to
|
8
|
+
# derive the cycle period.
|
7
9
|
class Differential < DominantCycle
|
8
10
|
def compute_period
|
9
11
|
p0.ddd = (p0.q2 * (p0.i2 - p1.i2)) - (p0.i2 * (p0.q2 - p1.q2))
|
@@ -2,6 +2,20 @@ require_relative "../indicator"
|
|
2
2
|
|
3
3
|
module Quant
|
4
4
|
class Indicators
|
5
|
+
# Dominant Cycles measure the primary cycle within a given range. By default, the library
|
6
|
+
# is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
|
7
|
+
# the `min_period` and `max_period` configuration values in {Quant::Config}.
|
8
|
+
#
|
9
|
+
# Quant.configure_indicators(min_period: 8, max_period: 32)
|
10
|
+
#
|
11
|
+
# The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
|
12
|
+
# the `dominant_cycle_kind` configuration value in {Quant::Config}.
|
13
|
+
#
|
14
|
+
# Quant.configure_indicators(dominant_cycle_kind: :band_pass)
|
15
|
+
#
|
16
|
+
# The purpose of these indicators is to compute the dominant cycle and underpin the various
|
17
|
+
# indicators that would otherwise be setting an arbitrary lookback period. This makes the
|
18
|
+
# indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
|
5
19
|
class DominantCycles
|
6
20
|
class DominantCyclePoint < Quant::Indicators::IndicatorPoint
|
7
21
|
attribute :smooth, default: 0.0
|
@@ -39,6 +53,8 @@ module Quant
|
|
39
53
|
p0.inst_period = p0.inst_period.clamp(min_period, max_period)
|
40
54
|
end
|
41
55
|
|
56
|
+
attr_reader :points
|
57
|
+
|
42
58
|
# constrain magnitude of change in phase
|
43
59
|
def constrain_period_magnitude_change
|
44
60
|
p0.inst_period = [1.5 * p1.inst_period, p0.inst_period].min
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative "dominant_cycle"
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
class DominantCycles
|
6
|
+
# This dominant cycle indicator is based on the half period
|
7
|
+
# that is the midpoint of the `min_period` and `max_period`
|
8
|
+
# configured in the `Quant.config.indicators` object.
|
9
|
+
# Effectively providing a static, arbitrarily set period.
|
10
|
+
class HalfPeriodPoint < Quant::Indicators::IndicatorPoint
|
11
|
+
attribute :period, default: :half_period
|
12
|
+
end
|
13
|
+
|
14
|
+
class HalfPeriod < DominantCycle
|
15
|
+
def compute
|
16
|
+
# No-Op
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -4,8 +4,9 @@ require_relative "dominant_cycle"
|
|
4
4
|
module Quant
|
5
5
|
class Indicators
|
6
6
|
class DominantCycles
|
7
|
-
# Homodyne means the signal is multiplied by itself. More precisely,
|
8
|
-
#
|
7
|
+
# Homodyne means the signal is multiplied by itself. More precisely,
|
8
|
+
# we want to multiply the signal of the current bar with the complex
|
9
|
+
# value of the signal one bar ago
|
9
10
|
class Homodyne < DominantCycle
|
10
11
|
def compute_period
|
11
12
|
p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
|
@@ -8,7 +8,7 @@ module Quant
|
|
8
8
|
# at each sample by taking the arctangent of the ratio of the quadrature
|
9
9
|
# component to the in-phase component. A delta phase is generated by
|
10
10
|
# taking the difference of the phase between successive samples.
|
11
|
-
# At each
|
11
|
+
# At each sample we can then look backwards, adding up the delta
|
12
12
|
# phases. When the sum of the delta phases reaches 360 degrees,
|
13
13
|
# we must have passed through one full cycle, on average. The process
|
14
14
|
# is repeated for each new sample.
|
@@ -19,6 +19,7 @@ module Quant
|
|
19
19
|
@series = series
|
20
20
|
@source = source
|
21
21
|
@points = {}
|
22
|
+
series.new_indicator(self)
|
22
23
|
series.each { |tick| self << tick }
|
23
24
|
end
|
24
25
|
|
@@ -46,6 +47,14 @@ module Quant
|
|
46
47
|
Quant.config.indicators.pivot_kind
|
47
48
|
end
|
48
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
|
+
|
49
58
|
def ticks
|
50
59
|
@points.keys
|
51
60
|
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
|
|
@@ -7,6 +7,16 @@ 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
|
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
data/lib/quant/version.rb
CHANGED
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.2.
|
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
|
@@ -56,6 +56,7 @@ files:
|
|
56
56
|
- lib/quant/indicators/dominant_cycles/band_pass.rb
|
57
57
|
- lib/quant/indicators/dominant_cycles/differential.rb
|
58
58
|
- lib/quant/indicators/dominant_cycles/dominant_cycle.rb
|
59
|
+
- lib/quant/indicators/dominant_cycles/half_period.rb
|
59
60
|
- lib/quant/indicators/dominant_cycles/homodyne.rb
|
60
61
|
- lib/quant/indicators/dominant_cycles/phase_accumulator.rb
|
61
62
|
- lib/quant/indicators/indicator.rb
|