quantitative 0.2.1 → 0.2.2
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 +9 -4
- data/Gemfile.lock +9 -1
- data/lib/quant/{indicators/dominant_cycle_indicators.rb → dominant_cycle_indicators.rb} +3 -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 +2 -0
- data/lib/quant/indicators/dominant_cycles/band_pass.rb +2 -0
- data/lib/quant/indicators/dominant_cycles/differential.rb +3 -1
- data/lib/quant/indicators/dominant_cycles/dominant_cycle.rb +3 -6
- data/lib/quant/indicators/dominant_cycles/half_period.rb +2 -0
- data/lib/quant/indicators/dominant_cycles/homodyne.rb +4 -2
- data/lib/quant/indicators/dominant_cycles/phase_accumulator.rb +8 -4
- data/lib/quant/indicators/frama.rb +53 -0
- data/lib/quant/indicators/indicator.rb +5 -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/mixins/stochastic.rb +1 -1
- data/lib/quant/mixins/super_smoother.rb +11 -2
- data/lib/quant/version.rb +1 -1
- metadata +10 -3
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,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"
|
|
2
4
|
|
|
3
5
|
module Quant
|
|
@@ -76,11 +78,6 @@ module Quant
|
|
|
76
78
|
[p0.period.to_i, min_period].max
|
|
77
79
|
end
|
|
78
80
|
|
|
79
|
-
def period_points(max_period)
|
|
80
|
-
extent = [values.size, max_period].min
|
|
81
|
-
values[-extent, extent]
|
|
82
|
-
end
|
|
83
|
-
|
|
84
81
|
def compute
|
|
85
82
|
compute_input_data_points
|
|
86
83
|
compute_quadrature_components
|
|
@@ -141,4 +138,4 @@ module Quant
|
|
|
141
138
|
end
|
|
142
139
|
end
|
|
143
140
|
end
|
|
144
|
-
end
|
|
141
|
+
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
|
|
|
@@ -15,7 +17,7 @@ module Quant
|
|
|
15
17
|
p0.re = (0.2 * p0.re) + (0.8 * p1.re)
|
|
16
18
|
p0.im = (0.2 * p0.im) + (0.8 * p1.im)
|
|
17
19
|
|
|
18
|
-
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)
|
|
19
21
|
|
|
20
22
|
constrain_period_magnitude_change
|
|
21
23
|
constrain_period_bars
|
|
@@ -25,4 +27,4 @@ module Quant
|
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
|
-
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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/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,10 +48,14 @@ 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
|
|
@@ -59,8 +63,11 @@ files:
|
|
|
59
63
|
- lib/quant/indicators/dominant_cycles/half_period.rb
|
|
60
64
|
- lib/quant/indicators/dominant_cycles/homodyne.rb
|
|
61
65
|
- lib/quant/indicators/dominant_cycles/phase_accumulator.rb
|
|
66
|
+
- lib/quant/indicators/frama.rb
|
|
62
67
|
- lib/quant/indicators/indicator.rb
|
|
63
68
|
- lib/quant/indicators/indicator_point.rb
|
|
69
|
+
- lib/quant/indicators/mama.rb
|
|
70
|
+
- lib/quant/indicators/mesa.rb
|
|
64
71
|
- lib/quant/indicators/ping.rb
|
|
65
72
|
- lib/quant/indicators_proxy.rb
|
|
66
73
|
- lib/quant/indicators_sources.rb
|