quantitative 0.1.4 → 0.1.6
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/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
|