quantitative 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|