quantitative 0.1.6 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/Gemfile +4 -0
- data/Gemfile.lock +17 -1
- data/README.md +14 -2
- data/lib/quant/asset.rb +1 -1
- data/lib/quant/attributes.rb +2 -2
- data/lib/quant/indicators/indicator.rb +4 -5
- data/lib/quant/indicators/indicator_point.rb +1 -0
- data/lib/quant/indicators_proxy.rb +1 -1
- data/lib/quant/mixins/butterworth_filters.rb +48 -0
- data/lib/quant/mixins/exponential_moving_average.rb +35 -0
- data/lib/quant/mixins/filters.rb +41 -104
- data/lib/quant/mixins/fisher_transform.rb +27 -18
- data/lib/quant/mixins/{trig.rb → functions.rb} +24 -1
- data/lib/quant/mixins/hilbert_transform.rb +3 -3
- data/lib/quant/mixins/moving_averages.rb +6 -78
- data/lib/quant/mixins/simple_moving_average.rb +21 -0
- data/lib/quant/mixins/super_smoother.rb +20 -106
- data/lib/quant/mixins/weighted_moving_average.rb +45 -0
- data/lib/quant/refinements/array.rb +1 -1
- data/lib/quant/series.rb +9 -9
- data/lib/quant/ticks/ohlc.rb +9 -5
- data/lib/quant/ticks/serializers/ohlc.rb +2 -2
- data/lib/quant/ticks/serializers/spot.rb +1 -1
- data/lib/quant/ticks/serializers/tick.rb +1 -1
- data/lib/quant/ticks/tick.rb +31 -20
- data/lib/quant/time_period.rb +1 -1
- data/lib/quant/version.rb +1 -1
- metadata +12 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7491465248d383f459899467e14c9bafa16376f8fe7dc2cac657fca8a9170200
|
4
|
+
data.tar.gz: 956074c17983ee22d6480a176c48b0cc15257aaca2649086eacc71a52688cb12
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f932bc68ca6f3e47261aaf2d19b34465b0f242bc13b5935ab55c0eea0ee736b0ea183b4bacc6a99d9beecd9a2457dd07957a639bd2d85e35f0a5584b35c2f2a
|
7
|
+
data.tar.gz: c0baa0af0133cbdd46439fc0ce8d068a583f05688371025275afe4297d0bd01a7aad90c4f3ebae98afaa1a4b28b56e43fa2aa68aaafc018bd34e22ec037d1b07
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
quantitative (0.1.
|
4
|
+
quantitative (0.1.8)
|
5
5
|
oj (~> 3.10)
|
6
6
|
|
7
7
|
GEM
|
@@ -15,6 +15,7 @@ GEM
|
|
15
15
|
irb (>= 1.5.0)
|
16
16
|
reline (>= 0.3.1)
|
17
17
|
diff-lcs (1.5.0)
|
18
|
+
docile (1.4.0)
|
18
19
|
ffi (1.16.3)
|
19
20
|
formatador (1.1.0)
|
20
21
|
guard (2.18.1)
|
@@ -79,6 +80,8 @@ GEM
|
|
79
80
|
rspec-expectations (3.12.3)
|
80
81
|
diff-lcs (>= 1.2.0, < 2.0)
|
81
82
|
rspec-support (~> 3.12.0)
|
83
|
+
rspec-github (2.4.0)
|
84
|
+
rspec-core (~> 3.0)
|
82
85
|
rspec-mocks (3.12.6)
|
83
86
|
diff-lcs (>= 1.2.0, < 2.0)
|
84
87
|
rspec-support (~> 3.12.0)
|
@@ -106,6 +109,15 @@ GEM
|
|
106
109
|
rubocop-factory_bot (~> 2.22)
|
107
110
|
ruby-progressbar (1.13.0)
|
108
111
|
shellany (0.0.1)
|
112
|
+
simplecov (0.22.0)
|
113
|
+
docile (~> 1.1)
|
114
|
+
simplecov-html (~> 0.11)
|
115
|
+
simplecov_json_formatter (~> 0.1)
|
116
|
+
simplecov-cobertura (2.1.0)
|
117
|
+
rexml
|
118
|
+
simplecov (~> 0.19)
|
119
|
+
simplecov-html (0.12.3)
|
120
|
+
simplecov_json_formatter (0.1.4)
|
109
121
|
stringio (3.1.0)
|
110
122
|
thor (1.3.0)
|
111
123
|
unicode-display_width (2.5.0)
|
@@ -113,6 +125,7 @@ GEM
|
|
113
125
|
|
114
126
|
PLATFORMS
|
115
127
|
arm64-darwin-22
|
128
|
+
x86_64-linux
|
116
129
|
|
117
130
|
DEPENDENCIES
|
118
131
|
benchmark-ips (~> 2.9)
|
@@ -122,8 +135,11 @@ DEPENDENCIES
|
|
122
135
|
rake (~> 13.0)
|
123
136
|
relaxed-rubocop
|
124
137
|
rspec (~> 3.0)
|
138
|
+
rspec-github
|
125
139
|
rubocop (~> 1.21)
|
126
140
|
rubocop-rspec
|
141
|
+
simplecov
|
142
|
+
simplecov-cobertura
|
127
143
|
yard (~> 0.9)
|
128
144
|
|
129
145
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Quantitative
|
2
2
|
|
3
|
-
[![Gem Version](https://badge.fury.io/rb/quantitative.svg)](https://badge.fury.io/rb/quantitative)
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/quantitative.svg)](https://badge.fury.io/rb/quantitative) [![codecov](https://codecov.io/gh/mwlang/quantitative/graph/badge.svg?token=ZXMSKQZKD5)](https://codecov.io/gh/mwlang/quantitative)
|
4
4
|
|
5
5
|
STATUS: ALPHA - very early stages! The framework is very much a work in progress and I am rapidly introducing new things and changing existing things around.
|
6
6
|
|
7
|
-
Quantitative is a statistical and quantitative library for Ruby 3.
|
7
|
+
Quantitative is a statistical and quantitative library for Ruby 3.2+ for trading stocks, cryptocurrency, and forex. It provides a number of classes and modules for working with time-series data, financial data, and other quantitative data. It is designed to be fast, efficient, and easy to use.
|
8
8
|
|
9
9
|
It has been highly optimized for fairly high-frequency trading purely in Ruby (no external numerical/statistical native extensions). The one exception is that I opted to depend on `Oj` which is a high-performant JSON parser that greatly speeds up serializing data between disk and memory. In practice, Quantitative is performant enough to trade one minute tickers on down to 30 second ticks for around 100 or so ticker symbols. Trading anything lower depends on the amount of analysis you're doing and your mileage may vary. It is possible, but you will find yourself with tradeoffs between the amount of data you can crunch and how fast you can react to live trading situations.
|
10
10
|
|
@@ -46,6 +46,18 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
46
46
|
|
47
47
|
Bug reports and pull requests are welcome on GitHub at https://github.com/mwlang/quantitative. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/mwlang/quantitative/blob/main/CODE_OF_CONDUCT.md).
|
48
48
|
|
49
|
+
The Relaxed Ruby Style Guide is adopted for RuboCop.
|
50
|
+
|
51
|
+
### Keeping Test Coverage High
|
52
|
+
|
53
|
+
TDD/BDD is fully embraced for this project. If you opt to contribute, please include tests to coverage new features and behavior tests.
|
54
|
+
|
55
|
+
RSpec is the test framework. SimpleCov is used for coverage reports.
|
56
|
+
|
57
|
+
### Test Driven / Behavior Driven development Coverage Map:
|
58
|
+
|
59
|
+
![Coverage Map](https://codecov.io/gh/mwlang/quantitative/graphs/sunburst.svg?token=ZXMSKQZKD5)
|
60
|
+
|
49
61
|
## License
|
50
62
|
|
51
63
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/quant/asset.rb
CHANGED
data/lib/quant/attributes.rb
CHANGED
@@ -93,7 +93,7 @@ module Quant
|
|
93
93
|
end
|
94
94
|
|
95
95
|
registry[klass] ||= {}
|
96
|
-
registry[klass][name] = { key
|
96
|
+
registry[klass][name] = { key:, default: }
|
97
97
|
end
|
98
98
|
|
99
99
|
module InstanceMethods
|
@@ -138,7 +138,7 @@ module Quant
|
|
138
138
|
if entry[:default].is_a?(Symbol) && respond_to?(entry[:default])
|
139
139
|
send(entry[:default])
|
140
140
|
|
141
|
-
elsif entry[:default].is_a?(Symbol) && current_tick
|
141
|
+
elsif entry[:default].is_a?(Symbol) && current_tick.respond_to?(entry[:default])
|
142
142
|
current_tick.send(entry[:default])
|
143
143
|
|
144
144
|
elsif entry[:default].is_a?(Proc)
|
@@ -4,10 +4,9 @@ module Quant
|
|
4
4
|
class Indicators
|
5
5
|
class Indicator
|
6
6
|
include Enumerable
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# include Mixins::WeightedAverage
|
7
|
+
include Mixins::Functions
|
8
|
+
include Mixins::Filters
|
9
|
+
include Mixins::MovingAverages
|
11
10
|
# include Mixins::HilbertTransform
|
12
11
|
# include Mixins::SuperSmoother
|
13
12
|
# include Mixins::Stochastic
|
@@ -46,7 +45,7 @@ module Quant
|
|
46
45
|
|
47
46
|
def <<(tick)
|
48
47
|
@t0 = tick
|
49
|
-
@p0 = points_class.new(tick
|
48
|
+
@p0 = points_class.new(tick:, source:)
|
50
49
|
@points[tick] = @p0
|
51
50
|
|
52
51
|
@p1 = values[-2] || @p0
|
@@ -25,7 +25,7 @@ module Quant
|
|
25
25
|
# prepared, the indicator becomes active and all ticks pushed into the series
|
26
26
|
# are sent to the indicator for processing.
|
27
27
|
def indicator(indicator_class)
|
28
|
-
indicators[indicator_class] ||= indicator_class.new(series
|
28
|
+
indicators[indicator_class] ||= indicator_class.new(series:, source:)
|
29
29
|
end
|
30
30
|
|
31
31
|
# Adds the tick to all active indicators, triggering them to compute
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module ButterworthFilters
|
6
|
+
def two_pole_butterworth(source, period:, previous: :bw)
|
7
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
8
|
+
|
9
|
+
v0 = p0.send(source)
|
10
|
+
|
11
|
+
v1 = 0.5 * (v0 + p1.send(source))
|
12
|
+
v2 = p1.send(previous)
|
13
|
+
v3 = p2.send(previous)
|
14
|
+
|
15
|
+
radians = Math.sqrt(2) * Math::PI / period
|
16
|
+
a = Math.exp(-radians)
|
17
|
+
b = 2 * a * Math.cos(radians)
|
18
|
+
|
19
|
+
c2 = b
|
20
|
+
c3 = -a**2
|
21
|
+
c1 = 1.0 - c2 - c3
|
22
|
+
|
23
|
+
(c1 * v1) + (c2 * v2) + (c3 * v3)
|
24
|
+
end
|
25
|
+
|
26
|
+
def three_pole_butterworth(source, period:, previous: :bw)
|
27
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
28
|
+
|
29
|
+
v0 = p0.send(source)
|
30
|
+
v1 = p1.send(previous)
|
31
|
+
v2 = p2.send(previous)
|
32
|
+
v3 = p3.send(previous)
|
33
|
+
|
34
|
+
radians = Math.sqrt(3) * Math::PI / period
|
35
|
+
a = Math.exp(-radians)
|
36
|
+
b = 2 * a * Math.cos(radians)
|
37
|
+
c = a**2
|
38
|
+
|
39
|
+
d4 = c**2
|
40
|
+
d3 = -(c + (b * c))
|
41
|
+
d2 = b + c
|
42
|
+
d1 = 1.0 - d2 - d3 - d4
|
43
|
+
|
44
|
+
(d1 * v0) + (d2 * v1) + (d3 * v2) + (d4 * v3)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module ExponentialMovingAverage
|
6
|
+
# Computes the Exponential Moving Average (EMA) of the given period.
|
7
|
+
#
|
8
|
+
# The EMA computation is optimized to compute using just the last two
|
9
|
+
# indicator data points and is expected to be called in each indicator's
|
10
|
+
# `#compute` method for each iteration on the series.
|
11
|
+
#
|
12
|
+
# @param source [Symbol] the source of the data points to be used in the calculation.
|
13
|
+
# @param previous [Symbol] the previous EMA value.
|
14
|
+
# @param period [Integer] the number of elements to compute the EMA over.
|
15
|
+
# @return [Float] the exponential moving average of the period.
|
16
|
+
# @raise [ArgumentError] if the source is not a Symbol.
|
17
|
+
# @example
|
18
|
+
# def compute
|
19
|
+
# p0.ema = exponential_moving_average(:close_price, period: 3)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# def compute
|
23
|
+
# p0.ema = exponential_moving_average(:close_price, previous: :ema, period: 3)
|
24
|
+
# end
|
25
|
+
def exponential_moving_average(source, period:, previous: :ema)
|
26
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
27
|
+
raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
|
28
|
+
|
29
|
+
alpha = bars_to_alpha(period)
|
30
|
+
(p0.send(source) * alpha) + (p1.send(previous) * (1.0 - alpha))
|
31
|
+
end
|
32
|
+
alias ema exponential_moving_average
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/quant/mixins/filters.rb
CHANGED
@@ -1,114 +1,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "trig"
|
4
|
-
|
5
3
|
module Quant
|
6
4
|
module Mixins
|
7
|
-
# 1. All the common filters useful for traders have a transfer response
|
8
|
-
# as a ratio of two polynomials.
|
9
|
-
# 2. Lag is very important to traders. More complex filters can be
|
10
|
-
# but more input data increases lag.
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
5
|
+
# 1. All the common filters useful for traders have a transfer response
|
6
|
+
# that can be written as a ratio of two polynomials.
|
7
|
+
# 2. Lag is very important to traders. More complex filters can be
|
8
|
+
# created using more input data, but more input data increases lag.
|
9
|
+
# Sophisticated filters are not very useful for trading because they
|
10
|
+
# incur too much lag.
|
11
|
+
# 3. Filter transfer response can be viewed in the time domain and
|
12
|
+
# the frequency domain with equal validity.
|
13
|
+
# 4. Nonrecursive filters can have zeros in the transfer response, enabling
|
14
|
+
# the complete cancellation of some selected frequency components.
|
15
|
+
# 5. Nonrecursive filters having coefficients symmetrical about the
|
16
|
+
# center of the filter will have a delay of half the degree of the
|
17
|
+
# transfer response polynomial at all frequencies.
|
18
|
+
# 6. Low-pass filters are smoothers because they attenuate the high-frequency
|
19
|
+
# components of the input data.
|
20
|
+
# 7. High-pass filters are detrenders because they attenuate the
|
21
|
+
# low-frequency components of trends.
|
22
|
+
# 8. Band-pass filters are both detrenders and smoothers because they
|
23
|
+
# attenuate all but the desired frequency components.
|
24
|
+
# 9. Filters provide an output only through their transfer response.
|
25
|
+
# The transfer response is strictly a mathematical function, and
|
26
|
+
# interpretations such as overbought, oversold, convergence, divergence,
|
27
|
+
# and so on are not implied. The validity of such interpretations
|
28
|
+
# must be made on the basis of statistics apart from the filter.
|
29
|
+
# 10. The critical period of a filter output is the frequency at which
|
30
|
+
# the output power of the filter is half the power of the input
|
31
|
+
# wave at that frequency.
|
26
32
|
# 11. A WMA has little or no redeeming virtue.
|
27
|
-
# 12. A median filter is best used when the data contain impulsive noise
|
28
|
-
# variations in the data. Smoothing volume
|
29
|
-
# median filter.
|
33
|
+
# 12. A median filter is best used when the data contain impulsive noise
|
34
|
+
# or when there are wild variations in the data. Smoothing volume
|
35
|
+
# data is one example of a good application for a median filter.
|
36
|
+
#
|
37
|
+
# == Filter Coefficients forVariousTypes of Filters
|
38
|
+
#
|
39
|
+
# Filter Type b0 b1 b2 a0 a1 a2
|
40
|
+
# EMA α 0 0 1 −(1−α) 0
|
41
|
+
# Two-pole low-pass α**2 0 0 1 −2*(1-α) (1-α)**2
|
42
|
+
# High-pass (1-α/2) -(1-α/2) 0 1 −(1−α) 0
|
43
|
+
# Two-pole high-pass (1-α/2)**2 -2*(1-α/2)**2 (1-α/2)**2 1 -2*(1-α) (1-α)**2
|
44
|
+
# Band-pass (1-σ)/2 0 -(1-σ)/2 1 -λ*(1+σ) σ
|
45
|
+
# Band-stop (1+σ)/2 -2λ*(1+σ)/2 (1+σ)/2 1 -λ*(1+σ) σ
|
30
46
|
#
|
31
|
-
# Filter Coefficients forVariousTypes of Filters
|
32
|
-
# Filter Type b0 b1 b2 a0 a1 a2
|
33
|
-
# EMA α 0 0 1 −(1−α) 0
|
34
|
-
# Two-pole low-pass α**2 0 0 1 −2*(1-α) (1-α)**2
|
35
|
-
# High-pass (1-α/2) -(1-α/2) 0 1 −(1−α) 0
|
36
|
-
# Two-pole high-pass (1-α/2)**2 -2*(1-α/2)**2 (1-α/2)**2 1 -2*(1-α) (1-α)**2
|
37
|
-
# Band-pass (1-σ)/2 0 -(1-σ)/2 1 -λ*(1+σ) σ
|
38
|
-
# Band-stop (1+σ)/2 -2λ*(1+σ)/2 (1+σ)/2 1 -λ*(1+σ) σ
|
39
47
|
module Filters
|
40
|
-
include Mixins::
|
41
|
-
|
42
|
-
# α = Cos(K*360/Period)+Sin(K*360/Period)−1 / Cos(K*360/Period)
|
43
|
-
# k = 1.0 for single-pole filters
|
44
|
-
# k = 0.707 for two-pole high-pass filters
|
45
|
-
# k = 1.414 for two-pole low-pass filters
|
46
|
-
def period_to_alpha(period, k: 1.0)
|
47
|
-
radians = deg2rad(k * 360 / period)
|
48
|
-
cos = Math.cos(radians)
|
49
|
-
sin = Math.sin(radians)
|
50
|
-
(cos + sin - 1) / cos
|
51
|
-
end
|
52
|
-
|
53
|
-
# 3 bars = 0.5
|
54
|
-
# 4 bars = 0.4
|
55
|
-
# 5 bars = 0.333
|
56
|
-
# 6 bars = 0.285
|
57
|
-
# 10 bars = 0.182
|
58
|
-
# 20 bars = 0.0952
|
59
|
-
# 40 bars = 0.0488
|
60
|
-
# 50 bars = 0.0392
|
61
|
-
def bars_to_alpha(bars)
|
62
|
-
2.0 / (bars + 1)
|
63
|
-
end
|
64
|
-
|
65
|
-
def ema(source, prev_source, period)
|
66
|
-
alpha = bars_to_alpha(period)
|
67
|
-
v0 = source.is_a?(Symbol) ? p0.send(source) : source
|
68
|
-
v1 = p1.send(prev_source)
|
69
|
-
(v0 * alpha) + (v1 * (1 - alpha))
|
70
|
-
end
|
71
|
-
|
72
|
-
def band_pass(source, prev_source, period, bandwidth); end
|
73
|
-
|
74
|
-
def two_pole_butterworth(source, prev_source, period)
|
75
|
-
v0 = source.is_a?(Symbol) ? p0.send(source) : source
|
76
|
-
|
77
|
-
v1 = 0.5 * (v0 + p1.send(source))
|
78
|
-
v2 = p1.send(prev_source)
|
79
|
-
v3 = p2.send(prev_source)
|
80
|
-
|
81
|
-
radians = Math.sqrt(2) * Math::PI / period
|
82
|
-
a = Math.exp(-radians)
|
83
|
-
b = 2 * a * Math.cos(radians)
|
84
|
-
|
85
|
-
c2 = b
|
86
|
-
c3 = -a**2
|
87
|
-
c1 = 1.0 - c2 - c3
|
88
|
-
|
89
|
-
(c1 * v1) + (c2 * v2) + (c3 * v3)
|
90
|
-
end
|
91
|
-
|
92
|
-
def three_pole_butterworth(source, prev_source, period)
|
93
|
-
v0 = source.is_a?(Symbol) ? p0.send(source) : source
|
94
|
-
return v0 if p2 == p3
|
95
|
-
|
96
|
-
v1 = p1.send(prev_source)
|
97
|
-
v2 = p2.send(prev_source)
|
98
|
-
v3 = p3.send(prev_source)
|
99
|
-
|
100
|
-
radians = Math.sqrt(3) * Math::PI / period
|
101
|
-
a = Math.exp(-radians)
|
102
|
-
b = 2 * a * Math.cos(radians)
|
103
|
-
c = a**2
|
104
|
-
|
105
|
-
d4 = c**2
|
106
|
-
d3 = -(c + (b * c))
|
107
|
-
d2 = b + c
|
108
|
-
d1 = 1.0 - d2 - d3 - d4
|
109
|
-
|
110
|
-
(d1 * v0) + (d2 * v1) + (d3 * v2) + (d4 * v3)
|
111
|
-
end
|
48
|
+
include Mixins::ButterworthFilters
|
112
49
|
end
|
113
50
|
end
|
114
51
|
end
|
@@ -3,36 +3,45 @@
|
|
3
3
|
module Quant
|
4
4
|
module Mixins
|
5
5
|
# Fisher Transforms
|
6
|
-
# • Price is not a Gaussian (Bell Curve) distribution, even though many
|
7
|
-
# falsely assume that it is. Bell Curve tails
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
6
|
+
# • Price is not a Gaussian (Bell Curve) distribution, even though many
|
7
|
+
# technical analysis formulas falsely assume that it is. Bell Curve tails
|
8
|
+
# are missing.
|
9
|
+
# – If $10 stock were Gaussian, it could go up or down $20
|
10
|
+
# – Standard deviation based indicators like Bollinger Bands
|
11
|
+
# and zScore make the Gaussian assumption error
|
12
|
+
#
|
13
|
+
# • TheFisher Transform converts almost any probability distribution
|
14
|
+
# in a Gaussian-like one
|
15
|
+
# – Expands the distribution and creates tails
|
16
|
+
#
|
17
|
+
# • The Inverse Fisher Transform converts almost any probability
|
18
|
+
# distribution into a square wave
|
19
|
+
# – Compresses, removes low amplitude variations
|
14
20
|
module FisherTransform
|
15
21
|
# inverse fisher transform
|
16
22
|
# https://www.mql5.com/en/articles/303
|
17
|
-
def
|
23
|
+
def inverse_fisher_transform(value, scale_factor: 1.0)
|
18
24
|
r = (Math.exp(2.0 * scale_factor * value) - 1.0) / (Math.exp(2.0 * scale_factor * value) + 1.0)
|
19
25
|
r.nan? ? 0.0 : r
|
20
26
|
end
|
27
|
+
alias ift inverse_fisher_transform
|
21
28
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
# r.nan? ? 0.0 : r
|
27
|
-
# end
|
29
|
+
def relative_fisher_transform(value, max_value:)
|
30
|
+
max_value.zero? ? 0.0 : fisher_transform(value / max_value)
|
31
|
+
end
|
32
|
+
alias rft relative_fisher_transform
|
28
33
|
|
29
34
|
# The absolute value passed must be < 1.0
|
30
35
|
def fisher_transform(value)
|
31
|
-
|
32
|
-
|
36
|
+
raise ArgumentError, "value (#{value}) must be between -1.0 and 1.0" unless value.abs <= 1.0
|
37
|
+
|
38
|
+
result = 0.5 * Math.log((1.0 + value) / (1.0 - value))
|
39
|
+
result.nan? ? 0.0 : result
|
33
40
|
rescue Math::DomainError => e
|
34
|
-
raise "
|
41
|
+
raise Math::DomainError, "#{e.message}: cannot take the Log of #{value}: #{(1 + value) / (1 - value)}"
|
35
42
|
end
|
43
|
+
alias fisher fisher_transform
|
44
|
+
alias ft fisher_transform
|
36
45
|
end
|
37
46
|
end
|
38
47
|
end
|
@@ -2,7 +2,30 @@
|
|
2
2
|
|
3
3
|
module Quant
|
4
4
|
module Mixins
|
5
|
-
module
|
5
|
+
module Functions
|
6
|
+
# α = Cos(K*360/Period)+Sin(K*360/Period)−1 / Cos(K*360/Period)
|
7
|
+
# k = 1.0 for single-pole filters
|
8
|
+
# k = 0.707 for two-pole high-pass filters
|
9
|
+
# k = 1.414 for two-pole low-pass filters
|
10
|
+
def period_to_alpha(period, k: 1.0)
|
11
|
+
radians = deg2rad(k * 360 / period)
|
12
|
+
cos = Math.cos(radians)
|
13
|
+
sin = Math.sin(radians)
|
14
|
+
(cos + sin - 1) / cos
|
15
|
+
end
|
16
|
+
|
17
|
+
# 3 bars = 0.5
|
18
|
+
# 4 bars = 0.4
|
19
|
+
# 5 bars = 0.333
|
20
|
+
# 6 bars = 0.285
|
21
|
+
# 10 bars = 0.182
|
22
|
+
# 20 bars = 0.0952
|
23
|
+
# 40 bars = 0.0488
|
24
|
+
# 50 bars = 0.0392
|
25
|
+
def bars_to_alpha(bars)
|
26
|
+
2.0 / (bars + 1)
|
27
|
+
end
|
28
|
+
|
6
29
|
def deg2rad(degrees)
|
7
30
|
degrees * Math::PI / 180.0
|
8
31
|
end
|
@@ -3,11 +3,11 @@
|
|
3
3
|
module Quant
|
4
4
|
module Mixins
|
5
5
|
module HilbertTransform
|
6
|
-
def hilbert_transform(source, period
|
6
|
+
def hilbert_transform(source, period:)
|
7
7
|
[0.0962 * p0.send(source),
|
8
8
|
0.5769 * p2.send(source),
|
9
|
-
-0.5769 *
|
10
|
-
-0.0962 *
|
9
|
+
-0.5769 * p(4).send(source),
|
10
|
+
-0.0962 * p(6).send(source),].sum * ((0.075 * period) + 0.54)
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
@@ -1,86 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "weighted_moving_average"
|
4
|
+
require_relative "simple_moving_average"
|
5
|
+
require_relative "exponential_moving_average"
|
3
6
|
module Quant
|
4
7
|
module Mixins
|
5
8
|
module MovingAverages
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
#
|
10
|
-
# @param source [Symbol] the source of the data points to be used in the calculation.
|
11
|
-
# @return [Float] the weighted average of the series.
|
12
|
-
# @raise [ArgumentError] if the source is not a Symbol.
|
13
|
-
# @example
|
14
|
-
# p0.wma = weighted_average(:close_price)
|
15
|
-
def weighted_moving_average(source)
|
16
|
-
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
17
|
-
|
18
|
-
[4.0 * p0.send(source),
|
19
|
-
3.0 * p1.send(source),
|
20
|
-
2.0 * p2.send(source),
|
21
|
-
p3.send(source)].sum / 10.0
|
22
|
-
end
|
23
|
-
alias wma weighted_moving_average
|
24
|
-
|
25
|
-
# Computes the Weighted Moving Average (WMA) of the series, using the seven most recent data points.
|
26
|
-
#
|
27
|
-
# @param source [Symbol] the source of the data points to be used in the calculation.
|
28
|
-
# @return [Float] the weighted average of the series.
|
29
|
-
# @raise [ArgumentError] if the source is not a Symbol.
|
30
|
-
# @example
|
31
|
-
# p0.wma = weighted_average(:close_price)
|
32
|
-
def extended_weighted_moving_average(source)
|
33
|
-
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
34
|
-
|
35
|
-
[7.0 * p0.send(source),
|
36
|
-
6.0 * p1.send(source),
|
37
|
-
5.0 * p2.send(source),
|
38
|
-
4.0 * p3.send(source),
|
39
|
-
3.0 * p(4).send(source),
|
40
|
-
2.0 * p(5).send(source),
|
41
|
-
p(6).send(source)].sum / 28.0
|
42
|
-
end
|
43
|
-
alias ewma extended_weighted_moving_average
|
44
|
-
|
45
|
-
# Computes the Simple Moving Average (SMA) of the given period.
|
46
|
-
#
|
47
|
-
# @param source [Symbol] the source of the data points to be used in the calculation.
|
48
|
-
# @param period [Integer] the number of elements to compute the SMA over.
|
49
|
-
# @return [Float] the simple moving average of the period.
|
50
|
-
def simple_moving_average(source, period:)
|
51
|
-
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
52
|
-
|
53
|
-
values.last(period).map { |value| value.send(source) }.mean
|
54
|
-
end
|
55
|
-
alias sma simple_moving_average
|
56
|
-
|
57
|
-
# Computes the Exponential Moving Average (EMA) of the given period.
|
58
|
-
#
|
59
|
-
# The EMA computation is optimized to compute using just the last two
|
60
|
-
# indicator data points and is expected to be called in each indicator's
|
61
|
-
# `#compute` method for each iteration on the series.
|
62
|
-
#
|
63
|
-
# @param source [Symbol] the source of the data points to be used in the calculation.
|
64
|
-
# @param previous [Symbol] the previous EMA value.
|
65
|
-
# @param period [Integer] the number of elements to compute the EMA over.
|
66
|
-
# @return [Float] the exponential moving average of the period.
|
67
|
-
# @raise [ArgumentError] if the source is not a Symbol.
|
68
|
-
# @example
|
69
|
-
# def compute
|
70
|
-
# p0.ema = exponential_moving_average(:close_price, period: 3)
|
71
|
-
# end
|
72
|
-
#
|
73
|
-
# def compute
|
74
|
-
# p0.ema = exponential_moving_average(:close_price, previous: :ema, period: 3)
|
75
|
-
# end
|
76
|
-
def exponential_moving_average(source, previous: :ema, period:)
|
77
|
-
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
78
|
-
raise ArgumentError, "previous must be a Symbol" unless previous.is_a?(Symbol)
|
79
|
-
|
80
|
-
alpha = 2.0 / (period + 1)
|
81
|
-
p0.send(source) * alpha + p1.send(previous) * (1.0 - alpha)
|
82
|
-
end
|
83
|
-
alias ema exponential_moving_average
|
9
|
+
include WeightedMovingAverage
|
10
|
+
include SimpleMovingAverage
|
11
|
+
include ExponentialMovingAverage
|
84
12
|
end
|
85
13
|
end
|
86
14
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module SimpleMovingAverage
|
6
|
+
using Quant
|
7
|
+
|
8
|
+
# Computes the Simple Moving Average (SMA) of the given period.
|
9
|
+
#
|
10
|
+
# @param source [Symbol] the source of the data points to be used in the calculation.
|
11
|
+
# @param period [Integer] the number of elements to compute the SMA over.
|
12
|
+
# @return [Float] the simple moving average of the period.
|
13
|
+
def simple_moving_average(source, period:)
|
14
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
15
|
+
|
16
|
+
values.last(period).map { |value| value.send(source) }.mean
|
17
|
+
end
|
18
|
+
alias sma simple_moving_average
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -3,73 +3,29 @@
|
|
3
3
|
module Quant
|
4
4
|
module Mixins
|
5
5
|
module SuperSmoother
|
6
|
-
def
|
7
|
-
|
8
|
-
return v0.to_f if points.size < 4
|
6
|
+
def two_pole_super_smooth(source, period:, previous: :ss)
|
7
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
9
8
|
|
10
|
-
|
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
|
9
|
+
radians = Math::PI * Math.sqrt(2) / period
|
25
10
|
a1 = Math.exp(-radians)
|
26
11
|
|
27
|
-
coef2 = 2.
|
12
|
+
coef2 = 2.0r * a1 * Math.cos(radians)
|
28
13
|
coef3 = -a1 * a1
|
29
14
|
coef1 = 1.0 - coef2 - coef3
|
30
15
|
|
31
|
-
v0 = (
|
32
|
-
v1 = p2.send(
|
33
|
-
v2 = p3.send(
|
34
|
-
(coef1 * v0) + (coef2 * v1) + (coef3 * v2)
|
16
|
+
v0 = (p0.send(source) + p1.send(source)) / 2.0
|
17
|
+
v1 = p2.send(previous)
|
18
|
+
v2 = p3.send(previous)
|
19
|
+
((coef1 * v0) + (coef2 * v1) + (coef3 * v2)).to_f
|
35
20
|
end
|
21
|
+
alias super_smoother two_pole_super_smooth
|
22
|
+
alias ss2p two_pole_super_smooth
|
36
23
|
|
37
|
-
def three_pole_super_smooth(source,
|
38
|
-
|
39
|
-
b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / ssperiod)
|
40
|
-
c1 = a1**2
|
24
|
+
def three_pole_super_smooth(source, period:, previous: :ss)
|
25
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
41
26
|
|
42
|
-
|
43
|
-
|
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
|
-
a1 = Math.exp(-Math::PI / ssperiod)
|
72
|
-
b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / ssperiod)
|
27
|
+
a1 = Math.exp(-Math::PI / period)
|
28
|
+
b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / period)
|
73
29
|
c1 = a1**2
|
74
30
|
|
75
31
|
coef2 = b1 + c1
|
@@ -77,56 +33,14 @@ module Quant
|
|
77
33
|
coef4 = c1**2
|
78
34
|
coef1 = 1 - coef2 - coef3 - coef4
|
79
35
|
|
80
|
-
|
81
|
-
|
82
|
-
|
36
|
+
v0 = p0.send(source)
|
37
|
+
v1 = p1.send(previous)
|
38
|
+
v2 = p2.send(previous)
|
39
|
+
v3 = p3.send(previous)
|
40
|
+
|
83
41
|
(coef1 * v0) + (coef2 * v1) + (coef3 * v2) + (coef4 * v3)
|
84
42
|
end
|
85
|
-
|
86
|
-
# attr_reader :hpfs, :value1s, :hpf_psns
|
87
|
-
|
88
|
-
# def hpf
|
89
|
-
# @hpfs[-1]
|
90
|
-
# end
|
91
|
-
|
92
|
-
# def hpf_psn
|
93
|
-
# @hpf_psns[-1]
|
94
|
-
# end
|
95
|
-
|
96
|
-
# def prev offset, source
|
97
|
-
# idx = offset + 1
|
98
|
-
# source[[-idx, -source.size].max]
|
99
|
-
# end
|
100
|
-
|
101
|
-
# def weighted_average source
|
102
|
-
# [ 4.0 * prev(0, source),
|
103
|
-
# 3.0 * prev(1, source),
|
104
|
-
# 2.0 * prev(2, source),
|
105
|
-
# prev(3, source),
|
106
|
-
# ].sum / 10.0
|
107
|
-
# end
|
108
|
-
|
109
|
-
# def compute_hpf
|
110
|
-
# @hpfs ||= []
|
111
|
-
# @value1s ||= []
|
112
|
-
# @hpf_psns ||= []
|
113
|
-
# max_cycle = period * 10
|
114
|
-
|
115
|
-
# r = (360.0 / max_cycle) * (Math::PI / 180)
|
116
|
-
# alpha = (1 - Math::sin(r)) / Math::cos(r)
|
117
|
-
# hpf = @hpfs.empty? ? 0.0 : (0.5 * (1.0 + alpha) * (current_value - prev_value(1))) + (alpha * (@hpfs[-1]))
|
118
|
-
|
119
|
-
# @hpfs << hpf
|
120
|
-
# @hpfs.shift if @hpfs.size > max_cycle
|
121
|
-
|
122
|
-
# hh = @hpfs.max
|
123
|
-
# ll = @hpfs.min
|
124
|
-
# @value1s << value1 = (hh == ll ? 0.0 : 100 * (hpf - ll) / (hh - ll))
|
125
|
-
# @value1s.shift if @value1s.size > max_cycle
|
126
|
-
|
127
|
-
# @hpf_psns << weighted_average(@value1s)
|
128
|
-
# @hpf_psns.shift if @hpf_psns.size > max_cycle
|
129
|
-
# end
|
43
|
+
alias ss3p three_pole_super_smooth
|
130
44
|
end
|
131
45
|
end
|
132
46
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module WeightedMovingAverage
|
6
|
+
using Quant
|
7
|
+
# Computes the Weighted Moving Average (WMA) of the series, using the four most recent data points.
|
8
|
+
#
|
9
|
+
# @param source [Symbol] the source of the data points to be used in the calculation.
|
10
|
+
# @return [Float] the weighted average of the series.
|
11
|
+
# @raise [ArgumentError] if the source is not a Symbol.
|
12
|
+
# @example
|
13
|
+
# p0.wma = weighted_average(:close_price)
|
14
|
+
def weighted_moving_average(source)
|
15
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
16
|
+
|
17
|
+
[4.0 * p0.send(source),
|
18
|
+
3.0 * p1.send(source),
|
19
|
+
2.0 * p2.send(source),
|
20
|
+
p3.send(source)].sum / 10.0
|
21
|
+
end
|
22
|
+
alias wma weighted_moving_average
|
23
|
+
|
24
|
+
# Computes the Weighted Moving Average (WMA) of the series, using the seven most recent data points.
|
25
|
+
#
|
26
|
+
# @param source [Symbol] the source of the data points to be used in the calculation.
|
27
|
+
# @return [Float] the weighted average of the series.
|
28
|
+
# @raise [ArgumentError] if the source is not a Symbol.
|
29
|
+
# @example
|
30
|
+
# p0.wma = weighted_average(:close_price)
|
31
|
+
def extended_weighted_moving_average(source)
|
32
|
+
raise ArgumentError, "source must be a Symbol" unless source.is_a?(Symbol)
|
33
|
+
|
34
|
+
[7.0 * p0.send(source),
|
35
|
+
6.0 * p1.send(source),
|
36
|
+
5.0 * p2.send(source),
|
37
|
+
4.0 * p3.send(source),
|
38
|
+
3.0 * p(4).send(source),
|
39
|
+
2.0 * p(5).send(source),
|
40
|
+
p(6).send(source)].sum / 28.0
|
41
|
+
end
|
42
|
+
alias ewma extended_weighted_moving_average
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -176,7 +176,7 @@ module Quant
|
|
176
176
|
# @param n [Integer] the number of elements to compute the Standard Deviation over.
|
177
177
|
# @return [Float]
|
178
178
|
def stddev(reference_value, n: size)
|
179
|
-
variance(reference_value, n:
|
179
|
+
variance(reference_value, n:)**0.5
|
180
180
|
end
|
181
181
|
|
182
182
|
def variance(reference_value, n: size)
|
data/lib/quant/series.rb
CHANGED
@@ -19,7 +19,7 @@ module Quant
|
|
19
19
|
raise "File #{filename} does not exist" unless File.exist?(filename)
|
20
20
|
|
21
21
|
ticks = File.read(filename).split("\n").map{ |line| Oj.load(line) }
|
22
|
-
from_hash symbol
|
22
|
+
from_hash symbol:, interval:, hash: ticks, serializer_class:
|
23
23
|
end
|
24
24
|
|
25
25
|
# Loads a series of ticks when the JSON string represents an array of ticks.
|
@@ -29,8 +29,8 @@ module Quant
|
|
29
29
|
# @param json [String] The JSON string to parse into ticks.
|
30
30
|
# @param serializer_class [Class] {Quant::Ticks::TickSerializer} class to use for the conversion.
|
31
31
|
def self.from_json(symbol:, interval:, json:, serializer_class: nil)
|
32
|
-
|
33
|
-
from_hash symbol
|
32
|
+
hash = Oj.load(json)
|
33
|
+
from_hash symbol:, interval:, hash:, serializer_class:
|
34
34
|
end
|
35
35
|
|
36
36
|
# Loads a series of ticks where the hash must be cast to an array of {Quant::Ticks::Tick} objects.
|
@@ -39,15 +39,15 @@ module Quant
|
|
39
39
|
# @param hash [Array<Hash>] The array of hashes to convert to {Quant::Ticks::Tick} objects.
|
40
40
|
# @param serializer_class [Class] {Quant::Ticks::TickSerializer} class to use for the conversion.
|
41
41
|
def self.from_hash(symbol:, interval:, hash:, serializer_class: nil)
|
42
|
-
ticks = hash.map { |tick_hash| Quant::Ticks::OHLC.from(tick_hash, serializer_class:
|
43
|
-
from_ticks symbol
|
42
|
+
ticks = hash.map { |tick_hash| Quant::Ticks::OHLC.from(tick_hash, serializer_class:) }
|
43
|
+
from_ticks symbol:, interval:, ticks:
|
44
44
|
end
|
45
45
|
|
46
46
|
# Loads a series of ticks where the array represents an array of {Quant::Ticks::Tick} objects.
|
47
47
|
def self.from_ticks(symbol:, interval:, ticks:)
|
48
48
|
ticks = ticks.sort_by(&:close_timestamp)
|
49
49
|
|
50
|
-
new(symbol
|
50
|
+
new(symbol:, interval:).tap do |series|
|
51
51
|
ticks.each { |tick| series << tick }
|
52
52
|
end
|
53
53
|
end
|
@@ -64,14 +64,14 @@ module Quant
|
|
64
64
|
selected_ticks = ticks[start_iteration..stop_iteration]
|
65
65
|
return self if selected_ticks.size == ticks.size
|
66
66
|
|
67
|
-
self.class.from_ticks(symbol
|
67
|
+
self.class.from_ticks(symbol:, interval:, ticks: selected_ticks)
|
68
68
|
end
|
69
69
|
|
70
70
|
def limit(period)
|
71
71
|
selected_ticks = ticks.select{ |tick| period.cover?(tick.close_timestamp) }
|
72
72
|
return self if selected_ticks.size == ticks.size
|
73
73
|
|
74
|
-
self.class.from_ticks(symbol
|
74
|
+
self.class.from_ticks(symbol:, interval:, ticks: selected_ticks)
|
75
75
|
end
|
76
76
|
|
77
77
|
def_delegator :@ticks, :[]
|
@@ -94,7 +94,7 @@ module Quant
|
|
94
94
|
end
|
95
95
|
|
96
96
|
def dup
|
97
|
-
self.class.from_ticks(symbol
|
97
|
+
self.class.from_ticks(symbol:, interval:, ticks:)
|
98
98
|
end
|
99
99
|
|
100
100
|
def inspect
|
data/lib/quant/ticks/ohlc.rb
CHANGED
@@ -4,11 +4,15 @@ require_relative "tick"
|
|
4
4
|
|
5
5
|
module Quant
|
6
6
|
module Ticks
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
7
|
+
# A {Quant::Ticks::OHLC} is a bar or candle for a point in time that
|
8
|
+
# has an open, high, low, and close price. It is the most common form
|
9
|
+
# of a {Quant::Ticks::Tick} and is usually used to representa time
|
10
|
+
# period such as a minute, hour, day, week, or month.
|
11
|
+
#
|
12
|
+
# The {Quant::Ticks::OHLC} is used to represent the price action of
|
13
|
+
# an asset The interval of the {Quant::Ticks::OHLC} is the time period
|
14
|
+
# that the {Quant::Ticks::OHLC} represents, such has hourly, daily,
|
15
|
+
# weekly, etc.
|
12
16
|
class OHLC < Tick
|
13
17
|
include TimeMethods
|
14
18
|
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Quant
|
4
4
|
module Ticks
|
5
5
|
module Serializers
|
6
|
-
|
6
|
+
class OHLC < Tick
|
7
7
|
# Returns a +Quant::Ticks::Tick+ from a valid JSON +String+.
|
8
8
|
# @param json [String]
|
9
9
|
# @param tick_class [Quant::Ticks::Tick]
|
@@ -13,7 +13,7 @@ module Quant
|
|
13
13
|
# Quant::Ticks::Serializers::Tick.from_json(json, tick_class: Quant::Ticks::Spot)
|
14
14
|
def self.from_json(json, tick_class:)
|
15
15
|
hash = Oj.load(json)
|
16
|
-
from(hash, tick_class:
|
16
|
+
from(hash, tick_class:)
|
17
17
|
end
|
18
18
|
|
19
19
|
# Instantiates a tick from a +Hash+. The hash keys are expected to be the same as the serialized keys.
|
@@ -13,7 +13,7 @@ module Quant
|
|
13
13
|
# Quant::Ticks::Serializers::Tick.from_json(json, tick_class: Quant::Ticks::Spot)
|
14
14
|
def self.from_json(json, tick_class:)
|
15
15
|
hash = Oj.load(json)
|
16
|
-
from(hash, tick_class:
|
16
|
+
from(hash, tick_class:)
|
17
17
|
end
|
18
18
|
|
19
19
|
# Returns a +Hash+ of the Spot tick's key properties
|
data/lib/quant/ticks/tick.rb
CHANGED
@@ -2,22 +2,29 @@
|
|
2
2
|
|
3
3
|
module Quant
|
4
4
|
module Ticks
|
5
|
-
# {Quant::Ticks::Tick} is the abstract ancestor for all Ticks and holds
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# for the integrity of the series and indicators that depend on the
|
15
|
-
#
|
16
|
-
|
17
|
-
#
|
5
|
+
# {Quant::Ticks::Tick} is the abstract ancestor for all Ticks and holds
|
6
|
+
# the logic for interacting with series and indicators. The public
|
7
|
+
# interface is devoid of properties around price, volume, and timestamp, etc.
|
8
|
+
# Descendant classes are responsible for defining the properties and
|
9
|
+
# how they are represented.
|
10
|
+
|
11
|
+
# The {Quant::Ticks::Tick} class is designed to be immutable and is
|
12
|
+
# intended to be used as a value object. This means that once a
|
13
|
+
# {Quant::Ticks::Tick} is created, it cannot be changed. This is important
|
14
|
+
# for the integrity of the series and indicators that depend on the
|
15
|
+
# ticks within the series.
|
16
|
+
|
17
|
+
# When a tick is added to a series, it is locked into the series and
|
18
|
+
# ownership cannot be changed. This is important for the integrity
|
19
|
+
# of the series and indicators that depend on the ticks within the series.
|
20
|
+
# This is a key design to being able to being able to not only compute
|
21
|
+
# indicators on the ticks just once, but also avoid recomputing indicators
|
22
|
+
# when series are limited/sliced/filtered into subsets of the original series.
|
23
|
+
|
18
24
|
# Ticks can be serialized to and from Ruby Hash, JSON strings, and CSV strings.
|
19
25
|
class Tick
|
20
|
-
# Returns a {Quant::Ticks::Tick} from a Ruby +Hash+. The default
|
26
|
+
# Returns a {Quant::Ticks::Tick} from a Ruby +Hash+. The default
|
27
|
+
# serializer is used to generate the {Quant::Ticks::Tick}.
|
21
28
|
# @param hash [Hash]
|
22
29
|
# @param serializer_class [Class] The serializer class to use for the conversion.
|
23
30
|
# @return [Quant::Ticks::Tick]
|
@@ -30,7 +37,8 @@ module Quant
|
|
30
37
|
serializer_class.from(hash, tick_class: self)
|
31
38
|
end
|
32
39
|
|
33
|
-
# Returns a {Quant::Ticks::Tick} from a JSON string. The default
|
40
|
+
# Returns a {Quant::Ticks::Tick} from a JSON string. The default
|
41
|
+
# serializer is used to generate the {Quant::Ticks::Tick}.
|
34
42
|
# @param json [String]
|
35
43
|
# @param serializer_class [Class] The serializer class to use for the conversion.
|
36
44
|
# @return [Quant::Ticks::Tick]
|
@@ -80,7 +88,8 @@ module Quant
|
|
80
88
|
self
|
81
89
|
end
|
82
90
|
|
83
|
-
# Returns a Ruby hash for the Tick. The default serializer is used
|
91
|
+
# Returns a Ruby hash for the Tick. The default serializer is used
|
92
|
+
# to generate the hash.
|
84
93
|
#
|
85
94
|
# @param serializer_class [Class] the serializer class to use for the conversion.
|
86
95
|
# @example
|
@@ -90,7 +99,8 @@ module Quant
|
|
90
99
|
serializer_class.to_h(self)
|
91
100
|
end
|
92
101
|
|
93
|
-
# Returns a JSON string for the Tick. The default serializer is used
|
102
|
+
# Returns a JSON string for the Tick. The default serializer is used
|
103
|
+
# to generate the JSON string.
|
94
104
|
#
|
95
105
|
# @param serializer_class [Class] the serializer class to use for the conversion.
|
96
106
|
# @example
|
@@ -100,8 +110,9 @@ module Quant
|
|
100
110
|
serializer_class.to_json(self)
|
101
111
|
end
|
102
112
|
|
103
|
-
# Returns a CSV row as a String for the Tick. The default serializer
|
104
|
-
# If headers is true, two lines
|
113
|
+
# Returns a CSV row as a String for the Tick. The default serializer
|
114
|
+
# is used to generate the CSV string. If headers is true, two lines
|
115
|
+
# returned separated by newline.
|
105
116
|
# The first line is the header row and the second line is the data row.
|
106
117
|
#
|
107
118
|
# @param serializer_class [Class] the serializer class to use for the conversion.
|
@@ -109,7 +120,7 @@ module Quant
|
|
109
120
|
# tick.to_csv(headers: true)
|
110
121
|
# # => "timestamp,price,volume\n2018-01-01 12:00:00 UTC,100.0,1000\n"
|
111
122
|
def to_csv(serializer_class: default_serializer_class, headers: false)
|
112
|
-
serializer_class.to_csv(self, headers:
|
123
|
+
serializer_class.to_csv(self, headers:)
|
113
124
|
end
|
114
125
|
|
115
126
|
# Reflects the serializer class from the tick's class name.
|
data/lib/quant/time_period.rb
CHANGED
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.1.
|
4
|
+
version: 0.1.8
|
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-02-
|
11
|
+
date: 2024-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -24,8 +24,8 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.10'
|
27
|
-
description: Quantitative and statistical tools written for Ruby 3.
|
28
|
-
finance.
|
27
|
+
description: Quantitative and statistical tools written for Ruby 3.2+ for trading
|
28
|
+
and finance.
|
29
29
|
email:
|
30
30
|
- mwlang@cybrains.net
|
31
31
|
executables: []
|
@@ -57,15 +57,19 @@ files:
|
|
57
57
|
- lib/quant/indicators_proxy.rb
|
58
58
|
- lib/quant/indicators_sources.rb
|
59
59
|
- lib/quant/interval.rb
|
60
|
+
- lib/quant/mixins/butterworth_filters.rb
|
60
61
|
- lib/quant/mixins/direction.rb
|
62
|
+
- lib/quant/mixins/exponential_moving_average.rb
|
61
63
|
- lib/quant/mixins/filters.rb
|
62
64
|
- lib/quant/mixins/fisher_transform.rb
|
65
|
+
- lib/quant/mixins/functions.rb
|
63
66
|
- lib/quant/mixins/high_pass_filter.rb
|
64
67
|
- lib/quant/mixins/hilbert_transform.rb
|
65
68
|
- lib/quant/mixins/moving_averages.rb
|
69
|
+
- lib/quant/mixins/simple_moving_average.rb
|
66
70
|
- lib/quant/mixins/stochastic.rb
|
67
71
|
- lib/quant/mixins/super_smoother.rb
|
68
|
-
- lib/quant/mixins/
|
72
|
+
- lib/quant/mixins/weighted_moving_average.rb
|
69
73
|
- lib/quant/refinements/array.rb
|
70
74
|
- lib/quant/series.rb
|
71
75
|
- lib/quant/settings.rb
|
@@ -96,7 +100,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
100
|
requirements:
|
97
101
|
- - ">="
|
98
102
|
- !ruby/object:Gem::Version
|
99
|
-
version: 3.
|
103
|
+
version: '3.2'
|
100
104
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
105
|
requirements:
|
102
106
|
- - ">="
|
@@ -106,5 +110,6 @@ requirements: []
|
|
106
110
|
rubygems_version: 3.5.6
|
107
111
|
signing_key:
|
108
112
|
specification_version: 4
|
109
|
-
summary: Quantitative and statistical tools written for Ruby 3.
|
113
|
+
summary: Quantitative and statistical tools written for Ruby 3.2+ for trading and
|
114
|
+
finance.
|
110
115
|
test_files: []
|