quantitative 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +25 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/DISCLAIMER.txt +14 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +126 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +53 -0
- data/Rakefile +12 -0
- data/lib/quant/errors.rb +7 -0
- data/lib/quant/interval.rb +226 -0
- data/lib/quant/mixins/direction.rb +45 -0
- data/lib/quant/mixins/filters.rb +112 -0
- data/lib/quant/mixins/fisher_transform.rb +38 -0
- data/lib/quant/mixins/high_pass_filter.rb +54 -0
- data/lib/quant/mixins/hilbert_transform.rb +14 -0
- data/lib/quant/mixins/stochastic.rb +30 -0
- data/lib/quant/mixins/super_smoother.rb +133 -0
- data/lib/quant/mixins/trig.rb +43 -0
- data/lib/quant/mixins/weighted_average.rb +26 -0
- data/lib/quant/ticks/core.rb +8 -0
- data/lib/quant/ticks/indicator_points.rb +22 -0
- data/lib/quant/ticks/ohlc.rb +152 -0
- data/lib/quant/ticks/serializers/value.rb +23 -0
- data/lib/quant/ticks/spot.rb +33 -0
- data/lib/quant/ticks/value.rb +88 -0
- data/lib/quant/time_methods.rb +53 -0
- data/lib/quant/time_period.rb +78 -0
- data/lib/quant/version.rb +5 -0
- data/lib/quantitative.rb +17 -0
- metadata +149 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
# 1. All the common filters useful for traders have a transfer response that can be written
|
6
|
+
# as a ratio of two polynomials.
|
7
|
+
# 2. Lag is very important to traders. More complex filters can be created using more input data,
|
8
|
+
# but more input data increases lag. Sophisticated filters are not very useful for trading
|
9
|
+
# because they incur too much lag.
|
10
|
+
# 3. Filter transfer response can be viewed in the time domain and the frequency domain with equal validity.
|
11
|
+
# 4. Nonrecursive filters can have zeros in the transfer response, enabling the complete cancellation of
|
12
|
+
# some selected frequency components.
|
13
|
+
# 5. Nonrecursive filters having coefficients symmetrical about the center of the filter will have a delay
|
14
|
+
# of half the degree of the transfer response polynomial at all frequencies.
|
15
|
+
# 6. Low-pass filters are smoothers because they attenuate the high-frequency components of the input data.
|
16
|
+
# 7. High-pass filters are detrenders because they attenuate the low-frequency components of trends.
|
17
|
+
# 8. Band-pass filters are both detrenders and smoothers because they attenuate all but the desired frequency components.
|
18
|
+
# 9. Filters provide an output only through their transfer response. The transfer response is strictly a
|
19
|
+
# mathematical function, and interpretations such as overbought, oversold, convergence, divergence,
|
20
|
+
# and so on are not implied. The validity of such interpretations must be made on the basis of
|
21
|
+
# statistics apart from the filter.
|
22
|
+
# 10. The critical period of a filter output is the frequency at which the output power of the filter
|
23
|
+
# is half the power of the input wave at that frequency.
|
24
|
+
# 11. A WMA has little or no redeeming virtue.
|
25
|
+
# 12. A median filter is best used when the data contain impulsive noise or when there are wild
|
26
|
+
# variations in the data. Smoothing volume data is one example of a good application for a
|
27
|
+
# median filter.
|
28
|
+
#
|
29
|
+
# Filter Coefficients forVariousTypes of Filters
|
30
|
+
# Filter Type b0 b1 b2 a0 a1 a2
|
31
|
+
# EMA α 0 0 1 −(1−α) 0
|
32
|
+
# Two-pole low-pass α**2 0 0 1 −2*(1-α) (1-α)**2
|
33
|
+
# High-pass (1-α/2) -(1-α/2) 0 1 −(1−α) 0
|
34
|
+
# Two-pole high-pass (1-α/2)**2 -2*(1-α/2)**2 (1-α/2)**2 1 -2*(1-α) (1-α)**2
|
35
|
+
# Band-pass (1-σ)/2 0 -(1-σ)/2 1 -λ*(1+σ) σ
|
36
|
+
# Band-stop (1+σ)/2 -2λ*(1+σ)/2 (1+σ)/2 1 -λ*(1+σ) σ
|
37
|
+
module Filters
|
38
|
+
include Mixins::Trig
|
39
|
+
|
40
|
+
# α = Cos(K*360/Period)+Sin(K*360/Period)−1 / Cos(K*360/Period)
|
41
|
+
# k = 1.0 for single-pole filters
|
42
|
+
# k = 0.707 for two-pole high-pass filters
|
43
|
+
# k = 1.414 for two-pole low-pass filters
|
44
|
+
def period_to_alpha(period, k: 1.0)
|
45
|
+
radians = deg2rad(k * 360 / period)
|
46
|
+
cos = Math.cos(radians)
|
47
|
+
sin = Math.sin(radians)
|
48
|
+
(cos + sin - 1) / cos
|
49
|
+
end
|
50
|
+
|
51
|
+
# 3 bars = 0.5
|
52
|
+
# 4 bars = 0.4
|
53
|
+
# 5 bars = 0.333
|
54
|
+
# 6 bars = 0.285
|
55
|
+
# 10 bars = 0.182
|
56
|
+
# 20 bars = 0.0952
|
57
|
+
# 40 bars = 0.0488
|
58
|
+
# 50 bars = 0.0392
|
59
|
+
def bars_to_alpha(bars)
|
60
|
+
2.0 / (bars + 1)
|
61
|
+
end
|
62
|
+
|
63
|
+
def ema(source, prev_source, period)
|
64
|
+
alpha = bars_to_alpha(period)
|
65
|
+
v0 = source.is_a?(Symbol) ? p0.send(source) : source
|
66
|
+
v1 = p1.send(prev_source)
|
67
|
+
(v0 * alpha) + (v1 * (1 - alpha))
|
68
|
+
end
|
69
|
+
|
70
|
+
def band_pass(source, prev_source, period, bandwidth); end
|
71
|
+
|
72
|
+
def two_pole_butterworth(source, prev_source, period)
|
73
|
+
v0 = source.is_a?(Symbol) ? p0.send(source) : source
|
74
|
+
|
75
|
+
v1 = 0.5 * (v0 + p1.send(source))
|
76
|
+
v2 = p1.send(prev_source)
|
77
|
+
v3 = p2.send(prev_source)
|
78
|
+
|
79
|
+
radians = Math.sqrt(2) * Math::PI / period
|
80
|
+
a = Math.exp(-radians)
|
81
|
+
b = 2 * a * Math.cos(radians)
|
82
|
+
|
83
|
+
c2 = b
|
84
|
+
c3 = -a**2
|
85
|
+
c1 = 1.0 - c2 - c3
|
86
|
+
|
87
|
+
(c1 * v1) + (c2 * v2) + (c3 * v3)
|
88
|
+
end
|
89
|
+
|
90
|
+
def three_pole_butterworth(source, prev_source, period)
|
91
|
+
v0 = source.is_a?(Symbol) ? p0.send(source) : source
|
92
|
+
return v0 if p2 == p3
|
93
|
+
|
94
|
+
v1 = p1.send(prev_source)
|
95
|
+
v2 = p2.send(prev_source)
|
96
|
+
v3 = p3.send(prev_source)
|
97
|
+
|
98
|
+
radians = Math.sqrt(3) * Math::PI / period
|
99
|
+
a = Math.exp(-radians)
|
100
|
+
b = 2 * a * Math.cos(radians)
|
101
|
+
c = a**2
|
102
|
+
|
103
|
+
d4 = c**2
|
104
|
+
d3 = -(c + (b * c))
|
105
|
+
d2 = b + c
|
106
|
+
d1 = 1.0 - d2 - d3 - d4
|
107
|
+
|
108
|
+
(d1 * v0) + (d2 * v1) + (d3 * v2) + (d4 * v3)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
# Fisher Transforms
|
6
|
+
# • Price is not a Gaussian (Bell Curve) distribution, even though many technical analysis formulas
|
7
|
+
# falsely assume that it is. Bell Curve tails are missing.
|
8
|
+
# – If $10 stock were Gaussian, it could go up or down $20 – Standard deviation based indicators like Bollinger Bands
|
9
|
+
# and zScore make the Gaussian assumption error
|
10
|
+
# • TheFisher Transform converts almost any probability distribution in a Gaussian-like one
|
11
|
+
# – Expands the distribution and creates tails
|
12
|
+
# • The Inverse Fisher Transform converts almost any probability distribution into a square wave
|
13
|
+
# – Compresses, removes low amplitude variations
|
14
|
+
module FisherTransform
|
15
|
+
# inverse fisher transform
|
16
|
+
# https://www.mql5.com/en/articles/303
|
17
|
+
def ift(value, scale_factor = 1.0)
|
18
|
+
r = (Math.exp(2.0 * scale_factor * value) - 1.0) / (Math.exp(2.0 * scale_factor * value) + 1.0)
|
19
|
+
r.nan? ? 0.0 : r
|
20
|
+
end
|
21
|
+
|
22
|
+
# def fisher_transform(value, max_value)
|
23
|
+
# return 0.0 if max_value.zero?
|
24
|
+
# x = (value / max_value).abs
|
25
|
+
# r = 0.5 * Math.log((1 + x) / (1 - x))
|
26
|
+
# r.nan? ? 0.0 : r
|
27
|
+
# end
|
28
|
+
|
29
|
+
# The absolute value passed must be < 1.0
|
30
|
+
def fisher_transform(value)
|
31
|
+
r = 0.5 * Math.log((1.0 + value) / (1.0 - value))
|
32
|
+
r.nan? ? 0.0 : r
|
33
|
+
rescue Math::DomainError => e
|
34
|
+
raise "value #{value}: #{(1 + value) / (1 - value)}, e: #{e}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module HighPassFilter
|
6
|
+
# HighPass Filters are “detrenders” because they attenuate low frequency components
|
7
|
+
# One pole HighPass and SuperSmoother does not produce a zero mean because low
|
8
|
+
# frequency spectral dilation components are “leaking” through The one pole
|
9
|
+
# HighPass Filter response
|
10
|
+
def two_pole_high_pass_filter(source, prev_source, min_period, max_period = nil)
|
11
|
+
raise "source must be a symbol" unless source.is_a?(Symbol)
|
12
|
+
return p0.send(source) if p0 == p2
|
13
|
+
|
14
|
+
max_period ||= min_period * 2
|
15
|
+
(min_period * Math.sqrt(2))
|
16
|
+
max_radians = 2.0 * Math::PI / (max_period * Math.sqrt(2))
|
17
|
+
|
18
|
+
v1 = p0.send(source) - (2.0 * p1.send(source)) + p2.send(source)
|
19
|
+
v2 = p1.send(prev_source)
|
20
|
+
v3 = p2.send(prev_source)
|
21
|
+
|
22
|
+
alpha = period_to_alpha(max_radians)
|
23
|
+
|
24
|
+
a = (1 - (alpha * 0.5))**2 * v1
|
25
|
+
b = 2 * (1 - alpha) * v2
|
26
|
+
c = (1 - alpha)**2 * v3
|
27
|
+
|
28
|
+
a + b - c
|
29
|
+
end
|
30
|
+
|
31
|
+
# alpha = (Cosine(.707* 2 * PI / 48) + Sine (.707*360 / 48) - 1) / Cosine(.707*360 / 48);
|
32
|
+
# is the same as the following:
|
33
|
+
# radians = Math.sqrt(2) * Math::PI / period
|
34
|
+
# alpha = (Math.cos(radians) + Math.sin(radians) - 1) / Math.cos(radians)
|
35
|
+
def high_pass_filter(source, period)
|
36
|
+
v0 = source.is_a?(Symbol) ? p0.send(source) : source
|
37
|
+
return v0 if p3 == p0
|
38
|
+
|
39
|
+
v1 = p1.send(source)
|
40
|
+
v2 = p2.send(source)
|
41
|
+
|
42
|
+
radians = Math.sqrt(2) * Math::PI / period
|
43
|
+
a = Math.exp(-radians)
|
44
|
+
b = 2 * a * Math.cos(radians)
|
45
|
+
|
46
|
+
c2 = b
|
47
|
+
c3 = -a**2
|
48
|
+
c1 = (1 + c2 - c3) / 4
|
49
|
+
|
50
|
+
(c1 * (v0 - (2 * v1) + v2)) + (c2 * p1.hp) + (c3 * p2.hp)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module HilbertTransform
|
6
|
+
def hilbert_transform(source, period = p1.period)
|
7
|
+
[0.0962 * p0.send(source),
|
8
|
+
0.5769 * p2.send(source),
|
9
|
+
-0.5769 * prev(4).send(source),
|
10
|
+
-0.0962 * prev(6).send(source),].sum * ((0.075 * period) + 0.54)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module Stochastic
|
6
|
+
def stochastic(source, period = max_period)
|
7
|
+
stoch_period = [points.size, period.to_i].min
|
8
|
+
return 0.0 if stoch_period < 1
|
9
|
+
|
10
|
+
subset = points[-stoch_period, stoch_period].map{ |p| p.send(source) }
|
11
|
+
ll = subset.min
|
12
|
+
hh = subset.max
|
13
|
+
|
14
|
+
v0 = points[-1].send(source)
|
15
|
+
(hh - ll).zero? ? 0.0 : 100.0 * (v0 - ll) / (hh - ll)
|
16
|
+
end
|
17
|
+
|
18
|
+
# module Fields
|
19
|
+
# @[JSON::Field(key: "ish")]
|
20
|
+
# property inst_stoch : Float64 = 0.0
|
21
|
+
# @[JSON::Field(key: "sh")]
|
22
|
+
# property stoch : Float64 = 0.0
|
23
|
+
# @[JSON::Field(key: "su")]
|
24
|
+
# property stoch_up : Bool = false
|
25
|
+
# @[JSON::Field(key: "st")]
|
26
|
+
# property stoch_turned : Bool = false
|
27
|
+
# end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module SuperSmoother
|
6
|
+
def super_smoother(source, prev_source, period)
|
7
|
+
v0 = (source.is_a?(Symbol) ? p0.send(source) : source).to_d
|
8
|
+
return v0.to_f if points.size < 4
|
9
|
+
|
10
|
+
k = Math.exp(-Math.sqrt(2) * Math::PI / period)
|
11
|
+
coef3 = -k**2
|
12
|
+
coef2 = 2.0 * k * Math.cos(Math.sqrt(2) * (Math::PI / 2) / period)
|
13
|
+
coef1 = 1.0 - coef2 - coef3
|
14
|
+
|
15
|
+
v1 = p1.send(prev_source).to_d
|
16
|
+
v2 = p2.send(prev_source).to_d
|
17
|
+
p3.send(prev_source).to_d
|
18
|
+
((coef1 * (v0 + v1)) / 2.0 + (coef2 * v1) + (coef3 * v2)).to_f
|
19
|
+
end
|
20
|
+
|
21
|
+
def two_pole_super_smooth(source, prev_source, ssperiod)
|
22
|
+
return p1.send(source) if [p1 == p3]
|
23
|
+
|
24
|
+
radians = Math::PI * Math.sqrt(2) / ssperiod
|
25
|
+
a1 = Math.exp(-radians)
|
26
|
+
|
27
|
+
coef2 = 2.0 * a1 * Math.cos(radians)
|
28
|
+
coef3 = -a1 * a1
|
29
|
+
coef1 = 1.0 - coef2 - coef3
|
30
|
+
|
31
|
+
v0 = (p1.send(source) + p2.send(source)) / 2.0
|
32
|
+
v1 = p2.send(prev_source)
|
33
|
+
v2 = p3.send(prev_source)
|
34
|
+
(coef1 * v0) + (coef2 * v1) + (coef3 * v2)
|
35
|
+
end
|
36
|
+
|
37
|
+
def three_pole_super_smooth(source, prev_source, ssperiod)
|
38
|
+
a1 = Math.exp(-Math::PI / ssperiod)
|
39
|
+
b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / ssperiod)
|
40
|
+
c1 = a1**2
|
41
|
+
|
42
|
+
coef2 = b1 + c1
|
43
|
+
coef3 = -(c1 + b1 * c1)
|
44
|
+
coef4 = c1**2
|
45
|
+
coef1 = 1 - coef2 - coef3 - coef4
|
46
|
+
|
47
|
+
p0 = prev(0)
|
48
|
+
p1 = prev(1)
|
49
|
+
p2 = prev(2)
|
50
|
+
p3 = prev(3)
|
51
|
+
|
52
|
+
v0 = source.is_a?(Symbol) ? p0.send(source) : source
|
53
|
+
return v0 if [p0, p1, p2].include?(p3)
|
54
|
+
|
55
|
+
v1 = p1.send(prev_source)
|
56
|
+
v2 = p2.send(prev_source)
|
57
|
+
v3 = p3.send(prev_source)
|
58
|
+
(coef1 * v0) + (coef2 * v1) + (coef3 * v2) + (coef4 * v3)
|
59
|
+
end
|
60
|
+
|
61
|
+
# super smoother 3 pole
|
62
|
+
def ss3p(source, prev_source, ssperiod)
|
63
|
+
p0 = points[-1]
|
64
|
+
p1 = points[-2] || p0
|
65
|
+
p2 = points[-3] || p1
|
66
|
+
p3 = points[-4] || p2
|
67
|
+
|
68
|
+
v0 = source.is_a?(Symbol) ? p0.send(source) : source
|
69
|
+
return v0 if [p0 == p3]
|
70
|
+
|
71
|
+
debugger if points.size > 4
|
72
|
+
a1 = Math.exp(-Math::PI / ssperiod)
|
73
|
+
b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / ssperiod)
|
74
|
+
c1 = a1**2
|
75
|
+
|
76
|
+
coef2 = b1 + c1
|
77
|
+
coef3 = -(c1 + b1 * c1)
|
78
|
+
coef4 = c1**2
|
79
|
+
coef1 = 1 - coef2 - coef3 - coef4
|
80
|
+
|
81
|
+
v1 = p1.send(prev_source)
|
82
|
+
v2 = p2.send(prev_source)
|
83
|
+
v3 = p3.send(prev_source)
|
84
|
+
(coef1 * v0) + (coef2 * v1) + (coef3 * v2) + (coef4 * v3)
|
85
|
+
end
|
86
|
+
|
87
|
+
# attr_reader :hpfs, :value1s, :hpf_psns
|
88
|
+
|
89
|
+
# def hpf
|
90
|
+
# @hpfs[-1]
|
91
|
+
# end
|
92
|
+
|
93
|
+
# def hpf_psn
|
94
|
+
# @hpf_psns[-1]
|
95
|
+
# end
|
96
|
+
|
97
|
+
# def prev offset, source
|
98
|
+
# idx = offset + 1
|
99
|
+
# source[[-idx, -source.size].max]
|
100
|
+
# end
|
101
|
+
|
102
|
+
# def weighted_average source
|
103
|
+
# [ 4.0 * prev(0, source),
|
104
|
+
# 3.0 * prev(1, source),
|
105
|
+
# 2.0 * prev(2, source),
|
106
|
+
# prev(3, source),
|
107
|
+
# ].sum / 10.0
|
108
|
+
# end
|
109
|
+
|
110
|
+
# def compute_hpf
|
111
|
+
# @hpfs ||= []
|
112
|
+
# @value1s ||= []
|
113
|
+
# @hpf_psns ||= []
|
114
|
+
# max_cycle = period * 10
|
115
|
+
|
116
|
+
# r = (360.0 / max_cycle) * (Math::PI / 180)
|
117
|
+
# alpha = (1 - Math::sin(r)) / Math::cos(r)
|
118
|
+
# hpf = @hpfs.empty? ? 0.0 : (0.5 * (1.0 + alpha) * (current_value - prev_value(1))) + (alpha * (@hpfs[-1]))
|
119
|
+
|
120
|
+
# @hpfs << hpf
|
121
|
+
# @hpfs.shift if @hpfs.size > max_cycle
|
122
|
+
|
123
|
+
# hh = @hpfs.max
|
124
|
+
# ll = @hpfs.min
|
125
|
+
# @value1s << value1 = (hh == ll ? 0.0 : 100 * (hpf - ll) / (hh - ll))
|
126
|
+
# @value1s.shift if @value1s.size > max_cycle
|
127
|
+
|
128
|
+
# @hpf_psns << weighted_average(@value1s)
|
129
|
+
# @hpf_psns.shift if @hpf_psns.size > max_cycle
|
130
|
+
# end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module Trig
|
6
|
+
def deg2rad(degrees)
|
7
|
+
degrees * Math::PI / 180.0
|
8
|
+
end
|
9
|
+
|
10
|
+
def rad2deg(radians)
|
11
|
+
radians * 180.0 / Math::PI
|
12
|
+
end
|
13
|
+
|
14
|
+
# dx1 = x2-x1;
|
15
|
+
# dy1 = y2-y1;
|
16
|
+
# dx2 = x4-x3;
|
17
|
+
# dy2 = y4-y3;
|
18
|
+
|
19
|
+
# d = dx1*dx2 + dy1*dy2; // dot product of the 2 vectors
|
20
|
+
# l2 = (dx1*dx1+dy1*dy1)*(dx2*dx2+dy2*dy2) // product of the squared lengths
|
21
|
+
def angle(line1, line2)
|
22
|
+
dx1 = line2[0][0] - line1[0][0]
|
23
|
+
dy1 = line2[0][1] - line1[0][1]
|
24
|
+
dx2 = line2[1][0] - line1[1][0]
|
25
|
+
dy2 = line2[1][1] - line1[1][1]
|
26
|
+
|
27
|
+
d = dx1 * dx2 + dy1 * dy2
|
28
|
+
l2 = (dx1**2 + dy1**2) * (dx2**2 + dy2**2)
|
29
|
+
rad2deg Math.acos(d / Math.sqrt(l2))
|
30
|
+
end
|
31
|
+
|
32
|
+
# angle = acos(d/sqrt(l2))
|
33
|
+
# public static double angleBetween2Lines(Line2D line1, Line2D line2)
|
34
|
+
# {
|
35
|
+
# double angle1 = Math.atan2(line1.getY1() - line1.getY2(),
|
36
|
+
# line1.getX1() - line1.getX2());
|
37
|
+
# double angle2 = Math.atan2(line2.getY1() - line2.getY2(),
|
38
|
+
# line2.getX1() - line2.getX2());
|
39
|
+
# return angle1-angle2;
|
40
|
+
# }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module WeightedAverage
|
6
|
+
def weighted_average(source)
|
7
|
+
value = source.is_a?(Symbol) ? p0.send(source) : source
|
8
|
+
[4.0 * value,
|
9
|
+
3.0 * p1.send(source),
|
10
|
+
2.0 * p2.send(source),
|
11
|
+
p3.send(source),].sum / 10.0
|
12
|
+
end
|
13
|
+
|
14
|
+
def extended_weighted_average(source)
|
15
|
+
value = source.is_a?(Symbol) ? p0.send(source) : source
|
16
|
+
[7.0 * value,
|
17
|
+
6.0 * p1.send(source),
|
18
|
+
5.0 * p2.send(source),
|
19
|
+
4.0 * p3.send(source),
|
20
|
+
3.0 * prev(4).send(source),
|
21
|
+
2.0 * prev(5).send(source),
|
22
|
+
prev(6).send(source),].sum / 28.0
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Ticks
|
5
|
+
class IndicatorPoints
|
6
|
+
attr_reader :points
|
7
|
+
|
8
|
+
def initialize(tick:)
|
9
|
+
@tick = tick
|
10
|
+
@points = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](indicator)
|
14
|
+
points[indicator]
|
15
|
+
end
|
16
|
+
|
17
|
+
def []=(indicator, point)
|
18
|
+
points[indicator] = point
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require_relative 'value'
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Ticks
|
5
|
+
# serialized keys
|
6
|
+
# ot: open timestamp
|
7
|
+
# ct: close timestamp
|
8
|
+
# iv: interval
|
9
|
+
|
10
|
+
# o: open price
|
11
|
+
# h: high price
|
12
|
+
# l: low price
|
13
|
+
# c: close price
|
14
|
+
|
15
|
+
# bv: base volume
|
16
|
+
# tv: target volume
|
17
|
+
# ct: close timestamp
|
18
|
+
|
19
|
+
# t: trades
|
20
|
+
# g: green
|
21
|
+
# j: doji
|
22
|
+
class OHLC < Value
|
23
|
+
def self.from(hash)
|
24
|
+
new \
|
25
|
+
open_timestamp: hash["ot"],
|
26
|
+
close_timestamp: hash["ct"],
|
27
|
+
interval: hash["iv"],
|
28
|
+
|
29
|
+
open_price: hash["o"],
|
30
|
+
high_price: hash["h"],
|
31
|
+
low_price: hash["l"],
|
32
|
+
close_price: hash["c"],
|
33
|
+
|
34
|
+
base_volume: hash["bv"],
|
35
|
+
target_volume: hash["tv"],
|
36
|
+
|
37
|
+
trades: hash["t"],
|
38
|
+
green: hash["g"],
|
39
|
+
doji: hash["j"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.from_json(json)
|
43
|
+
from Oj.load(json)
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(open_timestamp:,
|
47
|
+
close_timestamp:,
|
48
|
+
interval: nil,
|
49
|
+
|
50
|
+
open_price:,
|
51
|
+
high_price:,
|
52
|
+
low_price:,
|
53
|
+
close_price:,
|
54
|
+
|
55
|
+
base_volume: 0.0,
|
56
|
+
target_volume: 0.0,
|
57
|
+
|
58
|
+
trades: 0,
|
59
|
+
green: false,
|
60
|
+
doji: nil)
|
61
|
+
|
62
|
+
super(price: close_price, timestamp: close_timestamp, interval:, trades:)
|
63
|
+
@open_timestamp = extract_time(open_timestamp)
|
64
|
+
@open_price = open_price.to_f
|
65
|
+
@high_price = high_price.to_f
|
66
|
+
@low_price = low_price.to_f
|
67
|
+
|
68
|
+
@base_volume = base_volume.to_i
|
69
|
+
@target_volume = target_volume.to_i
|
70
|
+
|
71
|
+
@green = green.nil? ? compute_green : green
|
72
|
+
@doji = doji.nil? ? compute_doji : doji
|
73
|
+
end
|
74
|
+
|
75
|
+
def hl2; ((high_price + low_price) / 2.0) end
|
76
|
+
def oc2; ((open_price + close_price) / 2.0) end
|
77
|
+
def hlc3; ((high_price + low_price + close_price) / 3.0) end
|
78
|
+
def ohlc4; ((open_price + high_price + low_price + close_price) / 4.0) end
|
79
|
+
|
80
|
+
def corresponding?(other)
|
81
|
+
[open_timestamp, close_timestamp] == [other.open_timestamp, other.close_timestamp]
|
82
|
+
end
|
83
|
+
|
84
|
+
# percent change from open to close
|
85
|
+
def delta
|
86
|
+
((open_price / close_price) - 1.0) * 100
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_h
|
90
|
+
{ "ot" => open_timestamp,
|
91
|
+
"ct" => close_timestamp,
|
92
|
+
"iv" => interval.to_s,
|
93
|
+
|
94
|
+
"o" => open_price,
|
95
|
+
"h" => high_price,
|
96
|
+
"l" => low_price,
|
97
|
+
"c" => close_price,
|
98
|
+
|
99
|
+
"bv" => base_volume,
|
100
|
+
"tv" => target_volume,
|
101
|
+
|
102
|
+
"t" => trades,
|
103
|
+
"g" => green,
|
104
|
+
"j" => doji }
|
105
|
+
end
|
106
|
+
|
107
|
+
def as_price(value)
|
108
|
+
series.nil? ? value : series.as_price(value)
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_s
|
112
|
+
ots = interval.daily? ? open_timestamp.strftime('%Y-%m-%d') : open_timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
113
|
+
cts = interval.daily? ? close_timestamp.strftime('%Y-%m-%d') : close_timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
114
|
+
"#{ots}: o: #{as_price(open_price)}, h: #{as_price(high_price)}, l: #{as_price(low_price)}, c: #{as_price(close_price)} :#{cts}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def compute_green
|
118
|
+
close_price >= open_price
|
119
|
+
end
|
120
|
+
|
121
|
+
def green?
|
122
|
+
close_price > open_price
|
123
|
+
end
|
124
|
+
|
125
|
+
def red?
|
126
|
+
!green?
|
127
|
+
end
|
128
|
+
|
129
|
+
def doji?
|
130
|
+
@doji
|
131
|
+
end
|
132
|
+
|
133
|
+
def price_change
|
134
|
+
@price_change ||= ((open_price - close_price) / oc2).abs
|
135
|
+
end
|
136
|
+
|
137
|
+
def compute_doji
|
138
|
+
body_bottom, body_top = [open_price, close_price].sort
|
139
|
+
|
140
|
+
body_length = body_top - body_bottom
|
141
|
+
head_length = high_price - [open_price, close_price].max
|
142
|
+
tail_length = [open_price, close_price].max - low_price
|
143
|
+
|
144
|
+
body_ratio = 100.0 * (1 - (body_bottom / body_top))
|
145
|
+
head_ratio = head_length / body_length
|
146
|
+
tail_ratio = tail_length / body_length
|
147
|
+
|
148
|
+
body_ratio < 0.025 && head_ratio > 1.0 && tail_ratio > 1.0
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Ticks
|
5
|
+
module Serializers
|
6
|
+
module Value
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def to_h(instance)
|
10
|
+
{ "iv" => instance.interval.to_s,
|
11
|
+
"ct" => instance.close_timestamp.to_i,
|
12
|
+
"cp" => instance.close_price,
|
13
|
+
"bv" => instance.base_volume,
|
14
|
+
"tv" => instance.target_volume }
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_json(instance)
|
18
|
+
Oj.dump to_h(instance)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Ticks
|
5
|
+
class Spot < Value
|
6
|
+
def self.from(hash)
|
7
|
+
new(close_timestamp: hash["ct"], close_price: hash["c"], base_volume: hash["bv"], target_volume: hash["tv"])
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_json(json)
|
11
|
+
from Oj.load(json)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(close_timestamp:, close_price:, interval: nil, base_volume: 0.0, target_volume: 0.0, trades: 0)
|
15
|
+
super(price: close_price, timestamp: close_timestamp, interval: interval, volume: base_volume, trades: trades)
|
16
|
+
@target_volume = target_volume.to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def corresponding?(other)
|
20
|
+
close_timestamp == other.close_timestamp
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_h
|
24
|
+
{ "ct" => close_timestamp,
|
25
|
+
"c" => close_price,
|
26
|
+
"iv" => interval.to_s,
|
27
|
+
"bv" => base_volume,
|
28
|
+
"tv" => target_volume,
|
29
|
+
"t" => trades }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|