quantitative 0.1.4 → 0.1.6
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/asset.rb +88 -0
- data/lib/quant/{security_class.rb → asset_class.rb} +14 -14
- data/lib/quant/attributes.rb +218 -0
- data/lib/quant/errors.rb +30 -5
- data/lib/quant/indicators/indicator.rb +94 -53
- data/lib/quant/indicators/indicator_point.rb +9 -21
- data/lib/quant/indicators/ma.rb +14 -20
- data/lib/quant/indicators/ping.rb +22 -0
- data/lib/quant/indicators.rb +1 -23
- data/lib/quant/indicators_proxy.rb +61 -0
- data/lib/quant/indicators_sources.rb +18 -0
- data/lib/quant/interval.rb +37 -12
- data/lib/quant/mixins/filters.rb +2 -0
- data/lib/quant/mixins/moving_averages.rb +86 -0
- data/lib/quant/mixins/super_smoother.rb +1 -2
- data/lib/quant/refinements/array.rb +12 -10
- data/lib/quant/series.rb +36 -19
- data/lib/quant/settings.rb +2 -0
- data/lib/quant/ticks/ohlc.rb +8 -11
- data/lib/quant/ticks/serializers/ohlc.rb +33 -22
- data/lib/quant/ticks/serializers/spot.rb +1 -4
- data/lib/quant/ticks/serializers/tick.rb +3 -3
- data/lib/quant/ticks/spot.rb +7 -7
- data/lib/quant/ticks/tick.rb +22 -9
- data/lib/quant/version.rb +1 -1
- data/lib/quantitative.rb +2 -2
- metadata +9 -5
- data/lib/quant/mixins/weighted_average.rb +0 -26
- data/lib/quant/security.rb +0 -80
data/lib/quant/indicators/ma.rb
CHANGED
@@ -1,46 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Quant
|
2
4
|
class Indicators
|
3
5
|
class MaPoint < IndicatorPoint
|
6
|
+
attribute :ss, key: "ss"
|
7
|
+
attribute :ema, key: "ema"
|
4
8
|
attr_accessor :ss, :ema, :osc
|
5
9
|
|
6
|
-
def
|
7
|
-
|
8
|
-
|
9
|
-
"ema" => delta_phase,
|
10
|
-
"osc" => osc
|
11
|
-
}
|
12
|
-
end
|
13
|
-
|
14
|
-
def initialize_data_points(indicator:)
|
15
|
-
@ss = oc2
|
16
|
-
@ema = oc2
|
10
|
+
def initialize_data_points
|
11
|
+
@ss = input
|
12
|
+
@ema = input
|
17
13
|
@osc = nil
|
18
14
|
end
|
19
15
|
end
|
20
16
|
|
21
17
|
# Moving Averages
|
22
18
|
class Ma < Indicator
|
23
|
-
|
24
|
-
"ma"
|
25
|
-
end
|
19
|
+
include Quant::Mixins::Filters
|
26
20
|
|
27
21
|
def alpha(period)
|
28
22
|
bars_to_alpha(period)
|
29
23
|
end
|
30
24
|
|
31
25
|
def min_period
|
32
|
-
|
26
|
+
8 # Quant.config.indicators.min_period
|
33
27
|
end
|
34
28
|
|
35
|
-
def
|
36
|
-
|
29
|
+
def max_period
|
30
|
+
48 # Quant.config.indicators.max_period
|
37
31
|
end
|
38
32
|
|
39
33
|
def compute
|
40
|
-
p0.ss = super_smoother
|
41
|
-
p0.ema = alpha(
|
34
|
+
# p0.ss = super_smoother input, :ss, min_period
|
35
|
+
p0.ema = alpha(max_period) * input + (1 - alpha(max_period)) * p1.ema
|
42
36
|
p0.osc = p0.ss - p0.ema
|
43
37
|
end
|
44
38
|
end
|
45
39
|
end
|
46
|
-
end
|
40
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
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.
|
9
|
+
class PingPoint < IndicatorPoint
|
10
|
+
attribute :pong
|
11
|
+
attribute :compute_count, default: 0
|
12
|
+
end
|
13
|
+
|
14
|
+
# A simple idicator used primarily to test the indicator system
|
15
|
+
class Ping < Indicator
|
16
|
+
def compute
|
17
|
+
p0.pong = input
|
18
|
+
p0.compute_count += 1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/quant/indicators.rb
CHANGED
@@ -1,29 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Quant
|
4
|
-
#
|
4
|
+
# TODO: build an Indicator registry so new indicators can be added and used outside those shipped with the library.
|
5
5
|
class Indicators
|
6
|
-
# def atr; indicator(Indicators::Atr) end
|
7
|
-
# def adx; indicator(Indicators::Adx) end
|
8
|
-
# def cci; indicator(Indicators::Cci) end
|
9
|
-
# def cdi; indicator(Indicators::Cdi) end
|
10
|
-
# def decycler; indicator(Indicators::Decycler) end
|
11
|
-
# def frema; indicator(Indicators::Frema) end
|
12
|
-
# def hilo; indicator(Indicators::HiLo) end
|
13
|
-
# def ma; indicator(Indicators::Ma) end
|
14
|
-
# def mama; indicator(Indicators::Mama) end
|
15
|
-
# def frama; indicator(Indicators::Frama) end
|
16
|
-
# def mesa; indicator(Indicators::Mesa) end
|
17
|
-
# def roofing; indicator(Indicators::Roofing) end
|
18
|
-
# def rsi; indicator(Indicators::Rsi) end
|
19
|
-
# def rrr; indicator(Indicators::Rrr) end
|
20
|
-
# def rrsi; indicator(Indicators::RocketRsi) end
|
21
|
-
# def samo; indicator(Indicators::Samo) end
|
22
|
-
# def snr; indicator(Indicators::Snr) end
|
23
|
-
# def ssf; indicator(Indicators::Ssf) end
|
24
|
-
# def volume; indicator(Indicators::VolumeSsf) end
|
25
|
-
# def vol; indicator(Indicators::Vol) end
|
26
|
-
# def vrsi; indicator(Indicators::VolumeRsi) end
|
27
|
-
# def weibull; indicator(Indicators::Weibull) end
|
28
6
|
end
|
29
7
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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.
|
15
|
+
class IndicatorsProxy
|
16
|
+
attr_reader :series, :source, :indicators
|
17
|
+
|
18
|
+
def initialize(series:, source:)
|
19
|
+
@series = series
|
20
|
+
@source = source
|
21
|
+
@indicators = {}
|
22
|
+
end
|
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.
|
27
|
+
def indicator(indicator_class)
|
28
|
+
indicators[indicator_class] ||= indicator_class.new(series: series, source: source)
|
29
|
+
end
|
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.
|
38
|
+
def <<(tick)
|
39
|
+
indicators.each_value { |indicator| indicator << tick }
|
40
|
+
end
|
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
|
+
|
58
|
+
def ma; indicator(Indicators::Ma) end
|
59
|
+
def ping; indicator(Indicators::Ping) end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class IndicatorsSources
|
5
|
+
def initialize(series:)
|
6
|
+
@series = series
|
7
|
+
@indicator_sources = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def <<(tick)
|
11
|
+
@indicator_sources.each_value { |indicator| indicator << tick }
|
12
|
+
end
|
13
|
+
|
14
|
+
def oc2
|
15
|
+
@indicator_sources[:oc2] ||= IndicatorsProxy.new(series: @series, source: :oc2)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/quant/interval.rb
CHANGED
@@ -39,13 +39,13 @@
|
|
39
39
|
# pp series
|
40
40
|
#
|
41
41
|
module Quant
|
42
|
-
#
|
43
|
-
# working with multiple timeframes. Intervals are used in
|
42
|
+
# {Quant::Interval} abstracts away the concept of ticks (candles, bars, etc.) and their duration and offers some basic utilities for
|
43
|
+
# working with multiple timeframes. Intervals are used in {Quant::Ticks::Tick} and {Quant::Series} classes to define the duration of the ticks.
|
44
44
|
#
|
45
|
-
# When the
|
45
|
+
# When the {Quant::Interval} is unknown, it is set to +'na'+ (not available) and the duration is set to 0. The shorthand for this is
|
46
46
|
# +Interval.na+. and +Interval[:na]+. and +Interval[nil]+.
|
47
47
|
#
|
48
|
-
#
|
48
|
+
# {Quant::Interval} are instantiated in multple ways to support a wide variety of use-cases. Here's an example:
|
49
49
|
# Quant::Interval.new("1d") # => #<Quant::Interval @interval="1d"> (daily interval)
|
50
50
|
# Quant::Interval.new(:daily) # => #<Quant::Interval @interval="1d">
|
51
51
|
# Quant::Interval[:daily] # => #<Quant::Interval @interval="1d">
|
@@ -122,7 +122,7 @@ module Quant
|
|
122
122
|
|
123
123
|
# Instantiates an Interval from a resolution. For example, TradingView uses resolutions
|
124
124
|
# like "1", "3", "5", "15", "30", "60", "240", "D", "1D" to represent the duration of a
|
125
|
-
# candlestick. +from_resolution+ translates resolutions to the appropriate
|
125
|
+
# candlestick. +from_resolution+ translates resolutions to the appropriate {Quant::Interval}.
|
126
126
|
def self.from_resolution(resolution)
|
127
127
|
ensure_valid_resolution!(resolution)
|
128
128
|
|
@@ -130,7 +130,7 @@ module Quant
|
|
130
130
|
end
|
131
131
|
|
132
132
|
# Instantiates an Interval from a string or symbol. If the value is already
|
133
|
-
# an
|
133
|
+
# an {Quant::Interval}, it is returned as-is.
|
134
134
|
def self.[](value)
|
135
135
|
return value if value.is_a? Interval
|
136
136
|
|
@@ -153,19 +153,28 @@ module Quant
|
|
153
153
|
@interval = (interval || "na").to_s
|
154
154
|
end
|
155
155
|
|
156
|
+
# Returns true when the duration of the interval is zero, such as for the `na` interval.
|
156
157
|
def nil?
|
157
|
-
|
158
|
+
duration.zero?
|
158
159
|
end
|
159
160
|
|
160
161
|
def to_s
|
161
162
|
interval
|
162
163
|
end
|
163
164
|
|
165
|
+
# Returns the total span of seconds or duration for the interval.
|
166
|
+
# @example
|
167
|
+
# Quant::Interval.new("1d").duration => 86400
|
168
|
+
# Quant::Interval.new("1h").duration => 3600
|
169
|
+
# Quant::Interval.new("1m").duration => 60
|
170
|
+
# Quant::Interval.new("1s").duration => 1
|
171
|
+
# Quant::Interval.new("na").duration => 0
|
164
172
|
def duration
|
165
173
|
INTERVAL_DISTANCE[interval]
|
166
174
|
end
|
167
175
|
alias seconds duration
|
168
176
|
|
177
|
+
# Compares the interval to another interval, string, or symbol and returns true if they are equal.
|
169
178
|
def ==(other)
|
170
179
|
if other.is_a? String
|
171
180
|
interval.to_s == other
|
@@ -176,13 +185,22 @@ module Quant
|
|
176
185
|
end
|
177
186
|
end
|
178
187
|
|
188
|
+
# Returns the number of ticks this interval represents per minute.
|
189
|
+
# @example
|
190
|
+
# Quant::Interval.new("1d").ticks_per_minute => 0.0006944444444444445
|
191
|
+
# Quant::Interval.new("1h").ticks_per_minute => 0.016666666666666666
|
192
|
+
# Quant::Interval.new("1m").ticks_per_minute => 1.0
|
193
|
+
# Quant::Interval.new("1s").ticks_per_minute => 60.0
|
179
194
|
def ticks_per_minute
|
180
195
|
60.0 / seconds
|
181
196
|
end
|
182
197
|
|
198
|
+
# Returns the half-life of the interval in seconds.
|
199
|
+
# @example
|
200
|
+
# Quant::Interval.new("1d").half_life => 43200.0
|
201
|
+
# Quant::Interval.new("1h").half_life => 1800.0
|
202
|
+
# Quant::Interval.new("1m").half_life => 30.0
|
183
203
|
def half_life
|
184
|
-
raise "bad interval #{interval}" if duration.nil?
|
185
|
-
|
186
204
|
duration / 2.0
|
187
205
|
end
|
188
206
|
|
@@ -193,11 +211,16 @@ module Quant
|
|
193
211
|
Interval.new intervals[intervals.index(interval) + 1] || intervals[-1]
|
194
212
|
end
|
195
213
|
|
214
|
+
# Returns the full list of valid interval Strings that can be used to instantiate an {Quant::Interval}.
|
196
215
|
def self.valid_intervals
|
197
216
|
INTERVAL_DISTANCE.keys
|
198
217
|
end
|
199
218
|
|
200
|
-
#
|
219
|
+
# Computes the number of ticks from present to given timestamp.
|
220
|
+
# If timestamp doesn't cover a full interval, it will be rounded up to 1
|
221
|
+
# @example
|
222
|
+
# interval = Quant::Interval.new("1d")
|
223
|
+
# interval.ticks_to(Time.now + 5.days) # => 5 NOTE: `5.days` is an ActiveSupport method
|
201
224
|
def ticks_to(timestamp)
|
202
225
|
((timestamp - Quant.current_time) / duration).round(2).ceil
|
203
226
|
end
|
@@ -209,7 +232,8 @@ module Quant
|
|
209
232
|
def self.ensure_valid_resolution!(resolution)
|
210
233
|
return if RESOLUTIONS.keys.include? resolution
|
211
234
|
|
212
|
-
|
235
|
+
should_be_one_of = "Should be one of: (#{RESOLUTIONS.keys.join(", ")})"
|
236
|
+
raise Errors::InvalidResolution, "resolution (#{resolution}) not a valid resolution. #{should_be_one_of}"
|
213
237
|
end
|
214
238
|
|
215
239
|
private
|
@@ -221,7 +245,8 @@ module Quant
|
|
221
245
|
def ensure_valid_interval!(interval)
|
222
246
|
return if interval.nil? || valid_intervals.include?(interval.to_s)
|
223
247
|
|
224
|
-
|
248
|
+
should_be_one_of = "Should be one of: (#{valid_intervals.join(", ")})"
|
249
|
+
raise Errors::InvalidInterval, "interval (#{interval.inspect}) not a valid interval. #{should_be_one_of}"
|
225
250
|
end
|
226
251
|
|
227
252
|
def ensure_valid_resolution!(resolution)
|
data/lib/quant/mixins/filters.rb
CHANGED
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Mixins
|
5
|
+
module MovingAverages
|
6
|
+
using Quant
|
7
|
+
|
8
|
+
# Computes the Weighted Moving Average (WMA) of the series, using the four most recent data points.
|
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
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -66,9 +66,8 @@ module Quant
|
|
66
66
|
p3 = points[-4] || p2
|
67
67
|
|
68
68
|
v0 = source.is_a?(Symbol) ? p0.send(source) : source
|
69
|
-
return v0 if
|
69
|
+
return v0 if p0 == p3
|
70
70
|
|
71
|
-
debugger if points.size > 4
|
72
71
|
a1 = Math.exp(-Math::PI / ssperiod)
|
73
72
|
b1 = 2 * a1 * Math.cos(Math::PI * Math.sqrt(3) / ssperiod)
|
74
73
|
c1 = a1**2
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Quant
|
2
4
|
module Refinements
|
3
|
-
# Refinements for the standard Ruby
|
5
|
+
# Refinements for the standard Ruby {Quant::Array} class.
|
4
6
|
# These refinements add statistical methods to the Array class as well as some optimizations that greatly
|
5
7
|
# speed up some of the computations performed by the various indicators.
|
6
8
|
#
|
@@ -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)
|
@@ -67,7 +68,7 @@ module Quant
|
|
67
68
|
end
|
68
69
|
|
69
70
|
# Treats the tail of the array as starting at zero and counting up. Does not overflow the head of the array.
|
70
|
-
# That is, if the
|
71
|
+
# That is, if the {Quant::Array} has 5 elements, prev(10) would return the first element in the array.
|
71
72
|
#
|
72
73
|
# @example
|
73
74
|
# series = [1, 2, 3, 4]
|
@@ -89,11 +90,12 @@ module Quant
|
|
89
90
|
# +max_size+, the first element is removed from the array.
|
90
91
|
# This setting modifies :<< and :push methods.
|
91
92
|
def max_size!(max_size)
|
92
|
-
# These guards are maybe
|
93
|
-
#
|
94
|
-
|
95
|
-
raise
|
96
|
-
raise
|
93
|
+
# These guards are maybe unnecessary, but they are here until a use-case is found.
|
94
|
+
# Some indicators are built specifically against the +max_size+ of a given array.
|
95
|
+
# Adjusting the +max_size+ after the fact could lead to unexpected, unintended behavior.
|
96
|
+
raise Errors::ArrayMaxSizeError, "Cannot set max_size to nil." unless max_size
|
97
|
+
raise Errors::ArrayMaxSizeError, "The max_size can only be set once." if @max_size
|
98
|
+
raise Errors::ArrayMaxSizeError, "The size of Array #{size} exceeds max_size #{max_size}." if size > max_size
|
97
99
|
|
98
100
|
@max_size = max_size
|
99
101
|
self
|
@@ -108,7 +110,7 @@ module Quant
|
|
108
110
|
subset = last(n)
|
109
111
|
return 0.0 if subset.empty?
|
110
112
|
|
111
|
-
|
113
|
+
subset.sum / subset.size.to_f
|
112
114
|
end
|
113
115
|
|
114
116
|
# Computes the Exponential Moving Average (EMA) of the array. When +n+ is specified,
|
@@ -174,7 +176,7 @@ module Quant
|
|
174
176
|
# @param n [Integer] the number of elements to compute the Standard Deviation over.
|
175
177
|
# @return [Float]
|
176
178
|
def stddev(reference_value, n: size)
|
177
|
-
variance(reference_value, n: n)
|
179
|
+
variance(reference_value, n: n)**0.5
|
178
180
|
end
|
179
181
|
|
180
182
|
def variance(reference_value, n: size)
|
data/lib/quant/series.rb
CHANGED
@@ -3,34 +3,47 @@
|
|
3
3
|
module Quant
|
4
4
|
# Ticks belong to the first series they're associated with always.
|
5
5
|
# There are no provisions for series merging their ticks to one series!
|
6
|
-
# Indicators will be computed against the parent series of a list of ticks, so we
|
6
|
+
# {Indicators} will be computed against the parent series of a list of ticks, so we
|
7
7
|
# can safely work with subsets of a series and indicators will compute just once.
|
8
8
|
class Series
|
9
9
|
include Enumerable
|
10
10
|
extend Forwardable
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
# Loads a series of ticks when each line is a parsible JSON string that represents a tick.
|
13
|
+
# A {Quant::Ticks::TickSerializer} may be passed to convert the parsed JSON to {Quant::Ticks::Tick} object.
|
14
|
+
# @param filename [String] The filename to load the ticks from.
|
15
|
+
# @param symbol [String] The symbol of the series.
|
16
|
+
# @param interval [String] The interval of the series.
|
17
|
+
# @param serializer_class [Class] {Quant::Ticks::TickSerializer} class to use for the conversion.
|
18
|
+
def self.from_file(filename:, symbol:, interval:, serializer_class: nil)
|
17
19
|
raise "File #{filename} does not exist" unless File.exist?(filename)
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
from_ticks(symbol: symbol, interval: interval, ticks: ticks)
|
21
|
+
ticks = File.read(filename).split("\n").map{ |line| Oj.load(line) }
|
22
|
+
from_hash symbol: symbol, interval: interval, hash: ticks, serializer_class: serializer_class
|
23
23
|
end
|
24
24
|
|
25
|
-
|
26
|
-
|
25
|
+
# Loads a series of ticks when the JSON string represents an array of ticks.
|
26
|
+
# A {Quant::Ticks::TickSerializer} may be passed to convert the parsed JSON to {Quant::Ticks::Tick} object.
|
27
|
+
# @param symbol [String] The symbol of the series.
|
28
|
+
# @param interval [String] The interval of the series.
|
29
|
+
# @param json [String] The JSON string to parse into ticks.
|
30
|
+
# @param serializer_class [Class] {Quant::Ticks::TickSerializer} class to use for the conversion.
|
31
|
+
def self.from_json(symbol:, interval:, json:, serializer_class: nil)
|
32
|
+
ticks = Oj.load(json)
|
33
|
+
from_hash symbol: symbol, interval: interval, hash: ticks, serializer_class: serializer_class
|
27
34
|
end
|
28
35
|
|
29
|
-
|
30
|
-
|
31
|
-
|
36
|
+
# Loads a series of ticks where the hash must be cast to an array of {Quant::Ticks::Tick} objects.
|
37
|
+
# @param symbol [String] The symbol of the series.
|
38
|
+
# @param interval [String] The interval of the series.
|
39
|
+
# @param hash [Array<Hash>] The array of hashes to convert to {Quant::Ticks::Tick} objects.
|
40
|
+
# @param serializer_class [Class] {Quant::Ticks::TickSerializer} class to use for the conversion.
|
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: serializer_class) }
|
43
|
+
from_ticks symbol: symbol, interval: interval, ticks: ticks
|
32
44
|
end
|
33
45
|
|
46
|
+
# Loads a series of ticks where the array represents an array of {Quant::Ticks::Tick} objects.
|
34
47
|
def self.from_ticks(symbol:, interval:, ticks:)
|
35
48
|
ticks = ticks.sort_by(&:close_timestamp)
|
36
49
|
|
@@ -43,7 +56,7 @@ module Quant
|
|
43
56
|
|
44
57
|
def initialize(symbol:, interval:)
|
45
58
|
@symbol = symbol
|
46
|
-
@interval = interval
|
59
|
+
@interval = Interval[interval]
|
47
60
|
@ticks = []
|
48
61
|
end
|
49
62
|
|
@@ -64,11 +77,8 @@ module Quant
|
|
64
77
|
def_delegator :@ticks, :[]
|
65
78
|
def_delegator :@ticks, :size
|
66
79
|
def_delegator :@ticks, :each
|
67
|
-
def_delegator :@ticks, :select
|
68
80
|
def_delegator :@ticks, :select!
|
69
|
-
def_delegator :@ticks, :reject
|
70
81
|
def_delegator :@ticks, :reject!
|
71
|
-
def_delegator :@ticks, :first
|
72
82
|
def_delegator :@ticks, :last
|
73
83
|
|
74
84
|
def highest
|
@@ -92,7 +102,14 @@ module Quant
|
|
92
102
|
end
|
93
103
|
|
94
104
|
def <<(tick)
|
105
|
+
tick = Ticks::Spot.new(price: tick) if tick.is_a?(Numeric)
|
106
|
+
indicators << tick unless tick.series?
|
95
107
|
@ticks << tick.assign_series(self)
|
108
|
+
self
|
109
|
+
end
|
110
|
+
|
111
|
+
def indicators
|
112
|
+
@indicators ||= IndicatorsSources.new(series: self)
|
96
113
|
end
|
97
114
|
|
98
115
|
def to_h
|
data/lib/quant/settings.rb
CHANGED
data/lib/quant/ticks/ohlc.rb
CHANGED
@@ -4,14 +4,15 @@ require_relative "tick"
|
|
4
4
|
|
5
5
|
module Quant
|
6
6
|
module Ticks
|
7
|
-
# An
|
8
|
-
# It is the most common form of a
|
9
|
-
# minute, hour, day, week, or month. The
|
10
|
-
# The interval of the
|
7
|
+
# An {Quant::Ticks::OHLC} is a bar or candle for a point in time that has an open, high, low, and close price.
|
8
|
+
# It is the most common form of a {Quant::Ticks::Tick} and is usually used to representa time period such as a
|
9
|
+
# minute, hour, day, week, or month. The {Quant::Ticks::OHLC} is used to represent the price action of an asset
|
10
|
+
# The interval of the {Quant::Ticks::OHLC} is the time period that the {Quant::Ticks::OHLC} represents,
|
11
|
+
# such has hourly, daily, weekly, etc.
|
11
12
|
class OHLC < Tick
|
12
13
|
include TimeMethods
|
13
14
|
|
14
|
-
attr_reader :
|
15
|
+
attr_reader :series
|
15
16
|
attr_reader :close_timestamp, :open_timestamp
|
16
17
|
attr_reader :open_price, :high_price, :low_price, :close_price
|
17
18
|
attr_reader :base_volume, :target_volume, :trades
|
@@ -26,8 +27,6 @@ module Quant
|
|
26
27
|
low_price:,
|
27
28
|
close_price:,
|
28
29
|
|
29
|
-
interval: nil,
|
30
|
-
|
31
30
|
volume: nil,
|
32
31
|
base_volume: nil,
|
33
32
|
target_volume: nil,
|
@@ -44,8 +43,6 @@ module Quant
|
|
44
43
|
@low_price = low_price.to_f
|
45
44
|
@close_price = close_price.to_f
|
46
45
|
|
47
|
-
@interval = Interval[interval]
|
48
|
-
|
49
46
|
@base_volume = (volume || base_volume).to_i
|
50
47
|
@target_volume = (target_volume || @base_volume).to_i
|
51
48
|
@trades = trades.to_i
|
@@ -92,7 +89,7 @@ module Quant
|
|
92
89
|
# This method is useful for comparing the volatility of different assets.
|
93
90
|
# @return [Float]
|
94
91
|
def daily_price_change_ratio
|
95
|
-
@
|
92
|
+
@daily_price_change_ratio ||= ((open_price - close_price) / oc2).abs
|
96
93
|
end
|
97
94
|
|
98
95
|
# Set the #green? property to true when the close_price is greater than or equal to the open_price.
|
@@ -131,7 +128,7 @@ module Quant
|
|
131
128
|
end
|
132
129
|
|
133
130
|
def inspect
|
134
|
-
"#<#{self.class.name}
|
131
|
+
"#<#{self.class.name} ct=#{close_timestamp.iso8601} o=#{open_price} h=#{high_price} l=#{low_price} c=#{close_price} v=#{volume}>"
|
135
132
|
end
|
136
133
|
end
|
137
134
|
end
|