quantitative 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quant
4
+ module Ticks
5
+ class Core
6
+ end
7
+ end
8
+ 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