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.
@@ -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