quantitative 0.1.4 → 0.1.5
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/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 +134 -0
- data/lib/quant/errors.rb +30 -5
- data/lib/quant/indicators/indicator.rb +87 -52
- data/lib/quant/indicators/indicator_point.rb +9 -21
- data/lib/quant/indicators/ma.rb +12 -20
- data/lib/quant/indicators/ping.rb +16 -0
- data/lib/quant/indicators.rb +1 -23
- data/lib/quant/indicators_proxy.rb +23 -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/refinements/array.rb +8 -7
- data/lib/quant/series.rb +36 -19
- data/lib/quant/ticks/ohlc.rb +7 -10
- 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 +1 -1
- metadata +8 -4
- data/lib/quant/security.rb +0 -80
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
class IndicatorsProxy
|
5
|
+
attr_reader :series, :source, :indicators
|
6
|
+
|
7
|
+
def initialize(series:, source:)
|
8
|
+
@series = series
|
9
|
+
@source = source
|
10
|
+
@indicators = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def indicator(indicator_class)
|
14
|
+
indicators[indicator_class] ||= indicator_class.new(series: series, source: source)
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(tick)
|
18
|
+
indicators.each_value { |indicator| indicator << tick }
|
19
|
+
end
|
20
|
+
|
21
|
+
def ma; indicator(Indicators::Ma) end
|
22
|
+
end
|
23
|
+
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
@@ -1,6 +1,6 @@
|
|
1
1
|
module Quant
|
2
2
|
module Refinements
|
3
|
-
# Refinements for the standard Ruby
|
3
|
+
# Refinements for the standard Ruby {Quant::Array} class.
|
4
4
|
# These refinements add statistical methods to the Array class as well as some optimizations that greatly
|
5
5
|
# speed up some of the computations performed by the various indicators.
|
6
6
|
#
|
@@ -67,7 +67,7 @@ module Quant
|
|
67
67
|
end
|
68
68
|
|
69
69
|
# 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
|
70
|
+
# That is, if the {Quant::Array} has 5 elements, prev(10) would return the first element in the array.
|
71
71
|
#
|
72
72
|
# @example
|
73
73
|
# series = [1, 2, 3, 4]
|
@@ -89,11 +89,12 @@ module Quant
|
|
89
89
|
# +max_size+, the first element is removed from the array.
|
90
90
|
# This setting modifies :<< and :push methods.
|
91
91
|
def max_size!(max_size)
|
92
|
-
# These guards are maybe
|
93
|
-
#
|
94
|
-
|
95
|
-
raise
|
96
|
-
raise
|
92
|
+
# These guards are maybe unnecessary, but they are here until a use-case is found.
|
93
|
+
# Some indicators are built specifically against the +max_size+ of a given array.
|
94
|
+
# Adjusting the +max_size+ after the fact could lead to unexpected, unintended behavior.
|
95
|
+
raise Errors::ArrayMaxSizeError, "Cannot set max_size to nil." unless max_size
|
96
|
+
raise Errors::ArrayMaxSizeError, "The max_size can only be set once." if @max_size
|
97
|
+
raise Errors::ArrayMaxSizeError, "The size of Array #{size} exceeds max_size #{max_size}." if size > max_size
|
97
98
|
|
98
99
|
@max_size = max_size
|
99
100
|
self
|
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/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
|
@@ -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
|
@@ -16,13 +16,44 @@ module Quant
|
|
16
16
|
from(hash, tick_class: tick_class)
|
17
17
|
end
|
18
18
|
|
19
|
+
# Instantiates a tick from a +Hash+. The hash keys are expected to be the same as the serialized keys.
|
20
|
+
#
|
21
|
+
# Serialized Keys:
|
22
|
+
# - ot: open timestamp
|
23
|
+
# - ct: close timestamp
|
24
|
+
# - o: open price
|
25
|
+
# - h: high price
|
26
|
+
# - l: low price
|
27
|
+
# - c: close price
|
28
|
+
# - bv: base volume
|
29
|
+
# - tv: target volume
|
30
|
+
# - t: trades
|
31
|
+
# - g: green
|
32
|
+
# - j: doji
|
33
|
+
def self.from(hash, tick_class:)
|
34
|
+
tick_class.new \
|
35
|
+
open_timestamp: hash["ot"],
|
36
|
+
close_timestamp: hash["ct"],
|
37
|
+
|
38
|
+
open_price: hash["o"],
|
39
|
+
high_price: hash["h"],
|
40
|
+
low_price: hash["l"],
|
41
|
+
close_price: hash["c"],
|
42
|
+
|
43
|
+
base_volume: hash["bv"],
|
44
|
+
target_volume: hash["tv"],
|
45
|
+
|
46
|
+
trades: hash["t"],
|
47
|
+
green: hash["g"],
|
48
|
+
doji: hash["j"]
|
49
|
+
end
|
50
|
+
|
19
51
|
# Returns a +Hash+ of the Spot tick's key properties
|
20
52
|
#
|
21
53
|
# Serialized Keys:
|
22
54
|
#
|
23
55
|
# - ot: open timestamp
|
24
56
|
# - ct: close timestamp
|
25
|
-
# - iv: interval
|
26
57
|
# - o: open price
|
27
58
|
# - h: high price
|
28
59
|
# - l: low price
|
@@ -37,12 +68,11 @@ module Quant
|
|
37
68
|
# @return [Hash]
|
38
69
|
# @example
|
39
70
|
# Quant::Ticks::Serializers::Tick.to_h(tick)
|
40
|
-
# # => { "ot" => [Time], "ct" => [Time], "
|
71
|
+
# # => { "ot" => [Time], "ct" => [Time], "o" => 1.0, "h" => 2.0,
|
41
72
|
# # "l" => 0.5, "c" => 1.5, "bv" => 6.0, "tv" => 5.0, "t" => 1, "g" => true, "j" => true }
|
42
73
|
def self.to_h(tick)
|
43
74
|
{ "ot" => tick.open_timestamp,
|
44
75
|
"ct" => tick.close_timestamp,
|
45
|
-
"iv" => tick.interval.to_s,
|
46
76
|
|
47
77
|
"o" => tick.open_price,
|
48
78
|
"h" => tick.high_price,
|
@@ -56,25 +86,6 @@ module Quant
|
|
56
86
|
"g" => tick.green,
|
57
87
|
"j" => tick.doji }
|
58
88
|
end
|
59
|
-
|
60
|
-
def self.from(hash, tick_class:)
|
61
|
-
tick_class.new \
|
62
|
-
open_timestamp: hash["ot"],
|
63
|
-
close_timestamp: hash["ct"],
|
64
|
-
interval: hash["iv"],
|
65
|
-
|
66
|
-
open_price: hash["o"],
|
67
|
-
high_price: hash["h"],
|
68
|
-
low_price: hash["l"],
|
69
|
-
close_price: hash["c"],
|
70
|
-
|
71
|
-
base_volume: hash["bv"],
|
72
|
-
target_volume: hash["tv"],
|
73
|
-
|
74
|
-
trades: hash["t"],
|
75
|
-
green: hash["g"],
|
76
|
-
doji: hash["j"]
|
77
|
-
end
|
78
89
|
end
|
79
90
|
end
|
80
91
|
end
|
@@ -21,7 +21,6 @@ module Quant
|
|
21
21
|
# Serialized Keys:
|
22
22
|
#
|
23
23
|
# - ct: close timestamp
|
24
|
-
# - iv: interval
|
25
24
|
# - cp: close price
|
26
25
|
# - bv: base volume
|
27
26
|
# - tv: target volume
|
@@ -31,11 +30,10 @@ module Quant
|
|
31
30
|
# @return [Hash]
|
32
31
|
# @example
|
33
32
|
# Quant::Ticks::Serializers::Tick.to_h(tick)
|
34
|
-
# # => { "ct" => "2024-02-13 03:12:23 UTC", "cp" => 5.0, "
|
33
|
+
# # => { "ct" => "2024-02-13 03:12:23 UTC", "cp" => 5.0, "bv" => 0.0, "tv" => 0.0, "t" => 0 }
|
35
34
|
def self.to_h(tick)
|
36
35
|
{ "ct" => tick.close_timestamp,
|
37
36
|
"cp" => tick.close_price,
|
38
|
-
"iv" => tick.interval.to_s,
|
39
37
|
"bv" => tick.base_volume,
|
40
38
|
"tv" => tick.target_volume,
|
41
39
|
"t" => tick.trades }
|
@@ -45,7 +43,6 @@ module Quant
|
|
45
43
|
tick_class.new(
|
46
44
|
close_timestamp: hash["ct"],
|
47
45
|
close_price: hash["cp"],
|
48
|
-
interval: hash["iv"],
|
49
46
|
base_volume: hash["bv"],
|
50
47
|
target_volume: hash["tv"],
|
51
48
|
trades: hash["t"]
|
@@ -3,10 +3,10 @@
|
|
3
3
|
module Quant
|
4
4
|
module Ticks
|
5
5
|
module Serializers
|
6
|
-
# The
|
7
|
-
# These classes are wired into the Tick classes and are used to convert
|
6
|
+
# The {Quant::Ticks::Tick} class serializes and deserializes {Quant::Ticks::Tick} objects to and from various formats.
|
7
|
+
# These classes are wired into the Tick classes and are used to convert {Quant::Ticks::Tick} objects to and from
|
8
8
|
# Ruby hashes, JSON strings, and CSV strings. They're not typically used directly, but are extracted
|
9
|
-
# to make it easier to provide custom serialization and deserialization for
|
9
|
+
# to make it easier to provide custom serialization and deserialization for {Quant::Ticks::Tick} objects not shipped
|
10
10
|
# with the library.
|
11
11
|
# @abstract
|
12
12
|
class Tick
|
data/lib/quant/ticks/spot.rb
CHANGED
@@ -4,7 +4,7 @@ require_relative "tick"
|
|
4
4
|
|
5
5
|
module Quant
|
6
6
|
module Ticks
|
7
|
-
# A +Spot+ is a single price point in time. It is the most basic form of a
|
7
|
+
# A +Spot+ is a single price point in time. It is the most basic form of a {Quant::Ticks::Tick} and is usually used to represent
|
8
8
|
# a continuously streaming tick that just has a single price point at a given point in time.
|
9
9
|
# @example
|
10
10
|
# spot = Quant::Ticks::Spot.new(price: 100.0, timestamp: Time.now)
|
@@ -19,9 +19,9 @@ module Quant
|
|
19
19
|
class Spot < Tick
|
20
20
|
include TimeMethods
|
21
21
|
|
22
|
-
attr_reader :
|
22
|
+
attr_reader :series
|
23
23
|
attr_reader :close_timestamp, :open_timestamp
|
24
|
-
attr_reader :
|
24
|
+
attr_reader :close_price
|
25
25
|
attr_reader :base_volume, :target_volume, :trades
|
26
26
|
|
27
27
|
def initialize(
|
@@ -30,7 +30,6 @@ module Quant
|
|
30
30
|
close_price: nil,
|
31
31
|
close_timestamp: nil,
|
32
32
|
volume: nil,
|
33
|
-
interval: nil,
|
34
33
|
base_volume: nil,
|
35
34
|
target_volume: nil,
|
36
35
|
trades: nil
|
@@ -39,8 +38,6 @@ module Quant
|
|
39
38
|
|
40
39
|
@close_price = (close_price || price).to_f
|
41
40
|
|
42
|
-
@interval = Interval[interval]
|
43
|
-
|
44
41
|
@close_timestamp = extract_time(timestamp || close_timestamp || Quant.current_time)
|
45
42
|
@open_timestamp = @close_timestamp
|
46
43
|
|
@@ -53,6 +50,9 @@ module Quant
|
|
53
50
|
|
54
51
|
alias timestamp close_timestamp
|
55
52
|
alias price close_price
|
53
|
+
alias high_price close_price
|
54
|
+
alias low_price close_price
|
55
|
+
alias open_price close_price
|
56
56
|
alias oc2 close_price
|
57
57
|
alias hl2 close_price
|
58
58
|
alias hlc3 close_price
|
@@ -73,7 +73,7 @@ module Quant
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def inspect
|
76
|
-
"#<#{self.class.name}
|
76
|
+
"#<#{self.class.name} ct=#{close_timestamp} c=#{close_price.to_f} v=#{volume}>"
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
data/lib/quant/ticks/tick.rb
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
module Quant
|
4
4
|
module Ticks
|
5
|
-
#
|
5
|
+
# {Quant::Ticks::Tick} is the abstract ancestor for all Ticks and holds the logic for interacting with series and indicators.
|
6
6
|
# The public interface is devoid of properties around price, volume, and timestamp, etc. Descendant classes
|
7
7
|
# are responsible for defining the properties and how they are represented.
|
8
8
|
#
|
9
|
-
# The
|
10
|
-
# once a
|
9
|
+
# The {Quant::Ticks::Tick} class is designed to be immutable and is intended to be used as a value object. This means that
|
10
|
+
# once a {Quant::Ticks::Tick} is created, it cannot be changed. This is important for the integrity of the series and
|
11
11
|
# indicators that depend on the ticks within the series.
|
12
12
|
#
|
13
13
|
# When a tick is added to a series, it is locked into the series and ownership cannot be changed. This is important
|
@@ -17,7 +17,7 @@ module Quant
|
|
17
17
|
#
|
18
18
|
# Ticks can be serialized to and from Ruby Hash, JSON strings, and CSV strings.
|
19
19
|
class Tick
|
20
|
-
# Returns a
|
20
|
+
# Returns a {Quant::Ticks::Tick} from a Ruby +Hash+. The default serializer is used to generate the {Quant::Ticks::Tick}.
|
21
21
|
# @param hash [Hash]
|
22
22
|
# @param serializer_class [Class] The serializer class to use for the conversion.
|
23
23
|
# @return [Quant::Ticks::Tick]
|
@@ -25,11 +25,12 @@ module Quant
|
|
25
25
|
# hash = { "timestamp" => "2018-01-01 12:00:00 UTC", "price" => 100.0, "volume" => 1000 }
|
26
26
|
# Quant::Ticks::Tick.from(hash)
|
27
27
|
# # => #<Quant::Ticks::Spot:0x00007f9e3b8b3e08 @timestamp=2018-01-01 12:00:00 UTC, @price=100.0, @volume=1000>
|
28
|
-
def self.from(hash, serializer_class:
|
28
|
+
def self.from(hash, serializer_class: nil)
|
29
|
+
serializer_class ||= default_serializer_class
|
29
30
|
serializer_class.from(hash, tick_class: self)
|
30
31
|
end
|
31
32
|
|
32
|
-
# Returns a
|
33
|
+
# Returns a {Quant::Ticks::Tick} from a JSON string. The default serializer is used to generate the {Quant::Ticks::Tick}.
|
33
34
|
# @param json [String]
|
34
35
|
# @param serializer_class [Class] The serializer class to use for the conversion.
|
35
36
|
# @return [Quant::Ticks::Tick]
|
@@ -41,21 +42,33 @@ module Quant
|
|
41
42
|
serializer_class.from_json(json, tick_class: self)
|
42
43
|
end
|
43
44
|
|
44
|
-
attr_reader :series
|
45
|
+
attr_reader :series, :indicators
|
45
46
|
|
46
47
|
def initialize
|
47
48
|
# Set the series by appending to the series or calling #assign_series method
|
48
49
|
@series = nil
|
50
|
+
@interval = nil
|
51
|
+
@indicators = {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def interval
|
55
|
+
@series&.interval || Interval[nil]
|
49
56
|
end
|
50
57
|
|
51
58
|
# Ticks always belong to the first series they're assigned so we can easily spin off
|
52
59
|
# sub-sets or new series with the same ticks while allowing each series to have
|
53
60
|
# its own state and full control over the ticks within its series
|
54
61
|
def assign_series(new_series)
|
55
|
-
assign_series!(new_series)
|
62
|
+
assign_series!(new_series) unless series?
|
56
63
|
self
|
57
64
|
end
|
58
65
|
|
66
|
+
# Returns true if the tick is assigned to a series. The first series a tick is assigned
|
67
|
+
# to is the series against which the indicators compute.
|
68
|
+
def series?
|
69
|
+
!!@series
|
70
|
+
end
|
71
|
+
|
59
72
|
# Ticks always belong to the first series they're assigned so we can easily spin off
|
60
73
|
# sub-sets or new series with the same ticks. However, if you need to reassign the
|
61
74
|
# series, you can use this method to force the change of series ownership.
|
@@ -63,7 +76,7 @@ module Quant
|
|
63
76
|
# The series interval is also assigned to the tick if it is not already set.
|
64
77
|
def assign_series!(new_series)
|
65
78
|
@series = new_series
|
66
|
-
@interval
|
79
|
+
@interval ||= new_series.interval
|
67
80
|
self
|
68
81
|
end
|
69
82
|
|
data/lib/quant/version.rb
CHANGED
data/lib/quantitative.rb
CHANGED
@@ -12,6 +12,6 @@ quant_folder = File.join(lib_folder, "quant")
|
|
12
12
|
Dir.glob(File.join(quant_folder, "*.rb")).each { |fn| require fn }
|
13
13
|
|
14
14
|
# require sub-folders and their sub-folders
|
15
|
-
%w(refinements settings ticks indicators).each do |sub_folder|
|
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
17
|
end
|