quantitative 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +2 -0
- data/lib/quant/errors.rb +1 -0
- data/lib/quant/interval.rb +7 -1
- data/lib/quant/refinements/array.rb +6 -5
- data/lib/quant/security.rb +80 -0
- data/lib/quant/security_class.rb +116 -0
- data/lib/quant/series.rb +108 -0
- data/lib/quant/ticks/ohlc.rb +79 -93
- 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 +63 -16
- data/lib/quant/ticks/tick.rb +115 -0
- data/lib/quant/version.rb +1 -1
- data/lib/quantitative.rb +1 -0
- metadata +10 -7
- data/lib/quant/ticks/core.rb +0 -8
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5bfe61d98618c7cfb83052426689976be38c45e83fce9217a74a4f0392897b60
|
4
|
+
data.tar.gz: 061d9a0179eb913c5f5f5d9f1016998c2cbc0d65da9a774620a3b591f18a1f49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4154a9e589bcffd15b3415ace9c1ccc6b8d29ba5ecc0180e48259c649a746a157f58bd89aeabad4ffc4ac5edefe6f1892c94adcd001ba5d1f039a5bb46a8a9d0
|
7
|
+
data.tar.gz: 7ac2b4661e6c4b0e02fda9389f73b8d007b92e754e8b4fa821bb55dca1d085dfd7e2e3eb6608d520a2e2e6008eb837aa75e7c07c05addc28af577b92fe98f8c3
|
data/Gemfile.lock
CHANGED
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
|
@@ -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]
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "security_class"
|
4
|
+
|
5
|
+
module Quant
|
6
|
+
# A +Security+ is a representation of a financial instrument such as a stock, option, future, or currency.
|
7
|
+
# It is used to represent the instrument that is being traded, analyzed, or managed.
|
8
|
+
# @example
|
9
|
+
# security = Quant::Security.new(symbol: "AAPL", name: "Apple Inc.", security_class: :stock, exchange: "NASDAQ")
|
10
|
+
# security.symbol # => "AAPL"
|
11
|
+
# security.name # => "Apple Inc."
|
12
|
+
# security.stock? # => true
|
13
|
+
# security.option? # => false
|
14
|
+
# security.future? # => false
|
15
|
+
# security.currency? # => false
|
16
|
+
# security.exchange # => "NASDAQ"
|
17
|
+
# security.to_h # => { "s" => "AAPL", "n" => "Apple Inc.", "sc" => "stock", "x" => "NASDAQ" }
|
18
|
+
class Security
|
19
|
+
attr_reader :symbol, :name, :security_class, :id, :exchange, :source, :meta, :created_at, :updated_at
|
20
|
+
|
21
|
+
def initialize(
|
22
|
+
symbol:,
|
23
|
+
name: nil,
|
24
|
+
id: nil,
|
25
|
+
active: true,
|
26
|
+
tradeable: true,
|
27
|
+
exchange: nil,
|
28
|
+
source: nil,
|
29
|
+
security_class: nil,
|
30
|
+
created_at: Quant.current_time,
|
31
|
+
updated_at: Quant.current_time,
|
32
|
+
meta: {}
|
33
|
+
)
|
34
|
+
raise ArgumentError, "symbol is required" unless symbol
|
35
|
+
|
36
|
+
@symbol = symbol.to_s.upcase
|
37
|
+
@name = name
|
38
|
+
@id = id
|
39
|
+
@tradeable = tradeable
|
40
|
+
@active = active
|
41
|
+
@exchange = exchange
|
42
|
+
@source = source
|
43
|
+
@security_class = SecurityClass.new(security_class)
|
44
|
+
@created_at = created_at
|
45
|
+
@updated_at = updated_at
|
46
|
+
@meta = meta
|
47
|
+
end
|
48
|
+
|
49
|
+
def active?
|
50
|
+
!!@active
|
51
|
+
end
|
52
|
+
|
53
|
+
def tradeable?
|
54
|
+
!!@tradeable
|
55
|
+
end
|
56
|
+
|
57
|
+
SecurityClass::CLASSES.each do |class_name|
|
58
|
+
define_method("#{class_name}?") do
|
59
|
+
security_class == class_name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_h(full: false)
|
64
|
+
return { "s" => symbol } unless full
|
65
|
+
|
66
|
+
{ "s" => symbol,
|
67
|
+
"n" => name,
|
68
|
+
"id" => id,
|
69
|
+
"t" => tradeable?,
|
70
|
+
"a" => active?,
|
71
|
+
"x" => exchange,
|
72
|
+
"sc" => security_class.to_s,
|
73
|
+
"src" => source.to_s }
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_json(*args, full: false)
|
77
|
+
Oj.dump(to_h(full: full), *args)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
# Stocks (Equities): Represent ownership in a company. Stockholders are entitled to a share
|
5
|
+
# of the company's profits and have voting rights at shareholder meetings.
|
6
|
+
|
7
|
+
# Bonds (Fixed-Income Securities): Debt instruments where an investor lends money to an entity
|
8
|
+
# (government or corporation) in exchange for periodic interest payments and the return of
|
9
|
+
# the principal amount at maturity.
|
10
|
+
|
11
|
+
# Mutual Funds: Pooled funds managed by an investment company. Investors buy shares in the mutual
|
12
|
+
# fund, and the fund invests in a diversified portfolio of stocks, bonds, or other securities.
|
13
|
+
|
14
|
+
# Exchange-Traded Funds (ETFs): Similar to mutual funds but traded on stock exchanges.
|
15
|
+
# ETFs can track an index, commodity, bonds, or a basket of assets.
|
16
|
+
|
17
|
+
# Options: Derivative securities that give the holder the right (but not the obligation) to buy
|
18
|
+
# or sell an underlying asset at a predetermined price before or at expiration.
|
19
|
+
|
20
|
+
# Futures: Contracts that obligate the buyer to purchase or the seller to sell an
|
21
|
+
# asset at a predetermined future date and price.
|
22
|
+
|
23
|
+
# Real Estate Investment Trusts (REITs): Companies that own, operate, or finance income-generating
|
24
|
+
# real estate. Investors can buy shares in a REIT, which provides them with a share of the
|
25
|
+
# income produced by the real estate.
|
26
|
+
|
27
|
+
# Cryptocurrencies: Digital or virtual currencies that use cryptography for security and operate on
|
28
|
+
# decentralized networks, typically based on blockchain technology. Examples include Bitcoin, Ethereum, and Ripple.
|
29
|
+
|
30
|
+
# Preferred Stock: A type of stock that has priority over common stock in terms of dividend
|
31
|
+
# payments and asset distribution in the event of liquidation.
|
32
|
+
|
33
|
+
# Treasury Securities: Issued by the government to raise funds. Types include Treasury bills (T-bills),
|
34
|
+
# Treasury notes (T-notes), and Treasury bonds (T-bonds).
|
35
|
+
|
36
|
+
# Mortgage-Backed Securities (MBS): Securities that represent an ownership interest in a pool of mortgage loans.
|
37
|
+
# Investors receive payments based on the interest and principal of the underlying loans.
|
38
|
+
|
39
|
+
# Commodities: Physical goods such as gold, silver, oil, or agricultural products, traded on commodity exchanges.
|
40
|
+
|
41
|
+
# Foreign Exchange (Forex): The market where currencies are traded. Investors can buy and sell currencies to
|
42
|
+
# profit from changes in exchange rates.
|
43
|
+
class SecurityClass
|
44
|
+
CLASSES = %i(
|
45
|
+
bond
|
46
|
+
commodity
|
47
|
+
cryptocurrency
|
48
|
+
etf
|
49
|
+
forex
|
50
|
+
future
|
51
|
+
mbs
|
52
|
+
mutual_fund
|
53
|
+
option
|
54
|
+
preferred_stock
|
55
|
+
reit
|
56
|
+
stock
|
57
|
+
treasury_note
|
58
|
+
).freeze
|
59
|
+
|
60
|
+
attr_reader :security_class
|
61
|
+
|
62
|
+
def initialize(name)
|
63
|
+
return if @security_class = from_standard(name)
|
64
|
+
|
65
|
+
@security_class = from_alternate(name.to_s.downcase.to_sym) unless name.nil?
|
66
|
+
raise_unknown_security_class_error(name) unless security_class
|
67
|
+
end
|
68
|
+
|
69
|
+
CLASSES.each do |class_name|
|
70
|
+
define_method("#{class_name}?") do
|
71
|
+
security_class == class_name
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def raise_unknown_security_class_error(name)
|
76
|
+
raise SecurityClassError, "Unknown security class: #{name.inspect}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
security_class.to_s
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_h
|
84
|
+
{ "sc" => security_class }
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_json(*args)
|
88
|
+
Oj.dump(to_h, *args)
|
89
|
+
end
|
90
|
+
|
91
|
+
def ==(other)
|
92
|
+
case other
|
93
|
+
when String then from_alternate(other.to_sym) == security_class
|
94
|
+
when Symbol then from_alternate(other) == security_class
|
95
|
+
when SecurityClass then other.security_class == security_class
|
96
|
+
else
|
97
|
+
false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
ALTERNATE_NAMES = {
|
104
|
+
us_equity: :stock,
|
105
|
+
crypto: :cryptocurrency,
|
106
|
+
}.freeze
|
107
|
+
|
108
|
+
def from_standard(name)
|
109
|
+
name if CLASSES.include?(name)
|
110
|
+
end
|
111
|
+
|
112
|
+
def from_alternate(alt_name)
|
113
|
+
from_standard(alt_name) || ALTERNATE_NAMES[alt_name]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/quant/series.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
# Ticks belong to the first series they're associated with always.
|
5
|
+
# There are no provisions for series merging their ticks to one series!
|
6
|
+
# Indicators will be computed against the parent series of a list of ticks, so we
|
7
|
+
# can safely work with subsets of a series and indicators will compute just once.
|
8
|
+
class Series
|
9
|
+
include Enumerable
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def self.from_file(filename:, symbol:, interval:, folder: nil)
|
13
|
+
symbol = symbol.to_s.upcase
|
14
|
+
interval = Interval[interval]
|
15
|
+
|
16
|
+
filename = Rails.root.join("historical", folder, "#{symbol.upcase}.txt") if filename.nil?
|
17
|
+
raise "File #{filename} does not exist" unless File.exist?(filename)
|
18
|
+
|
19
|
+
lines = File.read(filename).split("\n")
|
20
|
+
ticks = lines.map{ |line| Quant::Ticks::OHLC.from_json(line) }
|
21
|
+
|
22
|
+
from_ticks(symbol: symbol, interval: interval, ticks: ticks)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.from_json(symbol:, interval:, json:)
|
26
|
+
from_hash symbol: symbol, interval: interval, hash: Oj.load(json)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.from_hash(symbol:, interval:, hash:)
|
30
|
+
ticks = hash.map { |tick_hash| Quant::Ticks::OHLC.from(tick_hash) }
|
31
|
+
from_ticks(symbol: symbol, interval: interval, ticks: ticks)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_ticks(symbol:, interval:, ticks:)
|
35
|
+
ticks = ticks.sort_by(&:close_timestamp)
|
36
|
+
|
37
|
+
new(symbol: symbol, interval: interval).tap do |series|
|
38
|
+
ticks.each { |tick| series << tick }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :symbol, :interval, :ticks
|
43
|
+
|
44
|
+
def initialize(symbol:, interval:)
|
45
|
+
@symbol = symbol
|
46
|
+
@interval = interval
|
47
|
+
@ticks = []
|
48
|
+
end
|
49
|
+
|
50
|
+
def limit_iterations(start_iteration, stop_iteration)
|
51
|
+
selected_ticks = ticks[start_iteration..stop_iteration]
|
52
|
+
return self if selected_ticks.size == ticks.size
|
53
|
+
|
54
|
+
self.class.from_ticks(symbol: symbol, interval: interval, ticks: selected_ticks)
|
55
|
+
end
|
56
|
+
|
57
|
+
def limit(period)
|
58
|
+
selected_ticks = ticks.select{ |tick| period.cover?(tick.close_timestamp) }
|
59
|
+
return self if selected_ticks.size == ticks.size
|
60
|
+
|
61
|
+
self.class.from_ticks(symbol: symbol, interval: interval, ticks: selected_ticks)
|
62
|
+
end
|
63
|
+
|
64
|
+
def_delegator :@ticks, :[]
|
65
|
+
def_delegator :@ticks, :size
|
66
|
+
def_delegator :@ticks, :each
|
67
|
+
def_delegator :@ticks, :select
|
68
|
+
def_delegator :@ticks, :select!
|
69
|
+
def_delegator :@ticks, :reject
|
70
|
+
def_delegator :@ticks, :reject!
|
71
|
+
def_delegator :@ticks, :first
|
72
|
+
def_delegator :@ticks, :last
|
73
|
+
|
74
|
+
def highest
|
75
|
+
ticks.max_by(&:high_price)
|
76
|
+
end
|
77
|
+
|
78
|
+
def lowest
|
79
|
+
ticks.min_by(&:low_price)
|
80
|
+
end
|
81
|
+
|
82
|
+
def ==(other)
|
83
|
+
[symbol, interval, ticks] == [other.symbol, other.interval, other.ticks]
|
84
|
+
end
|
85
|
+
|
86
|
+
def dup
|
87
|
+
self.class.from_ticks(symbol: symbol, interval: interval, ticks: ticks)
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect
|
91
|
+
"#<#{self.class.name} symbol=#{symbol} interval=#{interval} ticks=#{ticks.size}>"
|
92
|
+
end
|
93
|
+
|
94
|
+
def <<(tick)
|
95
|
+
@ticks << tick.assign_series(self)
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_h
|
99
|
+
{ "symbol" => symbol,
|
100
|
+
"interval" => interval,
|
101
|
+
"ticks" => ticks.map(&:to_h) }
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_json(*args)
|
105
|
+
Oj.dump(to_h, *args)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/quant/ticks/ohlc.rb
CHANGED
@@ -1,125 +1,107 @@
|
|
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
|
-
|
35
|
-
|
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
|
+
# An +OHLC+ is a bar or candle for a point in time that has an open, high, low, and close price.
|
8
|
+
# It is the most common form of a +Tick+ and is usually used to representa time period such as a
|
9
|
+
# minute, hour, day, week, or month. The +OHLC+ is used to represent the price action of an asset
|
10
|
+
# The interval of the +OHLC+ is the time period that the +OHLC+ represents, such has hourly, daily, weekly, etc.
|
11
|
+
class OHLC < Tick
|
12
|
+
include TimeMethods
|
13
|
+
|
14
|
+
attr_reader :interval, :series
|
15
|
+
attr_reader :close_timestamp, :open_timestamp
|
16
|
+
attr_reader :open_price, :high_price, :low_price, :close_price
|
17
|
+
attr_reader :base_volume, :target_volume, :trades
|
18
|
+
attr_reader :green, :doji
|
19
|
+
|
20
|
+
def initialize(
|
21
|
+
open_timestamp:,
|
22
|
+
close_timestamp:,
|
23
|
+
|
24
|
+
open_price:,
|
25
|
+
high_price:,
|
26
|
+
low_price:,
|
27
|
+
close_price:,
|
28
|
+
|
29
|
+
interval: nil,
|
30
|
+
|
31
|
+
volume: nil,
|
32
|
+
base_volume: nil,
|
33
|
+
target_volume: nil,
|
34
|
+
|
35
|
+
trades: nil,
|
36
|
+
green: nil,
|
37
|
+
doji: nil
|
38
|
+
)
|
63
39
|
@open_timestamp = extract_time(open_timestamp)
|
40
|
+
@close_timestamp = extract_time(close_timestamp)
|
41
|
+
|
64
42
|
@open_price = open_price.to_f
|
65
43
|
@high_price = high_price.to_f
|
66
44
|
@low_price = low_price.to_f
|
45
|
+
@close_price = close_price.to_f
|
46
|
+
|
47
|
+
@interval = Interval[interval]
|
67
48
|
|
68
|
-
@base_volume = base_volume.to_i
|
69
|
-
@target_volume = target_volume.to_i
|
49
|
+
@base_volume = (volume || base_volume).to_i
|
50
|
+
@target_volume = (target_volume || @base_volume).to_i
|
51
|
+
@trades = trades.to_i
|
70
52
|
|
71
53
|
@green = green.nil? ? compute_green : green
|
72
54
|
@doji = doji.nil? ? compute_doji : doji
|
55
|
+
super()
|
73
56
|
end
|
74
57
|
|
58
|
+
alias price close_price
|
59
|
+
alias timestamp close_timestamp
|
60
|
+
alias volume base_volume
|
61
|
+
|
75
62
|
def hl2; ((high_price + low_price) / 2.0) end
|
76
63
|
def oc2; ((open_price + close_price) / 2.0) end
|
77
64
|
def hlc3; ((high_price + low_price + close_price) / 3.0) end
|
78
65
|
def ohlc4; ((open_price + high_price + low_price + close_price) / 4.0) end
|
79
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.
|
80
70
|
def corresponding?(other)
|
81
71
|
[open_timestamp, close_timestamp] == [other.open_timestamp, other.close_timestamp]
|
82
72
|
end
|
83
73
|
|
84
|
-
#
|
85
|
-
def
|
86
|
-
|
74
|
+
# Two OHLC ticks are equal if their interval, close_timestamp, and close_price are equal.
|
75
|
+
def ==(other)
|
76
|
+
[interval, close_timestamp, close_price] == [other.interval, other.close_timestamp, other.close_price]
|
87
77
|
end
|
88
78
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
"c" => close_price,
|
98
|
-
|
99
|
-
"bv" => base_volume,
|
100
|
-
"tv" => target_volume,
|
101
|
-
|
102
|
-
"t" => trades,
|
103
|
-
"g" => green,
|
104
|
-
"j" => doji }
|
79
|
+
# Returns the percent daily price change from open_price to close_price, ranging from 0.0 to 1.0.
|
80
|
+
# A positive value means the price increased, and a negative value means the price decreased.
|
81
|
+
# A value of 0.0 means no change.
|
82
|
+
# @return [Float]
|
83
|
+
def daily_price_change
|
84
|
+
((open_price / close_price) - 1.0)
|
85
|
+
rescue ZeroDivisionError
|
86
|
+
0.0
|
105
87
|
end
|
106
88
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
"#{ots}: o: #{as_price(open_price)}, h: #{as_price(high_price)}, l: #{as_price(low_price)}, c: #{as_price(close_price)} :#{cts}"
|
89
|
+
# Calculates the absolute change from the open_price to the close_price, divided by the average of the
|
90
|
+
# open_price and close_price. This method will give a value between 0 and 2, where 0 means no change,
|
91
|
+
# 1 means the price doubled, and 2 means the price went to zero.
|
92
|
+
# This method is useful for comparing the volatility of different assets.
|
93
|
+
# @return [Float]
|
94
|
+
def daily_price_change_ratio
|
95
|
+
@price_change ||= ((open_price - close_price) / oc2).abs
|
115
96
|
end
|
116
97
|
|
98
|
+
# Set the #green? property to true when the close_price is greater than or equal to the open_price.
|
117
99
|
def compute_green
|
118
100
|
close_price >= open_price
|
119
101
|
end
|
120
102
|
|
121
103
|
def green?
|
122
|
-
|
104
|
+
@green
|
123
105
|
end
|
124
106
|
|
125
107
|
def red?
|
@@ -130,10 +112,10 @@ module Quant
|
|
130
112
|
@doji
|
131
113
|
end
|
132
114
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
115
|
+
# Computes a doji candlestick pattern. A doji is a candlestick pattern that occurs when the open and close
|
116
|
+
# are the same or very close to the same. The high and low are also very close to the same. The doji pattern
|
117
|
+
# is a sign of indecision in the market. It is a sign that the market is not sure which way to go.
|
118
|
+
# @return [Boolean]
|
137
119
|
def compute_doji
|
138
120
|
body_bottom, body_top = [open_price, close_price].sort
|
139
121
|
|
@@ -147,6 +129,10 @@ module Quant
|
|
147
129
|
|
148
130
|
body_ratio < 0.025 && head_ratio > 1.0 && tail_ratio > 1.0
|
149
131
|
end
|
132
|
+
|
133
|
+
def inspect
|
134
|
+
"#<#{self.class.name} #{interval} ct=#{close_timestamp.iso8601} o=#{open_price} h=#{high_price} l=#{low_price} c=#{close_price} v=#{volume}>"
|
135
|
+
end
|
150
136
|
end
|
151
137
|
end
|
152
138
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quant
|
4
|
+
module Ticks
|
5
|
+
module Serializers
|
6
|
+
module OHLC
|
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 key properties for the tick.
|
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,79 @@
|
|
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
|
+
# Two ticks are equal if they have the same close price and close timestamp.
|
64
|
+
def ==(other)
|
65
|
+
[close_price, close_timestamp] == [other.close_price, other.close_timestamp]
|
17
66
|
end
|
18
67
|
|
68
|
+
# The corresponding? method helps determine that the other tick's timestamp is the same as this tick's timestamp,
|
69
|
+
# which is useful when aligning ticks between two separate series where one starts or ends at a different time,
|
70
|
+
# or when there may be gaps in the data between the two series.
|
19
71
|
def corresponding?(other)
|
20
72
|
close_timestamp == other.close_timestamp
|
21
73
|
end
|
22
74
|
|
23
|
-
def
|
24
|
-
{
|
25
|
-
"c" => close_price,
|
26
|
-
"iv" => interval.to_s,
|
27
|
-
"bv" => base_volume,
|
28
|
-
"tv" => target_volume,
|
29
|
-
"t" => trades }
|
75
|
+
def inspect
|
76
|
+
"#<#{self.class.name} #{interval} ct=#{close_timestamp} c=#{close_price.to_f} v=#{volume}>"
|
30
77
|
end
|
31
78
|
end
|
32
79
|
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
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.3
|
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-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -56,12 +56,15 @@ files:
|
|
56
56
|
- lib/quant/mixins/trig.rb
|
57
57
|
- lib/quant/mixins/weighted_average.rb
|
58
58
|
- lib/quant/refinements/array.rb
|
59
|
-
- lib/quant/
|
60
|
-
- lib/quant/
|
59
|
+
- lib/quant/security.rb
|
60
|
+
- lib/quant/security_class.rb
|
61
|
+
- lib/quant/series.rb
|
61
62
|
- lib/quant/ticks/ohlc.rb
|
62
|
-
- lib/quant/ticks/serializers/
|
63
|
+
- lib/quant/ticks/serializers/ohlc.rb
|
64
|
+
- lib/quant/ticks/serializers/spot.rb
|
65
|
+
- lib/quant/ticks/serializers/tick.rb
|
63
66
|
- lib/quant/ticks/spot.rb
|
64
|
-
- lib/quant/ticks/
|
67
|
+
- lib/quant/ticks/tick.rb
|
65
68
|
- lib/quant/time_methods.rb
|
66
69
|
- lib/quant/time_period.rb
|
67
70
|
- lib/quant/version.rb
|
@@ -89,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
92
|
- !ruby/object:Gem::Version
|
90
93
|
version: '0'
|
91
94
|
requirements: []
|
92
|
-
rubygems_version: 3.5.
|
95
|
+
rubygems_version: 3.5.6
|
93
96
|
signing_key:
|
94
97
|
specification_version: 4
|
95
98
|
summary: Quantitative and statistical tools written for Ruby 3.x for trading and finance.
|
data/lib/quant/ticks/core.rb
DELETED
@@ -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
|