quantitative 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b99f939e67992ff9a3659ef69ef8ff4bd5897baba1d269c491a7db7d1717c85
4
- data.tar.gz: e9f456c34e16ed0c44b4b23f6e274ed44cce4ca6ad7fe654602dbd73afc281bc
3
+ metadata.gz: 19fe65cd5c50b0b503abaa0e7c5be3949a284cf5a2b537d79b9cbdd69448041d
4
+ data.tar.gz: fe03ed6b2ddef6609cee31547cd84c1eac948bb8d10efb8c24c316673c693a62
5
5
  SHA512:
6
- metadata.gz: 4fca7722561f54543b92a7b50700eed4cb706b9bfca65405a8a101908b6ae49b547bcdd2cd13c4656c94a30ad817225118c22c905e47076c88f8feff85947bc4
7
- data.tar.gz: 78b256560d7aec2ee19833618bd19e89f6c87ae718ad966b1d18caa18e08727b37d1232b169bce1fbc0b4ca4bac951e956fd4efefaa27450d533769f68d8ac23
6
+ metadata.gz: 9ce1d965a8e68f6fae76143dfc619ed98c8a5af559e9f26c67dd7685e60bf9b4cfa3ecf5f43908f01597862cb6d6389cebd40cf5897afed560a9e54ed79ea91e
7
+ data.tar.gz: ee8527e43aa4670175d20a20c6037af11227071ff0fe738344c4b9b91d2391335d210a9e5b305153e78edc75ca9acb802b56b2502223107a84366ae40ba55b7f
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.1.1)
4
+ quantitative (0.1.2)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
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.
@@ -167,7 +167,13 @@ module Quant
167
167
  alias seconds duration
168
168
 
169
169
  def ==(other)
170
- interval == other&.interval
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
@@ -100,7 +100,7 @@ module Quant
100
100
  end
101
101
 
102
102
  # Computes the mean of the array. When +n+ is specified, the mean is computed over
103
- # the last +n+ elements.
103
+ # the last +n+ elements, otherwise it is computed over the entire array.
104
104
  #
105
105
  # @param n [Integer] the number of elements to compute the mean over
106
106
  # @return [Float]
@@ -112,7 +112,7 @@ module Quant
112
112
  end
113
113
 
114
114
  # Computes the Exponential Moving Average (EMA) of the array. When +n+ is specified,
115
- # the EMA is computed over the last +n+ elements.
115
+ # the EMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
116
116
  # An Array of EMA's is returned, with the first entry always the first value in the subset.
117
117
  #
118
118
  # @params n [Integer] the number of elements to compute the EMA over.
@@ -130,7 +130,7 @@ module Quant
130
130
  end
131
131
 
132
132
  # Computes the Simple Moving Average (SMA) of the array. When +n+ is specified,
133
- # the SMA is computed over the last +n+ elements.
133
+ # the SMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
134
134
  # An Array of SMA's is returned, with the first entry always the first value in the subset.
135
135
  #
136
136
  # @param n [Integer] the number of elements to compute the SMA over
@@ -146,7 +146,7 @@ module Quant
146
146
  end
147
147
 
148
148
  # Computes the Weighted Moving Average (WMA) of the array. When +n+ is specified,
149
- # the WMA is computed over the last +n+ elements.
149
+ # the WMA is computed over the last +n+ elements, otherwise it is computed over the entire array.
150
150
  # An Array of WMA's is returned, with the first entry always the first value in the subset.
151
151
  #
152
152
  # @param n [Integer] the number of elements to compute the WMA over
@@ -168,7 +168,8 @@ module Quant
168
168
  end
169
169
 
170
170
  # Computes the Standard Deviation of the array. When +n+ is specified,
171
- # the Standard Deviation is computed over the last +n+ elements.
171
+ # the Standard Deviation is computed over the last +n+ elements,
172
+ # otherwise it is computed over the entire array.
172
173
  #
173
174
  # @param n [Integer] the number of elements to compute the Standard Deviation over.
174
175
  # @return [Float]
@@ -1,125 +1,98 @@
1
- require_relative 'value'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tick"
2
4
 
3
5
  module Quant
4
6
  module Ticks
5
- # serialized keys
6
- # ot: open timestamp
7
- # ct: close timestamp
8
- # iv: interval
9
-
10
- # o: open price
11
- # h: high price
12
- # l: low price
13
- # c: close price
14
-
15
- # bv: base volume
16
- # tv: target volume
17
- # ct: close timestamp
18
-
19
- # t: trades
20
- # g: green
21
- # j: doji
22
- class OHLC < Value
23
- def self.from(hash)
24
- new \
25
- open_timestamp: hash["ot"],
26
- close_timestamp: hash["ct"],
27
- interval: hash["iv"],
28
-
29
- open_price: hash["o"],
30
- high_price: hash["h"],
31
- low_price: hash["l"],
32
- close_price: hash["c"],
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: interval, trades: 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 open to close
85
- def delta
86
- ((open_price / close_price) - 1.0) * 100
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
- def to_h
90
- { "ot" => open_timestamp,
91
- "ct" => close_timestamp,
92
- "iv" => interval.to_s,
93
-
94
- "o" => open_price,
95
- "h" => high_price,
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
- close_price > open_price
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
- def price_change
134
- @price_change ||= ((open_price - close_price) / oc2).abs
135
- end
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
@@ -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
- class Spot < Value
6
- def self.from(hash)
7
- new(close_timestamp: hash["ct"], close_price: hash["c"], base_volume: hash["bv"], target_volume: hash["tv"])
8
- end
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
- def self.from_json(json)
11
- from Oj.load(json)
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
- def initialize(close_timestamp:, close_price:, interval: nil, base_volume: 0.0, target_volume: 0.0, trades: 0)
15
- super(price: close_price, timestamp: close_timestamp, interval: interval, volume: base_volume, trades: trades)
16
- @target_volume = target_volume.to_i
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 to_h
24
- { "ct" => close_timestamp,
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
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")
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.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-12 00:00:00.000000000 Z
11
+ date: 2024-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -57,11 +57,12 @@ files:
57
57
  - lib/quant/mixins/weighted_average.rb
58
58
  - lib/quant/refinements/array.rb
59
59
  - lib/quant/ticks/core.rb
60
- - lib/quant/ticks/indicator_points.rb
61
60
  - lib/quant/ticks/ohlc.rb
62
- - lib/quant/ticks/serializers/value.rb
61
+ - lib/quant/ticks/serializers/ohlc.rb
62
+ - lib/quant/ticks/serializers/spot.rb
63
+ - lib/quant/ticks/serializers/tick.rb
63
64
  - lib/quant/ticks/spot.rb
64
- - lib/quant/ticks/value.rb
65
+ - lib/quant/ticks/tick.rb
65
66
  - lib/quant/time_methods.rb
66
67
  - lib/quant/time_period.rb
67
68
  - lib/quant/version.rb
@@ -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
@@ -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