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