quantitative 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/Gemfile.lock +1 -1
- data/lib/quant/attributes.rb +92 -8
- data/lib/quant/indicators/indicator.rb +10 -6
- data/lib/quant/indicators/indicator_point.rb +1 -1
- data/lib/quant/indicators/ma.rb +2 -0
- data/lib/quant/indicators/ping.rb +6 -0
- data/lib/quant/indicators_proxy.rb +38 -0
- data/lib/quant/mixins/exponential_moving_average.rb +35 -0
- data/lib/quant/mixins/filters.rb +42 -55
- data/lib/quant/mixins/{trig.rb → functions.rb} +24 -1
- data/lib/quant/mixins/moving_averages.rb +14 -0
- data/lib/quant/mixins/simple_moving_average.rb +21 -0
- data/lib/quant/mixins/super_smoother.rb +20 -107
- data/lib/quant/mixins/weighted_moving_average.rb +45 -0
- data/lib/quant/refinements/array.rb +4 -3
- data/lib/quant/settings.rb +2 -0
- data/lib/quant/ticks/ohlc.rb +10 -6
- data/lib/quant/ticks/tick.rb +30 -19
- data/lib/quant/version.rb +1 -1
- data/lib/quantitative.rb +1 -1
- metadata +7 -4
- data/lib/quant/mixins/weighted_average.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c2f8345b245cd469b234752f2fb757a1006b6a04322412acbb3fc10bd759827
|
4
|
+
data.tar.gz: fa5388e69c9f82fe686424dcfd81d8795ea6814a4b2b98b0edb6a52598ffbdaf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb3c3cc8db589f5051d9dcd921fc1c01292897c440cfdb72c45df568051213ddd3fad5aa27e40652aff452619eefa835dcc4675120d050c9a46bbf34e83be3cc
|
7
|
+
data.tar.gz: cfcc8acf5989103a9a59b84965b3d1d3eb1f3288737afa3636f684ea5f378e10d82f4bda5e487f9a1ddcb867abd0fb82cef4048219acac6ee98bcfd412124db5
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
data/lib/quant/attributes.rb
CHANGED
@@ -1,10 +1,60 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Quant
|
4
|
+
# {Quant::Attributes} is similar to an +attr_accessor+ definition. It provides a simple DSL
|
5
|
+
# for defining attributes or properies on an {Quant::Indicators::IndicatorPoint} class.
|
6
|
+
#
|
7
|
+
# {Quant::Attributes} tracks all defined attributes from child to parent classes,
|
8
|
+
# allowing child classes to inherit their parent's attributes as well as redefine them.
|
9
|
+
#
|
10
|
+
# The exception on redefining is that a serialized key cannot be redefined. Experience
|
11
|
+
# has proven that this leads to serialization surprises where what was written to a specific
|
12
|
+
# key is not what was expected!
|
13
|
+
#
|
14
|
+
# NOTE: The above design constraint could be improved with a force or overwrite option.
|
15
|
+
#
|
16
|
+
# If :default is an immediate value (Integer, Float, Boolean, etc.), it will be used as the
|
17
|
+
# initial value for the attribute. If :default is a Symbol, it will send a message on
|
18
|
+
# current instance of the class get the default value.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# class FooPoint < IndicatorPoint
|
22
|
+
# # will not serialize to a key
|
23
|
+
# attribute :bar
|
24
|
+
# # serializes to "bzz" key
|
25
|
+
# attribute :baz, key: "bzz"
|
26
|
+
# # calls the random method on the instance for the default value
|
27
|
+
# attribute :foobar, default: :random
|
28
|
+
# # delegated to the tick's high_price method
|
29
|
+
# attribute :high, default: :high_price
|
30
|
+
# # calls the lambda bound to instance for default
|
31
|
+
# attribute :low, default: -> { high_price - 5 }
|
32
|
+
#
|
33
|
+
# def random
|
34
|
+
# rand(100)
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# class BarPoint < FooPoint
|
39
|
+
# attribute :bar, key: "brr" # redefines and sets the key for bar
|
40
|
+
# attribute :qux, key: "qxx", default: 5.0 # serializes to "qxx" and defaults to 5.0
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# FooPoint.attributes
|
44
|
+
# # => { bar: { key: nil, default: nil },
|
45
|
+
# baz: { key: "bzz", default: nil } }
|
46
|
+
#
|
47
|
+
# BarPoint.attributes
|
48
|
+
# # => { bar: { key: "brr", default: nil },
|
49
|
+
# # baz: { key: "bzz", default: nil },
|
50
|
+
# # qux: { key: "qxx", default: nil } }
|
51
|
+
#
|
52
|
+
# BarPoint.new.bar # => nil
|
53
|
+
# BarPoint.new.qux # => 5.0
|
54
|
+
# BarPoint.new.bar = 2.0 => 2.0
|
4
55
|
module Attributes
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# and the attribute's key and default value.
|
56
|
+
# The +registry+ key is the class registering an attrbritute and is itself
|
57
|
+
# a hash of the attribute name and the attribute's key and default value.
|
8
58
|
# Internal use only.
|
9
59
|
#
|
10
60
|
# @example
|
@@ -47,9 +97,18 @@ module Quant
|
|
47
97
|
end
|
48
98
|
|
49
99
|
module InstanceMethods
|
50
|
-
|
51
|
-
|
52
|
-
|
100
|
+
# Makes some assumptions about the class's initialization having a +tick+ keyword argument.
|
101
|
+
#
|
102
|
+
# The challenge here is that we prepend this module to the class, and we are
|
103
|
+
# initializing attributes before the owning class gets the opportunity to initialize
|
104
|
+
# variables that we wanted to depend on with being able to define a default
|
105
|
+
# value that could set default values from a +tick+ method.
|
106
|
+
#
|
107
|
+
# Ok for now. May need to be more flexible in the future. Alternative strategy could be
|
108
|
+
# to lazy eval the default value the first time it is accessed.
|
109
|
+
def initialize(*args, **kwargs)
|
110
|
+
initialize_attributes(tick: kwargs[:tick])
|
111
|
+
super(*args, **kwargs)
|
53
112
|
end
|
54
113
|
|
55
114
|
# Iterates over all defined attributes in a child => parent hierarchy,
|
@@ -65,17 +124,42 @@ module Quant
|
|
65
124
|
end
|
66
125
|
end
|
67
126
|
|
127
|
+
# The default value can be one of the following:
|
128
|
+
# - A symbol that is a method on the instance responds to
|
129
|
+
# - A symbol that is a method that the instance's tick responds to
|
130
|
+
# - A Proc that is bound to the instance
|
131
|
+
# - An immediate value (Integer, Float, Boolean, etc.)
|
132
|
+
def default_value_for(entry, new_tick)
|
133
|
+
# let's not assume tick is always available/implemented
|
134
|
+
# can get from instance or from initializer passed here as `new_tick`
|
135
|
+
current_tick = new_tick
|
136
|
+
current_tick ||= tick if respond_to?(:tick)
|
137
|
+
|
138
|
+
if entry[:default].is_a?(Symbol) && respond_to?(entry[:default])
|
139
|
+
send(entry[:default])
|
140
|
+
|
141
|
+
elsif entry[:default].is_a?(Symbol) && current_tick&.respond_to?(entry[:default])
|
142
|
+
current_tick.send(entry[:default])
|
143
|
+
|
144
|
+
elsif entry[:default].is_a?(Proc)
|
145
|
+
instance_exec(&entry[:default])
|
146
|
+
|
147
|
+
else
|
148
|
+
entry[:default]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
68
152
|
# Initializes the defined attributes with default values and
|
69
153
|
# defines accessor methods for each attribute.
|
70
154
|
# If a child class redefines a parent's attribute, the child's
|
71
155
|
# definition will be used.
|
72
|
-
def initialize_attributes
|
156
|
+
def initialize_attributes(tick:)
|
73
157
|
each_attribute do |name, entry|
|
74
158
|
# use the child's definition, skipping the parent's
|
75
159
|
next if respond_to?(name)
|
76
160
|
|
77
161
|
ivar_name = "@#{name}"
|
78
|
-
instance_variable_set(ivar_name, entry
|
162
|
+
instance_variable_set(ivar_name, default_value_for(entry, tick))
|
79
163
|
define_singleton_method(name) { instance_variable_get(ivar_name) }
|
80
164
|
define_singleton_method("#{name}=") { |value| instance_variable_set(ivar_name, value) }
|
81
165
|
end
|
@@ -1,11 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Quant
|
2
4
|
class Indicators
|
3
5
|
class Indicator
|
4
6
|
include Enumerable
|
5
|
-
|
6
|
-
|
7
|
-
# include Mixins::Trig
|
8
|
-
# include Mixins::WeightedAverage
|
7
|
+
include Mixins::Functions
|
8
|
+
include Mixins::MovingAverages
|
9
9
|
# include Mixins::HilbertTransform
|
10
10
|
# include Mixins::SuperSmoother
|
11
11
|
# include Mixins::Stochastic
|
@@ -27,6 +27,10 @@ module Quant
|
|
27
27
|
@points.keys
|
28
28
|
end
|
29
29
|
|
30
|
+
def [](index)
|
31
|
+
values[index]
|
32
|
+
end
|
33
|
+
|
30
34
|
def values
|
31
35
|
@points.values
|
32
36
|
end
|
@@ -59,7 +63,7 @@ module Quant
|
|
59
63
|
end
|
60
64
|
|
61
65
|
def inspect
|
62
|
-
"#<#{self.class.name} symbol=#{series.symbol} source=#{source} #{
|
66
|
+
"#<#{self.class.name} symbol=#{series.symbol} source=#{source} #{ticks.size} ticks>"
|
63
67
|
end
|
64
68
|
|
65
69
|
def compute
|
@@ -168,4 +172,4 @@ module Quant
|
|
168
172
|
# end
|
169
173
|
end
|
170
174
|
end
|
171
|
-
end
|
175
|
+
end
|
data/lib/quant/indicators/ma.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Quant
|
2
4
|
class Indicators
|
5
|
+
# A simple point used primarily to test the indicator system in unit tests.
|
6
|
+
# It has a simple computation that just sets the pong value to the input value
|
7
|
+
# and increments the compute_count by 1 each time compute is called.
|
8
|
+
# Sometimes you just gotta play ping pong to win.
|
3
9
|
class PingPoint < IndicatorPoint
|
4
10
|
attribute :pong
|
5
11
|
attribute :compute_count, default: 0
|
@@ -1,6 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Quant
|
4
|
+
# The {Quant::IndicatorsProxy} class is responsible for lazily loading indicators
|
5
|
+
# so that not all indicators are always engaged and computing their values.
|
6
|
+
# If the indicator is never accessed, it's never computed, saving valuable
|
7
|
+
# processing CPU cycles.
|
8
|
+
#
|
9
|
+
# Indicators are generally built around the concept of a source input value and
|
10
|
+
# that source is designated by the source parameter when instantiating the
|
11
|
+
# {Quant::IndicatorsProxy} class.
|
12
|
+
#
|
13
|
+
# By design, the {Quant::Indicator} class holds the {Quant::Ticks::Tick} instance
|
14
|
+
# alongside the indicator's computed values for that tick.
|
4
15
|
class IndicatorsProxy
|
5
16
|
attr_reader :series, :source, :indicators
|
6
17
|
|
@@ -10,14 +21,41 @@ module Quant
|
|
10
21
|
@indicators = {}
|
11
22
|
end
|
12
23
|
|
24
|
+
# Instantiates the indicator class and stores it in the indicators hash. Once
|
25
|
+
# prepared, the indicator becomes active and all ticks pushed into the series
|
26
|
+
# are sent to the indicator for processing.
|
13
27
|
def indicator(indicator_class)
|
14
28
|
indicators[indicator_class] ||= indicator_class.new(series: series, source: source)
|
15
29
|
end
|
16
30
|
|
31
|
+
# Adds the tick to all active indicators, triggering them to compute
|
32
|
+
# new values against the latest tick.
|
33
|
+
#
|
34
|
+
# NOTE: Dominant cycle indicators must be computed first as many
|
35
|
+
# indicators are adaptive and require the dominant cycle period.
|
36
|
+
# The IndicatorsProxy class is not responsible for enforcing
|
37
|
+
# this order of events.
|
17
38
|
def <<(tick)
|
18
39
|
indicators.each_value { |indicator| indicator << tick }
|
19
40
|
end
|
20
41
|
|
42
|
+
# Attaches a given Indicator class and defines the method for
|
43
|
+
# accessing it using the given name. Indicators take care of
|
44
|
+
# computing their values when first attached to a populated
|
45
|
+
# series.
|
46
|
+
#
|
47
|
+
# The indicators shipped with the library are all wired into the framework, thus
|
48
|
+
# this method should be used for custom indicators not shipped with the library.
|
49
|
+
#
|
50
|
+
# @param name [Symbol] The name of the method to define for accessing the indicator.
|
51
|
+
# @param indicator_class [Class] The class of the indicator to attach.
|
52
|
+
# @example
|
53
|
+
# series.indicators.oc2.attach(name: :foo, indicator_class: Indicators::Foo)
|
54
|
+
def attach(name:, indicator_class:)
|
55
|
+
define_singleton_method(name) { indicator(indicator_class) }
|
56
|
+
end
|
57
|
+
|
21
58
|
def ma; indicator(Indicators::Ma) end
|
59
|
+
def ping; indicator(Indicators::Ping) end
|
22
60
|
end
|
23
61
|
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,66 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
3
|
+
require_relative "functions"
|
4
4
|
|
5
5
|
module Quant
|
6
6
|
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
|
-
#
|
7
|
+
# 1. All the common filters useful for traders have a transfer response
|
8
|
+
# that can be written as a ratio of two polynomials.
|
9
|
+
# 2. Lag is very important to traders. More complex filters can be
|
10
|
+
# created using more input data, but more input data increases lag.
|
11
|
+
# Sophisticated filters are not very useful for trading because they
|
12
|
+
# incur too much lag.
|
13
|
+
# 3. Filter transfer response can be viewed in the time domain and
|
14
|
+
# the frequency domain with equal validity.
|
15
|
+
# 4. Nonrecursive filters can have zeros in the transfer response, enabling
|
16
|
+
# the complete cancellation of some selected frequency components.
|
17
|
+
# 5. Nonrecursive filters having coefficients symmetrical about the
|
18
|
+
# center of the filter will have a delay of half the degree of the
|
19
|
+
# transfer response polynomial at all frequencies.
|
20
|
+
# 6. Low-pass filters are smoothers because they attenuate the high-frequency
|
21
|
+
# components of the input data.
|
22
|
+
# 7. High-pass filters are detrenders because they attenuate the
|
23
|
+
# low-frequency components of trends.
|
24
|
+
# 8. Band-pass filters are both detrenders and smoothers because they
|
25
|
+
# attenuate all but the desired frequency components.
|
26
|
+
# 9. Filters provide an output only through their transfer response.
|
27
|
+
# The transfer response is strictly a mathematical function, and
|
28
|
+
# interpretations such as overbought, oversold, convergence, divergence,
|
29
|
+
# and so on are not implied. The validity of such interpretations
|
30
|
+
# must be made on the basis of statistics apart from the filter.
|
31
|
+
# 10. The critical period of a filter output is the frequency at which
|
32
|
+
# the output power of the filter is half the power of the input
|
33
|
+
# wave at that frequency.
|
26
34
|
# 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.
|
35
|
+
# 12. A median filter is best used when the data contain impulsive noise
|
36
|
+
# or when there are wild variations in the data. Smoothing volume
|
37
|
+
# data is one example of a good application for a median filter.
|
38
|
+
#
|
39
|
+
# == Filter Coefficients forVariousTypes of Filters
|
40
|
+
#
|
41
|
+
# Filter Type b0 b1 b2 a0 a1 a2
|
42
|
+
# EMA α 0 0 1 −(1−α) 0
|
43
|
+
# Two-pole low-pass α**2 0 0 1 −2*(1-α) (1-α)**2
|
44
|
+
# High-pass (1-α/2) -(1-α/2) 0 1 −(1−α) 0
|
45
|
+
# Two-pole high-pass (1-α/2)**2 -2*(1-α/2)**2 (1-α/2)**2 1 -2*(1-α) (1-α)**2
|
46
|
+
# Band-pass (1-σ)/2 0 -(1-σ)/2 1 -λ*(1+σ) σ
|
47
|
+
# Band-stop (1+σ)/2 -2λ*(1+σ)/2 (1+σ)/2 1 -λ*(1+σ) σ
|
30
48
|
#
|
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
49
|
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
|
50
|
+
include Mixins::Functions
|
64
51
|
|
65
52
|
def ema(source, prev_source, period)
|
66
53
|
alpha = bars_to_alpha(period)
|
@@ -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
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "weighted_moving_average"
|
4
|
+
require_relative "simple_moving_average"
|
5
|
+
require_relative "exponential_moving_average"
|
6
|
+
module Quant
|
7
|
+
module Mixins
|
8
|
+
module MovingAverages
|
9
|
+
include WeightedMovingAverage
|
10
|
+
include SimpleMovingAverage
|
11
|
+
include ExponentialMovingAverage
|
12
|
+
end
|
13
|
+
end
|
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,74 +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
|
-
debugger if points.size > 4
|
72
|
-
a1 = Math.exp(-Math::PI / ssperiod)
|
73
|
-
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)
|
74
29
|
c1 = a1**2
|
75
30
|
|
76
31
|
coef2 = b1 + c1
|
@@ -78,56 +33,14 @@ module Quant
|
|
78
33
|
coef4 = c1**2
|
79
34
|
coef1 = 1 - coef2 - coef3 - coef4
|
80
35
|
|
81
|
-
|
82
|
-
|
83
|
-
|
36
|
+
v0 = p0.send(source)
|
37
|
+
v1 = p1.send(previous)
|
38
|
+
v2 = p2.send(previous)
|
39
|
+
v3 = p3.send(previous)
|
40
|
+
|
84
41
|
(coef1 * v0) + (coef2 * v1) + (coef3 * v2) + (coef4 * v3)
|
85
42
|
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
|
43
|
+
alias ss3p three_pole_super_smooth
|
131
44
|
end
|
132
45
|
end
|
133
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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Quant
|
2
4
|
module Refinements
|
3
5
|
# Refinements for the standard Ruby {Quant::Array} class.
|
@@ -32,7 +34,6 @@ module Quant
|
|
32
34
|
# The refined behavior generally only exists within the library's scope, but if you call `using Quant` in your
|
33
35
|
# own code, you may encounter the changed behavior unexpectedly.
|
34
36
|
module Array
|
35
|
-
|
36
37
|
# Overrides the standard +<<+ method to track the +maximum+ and +minimum+ values
|
37
38
|
# while also respecting the +max_size+ setting.
|
38
39
|
def <<(value)
|
@@ -109,7 +110,7 @@ module Quant
|
|
109
110
|
subset = last(n)
|
110
111
|
return 0.0 if subset.empty?
|
111
112
|
|
112
|
-
|
113
|
+
subset.sum / subset.size.to_f
|
113
114
|
end
|
114
115
|
|
115
116
|
# Computes the Exponential Moving Average (EMA) of the array. When +n+ is specified,
|
@@ -175,7 +176,7 @@ module Quant
|
|
175
176
|
# @param n [Integer] the number of elements to compute the Standard Deviation over.
|
176
177
|
# @return [Float]
|
177
178
|
def stddev(reference_value, n: size)
|
178
|
-
variance(reference_value, n: n)
|
179
|
+
variance(reference_value, n: n)**0.5
|
179
180
|
end
|
180
181
|
|
181
182
|
def variance(reference_value, n: size)
|
data/lib/quant/settings.rb
CHANGED
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
|
|
@@ -89,7 +93,7 @@ module Quant
|
|
89
93
|
# This method is useful for comparing the volatility of different assets.
|
90
94
|
# @return [Float]
|
91
95
|
def daily_price_change_ratio
|
92
|
-
@
|
96
|
+
@daily_price_change_ratio ||= ((open_price - close_price) / oc2).abs
|
93
97
|
end
|
94
98
|
|
95
99
|
# Set the #green? property to true when the close_price is greater than or equal to the open_price.
|
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.
|
data/lib/quant/version.rb
CHANGED
data/lib/quantitative.rb
CHANGED
@@ -14,4 +14,4 @@ Dir.glob(File.join(quant_folder, "*.rb")).each { |fn| require fn }
|
|
14
14
|
# require sub-folders and their sub-folders
|
15
15
|
%w(refinements mixins settings ticks indicators).each do |sub_folder|
|
16
16
|
Dir.glob(File.join(quant_folder, sub_folder, "**/*.rb")).each { |fn| require fn }
|
17
|
-
end
|
17
|
+
end
|
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.7
|
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-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -58,14 +58,17 @@ files:
|
|
58
58
|
- lib/quant/indicators_sources.rb
|
59
59
|
- lib/quant/interval.rb
|
60
60
|
- lib/quant/mixins/direction.rb
|
61
|
+
- lib/quant/mixins/exponential_moving_average.rb
|
61
62
|
- lib/quant/mixins/filters.rb
|
62
63
|
- lib/quant/mixins/fisher_transform.rb
|
64
|
+
- lib/quant/mixins/functions.rb
|
63
65
|
- lib/quant/mixins/high_pass_filter.rb
|
64
66
|
- lib/quant/mixins/hilbert_transform.rb
|
67
|
+
- lib/quant/mixins/moving_averages.rb
|
68
|
+
- lib/quant/mixins/simple_moving_average.rb
|
65
69
|
- lib/quant/mixins/stochastic.rb
|
66
70
|
- lib/quant/mixins/super_smoother.rb
|
67
|
-
- lib/quant/mixins/
|
68
|
-
- lib/quant/mixins/weighted_average.rb
|
71
|
+
- lib/quant/mixins/weighted_moving_average.rb
|
69
72
|
- lib/quant/refinements/array.rb
|
70
73
|
- lib/quant/series.rb
|
71
74
|
- lib/quant/settings.rb
|
@@ -1,26 +0,0 @@
|
|
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
|