quantitative 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/quantitative.svg)](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
|