quantitative 0.2.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +9 -4
- data/Gemfile.lock +9 -1
- data/lib/quant/dominant_cycle_indicators.rb +52 -0
- data/lib/quant/errors.rb +4 -0
- data/lib/quant/indicators/adx.rb +80 -0
- data/lib/quant/indicators/atr.rb +79 -0
- data/lib/quant/indicators/cci.rb +63 -0
- data/lib/quant/indicators/decycler.rb +85 -0
- data/lib/quant/indicators/dominant_cycles/acr.rb +14 -12
- data/lib/quant/indicators/dominant_cycles/band_pass.rb +7 -0
- data/lib/quant/indicators/dominant_cycles/differential.rb +8 -4
- data/lib/quant/indicators/dominant_cycles/dominant_cycle.rb +19 -6
- data/lib/quant/indicators/dominant_cycles/half_period.rb +23 -0
- data/lib/quant/indicators/dominant_cycles/homodyne.rb +7 -4
- data/lib/quant/indicators/dominant_cycles/phase_accumulator.rb +9 -5
- data/lib/quant/indicators/frama.rb +53 -0
- data/lib/quant/indicators/indicator.rb +14 -0
- data/lib/quant/indicators/mama.rb +143 -0
- data/lib/quant/indicators/mesa.rb +86 -0
- data/lib/quant/indicators.rb +5 -1
- data/lib/quant/indicators_proxy.rb +11 -1
- data/lib/quant/indicators_sources.rb +10 -0
- data/lib/quant/mixins/stochastic.rb +1 -1
- data/lib/quant/mixins/super_smoother.rb +11 -2
- 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 +11 -3
- data/lib/quant/indicators/dominant_cycle_indicators.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6b5786b75635cde82e7919fce6b8fcd568724b27fc2810efe6db36a7e6dd09cc
|
4
|
+
data.tar.gz: 3a6fb86c25bb4f2b1e4994710539bfb528377ef57531e6845a4417c87fc86e29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ae30c5b971d6c7bd41e3c2b764e7d95438931790271133dc0e3a476a371affddfdcf06355ffa933240c0c9952fd8fa5e4212c496c95c80f028de7399d3b129f
|
7
|
+
data.tar.gz: 9ff8aeaf24800a7208e51f836dd7c7c3621b2e2e3a7f1e8820d89f6fc5cd56dd3c4b762c478d221c6ef7279f62c0df75ae1a19f6a4d34e7ba82e116e6a10a790
|
data/Gemfile
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
# Specify your gem
|
5
|
+
# Specify your gem"s dependencies in quantitative.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
8
|
gem "rake", "~> 13.0"
|
@@ -18,7 +18,12 @@ gem "guard-rspec", "~> 4.7"
|
|
18
18
|
gem "yard", "~> 0.9"
|
19
19
|
gem "benchmark-ips", "~> 2.9"
|
20
20
|
|
21
|
-
gem
|
22
|
-
gem 'simplecov'
|
23
|
-
gem 'simplecov-cobertura'
|
21
|
+
gem "rspec-github"
|
24
22
|
|
23
|
+
# Test coverage and profiling
|
24
|
+
gem "simplecov"
|
25
|
+
gem "simplecov-cobertura"
|
26
|
+
gem "stackprof", require: false
|
27
|
+
gem "test-prof", require: false
|
28
|
+
gem "vernier", require: false
|
29
|
+
gem "ruby-prof", require: false
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
quantitative (0.2.
|
4
|
+
quantitative (0.2.2)
|
5
5
|
oj (~> 3.10)
|
6
6
|
|
7
7
|
GEM
|
@@ -107,6 +107,7 @@ GEM
|
|
107
107
|
rubocop (~> 1.40)
|
108
108
|
rubocop-capybara (~> 2.17)
|
109
109
|
rubocop-factory_bot (~> 2.22)
|
110
|
+
ruby-prof (1.7.0)
|
110
111
|
ruby-progressbar (1.13.0)
|
111
112
|
shellany (0.0.1)
|
112
113
|
simplecov (0.22.0)
|
@@ -118,9 +119,12 @@ GEM
|
|
118
119
|
simplecov (~> 0.19)
|
119
120
|
simplecov-html (0.12.3)
|
120
121
|
simplecov_json_formatter (0.1.4)
|
122
|
+
stackprof (0.2.26)
|
121
123
|
stringio (3.1.0)
|
124
|
+
test-prof (1.3.2)
|
122
125
|
thor (1.3.0)
|
123
126
|
unicode-display_width (2.5.0)
|
127
|
+
vernier (0.5.1)
|
124
128
|
yard (0.9.34)
|
125
129
|
|
126
130
|
PLATFORMS
|
@@ -138,8 +142,12 @@ DEPENDENCIES
|
|
138
142
|
rspec-github
|
139
143
|
rubocop (~> 1.21)
|
140
144
|
rubocop-rspec
|
145
|
+
ruby-prof
|
141
146
|
simplecov
|
142
147
|
simplecov-cobertura
|
148
|
+
stackprof
|
149
|
+
test-prof
|
150
|
+
vernier
|
143
151
|
yard (~> 0.9)
|
144
152
|
|
145
153
|
BUNDLED WITH
|
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
require_relative 'indicators_proxy'
|
3
|
+
|
4
|
+
module Quant
|
5
|
+
# Dominant Cycles measure the primary cycle within a given range. By default, the library
|
6
|
+
# is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
|
7
|
+
# the `min_period` and `max_period` configuration values in {Quant::Config}.
|
8
|
+
#
|
9
|
+
# Quant.configure_indicators(min_period: 8, max_period: 32)
|
10
|
+
#
|
11
|
+
# The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
|
12
|
+
# the `dominant_cycle_kind` configuration value in {Quant::Config}.
|
13
|
+
#
|
14
|
+
# Quant.configure_indicators(dominant_cycle_kind: :band_pass)
|
15
|
+
#
|
16
|
+
# The purpose of these indicators is to compute the dominant cycle and underpin the various
|
17
|
+
# indicators that would otherwise be setting an arbitrary lookback period. This makes the
|
18
|
+
# indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
|
19
|
+
class DominantCycleIndicators < IndicatorsProxy
|
20
|
+
# Auto-Correlation Reversals is a method of computing the dominant cycle
|
21
|
+
# by correlating the data stream with itself delayed by a lag.
|
22
|
+
def acr; indicator(Indicators::DominantCycles::Acr) end
|
23
|
+
|
24
|
+
# The band-pass dominant cycle passes signals within a certain frequency
|
25
|
+
# range, and attenuates signals outside that range.
|
26
|
+
# The trend component of the signal is removed, leaving only the cyclical
|
27
|
+
# component. Then we count number of iterations between zero crossings
|
28
|
+
# and this is the `period` of the dominant cycle.
|
29
|
+
def band_pass; indicator(Indicators::DominantCycles::BandPass) end
|
30
|
+
|
31
|
+
# Homodyne means the signal is multiplied by itself. More precisely,
|
32
|
+
# we want to multiply the signal of the current bar with the complex
|
33
|
+
# value of the signal one bar ago
|
34
|
+
def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
|
35
|
+
|
36
|
+
# The Dual Differentiator algorithm computes the phase angle from the
|
37
|
+
# analytic signal as the arctangent of the ratio of the imaginary
|
38
|
+
# component to the real component. Further, the angular frequency
|
39
|
+
# is defined as the rate change of phase. We can use these facts to
|
40
|
+
# derive the cycle period.
|
41
|
+
def differential; indicator(Indicators::DominantCycles::Differential) end
|
42
|
+
|
43
|
+
# The phase accumulation method of computing the dominant cycle measures
|
44
|
+
# the phase at each sample by taking the arctangent of the ratio of the
|
45
|
+
# quadrature component to the in-phase component. The phase is then
|
46
|
+
# accumulated and the period is derived from the phase.
|
47
|
+
def phase_accumulator; indicator(Indicators::DominantCycles::PhaseAccumulator) end
|
48
|
+
|
49
|
+
# Static, arbitrarily set period.
|
50
|
+
def half_period; indicator(Indicators::DominantCycles::HalfPeriod) end
|
51
|
+
end
|
52
|
+
end
|
data/lib/quant/errors.rb
CHANGED
@@ -10,6 +10,10 @@ module Quant
|
|
10
10
|
# {Quant::Interval} with an invalid value.
|
11
11
|
class InvalidInterval < Error; end
|
12
12
|
|
13
|
+
# {InvalidIndicatorSource} is raised when attempting to reference
|
14
|
+
# an indicator through a source that has not been prepared, yet.
|
15
|
+
class InvalidIndicatorSource < Error; end
|
16
|
+
|
13
17
|
# {InvalidResolution} is raised when attempting to instantiate
|
14
18
|
# an {Quant::Resolution} with a resolution value that has not been defined.
|
15
19
|
class InvalidResolution < Error; end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "indicator_point"
|
4
|
+
require_relative "indicator"
|
5
|
+
|
6
|
+
module Quant
|
7
|
+
class Indicators
|
8
|
+
class AdxPoint < IndicatorPoint
|
9
|
+
attribute :dmu, default: 0.0
|
10
|
+
attribute :dmd, default: 0.0
|
11
|
+
attribute :dmu_ema, default: 0.0
|
12
|
+
attribute :dmd_ema, default: 0.0
|
13
|
+
attribute :diu, default: 0.0
|
14
|
+
attribute :did, default: 0.0
|
15
|
+
attribute :di, default: 0.0
|
16
|
+
attribute :di_ema, default: 0.0
|
17
|
+
attribute :value, default: 0.0
|
18
|
+
attribute :inst_stoch, default: 0.0
|
19
|
+
attribute :stoch, default: 0.0
|
20
|
+
attribute :stoch_up, default: false
|
21
|
+
attribute :stoch_turned, default: false
|
22
|
+
attribute :ssf, default: 0.0
|
23
|
+
attribute :hp, default: 0.0
|
24
|
+
end
|
25
|
+
|
26
|
+
class Adx < Indicator
|
27
|
+
def alpha
|
28
|
+
bars_to_alpha(dc_period)
|
29
|
+
end
|
30
|
+
|
31
|
+
def scale
|
32
|
+
1.0
|
33
|
+
end
|
34
|
+
|
35
|
+
def period
|
36
|
+
dc_period
|
37
|
+
end
|
38
|
+
|
39
|
+
def atr_point
|
40
|
+
series.indicators[source].atr.points[t0]
|
41
|
+
end
|
42
|
+
|
43
|
+
def compute
|
44
|
+
# To calculate the ADX, first determine the + and - directional movement, or DM.
|
45
|
+
# The +DM and -DM are found by calculating the "up-move," or current high minus
|
46
|
+
# the previous high, and "down-move," or current low minus the previous low.
|
47
|
+
# If the up-move is greater than the down-move and greater than zero, the +DM equals the up-move;
|
48
|
+
# otherwise, it equals zero. If the down-move is greater than the up-move and greater than zero,
|
49
|
+
# the -DM equals the down-move; otherwise, it equals zero.
|
50
|
+
dm_highs = [t0.high_price - t1.high_price, 0.0].max
|
51
|
+
dm_lows = [t0.low_price - t1.low_price, 0.0].max
|
52
|
+
|
53
|
+
p0.dmu = dm_highs > dm_lows ? 0.0 : dm_highs
|
54
|
+
p0.dmd = dm_lows > dm_highs ? 0.0 : dm_lows
|
55
|
+
|
56
|
+
p0.dmu_ema = three_pole_super_smooth :dmu, period:, previous: :dmu_ema
|
57
|
+
p0.dmd_ema = three_pole_super_smooth :dmd, period:, previous: :dmd_ema
|
58
|
+
|
59
|
+
atr_value = atr_point.fast * scale
|
60
|
+
return if atr_value == 0.0 || @points.size < period
|
61
|
+
|
62
|
+
# The positive directional indicator, or +DI, equals 100 times the EMA of +DM divided by the ATR
|
63
|
+
# over a given number of time periods. Welles usually used 14 periods.
|
64
|
+
# The negative directional indicator, or -DI, equals 100 times the EMA of -DM divided by the ATR.
|
65
|
+
p0.diu = (100.0 * p0.dmu_ema) / atr_value
|
66
|
+
p0.did = (100.0 * p0.dmd_ema) / atr_value
|
67
|
+
|
68
|
+
# The ADX indicator itself equals 100 times the EMA of the absolute value of (+DI minus -DI)
|
69
|
+
# divided by (+DI plus -DI).
|
70
|
+
delta = p0.diu + p0.did
|
71
|
+
p0.di = (p0.diu - p1.did).abs / delta
|
72
|
+
p0.di_ema = three_pole_super_smooth(:di, period:, previous: :di_ema).clamp(-10.0, 10.0)
|
73
|
+
|
74
|
+
p0.value = p0.di_ema
|
75
|
+
p0.inst_stoch = stochastic :di, period: dc_period
|
76
|
+
p0.stoch = three_pole_super_smooth :inst_stoch, period:, previous: :stoch
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "indicator_point"
|
4
|
+
require_relative "indicator"
|
5
|
+
|
6
|
+
module Quant
|
7
|
+
class Indicators
|
8
|
+
class AtrPoint < IndicatorPoint
|
9
|
+
attribute :tr, default: 0.0
|
10
|
+
attribute :period, default: :min_period
|
11
|
+
attribute :value, default: 0.0
|
12
|
+
attribute :slow, default: 0.0
|
13
|
+
attribute :fast, default: 0.0
|
14
|
+
attribute :inst_stoch, default: 0.0
|
15
|
+
attribute :stoch, default: 0.0
|
16
|
+
attribute :stoch_up, default: false
|
17
|
+
attribute :stoch_turned, default: false
|
18
|
+
attribute :osc, default: 0.0
|
19
|
+
attribute :crossed, default: :unchanged
|
20
|
+
|
21
|
+
def crossed_up?
|
22
|
+
@crossed == :up
|
23
|
+
end
|
24
|
+
|
25
|
+
def crossed_down?
|
26
|
+
@crossed == :down
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Atr < Indicator
|
31
|
+
attr_reader :points
|
32
|
+
|
33
|
+
def period
|
34
|
+
dc_period / 2
|
35
|
+
end
|
36
|
+
|
37
|
+
def fast_alpha
|
38
|
+
period_to_alpha(period)
|
39
|
+
end
|
40
|
+
|
41
|
+
def slow_alpha
|
42
|
+
period_to_alpha(2 * period)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Typically, the Average True Range (ATR) is based on 14 periods and can be calculated on an intraday, daily, weekly
|
46
|
+
# or monthly basis. For this example, the ATR will be based on daily data. Because there must be a beginning, the first
|
47
|
+
# TR value is simply the High minus the Low, and the first 14-day ATR is the average of the daily TR values for the
|
48
|
+
# last 14 days. After that, Wilder sought to smooth the data by incorporating the previous period's ATR value.
|
49
|
+
|
50
|
+
# Current ATR = [(Prior ATR x 13) + Current TR] / 14
|
51
|
+
|
52
|
+
# - Multiply the previous 14-day ATR by 13.
|
53
|
+
# - Add the most recent day's TR value.
|
54
|
+
# - Divide the total by 14
|
55
|
+
|
56
|
+
def compute
|
57
|
+
p0.period = period
|
58
|
+
p0.tr = (t1.high_price - t0.close_price).abs
|
59
|
+
|
60
|
+
p0.value = three_pole_super_smooth :tr, period:, previous: :value
|
61
|
+
|
62
|
+
p0.slow = (slow_alpha * p0.value) + ((1.0 - slow_alpha) * p1.slow)
|
63
|
+
p0.fast = (fast_alpha * p0.value) + ((1.0 - fast_alpha) * p1.fast)
|
64
|
+
|
65
|
+
p0.inst_stoch = stochastic :value, period:
|
66
|
+
p0.stoch = three_pole_super_smooth(:inst_stoch, previous: :stoch, period:).clamp(0, 100)
|
67
|
+
p0.stoch_up = p0.stoch >= 70
|
68
|
+
p0.stoch_turned = p0.stoch_up && !p1.stoch_up
|
69
|
+
compute_oscillator
|
70
|
+
end
|
71
|
+
|
72
|
+
def compute_oscillator
|
73
|
+
p0.osc = p0.value - wma(:value)
|
74
|
+
p0.crossed = :up if p0.osc >= 0 && p1.osc < 0
|
75
|
+
p0.crossed = :down if p0.osc <= 0 && p1.osc > 0
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
class CciPoint < IndicatorPoint
|
6
|
+
attribute :hp, default: 0.0
|
7
|
+
attribute :real, default: 0.0
|
8
|
+
attribute :imag, default: 0.0
|
9
|
+
attribute :angle, default: 0.0
|
10
|
+
attribute :state, default: 0
|
11
|
+
end
|
12
|
+
|
13
|
+
# Correlation Cycle Index
|
14
|
+
# The very definition of a trend mode and a cycle mode makes it simple
|
15
|
+
# to create a state variable that identifies the market state. If the
|
16
|
+
# state is zero, the market is in a cycle mode. If the state is +1 the
|
17
|
+
# market is in a trend up. If the state is -1 the market is in a trend down.
|
18
|
+
#
|
19
|
+
# SOURCE: https://www.mesasoftware.com/papers/CORRELATION%20AS%20A%20CYCLE%20INDICATOR.pdf
|
20
|
+
class Cci < Indicator
|
21
|
+
def max_period
|
22
|
+
[min_period, dc_period].max
|
23
|
+
end
|
24
|
+
|
25
|
+
def compute_correlations
|
26
|
+
corr_real = Statistics::Correlation.new
|
27
|
+
corr_imag = Statistics::Correlation.new
|
28
|
+
arc = 2.0 * Math::PI / max_period.to_f
|
29
|
+
(0...max_period).each do |period|
|
30
|
+
radians = arc * period
|
31
|
+
prev_hp = p(period).hp
|
32
|
+
corr_real.add(prev_hp, Math.cos(radians))
|
33
|
+
corr_imag.add(prev_hp, -Math.sin(radians))
|
34
|
+
end
|
35
|
+
p0.real = corr_real.coefficient
|
36
|
+
p0.imag = corr_imag.coefficient
|
37
|
+
end
|
38
|
+
|
39
|
+
def compute_angle
|
40
|
+
# Compute the angle as an arctangent and resolve the quadrant
|
41
|
+
p0.angle = 90 + rad2deg(Math.atan(p0.real / p0.imag))
|
42
|
+
p0.angle -= 180 if p0.imag > 0
|
43
|
+
|
44
|
+
# Do not allow the rate change of angle to go negative
|
45
|
+
p0.angle = p1.angle if (p0.angle < p1.angle) && (p1.angle - p0.angle) < 270
|
46
|
+
end
|
47
|
+
|
48
|
+
def compute_state
|
49
|
+
return unless (p0.angle - p1.angle).abs < 9
|
50
|
+
|
51
|
+
p0.state = p0.angle < 0 ? -1 : 1
|
52
|
+
end
|
53
|
+
|
54
|
+
def compute
|
55
|
+
p0.hp = two_pole_butterworth :input, previous: :hp, period: min_period
|
56
|
+
|
57
|
+
compute_correlations
|
58
|
+
compute_angle
|
59
|
+
compute_state
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
# The decycler oscillator can be useful for determining the transition be- tween uptrends and downtrends by the crossing of the zero
|
6
|
+
# line. Alternatively, the changes of slope of the decycler oscillator are easier to identify than the changes in slope of the
|
7
|
+
# original decycler. Optimum cutoff periods can easily be found by experimentation.
|
8
|
+
#
|
9
|
+
# 1. A decycler filter functions the same as a low-pass filter.
|
10
|
+
# 2. A decycler filter is created by subtracting the output of a high-pass filter from the input, thereby removing the
|
11
|
+
# high-frequency components by cancellation.
|
12
|
+
# 3. A decycler filter has very low lag.
|
13
|
+
# 4. A decycler oscillator is created by subtracting the output of a high-pass filter having a shorter cutoff period from the
|
14
|
+
# output of another high-pass filter having a longer cutoff period.
|
15
|
+
# 5. A decycler oscillator shows transitions between uptrends and down-trends at the zero crossings.
|
16
|
+
class DecyclerPoint < IndicatorPoint
|
17
|
+
attr_accessor :decycle, :hp1, :hp2, :osc, :peak, :agc, :ift
|
18
|
+
|
19
|
+
def to_h
|
20
|
+
{
|
21
|
+
"dc" => decycle,
|
22
|
+
"hp1" => hp1,
|
23
|
+
"hp2" => hp2,
|
24
|
+
"osc" => osc,
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize_data_points(indicator:)
|
29
|
+
@decycle = oc2
|
30
|
+
@hp1 = 0.0
|
31
|
+
@hp2 = 0.0
|
32
|
+
@osc = 0.0
|
33
|
+
@peak = 0.0
|
34
|
+
@agc = 0.0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Decycler < Indicator
|
39
|
+
def max_period
|
40
|
+
dc_period
|
41
|
+
end
|
42
|
+
|
43
|
+
def min_period
|
44
|
+
settings.min_period
|
45
|
+
end
|
46
|
+
|
47
|
+
def compute_decycler
|
48
|
+
alpha = period_to_alpha(max_period)
|
49
|
+
p0.decycle = (alpha / 2) * (p0.oc2 + p1.oc2) + (1.0 - alpha) * p1.decycle
|
50
|
+
end
|
51
|
+
|
52
|
+
# alpha1 = (Cosine(.707*360 / HPPeriod1) + Sine (.707*360 / HPPeriod1) - 1) / Cosine(.707*360 / HPPeriod1);
|
53
|
+
# HP1 = (1 - alpha1 / 2)*(1 - alpha1 / 2)*(Close - 2*Close[1] + Close[2]) + 2*(1 - alpha1)*HP1[1] - (1 - alpha1)*(1 - alpha1)*HP1[2];
|
54
|
+
def compute_hp(period, hp)
|
55
|
+
radians = deg2rad(360)
|
56
|
+
c = Math.cos(0.707 * radians / period)
|
57
|
+
s = Math.sin(0.707 * radians / period)
|
58
|
+
alpha = (c + s - 1) / c
|
59
|
+
(1 - alpha / 2)**2 * (p0.oc2 - 2 * p1.oc2 + p2.oc2) + 2 * (1 - alpha) * p1.send(hp) - (1 - alpha) * (1 - alpha) * p2.send(hp)
|
60
|
+
end
|
61
|
+
|
62
|
+
def compute_oscillator
|
63
|
+
p0.hp1 = compute_hp(min_period, :hp1)
|
64
|
+
p0.hp2 = compute_hp(max_period, :hp2)
|
65
|
+
p0.osc = p0.hp2 - p0.hp1
|
66
|
+
end
|
67
|
+
|
68
|
+
def compute_agc
|
69
|
+
p0.peak = [p0.osc.abs, 0.991 * p1.peak].max
|
70
|
+
p0.agc = p0.peak.zero? ? p0.osc : p0.osc / p0.peak
|
71
|
+
end
|
72
|
+
|
73
|
+
def compute_ift
|
74
|
+
p0.ift = ift(p0.agc, 5.0)
|
75
|
+
end
|
76
|
+
|
77
|
+
def compute
|
78
|
+
compute_decycler
|
79
|
+
compute_oscillator
|
80
|
+
compute_agc
|
81
|
+
compute_ift
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../indicator_point"
|
2
4
|
require_relative "dominant_cycle"
|
3
5
|
|
@@ -22,20 +24,20 @@ module Quant
|
|
22
24
|
attribute :reversal, default: false
|
23
25
|
end
|
24
26
|
|
25
|
-
# Auto-Correlation Reversals
|
27
|
+
# Auto-Correlation Reversals is a method of computing the dominant cycle
|
28
|
+
# by correlating the data stream with itself delayed by a lag.
|
29
|
+
# Construction of the autocorrelation periodogram starts with the
|
30
|
+
# autocorrelation function using the minimum three bars of averaging.
|
31
|
+
# The cyclic information is extracted using a discrete Fourier transform
|
32
|
+
# (DFT) of the autocorrelation results.
|
26
33
|
class Acr < DominantCycle
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
def bandwidth
|
32
|
-
deg2rad(370)
|
33
|
-
end
|
34
|
+
BANDWIDTH_DEGREES = 370
|
35
|
+
BANDWIDTH_RADIANS = BANDWIDTH_DEGREES * Math::PI / 180.0
|
34
36
|
|
35
37
|
def compute_auto_correlations
|
36
38
|
(min_period..max_period).each do |period|
|
37
39
|
corr = Statistics::Correlation.new
|
38
|
-
|
40
|
+
micro_period.times do |lookback_period|
|
39
41
|
corr.add(p(lookback_period).filter, p(period + lookback_period).filter)
|
40
42
|
end
|
41
43
|
p0.corr[period] = corr.coefficient
|
@@ -46,8 +48,8 @@ module Quant
|
|
46
48
|
p0.maxpwr = 0.995 * p1.maxpwr
|
47
49
|
|
48
50
|
(min_period..max_period).each do |period|
|
49
|
-
(
|
50
|
-
radians =
|
51
|
+
(micro_period..max_period).each do |n|
|
52
|
+
radians = BANDWIDTH_RADIANS * n / period
|
51
53
|
p0.cospart[period] += p0.corr[n] * Math.cos(radians)
|
52
54
|
p0.sinpart[period] += p0.corr[n] * Math.sin(radians)
|
53
55
|
end
|
@@ -98,4 +100,4 @@ module Quant
|
|
98
100
|
end
|
99
101
|
end
|
100
102
|
end
|
101
|
-
end
|
103
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "dominant_cycle"
|
2
4
|
|
3
5
|
module Quant
|
@@ -14,6 +16,11 @@ module Quant
|
|
14
16
|
attribute :direction, default: :flat
|
15
17
|
end
|
16
18
|
|
19
|
+
# The band-pass dominant cycle passes signals within a certain frequency
|
20
|
+
# range, and attenuates signals outside that range.
|
21
|
+
# The trend component of the signal is revoved, leaving only the cyclical
|
22
|
+
# component. Then we count number of iterations between zero crossings
|
23
|
+
# and this is the `period` of the dominant cycle.
|
17
24
|
class BandPass < DominantCycle
|
18
25
|
def bandwidth
|
19
26
|
0.75
|
@@ -1,9 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Quant
|
2
4
|
class Indicators
|
3
5
|
class DominantCycles
|
4
|
-
# The Dual Differentiator algorithm computes the phase angle from the
|
5
|
-
#
|
6
|
-
#
|
6
|
+
# The Dual Differentiator algorithm computes the phase angle from the
|
7
|
+
# analytic signal as the arctangent of the ratio of the imaginary
|
8
|
+
# component to the real component. Further, the angular frequency
|
9
|
+
# is defined as the rate change of phase. We can use these facts to
|
10
|
+
# derive the cycle period.
|
7
11
|
class Differential < DominantCycle
|
8
12
|
def compute_period
|
9
13
|
p0.ddd = (p0.q2 * (p0.i2 - p1.i2)) - (p0.i2 * (p0.q2 - p1.q2))
|
@@ -16,4 +20,4 @@ module Quant
|
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
19
|
-
end
|
23
|
+
end
|
@@ -1,7 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../indicator"
|
2
4
|
|
3
5
|
module Quant
|
4
6
|
class Indicators
|
7
|
+
# Dominant Cycles measure the primary cycle within a given range. By default, the library
|
8
|
+
# is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
|
9
|
+
# the `min_period` and `max_period` configuration values in {Quant::Config}.
|
10
|
+
#
|
11
|
+
# Quant.configure_indicators(min_period: 8, max_period: 32)
|
12
|
+
#
|
13
|
+
# The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
|
14
|
+
# the `dominant_cycle_kind` configuration value in {Quant::Config}.
|
15
|
+
#
|
16
|
+
# Quant.configure_indicators(dominant_cycle_kind: :band_pass)
|
17
|
+
#
|
18
|
+
# The purpose of these indicators is to compute the dominant cycle and underpin the various
|
19
|
+
# indicators that would otherwise be setting an arbitrary lookback period. This makes the
|
20
|
+
# indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
|
5
21
|
class DominantCycles
|
6
22
|
class DominantCyclePoint < Quant::Indicators::IndicatorPoint
|
7
23
|
attribute :smooth, default: 0.0
|
@@ -39,6 +55,8 @@ module Quant
|
|
39
55
|
p0.inst_period = p0.inst_period.clamp(min_period, max_period)
|
40
56
|
end
|
41
57
|
|
58
|
+
attr_reader :points
|
59
|
+
|
42
60
|
# constrain magnitude of change in phase
|
43
61
|
def constrain_period_magnitude_change
|
44
62
|
p0.inst_period = [1.5 * p1.inst_period, p0.inst_period].min
|
@@ -60,11 +78,6 @@ module Quant
|
|
60
78
|
[p0.period.to_i, min_period].max
|
61
79
|
end
|
62
80
|
|
63
|
-
def period_points(max_period)
|
64
|
-
extent = [values.size, max_period].min
|
65
|
-
values[-extent, extent]
|
66
|
-
end
|
67
|
-
|
68
81
|
def compute
|
69
82
|
compute_input_data_points
|
70
83
|
compute_quadrature_components
|
@@ -125,4 +138,4 @@ module Quant
|
|
125
138
|
end
|
126
139
|
end
|
127
140
|
end
|
128
|
-
end
|
141
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "dominant_cycle"
|
4
|
+
|
5
|
+
module Quant
|
6
|
+
class Indicators
|
7
|
+
class DominantCycles
|
8
|
+
# This dominant cycle indicator is based on the half period
|
9
|
+
# that is the midpoint of the `min_period` and `max_period`
|
10
|
+
# configured in the `Quant.config.indicators` object.
|
11
|
+
# Effectively providing a static, arbitrarily set period.
|
12
|
+
class HalfPeriodPoint < Quant::Indicators::IndicatorPoint
|
13
|
+
attribute :period, default: :half_period
|
14
|
+
end
|
15
|
+
|
16
|
+
class HalfPeriod < DominantCycle
|
17
|
+
def compute
|
18
|
+
# No-Op
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,11 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../indicator_point"
|
2
4
|
require_relative "dominant_cycle"
|
3
5
|
|
4
6
|
module Quant
|
5
7
|
class Indicators
|
6
8
|
class DominantCycles
|
7
|
-
# Homodyne means the signal is multiplied by itself. More precisely,
|
8
|
-
#
|
9
|
+
# Homodyne means the signal is multiplied by itself. More precisely,
|
10
|
+
# we want to multiply the signal of the current bar with the complex
|
11
|
+
# value of the signal one bar ago
|
9
12
|
class Homodyne < DominantCycle
|
10
13
|
def compute_period
|
11
14
|
p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
|
@@ -14,7 +17,7 @@ module Quant
|
|
14
17
|
p0.re = (0.2 * p0.re) + (0.8 * p1.re)
|
15
18
|
p0.im = (0.2 * p0.im) + (0.8 * p1.im)
|
16
19
|
|
17
|
-
p0.inst_period = 360.0 / rad2deg(Math.atan(p0.im/p0.re)) if (p0.im != 0) && (p0.re != 0)
|
20
|
+
p0.inst_period = 360.0 / rad2deg(Math.atan(p0.im / p0.re)) if (p0.im != 0) && (p0.re != 0)
|
18
21
|
|
19
22
|
constrain_period_magnitude_change
|
20
23
|
constrain_period_bars
|
@@ -24,4 +27,4 @@ module Quant
|
|
24
27
|
end
|
25
28
|
end
|
26
29
|
end
|
27
|
-
end
|
30
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "dominant_cycle"
|
2
4
|
|
3
5
|
module Quant
|
@@ -8,7 +10,7 @@ module Quant
|
|
8
10
|
# at each sample by taking the arctangent of the ratio of the quadrature
|
9
11
|
# component to the in-phase component. A delta phase is generated by
|
10
12
|
# taking the difference of the phase between successive samples.
|
11
|
-
# At each
|
13
|
+
# At each sample we can then look backwards, adding up the delta
|
12
14
|
# phases. When the sum of the delta phases reaches 360 degrees,
|
13
15
|
# we must have passed through one full cycle, on average. The process
|
14
16
|
# is repeated for each new sample.
|
@@ -30,10 +32,12 @@ module Quant
|
|
30
32
|
|
31
33
|
p0.accumulator_phase = Math.atan(p0.q1 / p0.i1) unless p0.i1.zero?
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
if p0.i1 < 0 && p0.q1 > 0
|
36
|
+
p0.accumulator_phase = 180.0 - p0.accumulator_phase
|
37
|
+
elsif p0.i1 < 0 && p0.q1 < 0
|
38
|
+
p0.accumulator_phase = 180.0 + p0.accumulator_phase
|
39
|
+
elsif p0.i1 > 0 && p0.q1 < 0
|
40
|
+
p0.accumulator_phase = 360.0 - p0.accumulator_phase
|
37
41
|
end
|
38
42
|
|
39
43
|
p0.delta_phase = p1.accumulator_phase - p0.accumulator_phase
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
class FramaPoint < IndicatorPoint
|
6
|
+
attribute :frama, default: :input
|
7
|
+
end
|
8
|
+
|
9
|
+
# FRAMA (FRactal Adaptive Moving Average). A nonlinear moving average
|
10
|
+
# is derived using the Hurst exponent. It rapidly follows significant
|
11
|
+
# changes in price but becomes very flat in congestion zones so that
|
12
|
+
# bad whipsaw trades can be eliminated.
|
13
|
+
#
|
14
|
+
# SOURCE: http://www.mesasoftware.com/papers/FRAMA.pdf
|
15
|
+
class Frama < Indicator
|
16
|
+
using Quant
|
17
|
+
|
18
|
+
# The max_period is divided into two smaller, equal periods, so must be even
|
19
|
+
def max_period
|
20
|
+
@max_period ||= begin
|
21
|
+
mp = super
|
22
|
+
mp.even? ? mp : mp + 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# def max_period
|
27
|
+
# mp = dc_period
|
28
|
+
# mp.even? ? mp : mp + 1
|
29
|
+
# end
|
30
|
+
|
31
|
+
def half_period
|
32
|
+
max_period / 2
|
33
|
+
end
|
34
|
+
|
35
|
+
def compute
|
36
|
+
pp = period_points(max_period).map(&:input)
|
37
|
+
return if pp.size < max_period
|
38
|
+
|
39
|
+
n3 = (pp.maximum - pp.minimum) / max_period
|
40
|
+
|
41
|
+
ppn2 = pp.first(half_period)
|
42
|
+
n2 = (ppn2.maximum - ppn2.minimum) / half_period
|
43
|
+
|
44
|
+
ppn1 = pp.last(half_period)
|
45
|
+
n1 = (ppn1.maximum - ppn1.minimum) / half_period
|
46
|
+
|
47
|
+
dimension = (Math.log(n1 + n2) - Math.log(n3)) / Math.log(2)
|
48
|
+
alpha = Math.exp(-4.6 * (dimension - 1.0)).clamp(0.01, 1.0)
|
49
|
+
p0.frama = (alpha * p0.input) + ((1 - alpha) * p1.frama)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -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
|
@@ -62,6 +71,11 @@ module Quant
|
|
62
71
|
@points.size
|
63
72
|
end
|
64
73
|
|
74
|
+
def period_points(max_period)
|
75
|
+
extent = [values.size, max_period].min
|
76
|
+
values[-extent, extent]
|
77
|
+
end
|
78
|
+
|
65
79
|
attr_reader :p0, :p1, :p2, :p3
|
66
80
|
attr_reader :t0, :t1, :t2, :t3
|
67
81
|
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
class MamaPoint < IndicatorPoint
|
6
|
+
attribute :smooth, default: 0.0
|
7
|
+
attribute :detrend, default: 0.0
|
8
|
+
attribute :re, default: 0.0
|
9
|
+
attribute :im, default: 0.0
|
10
|
+
attribute :i1, default: 0.0
|
11
|
+
attribute :q1, default: 0.0
|
12
|
+
attribute :ji, default: 0.0
|
13
|
+
attribute :jq, default: 0.0
|
14
|
+
attribute :i2, default: 0.0
|
15
|
+
attribute :q2, default: 0.0
|
16
|
+
attribute :period, default: :min_period
|
17
|
+
attribute :smooth_period, default: :min_period
|
18
|
+
attribute :mama, default: :input
|
19
|
+
attribute :fama, default: :input
|
20
|
+
attribute :gama, default: :input
|
21
|
+
attribute :dama, default: :input
|
22
|
+
attribute :lama, default: :input
|
23
|
+
attribute :faga, default: :input
|
24
|
+
attribute :phase, default: 0.0
|
25
|
+
attribute :delta_phase, default: 0.0
|
26
|
+
attribute :osc, default: 0.0
|
27
|
+
attribute :crossed, default: :unchanged
|
28
|
+
|
29
|
+
def crossed_up?
|
30
|
+
@crossed == :up
|
31
|
+
end
|
32
|
+
|
33
|
+
def crossed_down?
|
34
|
+
@crossed == :down
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# https://www.mesasoftware.com/papers/MAMA.pdf
|
39
|
+
# MESA Adaptive Moving Average (MAMA) adapts to price movement in an
|
40
|
+
# entirely new and unique way. The adapation is based on the rate change
|
41
|
+
# of phase as measured by the Hilbert Transform Discriminator.
|
42
|
+
#
|
43
|
+
# This version of Ehler's MAMA indicator duplicates the computations
|
44
|
+
# present in the homodyne version of the dominant cycle indicator.
|
45
|
+
# Use this version of the indicator when you're using a different
|
46
|
+
# dominant cycle indicator other than the homodyne for the rest
|
47
|
+
# of your indicators.
|
48
|
+
class Mama < Indicator
|
49
|
+
# constrain between 6 and 50 bars
|
50
|
+
def constrain_period_bars
|
51
|
+
p0.period = p0.period.clamp(min_period, max_period)
|
52
|
+
end
|
53
|
+
|
54
|
+
# constrain magnitude of change in phase
|
55
|
+
def constrain_period_magnitude_change
|
56
|
+
p0.period = [1.5 * p1.period, p0.period].min
|
57
|
+
p0.period = [0.67 * p1.period, p0.period].max
|
58
|
+
end
|
59
|
+
|
60
|
+
# amplitude correction using previous period value
|
61
|
+
def compute_smooth_period
|
62
|
+
p0.period = ((0.2 * p0.period) + (0.8 * p1.period)).round
|
63
|
+
p0.smooth_period = ((0.33333 * p0.period) + (0.666667 * p1.smooth_period)).round
|
64
|
+
end
|
65
|
+
|
66
|
+
def homodyne_discriminator
|
67
|
+
p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
|
68
|
+
p0.im = (p0.i2 * p1.q2) - (p0.q2 * p1.i2)
|
69
|
+
|
70
|
+
p0.re = (0.2 * p0.re) + (0.8 * p1.re)
|
71
|
+
p0.im = (0.2 * p0.im) + (0.8 * p1.im)
|
72
|
+
|
73
|
+
p0.period = 360.0 / rad2deg(Math.atan(p0.im / p0.re)) if (p0.im != 0) && (p0.re != 0)
|
74
|
+
|
75
|
+
constrain_period_magnitude_change
|
76
|
+
constrain_period_bars
|
77
|
+
compute_smooth_period
|
78
|
+
end
|
79
|
+
|
80
|
+
def compute_dominant_cycle
|
81
|
+
p0.smooth = wma :input
|
82
|
+
p0.detrend = hilbert_transform :smooth, period: p1.period
|
83
|
+
|
84
|
+
# { Compute Inphase and Quadrature components }
|
85
|
+
p0.q1 = hilbert_transform :detrend, period: p1.period
|
86
|
+
p0.i1 = p3.detrend
|
87
|
+
|
88
|
+
# { Advance the phase of I1 and Q1 by 90 degrees }
|
89
|
+
p0.ji = hilbert_transform :i1, period: p1.period
|
90
|
+
p0.jq = hilbert_transform :q1, period: p1.period
|
91
|
+
|
92
|
+
# { Smooth the I and Q components before applying the discriminator }
|
93
|
+
p0.i2 = (0.2 * (p0.i1 - p0.jq)) + 0.8 * (p1.i2 || (p0.i1 - p0.jq))
|
94
|
+
p0.q2 = (0.2 * (p0.q1 + p0.ji)) + 0.8 * (p1.q2 || (p0.q1 + p0.ji))
|
95
|
+
|
96
|
+
homodyne_discriminator
|
97
|
+
end
|
98
|
+
|
99
|
+
def fast_limit
|
100
|
+
@fast_limit ||= bars_to_alpha(min_period / 2)
|
101
|
+
end
|
102
|
+
|
103
|
+
def slow_limit
|
104
|
+
@slow_limit ||= bars_to_alpha(max_period)
|
105
|
+
end
|
106
|
+
|
107
|
+
def compute_dominant_cycle_phase
|
108
|
+
p0.delta_phase = p1.phase - p0.phase
|
109
|
+
p0.delta_phase = 1.0 if p0.delta_phase < 1.0
|
110
|
+
end
|
111
|
+
|
112
|
+
FAMA = 0.500
|
113
|
+
GAMA = 0.950
|
114
|
+
DAMA = 0.125
|
115
|
+
LAMA = 0.100
|
116
|
+
FAGA = 0.050
|
117
|
+
|
118
|
+
def compute_moving_averages
|
119
|
+
alpha = [fast_limit / p0.delta_phase, slow_limit].max
|
120
|
+
p0.mama = (alpha * p0.input) + ((1.0 - alpha) * p1.mama)
|
121
|
+
|
122
|
+
p0.fama = (FAMA * alpha * p0.mama) + ((1.0 - (FAMA * alpha)) * p1.fama)
|
123
|
+
p0.gama = (GAMA * alpha * p0.mama) + ((1.0 - (GAMA * alpha)) * p1.gama)
|
124
|
+
p0.dama = (DAMA * alpha * p0.mama) + ((1.0 - (DAMA * alpha)) * p1.dama)
|
125
|
+
p0.lama = (LAMA * alpha * p0.mama) + ((1.0 - (LAMA * alpha)) * p1.lama)
|
126
|
+
p0.faga = (FAGA * alpha * p0.fama) + ((1.0 - (FAGA * alpha)) * p1.faga)
|
127
|
+
end
|
128
|
+
|
129
|
+
def compute_oscillator
|
130
|
+
p0.osc = p0.mama - p0.fama
|
131
|
+
p0.crossed = :up if p0.osc >= 0 && p1.osc < 0
|
132
|
+
p0.crossed = :down if p0.osc <= 0 && p1.osc > 0
|
133
|
+
end
|
134
|
+
|
135
|
+
def compute
|
136
|
+
compute_dominant_cycle
|
137
|
+
compute_dominant_cycle_phase
|
138
|
+
compute_moving_averages
|
139
|
+
compute_oscillator
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class Indicators
|
5
|
+
# The MESA inidicator
|
6
|
+
class MesaPoint < IndicatorPoint
|
7
|
+
attribute :mama, default: :input
|
8
|
+
attribute :fama, default: :input
|
9
|
+
attribute :dama, default: :input
|
10
|
+
attribute :gama, default: :input
|
11
|
+
attribute :lama, default: :input
|
12
|
+
attribute :faga, default: :input
|
13
|
+
attribute :osc, default: 0.0
|
14
|
+
attribute :crossed, default: :unchanged
|
15
|
+
|
16
|
+
def crossed_up?
|
17
|
+
@crossed == :up
|
18
|
+
end
|
19
|
+
|
20
|
+
def crossed_down?
|
21
|
+
@crossed == :down
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# https://www.mesasoftware.com/papers/MAMA.pdf
|
26
|
+
# MESA Adaptive Moving Average (MAMA) adapts to price movement in an
|
27
|
+
# entirely new and unique way. The adapation is based on the rate change
|
28
|
+
# of phase as measured by the Hilbert Transform Discriminator.
|
29
|
+
#
|
30
|
+
# This version of Ehler's MAMA indicator ties into the homodyne
|
31
|
+
# dominant cycle indicator to provide a more efficient computation
|
32
|
+
# for this indicator. If you're using the homodyne in all your
|
33
|
+
# indicators for the dominant cycle, then this version is useful
|
34
|
+
# as it avoids extra computational steps.
|
35
|
+
class Mesa < Indicator
|
36
|
+
def period
|
37
|
+
dc_period
|
38
|
+
end
|
39
|
+
|
40
|
+
def fast_limit
|
41
|
+
@fast_limit ||= bars_to_alpha(min_period / 2)
|
42
|
+
end
|
43
|
+
|
44
|
+
def slow_limit
|
45
|
+
@slow_limit ||= bars_to_alpha(max_period)
|
46
|
+
end
|
47
|
+
|
48
|
+
def homodyne_dominant_cycle
|
49
|
+
series.indicators[source].dominant_cycles.homodyne
|
50
|
+
end
|
51
|
+
|
52
|
+
def current_dominant_cycle
|
53
|
+
homodyne_dominant_cycle.points[t0]
|
54
|
+
end
|
55
|
+
|
56
|
+
def delta_phase
|
57
|
+
current_dominant_cycle.delta_phase
|
58
|
+
end
|
59
|
+
|
60
|
+
FAMA = 0.500
|
61
|
+
GAMA = 0.950
|
62
|
+
DAMA = 0.125
|
63
|
+
LAMA = 0.100
|
64
|
+
FAGA = 0.050
|
65
|
+
|
66
|
+
def compute
|
67
|
+
alpha = [fast_limit / delta_phase, slow_limit].max
|
68
|
+
|
69
|
+
p0.mama = (alpha * p0.input) + ((1.0 - alpha) * p1.mama)
|
70
|
+
p0.fama = (FAMA * alpha * p0.mama) + ((1.0 - (FAMA * alpha)) * p1.fama)
|
71
|
+
p0.gama = (GAMA * alpha * p0.mama) + ((1.0 - (GAMA * alpha)) * p1.gama)
|
72
|
+
p0.dama = (DAMA * alpha * p0.mama) + ((1.0 - (DAMA * alpha)) * p1.dama)
|
73
|
+
p0.lama = (LAMA * alpha * p0.mama) + ((1.0 - (LAMA * alpha)) * p1.lama)
|
74
|
+
p0.faga = (FAGA * alpha * p0.fama) + ((1.0 - (FAGA * alpha)) * p1.faga)
|
75
|
+
|
76
|
+
compute_oscillator
|
77
|
+
end
|
78
|
+
|
79
|
+
def compute_oscillator
|
80
|
+
p0.osc = p0.mama - p0.fama
|
81
|
+
p0.crossed = :up if p0.osc >= 0 && p1.osc < 0
|
82
|
+
p0.crossed = :down if p0.osc <= 0 && p1.osc > 0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/quant/indicators.rb
CHANGED
@@ -6,9 +6,13 @@ module Quant
|
|
6
6
|
# used outside those shipped with the library.
|
7
7
|
class Indicators < IndicatorsProxy
|
8
8
|
def ping; indicator(Indicators::Ping) end
|
9
|
+
def adx; indicator(Indicators::Adx) end
|
10
|
+
def atr; indicator(Indicators::Atr) end
|
11
|
+
def mesa; indicator(Indicators::Mesa) end
|
12
|
+
def mama; indicator(Indicators::MAMA) end
|
9
13
|
|
10
14
|
def dominant_cycles
|
11
|
-
@dominant_cycles ||=
|
15
|
+
@dominant_cycles ||= Quant::DominantCycleIndicators.new(series:, source:)
|
12
16
|
end
|
13
17
|
end
|
14
18
|
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
|
@@ -16,7 +16,7 @@ module Quant
|
|
16
16
|
def stochastic(source, period:)
|
17
17
|
subset = values.last(period).map{ |p| p.send(source) }
|
18
18
|
|
19
|
-
lowest, highest = subset.minimum, subset.maximum
|
19
|
+
lowest, highest = subset.minimum.to_f, subset.maximum.to_f
|
20
20
|
return 0.0 if (highest - lowest).zero?
|
21
21
|
|
22
22
|
100.0 * (subset[-1] - lowest) / (highest - lowest)
|
@@ -2,8 +2,17 @@
|
|
2
2
|
|
3
3
|
module Quant
|
4
4
|
module Mixins
|
5
|
+
# Super Smoother Filters provide a way to smooth out the noise in a series
|
6
|
+
# without out introducing undesirable lag that you would other get with
|
7
|
+
# traditional moving averages.
|
8
|
+
#
|
9
|
+
# The EMA only reduces the amplitude at the Nyquist frequency by 13 dB.
|
10
|
+
# On the other hand, the SuperSmoother filter theoretically completely
|
11
|
+
# eliminates components at the Nyquist Frequency. The added benefit is
|
12
|
+
# that the SuperSmoother filter has significantly less lag than the EMA.
|
5
13
|
module SuperSmoother
|
6
|
-
|
14
|
+
# https://www.mesasoftware.com/papers/PredictiveIndicators.pdf
|
15
|
+
def two_pole_super_smooth(source, period:, previous:)
|
7
16
|
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
8
17
|
|
9
18
|
radians = Math.sqrt(2) * Math::PI / period
|
@@ -23,7 +32,7 @@ module Quant
|
|
23
32
|
alias super_smoother two_pole_super_smooth
|
24
33
|
alias ss2p two_pole_super_smooth
|
25
34
|
|
26
|
-
def three_pole_super_smooth(source, period:, previous:
|
35
|
+
def three_pole_super_smooth(source, period:, previous:)
|
27
36
|
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
28
37
|
|
29
38
|
radians = Math::PI / period
|
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.2
|
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-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -48,18 +48,26 @@ files:
|
|
48
48
|
- lib/quant/asset_class.rb
|
49
49
|
- lib/quant/attributes.rb
|
50
50
|
- lib/quant/config.rb
|
51
|
+
- lib/quant/dominant_cycle_indicators.rb
|
51
52
|
- lib/quant/errors.rb
|
52
53
|
- lib/quant/experimental.rb
|
53
54
|
- lib/quant/indicators.rb
|
54
|
-
- lib/quant/indicators/
|
55
|
+
- lib/quant/indicators/adx.rb
|
56
|
+
- lib/quant/indicators/atr.rb
|
57
|
+
- lib/quant/indicators/cci.rb
|
58
|
+
- lib/quant/indicators/decycler.rb
|
55
59
|
- lib/quant/indicators/dominant_cycles/acr.rb
|
56
60
|
- lib/quant/indicators/dominant_cycles/band_pass.rb
|
57
61
|
- lib/quant/indicators/dominant_cycles/differential.rb
|
58
62
|
- lib/quant/indicators/dominant_cycles/dominant_cycle.rb
|
63
|
+
- lib/quant/indicators/dominant_cycles/half_period.rb
|
59
64
|
- lib/quant/indicators/dominant_cycles/homodyne.rb
|
60
65
|
- lib/quant/indicators/dominant_cycles/phase_accumulator.rb
|
66
|
+
- lib/quant/indicators/frama.rb
|
61
67
|
- lib/quant/indicators/indicator.rb
|
62
68
|
- lib/quant/indicators/indicator_point.rb
|
69
|
+
- lib/quant/indicators/mama.rb
|
70
|
+
- lib/quant/indicators/mesa.rb
|
63
71
|
- lib/quant/indicators/ping.rb
|
64
72
|
- lib/quant/indicators_proxy.rb
|
65
73
|
- lib/quant/indicators_sources.rb
|
@@ -1,10 +0,0 @@
|
|
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
|