quantitative 0.1.0 → 0.1.2
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/.ruby-version +1 -0
- data/.yardopts +7 -0
- data/Gemfile +7 -1
- data/Gemfile.lock +6 -2
- data/README.md +2 -0
- data/lib/quant/errors.rb +1 -0
- data/lib/quant/interval.rb +7 -1
- data/lib/quant/refinements/array.rb +196 -0
- data/lib/quant/ticks/ohlc.rb +68 -95
- data/lib/quant/ticks/serializers/ohlc.rb +81 -0
- data/lib/quant/ticks/serializers/spot.rb +57 -0
- data/lib/quant/ticks/serializers/tick.rb +77 -0
- data/lib/quant/ticks/spot.rb +62 -16
- data/lib/quant/ticks/tick.rb +115 -0
- data/lib/quant/version.rb +1 -1
- data/lib/quantitative.rb +2 -2
- metadata +12 -64
- data/lib/quant/ticks/indicator_points.rb +0 -22
- data/lib/quant/ticks/serializers/value.rb +0 -23
- data/lib/quant/ticks/value.rb +0 -88
- /data/{DISCLAIMER.txt → DISCLAIMER.md} +0 -0
- /data/{LICENSE.txt → LICENSE} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19fe65cd5c50b0b503abaa0e7c5be3949a284cf5a2b537d79b9cbdd69448041d
|
4
|
+
data.tar.gz: fe03ed6b2ddef6609cee31547cd84c1eac948bb8d10efb8c24c316673c693a62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ce1d965a8e68f6fae76143dfc619ed98c8a5af559e9f26c67dd7685e60bf9b4cfa3ecf5f43908f01597862cb6d6389cebd40cf5897afed560a9e54ed79ea91e
|
7
|
+
data.tar.gz: ee8527e43aa4670175d20a20c6037af11227071ff0fe738344c4b9b91d2391335d210a9e5b305153e78edc75ca9acb802b56b2502223107a84366ae40ba55b7f
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.3.0
|
data/.yardopts
ADDED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
quantitative (0.1.
|
4
|
+
quantitative (0.1.2)
|
5
5
|
oj (~> 3.10)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
10
|
ast (2.4.2)
|
11
|
+
benchmark-ips (2.13.0)
|
12
|
+
bigdecimal (3.1.6)
|
11
13
|
coderay (1.1.3)
|
12
14
|
debug (1.8.0)
|
13
15
|
irb (>= 1.5.0)
|
@@ -44,7 +46,8 @@ GEM
|
|
44
46
|
notiffany (0.1.3)
|
45
47
|
nenv (~> 0.1)
|
46
48
|
shellany (~> 0.0)
|
47
|
-
oj (3.16.
|
49
|
+
oj (3.16.3)
|
50
|
+
bigdecimal (>= 3.0)
|
48
51
|
parallel (1.24.0)
|
49
52
|
parser (3.3.0.4)
|
50
53
|
ast (~> 2.4.1)
|
@@ -112,6 +115,7 @@ PLATFORMS
|
|
112
115
|
arm64-darwin-22
|
113
116
|
|
114
117
|
DEPENDENCIES
|
118
|
+
benchmark-ips (~> 2.9)
|
115
119
|
debug
|
116
120
|
guard-rspec (~> 4.7)
|
117
121
|
quantitative!
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Quantitative
|
2
2
|
|
3
|
+
[](https://badge.fury.io/rb/quantitative)
|
4
|
+
|
3
5
|
STATUS: ALPHA - very early stages! The framework is very much a work in progress and I am rapidly introducing new things and changing existing things around.
|
4
6
|
|
5
7
|
Quantitative is a statistical and quantitative library for Ruby 3.x for trading stocks, cryptocurrency, and forex. It provides a number of classes and modules for working with time-series data, financial data, and other quantitative data. It is designed to be fast, efficient, and easy to use.
|
data/lib/quant/errors.rb
CHANGED
data/lib/quant/interval.rb
CHANGED
@@ -167,7 +167,13 @@ module Quant
|
|
167
167
|
alias seconds duration
|
168
168
|
|
169
169
|
def ==(other)
|
170
|
-
|
170
|
+
if other.is_a? String
|
171
|
+
interval.to_s == other
|
172
|
+
elsif other.is_a? Symbol
|
173
|
+
interval == MAPPINGS[other]&.fetch(:interval, nil)
|
174
|
+
else
|
175
|
+
interval == other&.interval
|
176
|
+
end
|
171
177
|
end
|
172
178
|
|
173
179
|
def ticks_per_minute
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module Quant
|
2
|
+
module Refinements
|
3
|
+
# Refinements for the standard Ruby +Array+ class.
|
4
|
+
# These refinements add statistical methods to the Array class as well as some optimizations that greatly
|
5
|
+
# speed up some of the computations performed by the various indicators.
|
6
|
+
#
|
7
|
+
# In addtion to the statistical methods, the refinements also add a +max_size!+ method to the Array class, which
|
8
|
+
# allows us to bound the array to a maximum number of elements, which is useful for indicators that are computing
|
9
|
+
# averages or sums over a fixed number of lookback ticks.
|
10
|
+
#
|
11
|
+
# There are some various performance benchmarks in the spec/performance directory that show the performance
|
12
|
+
# improvements of using these refinements.
|
13
|
+
#
|
14
|
+
# Keep in mind that within this library, we're generally concerned with adding to the tail of the arrays and
|
15
|
+
# rarely with removing or popping, so there's few optimizations or protections for those operations in
|
16
|
+
# conjunction with the +max_size+ setting. The +max_size+ has also been designed to be set only once, to avoid
|
17
|
+
# adding additional complexity to the code that is unnecessary until a use-case presents itself.
|
18
|
+
#
|
19
|
+
# Usage: Call +using Quant+ in the file or scope where you want to use these refinements. It does not matter
|
20
|
+
# if the arrays were instantiated outside the scope of the refinement, the refinements will still be applied.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# using Quant
|
24
|
+
#
|
25
|
+
# array = [1, 2, 3, 4, 5]
|
26
|
+
# array.mean => 3.0
|
27
|
+
# another_array.max_size!(3).push(1, 2, 3, 4, 5, 6) => [4, 5, 6]
|
28
|
+
#
|
29
|
+
# @note The behavior of out of bound indexes into the Array deviates from standard Ruby and always returns an element.
|
30
|
+
# If an array only has three elements and 4 or more are requested for +n+, the method constrains itself to
|
31
|
+
# the size of the array. This is an intentional design decision, but it may be a gotcha if you're not expecting it.
|
32
|
+
# The refined behavior generally only exists within the library's scope, but if you call `using Quant` in your
|
33
|
+
# own code, you may encounter the changed behavior unexpectedly.
|
34
|
+
module Array
|
35
|
+
|
36
|
+
# Overrides the standard +<<+ method to track the +maximum+ and +minimum+ values
|
37
|
+
# while also respecting the +max_size+ setting.
|
38
|
+
def <<(value)
|
39
|
+
push(value)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Overrides the standard +push+ method to track the +maximum+ and +minimum+ values
|
43
|
+
# while also respecting the +max_size+ setting.
|
44
|
+
def push(*objects)
|
45
|
+
Array(objects).each do |object|
|
46
|
+
super(object)
|
47
|
+
if @max_size && size > @max_size
|
48
|
+
voted_off = shift
|
49
|
+
@minimum = min if voted_off == @minimum
|
50
|
+
@maximum = max if voted_off == @maximum
|
51
|
+
else
|
52
|
+
@maximum = object if @maximum.nil? || object > @maximum
|
53
|
+
@minimum = object if @minimum.nil? || object < @minimum
|
54
|
+
end
|
55
|
+
end
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the maximum element value in the array. It is an optimized version of the standard +max+ method.
|
60
|
+
def maximum
|
61
|
+
@maximum || max
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the minimum element value in the array. It is an optimized version of the standard +min+ method.
|
65
|
+
def minimum
|
66
|
+
@minimum || min
|
67
|
+
end
|
68
|
+
|
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 +Array+ has 5 elements, prev(10) would return the first element in the array.
|
71
|
+
#
|
72
|
+
# @example
|
73
|
+
# series = [1, 2, 3, 4]
|
74
|
+
# series.prev(0) # => series[-1] => series[3] => 4
|
75
|
+
# series.prev(1) # => series[-2] => series[3] => 3
|
76
|
+
# series.prev(2) # => series[-3] => series[2] => 2
|
77
|
+
# series.prev(3) # => series[-4] => series[1] => 1
|
78
|
+
# series.prev(4) # => series[-4] => series[0] => 1 (no out of bounds!)
|
79
|
+
#
|
80
|
+
# Useful for when translating TradingView or MQ4 indicators to Ruby where those programs' indexing starts at 0
|
81
|
+
# for most recent bar and counts up to the oldest bar.
|
82
|
+
def prev(index)
|
83
|
+
raise ArgumentError, "index must be positive" if index < 0
|
84
|
+
|
85
|
+
self[[size - (index + 1), 0].max]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Sets the maximum size of the array. When the size of the array exceeds the
|
89
|
+
# +max_size+, the first element is removed from the array.
|
90
|
+
# This setting modifies :<< and :push methods.
|
91
|
+
def max_size!(max_size)
|
92
|
+
# These guards are maybe not necessary, but they are here until a use-case is found.
|
93
|
+
# My concern lies with indicators that are built specifically against the +max_size+ of a given array.
|
94
|
+
raise Quant::ArrayMaxSizeError, 'cannot set max_size to nil.' unless max_size
|
95
|
+
raise Quant::ArrayMaxSizeError, 'can only max_size! once.' if @max_size
|
96
|
+
raise Quant::ArrayMaxSizeError, "size of Array #{size} exceeds max_size #{max_size}." if size > max_size
|
97
|
+
|
98
|
+
@max_size = max_size
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
# Computes the mean of the array. When +n+ is specified, the mean is computed over
|
103
|
+
# the last +n+ elements, otherwise it is computed over the entire array.
|
104
|
+
#
|
105
|
+
# @param n [Integer] the number of elements to compute the mean over
|
106
|
+
# @return [Float]
|
107
|
+
def mean(n: size)
|
108
|
+
subset = last(n)
|
109
|
+
return 0.0 if subset.empty?
|
110
|
+
|
111
|
+
sum = subset.sum / subset.size.to_f
|
112
|
+
end
|
113
|
+
|
114
|
+
# Computes the Exponential Moving Average (EMA) of the array. When +n+ is specified,
|
115
|
+
# the EMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
|
116
|
+
# An Array of EMA's is returned, with the first entry always the first value in the subset.
|
117
|
+
#
|
118
|
+
# @params n [Integer] the number of elements to compute the EMA over.
|
119
|
+
# @return [Array<Float>]
|
120
|
+
def ema(n: size)
|
121
|
+
subset = last(n)
|
122
|
+
return [] if subset.empty?
|
123
|
+
|
124
|
+
alpha = 2.0 / (subset.size + 1)
|
125
|
+
naught_alpha = (1.0 - alpha)
|
126
|
+
pvalue = subset[0]
|
127
|
+
subset.map do |value|
|
128
|
+
pvalue = (alpha * value) + (naught_alpha * pvalue)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Computes the Simple Moving Average (SMA) of the array. When +n+ is specified,
|
133
|
+
# the SMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
|
134
|
+
# An Array of SMA's is returned, with the first entry always the first value in the subset.
|
135
|
+
#
|
136
|
+
# @param n [Integer] the number of elements to compute the SMA over
|
137
|
+
# @return [Array<Float>]
|
138
|
+
def sma(n: size)
|
139
|
+
subset = last(n)
|
140
|
+
return [] if subset.empty?
|
141
|
+
|
142
|
+
pvalue = subset[0]
|
143
|
+
subset.map do |value|
|
144
|
+
pvalue = (pvalue + value) / 2.0
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Computes the Weighted Moving Average (WMA) of the array. When +n+ is specified,
|
149
|
+
# the WMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
|
150
|
+
# An Array of WMA's is returned, with the first entry always the first value in the subset.
|
151
|
+
#
|
152
|
+
# @param n [Integer] the number of elements to compute the WMA over
|
153
|
+
# @return [Array<Float>]
|
154
|
+
def wma(n: size)
|
155
|
+
subset = last(n)
|
156
|
+
return [] if subset.empty?
|
157
|
+
|
158
|
+
# ensures we return not more than number of elements in full array,
|
159
|
+
# yet have enough values to compute each iteration
|
160
|
+
max_size = [size, n].min
|
161
|
+
while subset.size <= max_size + 2
|
162
|
+
subset.unshift(subset[0])
|
163
|
+
end
|
164
|
+
|
165
|
+
subset.each_cons(4).map do |v1, v2, v3, v4|
|
166
|
+
(4.0 * v4 + 3.0 * v3 + 2.0 * v2 + v1) / 10.0
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Computes the Standard Deviation of the array. When +n+ is specified,
|
171
|
+
# the Standard Deviation is computed over the last +n+ elements,
|
172
|
+
# otherwise it is computed over the entire array.
|
173
|
+
#
|
174
|
+
# @param n [Integer] the number of elements to compute the Standard Deviation over.
|
175
|
+
# @return [Float]
|
176
|
+
def stddev(reference_value, n: size)
|
177
|
+
variance(reference_value, n: n) ** 0.5
|
178
|
+
end
|
179
|
+
|
180
|
+
def variance(reference_value, n: size)
|
181
|
+
subset = last(n)
|
182
|
+
return 0.0 if subset.empty?
|
183
|
+
|
184
|
+
subset.empty? ? 0.0 : subset.map{ |v| (v - reference_value)**2 }.mean
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
refine Array do
|
190
|
+
import_methods Quant::Refinements::Array
|
191
|
+
|
192
|
+
alias std_dev stddev
|
193
|
+
alias standard_deviation stddev
|
194
|
+
alias var variance
|
195
|
+
end
|
196
|
+
end
|
data/lib/quant/ticks/ohlc.rb
CHANGED
@@ -1,125 +1,98 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "tick"
|
2
4
|
|
3
5
|
module Quant
|
4
6
|
module Ticks
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
base_volume: hash["bv"],
|
35
|
-
target_volume: hash["tv"],
|
36
|
-
|
37
|
-
trades: hash["t"],
|
38
|
-
green: hash["g"],
|
39
|
-
doji: hash["j"]
|
40
|
-
end
|
41
|
-
|
42
|
-
def self.from_json(json)
|
43
|
-
from Oj.load(json)
|
44
|
-
end
|
45
|
-
|
46
|
-
def initialize(open_timestamp:,
|
47
|
-
close_timestamp:,
|
48
|
-
interval: nil,
|
49
|
-
|
50
|
-
open_price:,
|
51
|
-
high_price:,
|
52
|
-
low_price:,
|
53
|
-
close_price:,
|
54
|
-
|
55
|
-
base_volume: 0.0,
|
56
|
-
target_volume: 0.0,
|
57
|
-
|
58
|
-
trades: 0,
|
59
|
-
green: false,
|
60
|
-
doji: nil)
|
61
|
-
|
62
|
-
super(price: close_price, timestamp: close_timestamp, interval:, trades:)
|
7
|
+
class OHLC < Tick
|
8
|
+
include TimeMethods
|
9
|
+
|
10
|
+
attr_reader :interval, :series
|
11
|
+
attr_reader :close_timestamp, :open_timestamp
|
12
|
+
attr_reader :open_price, :high_price, :low_price, :close_price
|
13
|
+
attr_reader :base_volume, :target_volume, :trades
|
14
|
+
attr_reader :green, :doji
|
15
|
+
|
16
|
+
def initialize(
|
17
|
+
open_timestamp:,
|
18
|
+
close_timestamp:,
|
19
|
+
|
20
|
+
open_price:,
|
21
|
+
high_price:,
|
22
|
+
low_price:,
|
23
|
+
close_price:,
|
24
|
+
|
25
|
+
interval: nil,
|
26
|
+
|
27
|
+
volume: nil,
|
28
|
+
base_volume: nil,
|
29
|
+
target_volume: nil,
|
30
|
+
|
31
|
+
trades: nil,
|
32
|
+
green: nil,
|
33
|
+
doji: nil
|
34
|
+
)
|
63
35
|
@open_timestamp = extract_time(open_timestamp)
|
36
|
+
@close_timestamp = extract_time(close_timestamp)
|
37
|
+
|
64
38
|
@open_price = open_price.to_f
|
65
39
|
@high_price = high_price.to_f
|
66
40
|
@low_price = low_price.to_f
|
41
|
+
@close_price = close_price.to_f
|
42
|
+
|
43
|
+
@interval = Interval[interval]
|
67
44
|
|
68
|
-
@base_volume = base_volume.to_i
|
69
|
-
@target_volume = target_volume.to_i
|
45
|
+
@base_volume = (volume || base_volume).to_i
|
46
|
+
@target_volume = (target_volume || @base_volume).to_i
|
47
|
+
@trades = trades.to_i
|
70
48
|
|
71
49
|
@green = green.nil? ? compute_green : green
|
72
50
|
@doji = doji.nil? ? compute_doji : doji
|
51
|
+
super()
|
73
52
|
end
|
74
53
|
|
54
|
+
alias price close_price
|
55
|
+
alias timestamp close_timestamp
|
56
|
+
alias volume base_volume
|
57
|
+
|
75
58
|
def hl2; ((high_price + low_price) / 2.0) end
|
76
59
|
def oc2; ((open_price + close_price) / 2.0) end
|
77
60
|
def hlc3; ((high_price + low_price + close_price) / 3.0) end
|
78
61
|
def ohlc4; ((open_price + high_price + low_price + close_price) / 4.0) end
|
79
62
|
|
63
|
+
# The corresponding? method helps determine that the other tick's timestamp is the same as this tick's timestamp,
|
64
|
+
# which is useful when aligning ticks between two separate series where one starts or ends at a different time,
|
65
|
+
# or when there may be gaps in the data between the two series.
|
80
66
|
def corresponding?(other)
|
81
67
|
[open_timestamp, close_timestamp] == [other.open_timestamp, other.close_timestamp]
|
82
68
|
end
|
83
69
|
|
84
|
-
# percent change from
|
85
|
-
|
86
|
-
|
70
|
+
# Returns the percent daily price change from open_price to close_price, ranging from 0.0 to 1.0.
|
71
|
+
# A positive value means the price increased, and a negative value means the price decreased.
|
72
|
+
# A value of 0.0 means no change.
|
73
|
+
# @return [Float]
|
74
|
+
def daily_price_change
|
75
|
+
((open_price / close_price) - 1.0)
|
76
|
+
rescue ZeroDivisionError
|
77
|
+
0.0
|
87
78
|
end
|
88
79
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
"l" => low_price,
|
97
|
-
"c" => close_price,
|
98
|
-
|
99
|
-
"bv" => base_volume,
|
100
|
-
"tv" => target_volume,
|
101
|
-
|
102
|
-
"t" => trades,
|
103
|
-
"g" => green,
|
104
|
-
"j" => doji }
|
105
|
-
end
|
106
|
-
|
107
|
-
def as_price(value)
|
108
|
-
series.nil? ? value : series.as_price(value)
|
109
|
-
end
|
110
|
-
|
111
|
-
def to_s
|
112
|
-
ots = interval.daily? ? open_timestamp.strftime('%Y-%m-%d') : open_timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
113
|
-
cts = interval.daily? ? close_timestamp.strftime('%Y-%m-%d') : close_timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
114
|
-
"#{ots}: o: #{as_price(open_price)}, h: #{as_price(high_price)}, l: #{as_price(low_price)}, c: #{as_price(close_price)} :#{cts}"
|
80
|
+
# Calculates the absolute change from the open_price to the close_price, divided by the average of the
|
81
|
+
# open_price and close_price. This method will give a value between 0 and 2, where 0 means no change,
|
82
|
+
# 1 means the price doubled, and 2 means the price went to zero.
|
83
|
+
# This method is useful for comparing the volatility of different assets.
|
84
|
+
# @return [Float]
|
85
|
+
def daily_price_change_ratio
|
86
|
+
@price_change ||= ((open_price - close_price) / oc2).abs
|
115
87
|
end
|
116
88
|
|
89
|
+
# Set the #green? property to true when the close_price is greater than or equal to the open_price.
|
117
90
|
def compute_green
|
118
91
|
close_price >= open_price
|
119
92
|
end
|
120
93
|
|
121
94
|
def green?
|
122
|
-
|
95
|
+
@green
|
123
96
|
end
|
124
97
|
|
125
98
|
def red?
|
@@ -130,10 +103,10 @@ module Quant
|
|
130
103
|
@doji
|
131
104
|
end
|
132
105
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
106
|
+
# Computes a doji candlestick pattern. A doji is a candlestick pattern that occurs when the open and close
|
107
|
+
# are the same or very close to the same. The high and low are also very close to the same. The doji pattern
|
108
|
+
# is a sign of indecision in the market. It is a sign that the market is not sure which way to go.
|
109
|
+
# @return [Boolean]
|
137
110
|
def compute_doji
|
138
111
|
body_bottom, body_top = [open_price, close_price].sort
|
139
112
|
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Ticks
|
5
|
+
module Serializers
|
6
|
+
class OHLC < Tick
|
7
|
+
# Returns a +Quant::Ticks::Tick+ from a valid JSON +String+.
|
8
|
+
# @param json [String]
|
9
|
+
# @param tick_class [Quant::Ticks::Tick]
|
10
|
+
# @return [Quant::Ticks::Tick]
|
11
|
+
# @example
|
12
|
+
# json =
|
13
|
+
# Quant::Ticks::Serializers::Tick.from_json(json, tick_class: Quant::Ticks::Spot)
|
14
|
+
def self.from_json(json, tick_class:)
|
15
|
+
hash = Oj.load(json)
|
16
|
+
from(hash, tick_class: tick_class)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a +Hash+ of the Spot tick's key properties
|
20
|
+
#
|
21
|
+
# Serialized Keys:
|
22
|
+
#
|
23
|
+
# - ot: open timestamp
|
24
|
+
# - ct: close timestamp
|
25
|
+
# - iv: interval
|
26
|
+
# - o: open price
|
27
|
+
# - h: high price
|
28
|
+
# - l: low price
|
29
|
+
# - c: close price
|
30
|
+
# - bv: base volume
|
31
|
+
# - tv: target volume
|
32
|
+
# - t: trades
|
33
|
+
# - g: green
|
34
|
+
# - j: doji
|
35
|
+
#
|
36
|
+
# @param tick [Quant::Ticks::Tick]
|
37
|
+
# @return [Hash]
|
38
|
+
# @example
|
39
|
+
# Quant::Ticks::Serializers::Tick.to_h(tick)
|
40
|
+
# # => { "ot" => [Time], "ct" => [Time], "iv" => "1m", "o" => 1.0, "h" => 2.0,
|
41
|
+
# # "l" => 0.5, "c" => 1.5, "bv" => 6.0, "tv" => 5.0, "t" => 1, "g" => true, "j" => true }
|
42
|
+
def self.to_h(tick)
|
43
|
+
{ "ot" => tick.open_timestamp,
|
44
|
+
"ct" => tick.close_timestamp,
|
45
|
+
"iv" => tick.interval.to_s,
|
46
|
+
|
47
|
+
"o" => tick.open_price,
|
48
|
+
"h" => tick.high_price,
|
49
|
+
"l" => tick.low_price,
|
50
|
+
"c" => tick.close_price,
|
51
|
+
|
52
|
+
"bv" => tick.base_volume,
|
53
|
+
"tv" => tick.target_volume,
|
54
|
+
|
55
|
+
"t" => tick.trades,
|
56
|
+
"g" => tick.green,
|
57
|
+
"j" => tick.doji }
|
58
|
+
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
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Ticks
|
5
|
+
module Serializers
|
6
|
+
class Spot < Tick
|
7
|
+
# Returns a +Quant::Ticks::Tick+ from a valid JSON +String+.
|
8
|
+
# @param json [String]
|
9
|
+
# @param tick_class [Quant::Ticks::Tick]
|
10
|
+
# @return [Quant::Ticks::Tick]
|
11
|
+
# @example
|
12
|
+
# json = "{\"ct\":\"2024-01-15 03:12:23 UTC\", \"cp\":5.0, \"iv\":\"1d\", \"bv\":0.0, \"tv\":0.0, \"t\":0}"
|
13
|
+
# Quant::Ticks::Serializers::Tick.from_json(json, tick_class: Quant::Ticks::Spot)
|
14
|
+
def self.from_json(json, tick_class:)
|
15
|
+
hash = Oj.load(json)
|
16
|
+
from(hash, tick_class: tick_class)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a +Hash+ of the Spot tick's key properties
|
20
|
+
#
|
21
|
+
# Serialized Keys:
|
22
|
+
#
|
23
|
+
# - ct: close timestamp
|
24
|
+
# - iv: interval
|
25
|
+
# - cp: close price
|
26
|
+
# - bv: base volume
|
27
|
+
# - tv: target volume
|
28
|
+
# - t: trades
|
29
|
+
#
|
30
|
+
# @param tick [Quant::Ticks::Tick]
|
31
|
+
# @return [Hash]
|
32
|
+
# @example
|
33
|
+
# Quant::Ticks::Serializers::Tick.to_h(tick)
|
34
|
+
# # => { "ct" => "2024-02-13 03:12:23 UTC", "cp" => 5.0, "iv" => "1d", "bv" => 0.0, "tv" => 0.0, "t" => 0 }
|
35
|
+
def self.to_h(tick)
|
36
|
+
{ "ct" => tick.close_timestamp,
|
37
|
+
"cp" => tick.close_price,
|
38
|
+
"iv" => tick.interval.to_s,
|
39
|
+
"bv" => tick.base_volume,
|
40
|
+
"tv" => tick.target_volume,
|
41
|
+
"t" => tick.trades }
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.from(hash, tick_class:)
|
45
|
+
tick_class.new(
|
46
|
+
close_timestamp: hash["ct"],
|
47
|
+
close_price: hash["cp"],
|
48
|
+
interval: hash["iv"],
|
49
|
+
base_volume: hash["bv"],
|
50
|
+
target_volume: hash["tv"],
|
51
|
+
trades: hash["t"]
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Ticks
|
5
|
+
module Serializers
|
6
|
+
# The +Tick+ class serializes and deserializes +Tick+ objects to and from various formats.
|
7
|
+
# These classes are wired into the Tick classes and are used to convert +Tick+ objects to and from
|
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 +Tick+ objects not shipped
|
10
|
+
# with the library.
|
11
|
+
# @abstract
|
12
|
+
class Tick
|
13
|
+
# Returns a +String+ that is a valid CSV row
|
14
|
+
# @param tick [Quant::Ticks::Tick]
|
15
|
+
# @param headers [Boolean]
|
16
|
+
# @return [String]
|
17
|
+
# @example
|
18
|
+
# Quant::Ticks::Serializers::Tick.to_csv(tick)
|
19
|
+
# # => "1d,9999,5.0\n"
|
20
|
+
# @example
|
21
|
+
# Quant::Ticks::Serializers::Tick.to_csv(tick, headers: true)
|
22
|
+
# # => "iv,ct,cp\n1d,9999,5.0\n"
|
23
|
+
def self.to_csv(tick, headers: false)
|
24
|
+
hash = to_h(tick)
|
25
|
+
row = CSV::Row.new(hash.keys, hash.values)
|
26
|
+
return row.to_csv unless headers
|
27
|
+
|
28
|
+
header_row = CSV::Row.new(hash.keys, hash.keys)
|
29
|
+
header_row.to_csv << row.to_csv
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a +String+ that is a valid JSON representation of the tick's key properties.
|
33
|
+
# @param tick [Quant::Ticks::Tick]
|
34
|
+
# @return [String]
|
35
|
+
# @example
|
36
|
+
# Quant::Ticks::Serializers::Tick.to_json(tick)
|
37
|
+
# # => "{\"iv\":\"1d\",\"ct\":9999,\"cp\":5.0}"
|
38
|
+
def self.to_json(tick)
|
39
|
+
Oj.dump to_h(tick)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns a Ruby +Hash+ comprised of the tick's key properties.
|
43
|
+
# @param tick [Quant::Ticks::Tick]
|
44
|
+
# @return [Hash]
|
45
|
+
# @example
|
46
|
+
# Quant::Ticks::Serializers::Tick.to_h(tick)
|
47
|
+
# # => { "iv" => "1d", "ct" => 9999, "cp" => 5.0 }
|
48
|
+
def self.to_h(instance)
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a +Quant::Ticks::Tick+ from a Ruby +Hash+.
|
53
|
+
# @param hash [Hash]
|
54
|
+
# @param tick_class [Quant::Ticks::Tick]
|
55
|
+
# @return [Quant::Ticks::Tick]
|
56
|
+
# @example
|
57
|
+
# hash = { "ct" => 2024-01-15 03:12:23 UTC", "cp" => 5.0, "iv" => "1d", "bv" => 0.0, "tv" => 0.0, "t" => 0}
|
58
|
+
# Quant::Ticks::Serializers::Tick.from(hash, tick_class: Quant::Ticks::Spot)
|
59
|
+
def self.from(hash, tick_class:)
|
60
|
+
raise NotImplementedError
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns a +Quant::Ticks::Tick+ from a valid JSON +String+.
|
64
|
+
# @param json [String]
|
65
|
+
# @param tick_class [Quant::Ticks::Tick]
|
66
|
+
# @return [Quant::Ticks::Tick]
|
67
|
+
# @example
|
68
|
+
# json = "{\"ct\":\"2024-01-15 03:12:23 UTC\", \"cp\":5.0, \"iv\":\"1d\", \"bv\":0.0, \"tv\":0.0, \"t\":0}"
|
69
|
+
# Quant::Ticks::Serializers::Tick.from_json(json, tick_class: Quant::Ticks::Spot)
|
70
|
+
def self.from_json(json, tick_class:)
|
71
|
+
hash = Oj.load(json)
|
72
|
+
from(hash, tick_class: tick_class)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/quant/ticks/spot.rb
CHANGED
@@ -1,32 +1,78 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "tick"
|
4
|
+
|
3
5
|
module Quant
|
4
6
|
module Ticks
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
# A +Spot+ is a single price point in time. It is the most basic form of a +Tick+ and is usually used to represent
|
8
|
+
# a continuously streaming tick that just has a single price point at a given point in time.
|
9
|
+
# @example
|
10
|
+
# spot = Quant::Ticks::Spot.new(price: 100.0, timestamp: Time.now)
|
11
|
+
# spot.price # => 100.0
|
12
|
+
# spot.timestamp # => 2018-01-01 12:00:00 UTC
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# spot = Quant::Ticks::Spot.from({ "p" => 100.0, "t" => "2018-01-01 12:00:00 UTC", "bv" => 1000 })
|
16
|
+
# spot.price # => 100.0
|
17
|
+
# spot.timestamp # => 2018-01-01 12:00:00 UTC
|
18
|
+
# spot.volume # => 1000
|
19
|
+
class Spot < Tick
|
20
|
+
include TimeMethods
|
21
|
+
|
22
|
+
attr_reader :interval, :series
|
23
|
+
attr_reader :close_timestamp, :open_timestamp
|
24
|
+
attr_reader :open_price, :high_price, :low_price, :close_price
|
25
|
+
attr_reader :base_volume, :target_volume, :trades
|
26
|
+
|
27
|
+
def initialize(
|
28
|
+
price: nil,
|
29
|
+
timestamp: nil,
|
30
|
+
close_price: nil,
|
31
|
+
close_timestamp: nil,
|
32
|
+
volume: nil,
|
33
|
+
interval: nil,
|
34
|
+
base_volume: nil,
|
35
|
+
target_volume: nil,
|
36
|
+
trades: nil
|
37
|
+
)
|
38
|
+
raise ArgumentError, "Must supply a spot price as either :price or :close_price" unless price || close_price
|
39
|
+
|
40
|
+
@close_price = (close_price || price).to_f
|
9
41
|
|
10
|
-
|
11
|
-
|
42
|
+
@interval = Interval[interval]
|
43
|
+
|
44
|
+
@close_timestamp = extract_time(timestamp || close_timestamp || Quant.current_time)
|
45
|
+
@open_timestamp = @close_timestamp
|
46
|
+
|
47
|
+
@base_volume = (volume || base_volume).to_i
|
48
|
+
@target_volume = (target_volume || @base_volume).to_i
|
49
|
+
|
50
|
+
@trades = trades.to_i
|
51
|
+
super()
|
12
52
|
end
|
13
53
|
|
14
|
-
|
15
|
-
|
16
|
-
|
54
|
+
alias timestamp close_timestamp
|
55
|
+
alias price close_price
|
56
|
+
alias oc2 close_price
|
57
|
+
alias hl2 close_price
|
58
|
+
alias hlc3 close_price
|
59
|
+
alias ohlc4 close_price
|
60
|
+
alias delta close_price
|
61
|
+
alias volume base_volume
|
62
|
+
|
63
|
+
def ==(other)
|
64
|
+
[close_price, close_timestamp] == [other.close_price, other.close_timestamp]
|
17
65
|
end
|
18
66
|
|
67
|
+
# The corresponding? method helps determine that the other tick's timestamp is the same as this tick's timestamp,
|
68
|
+
# which is useful when aligning ticks between two separate series where one starts or ends at a different time,
|
69
|
+
# or when there may be gaps in the data between the two series.
|
19
70
|
def corresponding?(other)
|
20
71
|
close_timestamp == other.close_timestamp
|
21
72
|
end
|
22
73
|
|
23
|
-
def
|
24
|
-
{
|
25
|
-
"c" => close_price,
|
26
|
-
"iv" => interval.to_s,
|
27
|
-
"bv" => base_volume,
|
28
|
-
"tv" => target_volume,
|
29
|
-
"t" => trades }
|
74
|
+
def inspect
|
75
|
+
"#<#{self.class.name} cp=#{close_price.to_f} ct=#{close_timestamp.strftime("%Y-%m-%d")} iv=#{interval.to_s} v=#{volume}>"
|
30
76
|
end
|
31
77
|
end
|
32
78
|
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Ticks
|
5
|
+
# +Tick+ is the abstract ancestor for all Ticks and holds the logic for interacting with series and indicators.
|
6
|
+
# The public interface is devoid of properties around price, volume, and timestamp, etc. Descendant classes
|
7
|
+
# are responsible for defining the properties and how they are represented.
|
8
|
+
#
|
9
|
+
# The +Tick+ class is designed to be immutable and is intended to be used as a value object. This means that
|
10
|
+
# once a +Tick+ is created, it cannot be changed. This is important for the integrity of the series and
|
11
|
+
# indicators that depend on the ticks within the series.
|
12
|
+
#
|
13
|
+
# When a tick is added to a series, it is locked into the series and ownership cannot be changed. This is important
|
14
|
+
# for the integrity of the series and indicators that depend on the ticks within the series. This is a key design
|
15
|
+
# to being able to being able to not only compute indicators on the ticks just once, but also avoid recomputing
|
16
|
+
# indicators when series are limited/sliced/filtered into subsets of the original series.
|
17
|
+
#
|
18
|
+
# Ticks can be serialized to and from Ruby Hash, JSON strings, and CSV strings.
|
19
|
+
class Tick
|
20
|
+
# Returns a +Tick+ from a Ruby +Hash+. The default serializer is used to generate the +Tick+.
|
21
|
+
# @param hash [Hash]
|
22
|
+
# @param serializer_class [Class] The serializer class to use for the conversion.
|
23
|
+
# @return [Quant::Ticks::Tick]
|
24
|
+
# @example
|
25
|
+
# hash = { "timestamp" => "2018-01-01 12:00:00 UTC", "price" => 100.0, "volume" => 1000 }
|
26
|
+
# Quant::Ticks::Tick.from(hash)
|
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: default_serializer_class)
|
29
|
+
serializer_class.from(hash, tick_class: self)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a +Tick+ from a JSON string. The default serializer is used to generate the +Tick+.
|
33
|
+
# @param json [String]
|
34
|
+
# @param serializer_class [Class] The serializer class to use for the conversion.
|
35
|
+
# @return [Quant::Ticks::Tick]
|
36
|
+
# @example
|
37
|
+
# json = "{\"timestamp\":\"2018-01-01 12:00:00 UTC\",\"price\":100.0,\"volume\":1000}"
|
38
|
+
# Quant::Ticks::Tick.from_json(json)
|
39
|
+
# # => #<Quant::Ticks::Spot:0x00007f9e3b8b3e08 @timestamp=2018-01-01 12:00:00 UTC, @price=100.0, @volume=1000>
|
40
|
+
def self.from_json(json, serializer_class: default_serializer_class)
|
41
|
+
serializer_class.from_json(json, tick_class: self)
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :series
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
# Set the series by appending to the series or calling #assign_series method
|
48
|
+
@series = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
# Ticks always belong to the first series they're assigned so we can easily spin off
|
52
|
+
# sub-sets or new series with the same ticks while allowing each series to have
|
53
|
+
# its own state and full control over the ticks within its series
|
54
|
+
def assign_series(new_series)
|
55
|
+
assign_series!(new_series) if @series.nil?
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
# Ticks always belong to the first series they're assigned so we can easily spin off
|
60
|
+
# sub-sets or new series with the same ticks. However, if you need to reassign the
|
61
|
+
# series, you can use this method to force the change of series ownership.
|
62
|
+
#
|
63
|
+
# The series interval is also assigned to the tick if it is not already set.
|
64
|
+
def assign_series!(new_series)
|
65
|
+
@series = new_series
|
66
|
+
@interval = new_series.interval if @interval.nil?
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns a Ruby hash for the Tick. The default serializer is used to generate the hash.
|
71
|
+
#
|
72
|
+
# @param serializer_class [Class] the serializer class to use for the conversion.
|
73
|
+
# @example
|
74
|
+
# tick.to_h
|
75
|
+
# # => { timestamp: "2018-01-01 12:00:00 UTC", price: 100.0, volume: 1000 }
|
76
|
+
def to_h(serializer_class: default_serializer_class)
|
77
|
+
serializer_class.to_h(self)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns a JSON string for the Tick. The default serializer is used to generate the JSON string.
|
81
|
+
#
|
82
|
+
# @param serializer_class [Class] the serializer class to use for the conversion.
|
83
|
+
# @example
|
84
|
+
# tick.to_json
|
85
|
+
# # => "{\"timestamp\":\"2018-01-01 12:00:00 UTC\",\"price\":100.0,\"volume\":1000}"
|
86
|
+
def to_json(serializer_class: default_serializer_class)
|
87
|
+
serializer_class.to_json(self)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns a CSV row as a String for the Tick. The default serializer is used to generate the CSV string.
|
91
|
+
# If headers is true, two lines returned separated by newline.
|
92
|
+
# The first line is the header row and the second line is the data row.
|
93
|
+
#
|
94
|
+
# @param serializer_class [Class] the serializer class to use for the conversion.
|
95
|
+
# @example
|
96
|
+
# tick.to_csv(headers: true)
|
97
|
+
# # => "timestamp,price,volume\n2018-01-01 12:00:00 UTC,100.0,1000\n"
|
98
|
+
def to_csv(serializer_class: default_serializer_class, headers: false)
|
99
|
+
serializer_class.to_csv(self, headers: headers)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Reflects the serializer class from the tick's class name.
|
103
|
+
# @note internal use only.
|
104
|
+
def self.default_serializer_class
|
105
|
+
Object.const_get "Quant::Ticks::Serializers::#{name.split("::").last}"
|
106
|
+
end
|
107
|
+
|
108
|
+
# Reflects the serializer class from the tick's class name.
|
109
|
+
# @note internal use only.
|
110
|
+
def default_serializer_class
|
111
|
+
self.class.default_serializer_class
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/quant/version.rb
CHANGED
data/lib/quantitative.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require "time"
|
4
4
|
require "date"
|
5
5
|
require "oj"
|
6
|
+
require "csv"
|
6
7
|
|
7
8
|
lib_folder = File.expand_path(File.join(File.dirname(__FILE__)))
|
8
9
|
quant_folder = File.join(lib_folder, "quant")
|
@@ -11,7 +12,6 @@ quant_folder = File.join(lib_folder, "quant")
|
|
11
12
|
Dir.glob(File.join(quant_folder, "*.rb")).each { |fn| require fn }
|
12
13
|
|
13
14
|
# require sub-folders and their sub-folders
|
14
|
-
%w(ticks).each do |sub_folder|
|
15
|
-
Dir.glob(File.join(quant_folder, sub_folder, "*.rb")).each { |fn| require fn }
|
15
|
+
%w(refinements ticks).each do |sub_folder|
|
16
16
|
Dir.glob(File.join(quant_folder, sub_folder, "**/*.rb")).each { |fn| require fn }
|
17
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.2
|
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-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -24,62 +24,6 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.10'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: debug
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: guard-rspec
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '4.7'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '4.7'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rspec
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '3.2'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '3.2'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: yard
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0.9'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0.9'
|
83
27
|
description: Quantitative and statistical tools written for Ruby 3.x for trading and
|
84
28
|
finance.
|
85
29
|
email:
|
@@ -90,12 +34,14 @@ extra_rdoc_files: []
|
|
90
34
|
files:
|
91
35
|
- ".rspec"
|
92
36
|
- ".rubocop.yml"
|
37
|
+
- ".ruby-version"
|
38
|
+
- ".yardopts"
|
93
39
|
- CODE_OF_CONDUCT.md
|
94
|
-
- DISCLAIMER.
|
40
|
+
- DISCLAIMER.md
|
95
41
|
- Gemfile
|
96
42
|
- Gemfile.lock
|
97
43
|
- Guardfile
|
98
|
-
- LICENSE
|
44
|
+
- LICENSE
|
99
45
|
- README.md
|
100
46
|
- Rakefile
|
101
47
|
- lib/quant/errors.rb
|
@@ -109,12 +55,14 @@ files:
|
|
109
55
|
- lib/quant/mixins/super_smoother.rb
|
110
56
|
- lib/quant/mixins/trig.rb
|
111
57
|
- lib/quant/mixins/weighted_average.rb
|
58
|
+
- lib/quant/refinements/array.rb
|
112
59
|
- lib/quant/ticks/core.rb
|
113
|
-
- lib/quant/ticks/indicator_points.rb
|
114
60
|
- lib/quant/ticks/ohlc.rb
|
115
|
-
- lib/quant/ticks/serializers/
|
61
|
+
- lib/quant/ticks/serializers/ohlc.rb
|
62
|
+
- lib/quant/ticks/serializers/spot.rb
|
63
|
+
- lib/quant/ticks/serializers/tick.rb
|
116
64
|
- lib/quant/ticks/spot.rb
|
117
|
-
- lib/quant/ticks/
|
65
|
+
- lib/quant/ticks/tick.rb
|
118
66
|
- lib/quant/time_methods.rb
|
119
67
|
- lib/quant/time_period.rb
|
120
68
|
- lib/quant/version.rb
|
@@ -142,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
90
|
- !ruby/object:Gem::Version
|
143
91
|
version: '0'
|
144
92
|
requirements: []
|
145
|
-
rubygems_version: 3.
|
93
|
+
rubygems_version: 3.5.5
|
146
94
|
signing_key:
|
147
95
|
specification_version: 4
|
148
96
|
summary: Quantitative and statistical tools written for Ruby 3.x for trading and finance.
|
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quant
|
4
|
-
module Ticks
|
5
|
-
class IndicatorPoints
|
6
|
-
attr_reader :points
|
7
|
-
|
8
|
-
def initialize(tick:)
|
9
|
-
@tick = tick
|
10
|
-
@points = {}
|
11
|
-
end
|
12
|
-
|
13
|
-
def [](indicator)
|
14
|
-
points[indicator]
|
15
|
-
end
|
16
|
-
|
17
|
-
def []=(indicator, point)
|
18
|
-
points[indicator] = point
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quant
|
4
|
-
module Ticks
|
5
|
-
module Serializers
|
6
|
-
module Value
|
7
|
-
module_function
|
8
|
-
|
9
|
-
def to_h(instance)
|
10
|
-
{ "iv" => instance.interval.to_s,
|
11
|
-
"ct" => instance.close_timestamp.to_i,
|
12
|
-
"cp" => instance.close_price,
|
13
|
-
"bv" => instance.base_volume,
|
14
|
-
"tv" => instance.target_volume }
|
15
|
-
end
|
16
|
-
|
17
|
-
def to_json(instance)
|
18
|
-
Oj.dump to_h(instance)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
data/lib/quant/ticks/value.rb
DELETED
@@ -1,88 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quant
|
4
|
-
module Ticks
|
5
|
-
# Value ticks are the most basic ticks and are used to represent a single price point (no open, high, low, close, etc.)
|
6
|
-
# and a single timestamp. Usually, these are best used in streaming data where ticks are flowing in every second or whatever
|
7
|
-
# interval that's appropriate for the data source.
|
8
|
-
# Often indicators and charts still want a universal public interface (i.e. open_price, high_price, volume, etc.), so we add
|
9
|
-
# those methods here and inherit and redefine upstream as appropriate.
|
10
|
-
#
|
11
|
-
# For Value ticks:
|
12
|
-
# * The +price+ given is set for all *_price fields.
|
13
|
-
# * The +volume+ is set for both base and target volume.
|
14
|
-
# * The +timestamp+ is set for both open and close timestamps.
|
15
|
-
class Value
|
16
|
-
include TimeMethods
|
17
|
-
|
18
|
-
attr_reader :interval, :series
|
19
|
-
attr_reader :close_timestamp, :open_timestamp
|
20
|
-
attr_reader :open_price, :high_price, :low_price, :close_price
|
21
|
-
attr_reader :base_volume, :target_volume, :trades
|
22
|
-
attr_reader :green, :doji
|
23
|
-
|
24
|
-
def initialize(price:, timestamp: Quant.current_time, interval: nil, volume: 0, trades: 0)
|
25
|
-
@interval = Interval[interval]
|
26
|
-
|
27
|
-
@close_timestamp = extract_time(timestamp)
|
28
|
-
@open_timestamp = @close_timestamp
|
29
|
-
|
30
|
-
@close_price = price.to_f
|
31
|
-
@open_price = close_price
|
32
|
-
@high_price = close_price
|
33
|
-
@low_price = close_price
|
34
|
-
|
35
|
-
@base_volume = volume.to_i
|
36
|
-
@target_volume = volume.to_i
|
37
|
-
@trades = trades.to_i
|
38
|
-
|
39
|
-
# Set the series by appending to the series
|
40
|
-
@series = nil
|
41
|
-
end
|
42
|
-
|
43
|
-
alias oc2 close_price
|
44
|
-
alias hl2 close_price
|
45
|
-
alias hlc3 close_price
|
46
|
-
alias ohlc4 close_price
|
47
|
-
alias delta close_price
|
48
|
-
alias volume base_volume
|
49
|
-
|
50
|
-
def corresponding?(other)
|
51
|
-
close_timestamp == other.close_timestamp
|
52
|
-
end
|
53
|
-
|
54
|
-
def ==(other)
|
55
|
-
to_h == other.to_h
|
56
|
-
end
|
57
|
-
|
58
|
-
# ticks are immutable across series so we can easily initialize sub-sets or new series
|
59
|
-
# with the same ticks while allowing each series to have its own state and full
|
60
|
-
# control over the ticks within its series
|
61
|
-
def assign_series(new_series)
|
62
|
-
assign_series!(new_series) if @series.nil?
|
63
|
-
|
64
|
-
# dup.tap do |new_tick|
|
65
|
-
# # new_tick.instance_variable_set(:@series, new_series)
|
66
|
-
# new_tick.instance_variable_set(:@indicators, indicators)
|
67
|
-
# end
|
68
|
-
end
|
69
|
-
|
70
|
-
def assign_series!(new_series)
|
71
|
-
@series = new_series
|
72
|
-
self
|
73
|
-
end
|
74
|
-
|
75
|
-
def inspect
|
76
|
-
"#<#{self.class.name} iv=#{interval} ct=#{close_timestamp.strftime("%Y-%m-%d")} o=#{open_price} c=#{close_price} v=#{volume}>"
|
77
|
-
end
|
78
|
-
|
79
|
-
def to_h
|
80
|
-
Quant::Ticks::Serializers::Value.to_h(self)
|
81
|
-
end
|
82
|
-
|
83
|
-
def to_json(*_args)
|
84
|
-
Quant::Ticks::Serializers::Value.to_json(self)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
File without changes
|
/data/{LICENSE.txt → LICENSE}
RENAMED
File without changes
|