cotcube-level 0.2.0 → 0.3.4
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/CHANGELOG.md +49 -0
- data/README.md +14 -6
- data/VERSION +1 -1
- data/cotcube-level.gemspec +1 -1
- data/lib/cotcube-level/detect_slope.rb +79 -98
- data/lib/cotcube-level/{stencil.rb → eod_stencil.rb} +26 -16
- data/lib/cotcube-level/helpers.rb +198 -74
- data/lib/cotcube-level/intraday_stencil.rb +213 -0
- data/lib/cotcube-level/tritangulate.rb +332 -0
- data/lib/cotcube-level.rb +17 -14
- metadata +6 -6
- data/.irbrc.rb +0 -12
- data/lib/cotcube-level/triangulate.rb +0 -238
@@ -2,99 +2,223 @@
|
|
2
2
|
|
3
3
|
module Cotcube
|
4
4
|
module Level
|
5
|
-
def rad2deg(deg)
|
6
|
-
deg * 180 / Math::PI
|
7
|
-
end
|
8
|
-
|
9
|
-
def deg2rad(rad)
|
10
|
-
rad * Math::PI / 180
|
11
|
-
end
|
12
5
|
|
13
|
-
|
14
|
-
|
15
|
-
|
6
|
+
# 3 simple, self-explaining helpers
|
7
|
+
def rad2deg(deg); deg * 180 / Math::PI; end
|
8
|
+
def deg2rad(rad); rad * Math::PI / 180; end
|
9
|
+
def shear_to_deg(base:, deg:); shear_to_rad(base: base, rad: deg2rad(deg)); end
|
16
10
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
11
|
+
# the actual shearing takes place here. please not that shifting of :x takes place
|
12
|
+
# by setting the new :x as :dx. so if :dx is found, it is used, otherwise :x
|
13
|
+
def shear_to_rad(base: , rad:)
|
14
|
+
tan = Math.tan(rad)
|
15
|
+
base.map { |member|
|
16
|
+
# separating lines for easier debugging
|
17
|
+
member[:yy] =
|
18
|
+
member[:y] +
|
19
|
+
(member[:dx].nil? ? member[:x] : member[:dx]) * tan
|
20
|
+
member
|
21
|
+
}
|
22
|
+
end
|
27
23
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
# human readable output
|
25
|
+
# please note the format must be given, that should be taken from :sym
|
26
|
+
def member_to_human(member,side: ,format:, daily: false)
|
27
|
+
high = (side == :upper)
|
28
|
+
"#{ member[:datetime].strftime("%a, %Y-%m-%d#{daily ? "" :" %I:%M%p"}")
|
29
|
+
} x: #{format '%-4d', member[:x]
|
32
30
|
} dx: #{format '%-8.3f', (member[:dx].nil? ? member[:x] : member[:dx].round(3))
|
33
31
|
} #{high ? "high" : "low"
|
34
|
-
}: #{format format,
|
35
|
-
} i: #{(format '%4d',
|
36
|
-
|
37
|
-
|
32
|
+
}: #{format format, member[high ? :high : :low]
|
33
|
+
} i: #{(format '%4d', member[:i]) unless member[:i].nil?
|
34
|
+
} d: #{format '%6.2f', member[:dev] unless member[:dev].nil?
|
35
|
+
} #{member[:near].nil? ? '' : "near: #{member[:near]}"
|
36
|
+
}"
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
39
|
+
# human readable output
|
40
|
+
# format: e.g. sym[:format]
|
41
|
+
# short: print one line / less verbose
|
42
|
+
# notice: add this to output as well
|
43
|
+
def puts_swap(swap, format: , short: true, notice: nil, hash: 3)
|
44
|
+
return if swap[:empty]
|
45
|
+
daily = %i[ continuous daily ].include?(swap[:interval].to_sym) rescue false
|
46
|
+
datetime_format = daily ? '%Y-%m-%d' : '%Y-%m-%d %I:%M %p'
|
47
|
+
high = swap[:side] == :high
|
48
|
+
ohlc = high ? :high : :low
|
49
|
+
if notice.nil? and swap[:exceeded]
|
50
|
+
notice = "exceeded #{swap[:exceeded].strftime(datetime_format)}"
|
47
51
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
52
|
+
if short
|
53
|
+
puts (hash ? "#{swap[:digest][...hash]} ".colorize(:light_white) : '') +
|
54
|
+
"S: #{swap[:side]
|
55
|
+
} L: #{ format '%4d', swap[:length]
|
56
|
+
} R: #{ format '%4d', swap[:rating]
|
57
|
+
} D: #{ format '%4d', swap[:depth]
|
58
|
+
} P: #{ format '%10s', (format '%5.2f', swap[:ppi])
|
59
|
+
} F: #{ format format, swap[:members].last[ ohlc ]
|
60
|
+
} S: #{ swap[:members].first[:datetime].strftime(datetime_format)
|
61
|
+
} - #{ swap[:members].last[:datetime].strftime(datetime_format)
|
62
|
+
}#{" NOTE: #{notice}" unless notice.nil?}".colorize(swap[:color] || :white )
|
63
|
+
else
|
64
|
+
puts "side: #{swap[:side] }\tlen: #{swap[:length]} \trating: #{swap[:rating]}".colorize(swap[:color] || :white )
|
65
|
+
puts "diff: #{swap[:ticks]}\tdif: #{swap[:diff].round(7)}\tdepth: #{swap[:depth]}".colorize(swap[:color] || :white )
|
66
|
+
puts "tpi: #{swap[:tpi] }\tppi: #{swap[:ppi]}".colorize(swap[:color] || :white )
|
67
|
+
puts "NOTE: #{notice}".colorize(:light_white) unless notice.nil?
|
68
|
+
swap[:members].each {|x| puts member_to_human(x, side: swap[:side], format: format, daily: daily) }
|
61
69
|
end
|
70
|
+
end
|
62
71
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
72
|
+
# create a standardized name for the cache files
|
73
|
+
# and, on-the-fly, create these files plus their directory
|
74
|
+
def get_jsonl_name(interval:, swap_type:, contract:, sym: nil)
|
75
|
+
raise "Interval #{interval } is not supported, please choose from #{INTERVALS}" unless INTERVALS.include?(interval) || interval.is_a?(Integer)
|
76
|
+
raise "Swaptype #{swap_type} is not supported, please choose from #{SWAPTYPES}" unless SWAPTYPES.include? swap_type
|
77
|
+
sym ||= Cotcube::Helpers.get_id_set(contract: contract)
|
78
|
+
root = '/var/cotcube/level'
|
79
|
+
dir = "#{root}/#{sym[:id]}"
|
80
|
+
symlink = "#{root}/#{sym[:symbol]}"
|
81
|
+
`mkdir -p #{dir}` unless File.exist?(dir)
|
82
|
+
`ln -s #{dir} #{symlink}` unless File.exist?(symlink)
|
83
|
+
file = "#{dir}/#{contract}_#{interval.to_s}_#{swap_type.to_s}.jsonl"
|
84
|
+
`touch #{file}`
|
85
|
+
file
|
86
|
+
end
|
87
|
+
|
88
|
+
# the name says it all.
|
89
|
+
# just note the addition of a digest, that serves to check whether same swap has been yet saved
|
90
|
+
# to the cache.
|
91
|
+
#
|
92
|
+
# there are actually 3 types of information, that are saved here:
|
93
|
+
# 1. a swap
|
94
|
+
# 2. an 'empty' information, referring to an interval that has been processed but no swaps were found
|
95
|
+
# 3. an 'exceeded' information, referring to another swap, that has been exceeded
|
96
|
+
#
|
97
|
+
def save_swaps(swaps, interval:, swap_type:, contract:, sym: nil, quiet: false)
|
98
|
+
file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
|
99
|
+
swaps = [ swaps ] unless swaps.is_a? Array
|
100
|
+
swaps.each do |swap|
|
101
|
+
raise "Illegal swap info: Must contain keys :datetime, :side... #{swap}" unless (%i[ datetime side ] - swap.keys).empty?
|
102
|
+
%i[ interval swap_type ].map {|key| swap.delete(key) }
|
103
|
+
sorted_keys = [ :datetime, :side ] + ( swap.keys - [ :datetime, :side ])
|
104
|
+
swap_json = swap.slice(*sorted_keys).to_json
|
105
|
+
digest = Digest::SHA256.hexdigest swap_json
|
106
|
+
res = `cat #{file} | grep '"digest":"#{digest}"'`.strip
|
107
|
+
unless res.empty?
|
108
|
+
puts "Cannot save swap, it is already in #{file}:".light_red unless quiet
|
109
|
+
p swap unless quiet
|
110
|
+
else
|
111
|
+
swap[:digest] = digest
|
112
|
+
sorted_keys += %i[digest]
|
113
|
+
File.open(file, 'a+'){|f| f.write(swap.slice(*sorted_keys).to_json + "\n") }
|
76
114
|
end
|
77
115
|
end
|
116
|
+
end
|
78
117
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
118
|
+
# loading of swaps is also straight forward
|
119
|
+
# it takes few more efforts to normalize the values to their expected format
|
120
|
+
def load_swaps(interval:, swap_type:, contract:, sym: nil, datetime: nil, recent: false, digest: nil, quiet: false, exceed: false)
|
121
|
+
file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
|
122
|
+
jsonl = File.read(file)
|
123
|
+
data = jsonl.
|
124
|
+
each_line.
|
125
|
+
map do |x|
|
126
|
+
JSON.parse(x).
|
127
|
+
deep_transform_keys(&:to_sym).
|
128
|
+
tap do |sw|
|
88
129
|
sw[:datetime] = DateTime.parse(sw[:datetime]) rescue nil
|
89
|
-
sw[:
|
90
|
-
|
130
|
+
(sw[:exceeded] = DateTime.parse(sw[:exceeded]) rescue nil) if sw[:exceeded]
|
131
|
+
sw[:interval] = interval
|
132
|
+
sw[:swap_type] = swap_type
|
133
|
+
sw[:contract] = contract
|
134
|
+
%i[ side ].each {|key| sw[key] = sw[key].to_sym rescue false }
|
135
|
+
unless sw[:empty] or sw[:exceeded]
|
91
136
|
sw[:color] = sw[:color].to_sym
|
92
137
|
sw[:members].map{|mem| mem[:datetime] = DateTime.parse(mem[:datetime]) }
|
93
138
|
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
# assign exceedance data to actual swaps
|
142
|
+
data.select{|swap| swap[:exceeded] }.each do |exc|
|
143
|
+
swap = data.find{|ref| ref[:digest] == exc[:ref]}
|
144
|
+
raise RuntimeError, "Inconsistent history for '#{exc}'. Origin not found." if swap.nil?
|
145
|
+
swap[:exceeded] = exc[:exceeded]
|
146
|
+
end
|
147
|
+
# do not return bare exceedance information
|
148
|
+
data.reject!{|swap| swap[:exceeded] and swap[:members].nil? }
|
149
|
+
# do not return swaps that are found 'later'
|
150
|
+
data.reject!{|swap| swap[:datetime] > datetime } unless datetime.nil?
|
151
|
+
# do not return exceeded swaps, that are exceeded in the past
|
152
|
+
recent = 7.days if recent.is_a? TrueClass
|
153
|
+
recent += 5.hours if recent
|
154
|
+
data.reject!{|swap| swap[:exceeded] and swap[:exceeded] < datetime - (recent ? recent : 0) } unless datetime.nil?
|
155
|
+
# remove exceedance information that is found 'later'
|
156
|
+
data.map{|swap| swap.delete(:exceeded) if swap[:exceeded] and swap[:exceeded] > datetime} unless datetime.nil?
|
157
|
+
unless digest.nil?
|
158
|
+
data.select! do |z|
|
159
|
+
(Cotcube::Helpers.sub(minimum: digest.length){ z[:digest] } === digest) and
|
160
|
+
not z[:empty]
|
161
|
+
end
|
162
|
+
case data.size
|
163
|
+
when 0
|
164
|
+
puts "No swaps found for digest '#{digest}'." unless quiet
|
165
|
+
when 1
|
166
|
+
sym ||= Cotcube::Helpers.get_id_set(contract: contract)
|
167
|
+
if not quiet or exceed
|
168
|
+
puts "Found 1 digest: "
|
169
|
+
data.each {|d| puts_swap( d, format: sym[:format], short: true, hash: digest.size + 2) }
|
170
|
+
if exceed
|
171
|
+
exceed = DateTime.now if exceed.is_a? TrueClass
|
172
|
+
mark_exceeded(swap: data.first, datetime: exceed)
|
173
|
+
puts "Swap marked exceeded."
|
174
|
+
end
|
175
|
+
end
|
176
|
+
else
|
177
|
+
sym ||= Cotcube::Helpers.get_id_set(contract: contract)
|
178
|
+
unless quiet
|
179
|
+
puts "Too many digests found for digest '#{digest}', please consider sending more figures: "
|
180
|
+
data.each {|d| puts_swap( d, format: sym[:format], short: true, hash: digest.size + 3)}
|
94
181
|
end
|
95
182
|
end
|
96
183
|
end
|
97
|
-
|
184
|
+
data
|
185
|
+
end
|
186
|
+
|
187
|
+
# :swaps is an array of swaps
|
188
|
+
# :zero is the current interval (ohlc)
|
189
|
+
# :stencil is the according current stencil (eod or intraday)
|
190
|
+
def check_exceedance(swaps:, zero:, stencil:, contract:, sym:, debug: false)
|
191
|
+
swaps.map do |swap|
|
192
|
+
# swaps cannot exceed the day they are found (or if they are found in the future)
|
193
|
+
next if swap[:datetime] >= zero[:datetime] or swap[:empty]
|
194
|
+
update = stencil.use with: swap, sym: sym, zero: zero
|
195
|
+
if update[:exceeded]
|
196
|
+
to_save = {
|
197
|
+
datetime: zero[:datetime],
|
198
|
+
ref: swap[:digest],
|
199
|
+
side: swap[:side],
|
200
|
+
exceeded: update[:exceeded]
|
201
|
+
}
|
202
|
+
save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], contract: contract, sym: sym, quiet: (not debug)
|
203
|
+
swap[:exceeded] = update[:exceeded]
|
204
|
+
end
|
205
|
+
%i[ current_change current_value current_diff current_dist ].map{|key| swap[key] = update[key] }
|
206
|
+
swap
|
207
|
+
end.compact
|
208
|
+
end
|
98
209
|
|
210
|
+
def mark_exceeded(swap:, datetime:, debug: false)
|
211
|
+
to_save = {
|
212
|
+
datetime: datetime,
|
213
|
+
ref: swap[:digest],
|
214
|
+
side: swap[:side],
|
215
|
+
exceeded: datetime
|
216
|
+
}
|
217
|
+
save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], sym: Cotcube::Helpers.get_id_set(contract: swap[:contract]), contract: swap[:contract], quiet: (not debug)
|
218
|
+
swap[:exceeded] = datetime
|
219
|
+
swap
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
99
223
|
end
|
100
224
|
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cotcube
|
4
|
+
module Level
|
5
|
+
|
6
|
+
class Intraday_Stencil
|
7
|
+
|
8
|
+
|
9
|
+
# Class method that loads the (latest) shiftset for given asset
|
10
|
+
# These raw stencils are located in /var/cotcube/level/stencils/shiftsets.csv
|
11
|
+
#
|
12
|
+
|
13
|
+
def self.shiftset(asset:, sym: nil)
|
14
|
+
shiftset_file = '/var/cotcube/level/stencils/shiftsets.csv'
|
15
|
+
headers = %i[nr tz sod pre mpre rth post mpost rth5 mpost5 symbols]
|
16
|
+
shiftsets = CSV.read(shiftset_file, headers: headers).
|
17
|
+
map{|x| x.to_h}
|
18
|
+
current_set = shiftsets.find{|s| s[:symbols] =~ /#{asset}/ }
|
19
|
+
return current_set.tap{|s| headers.map{|h| s[h] = nil if s[h] == '---------' }; s[:rth5] ||= s[:rth]; s[:mpost5] ||= s[:mpost] } unless current_set.nil?
|
20
|
+
sym ||= Cotcube::Helpers.get_id_set(symbol: asset)
|
21
|
+
current_set = shiftsets.find{|s| s[:symbols] =~ /#{sym[:type]}/ }
|
22
|
+
return current_set.tap{|s| headers.map{|h| s[h] = nil if s[h] == '---------' }; s[:rth5] ||= s[:rth]; s[:mpost5] ||= s[:mpost] } unless current_set.nil?
|
23
|
+
raise "Cannot get shiftset for #{sym[:type]}: #{asset}, please prepare #{shiftset_file} before!"
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :base, :shiftset, :timezone, :datetime, :zero, :index
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
def initialize(
|
31
|
+
asset:,
|
32
|
+
interval: 30.minutes,
|
33
|
+
swap_type: :full,
|
34
|
+
datetime: nil,
|
35
|
+
debug: false,
|
36
|
+
weeks: 6,
|
37
|
+
future: 2,
|
38
|
+
version: nil, # when referring to a specicic version of the stencil
|
39
|
+
stencil: nil, # instead of preparing, use this one if set
|
40
|
+
warnings: true # be more quiet
|
41
|
+
)
|
42
|
+
@shiftset = Intraday_Stencil.shiftset(asset: asset)
|
43
|
+
@timezone = Cotcube::Level::TIMEZONES[@shiftset[:tz]]
|
44
|
+
@debug = debug
|
45
|
+
@interval = interval
|
46
|
+
@swap_type = swap_type
|
47
|
+
@warnings = warnings
|
48
|
+
datetime ||= DateTime.now
|
49
|
+
datetime = @timezone.at(datetime.to_i) unless datetime.is_a? ActiveSupport::TimeWithZone
|
50
|
+
@datetime = datetime.beginning_of_day
|
51
|
+
@datetime += interval while @datetime <= datetime - interval
|
52
|
+
@datetime -= interval
|
53
|
+
|
54
|
+
const = "RAW_INTRA_STENCIL_#{@shiftset[:nr]}_#{interval.in_minutes.to_i}".to_sym
|
55
|
+
if Object.const_defined? const
|
56
|
+
@base = (Object.const_get const).map{|z| z.dup}
|
57
|
+
else
|
58
|
+
start_time = lambda {|x| @shiftset[x].split('-').first rescue '' }
|
59
|
+
start_hours = lambda {|x| @shiftset[x].split('-').first[ 0.. 1].to_i.send(:hours) rescue 0 }
|
60
|
+
start_minutes = lambda {|x| @shiftset[x].split('-').first[-2..-1].to_i.send(:minutes) rescue 0 }
|
61
|
+
end_time = lambda {|x| @shiftset[x].split('-').last rescue '' }
|
62
|
+
end_hours = lambda {|x| @shiftset[x].split('-').last [ 0.. 1].to_i.send(:hours) rescue 0 }
|
63
|
+
end_minutes = lambda {|x| @shiftset[x].split('-').last [-2..-1].to_i.send(:minutes) rescue 0 }
|
64
|
+
|
65
|
+
runner = (@datetime -
|
66
|
+
weeks * 7.days).beginning_of_week(:sunday)
|
67
|
+
tm_runner = lambda { runner.strftime('%H%M') }
|
68
|
+
@base = []
|
69
|
+
(weeks+future).times do
|
70
|
+
while tm_runner.call < GLOBAL_SOW[@shiftset[:tz]].split('-').last
|
71
|
+
# if daylight is switched, this phase will be shorter or longer
|
72
|
+
@base << { datetime: runner, type: :sow }
|
73
|
+
runner += interval
|
74
|
+
end
|
75
|
+
end_of_week = runner + 6.days + 7.hours
|
76
|
+
|
77
|
+
5.times do |i|
|
78
|
+
# TODO: mark holidays as such
|
79
|
+
[:sod, :pre, :mpre, (i<4 ? :rth : :rth5), :post, (i<4 ? :mpost : :mpost5)].each do |phase|
|
80
|
+
yet_rth = false
|
81
|
+
unless start_time.call(phase).empty?
|
82
|
+
eophase = end_time.call(phase)
|
83
|
+
sophase = start_time.call(phase)
|
84
|
+
phase = :rth if phase == :rth5
|
85
|
+
phase = :mpost if phase == :mpost5
|
86
|
+
if %i[ pre rth ].include? phase and tm_runner.call > sophase
|
87
|
+
# fix previous interval
|
88
|
+
@base.last[:type] = phase
|
89
|
+
if phase == :rth and not yet_rth
|
90
|
+
@base.last[:block] = true
|
91
|
+
yet_rth = true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
while ((sophase > eophase) ? (tm_runner.call >= sophase or tm_runner.call < eophase) : (tm_runner.call < eophase))
|
95
|
+
current = { datetime: runner, type: phase }
|
96
|
+
if phase == :rth and not yet_rth
|
97
|
+
current[:block] = true
|
98
|
+
yet_rth = true
|
99
|
+
end
|
100
|
+
@base << current
|
101
|
+
runner += interval
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
while tm_runner.call < GLOBAL_EOD[@shiftset[:tz]].split('-').last
|
106
|
+
@base << { datetime: runner, type: :eod }
|
107
|
+
runner += interval
|
108
|
+
end
|
109
|
+
end # 5.times
|
110
|
+
while runner < end_of_week
|
111
|
+
@base << { datetime: runner, type: :eow }
|
112
|
+
runner += interval
|
113
|
+
end
|
114
|
+
end
|
115
|
+
Object.const_set(const, @base.map{|z| z.dup})
|
116
|
+
end
|
117
|
+
|
118
|
+
case swap_type
|
119
|
+
when :full
|
120
|
+
@base.select!{|x| %i[ pre rth post ].include?(x[:type]) }
|
121
|
+
when :rth
|
122
|
+
@base.select!{|x| x[:type] == :rth }
|
123
|
+
# to.map{ |x| [:high, :low, :volume].map{|z| x[z] = nil} if x[:block] }
|
124
|
+
when :flow
|
125
|
+
@base.reject!{|x| %i[ meow postmm postmm5 ].include?(x[:type]) }
|
126
|
+
@base.
|
127
|
+
map{ |x|
|
128
|
+
[:high, :low, :volume].map{|z| x[z] = nil} unless x[:type] == :rth
|
129
|
+
# [:high, :low, :volume].map{|z| x[z] = nil} if x[:block]
|
130
|
+
}
|
131
|
+
when :run
|
132
|
+
@base.select!{|x| %i[ premarket rth postmarket ].include? x[:type]}
|
133
|
+
else
|
134
|
+
raise ArgumentError, "Unknown stencil/swap type '#{type}'"
|
135
|
+
end
|
136
|
+
@base.map!{|z| z.dup}
|
137
|
+
|
138
|
+
@index = @base.index{|x| x[:datetime] == @datetime }
|
139
|
+
@index -= 1 while %i[sow sod mpre mpost eod eow].include? @base[@index][:type]
|
140
|
+
@datetime = @base[@index][:datetime]
|
141
|
+
@zero = @base[@index]
|
142
|
+
counter = 0
|
143
|
+
while @base[@index - counter] and @index - counter >= 0
|
144
|
+
@base[@index - counter][:x] = counter
|
145
|
+
counter += 1
|
146
|
+
end
|
147
|
+
counter = 0
|
148
|
+
while @base[@index + counter] and @index + counter < @base.length
|
149
|
+
@base[@index + counter][:x] = -counter
|
150
|
+
counter += 1
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
=begin
|
155
|
+
def dup
|
156
|
+
Intraday_Stencil.new(
|
157
|
+
debug: @debug,
|
158
|
+
interval: @interval,
|
159
|
+
swap_type: @swap_type,
|
160
|
+
datetime: @datetime,
|
161
|
+
stencil: @base.map{|x| x.dup}
|
162
|
+
)
|
163
|
+
end
|
164
|
+
=end
|
165
|
+
|
166
|
+
def zero
|
167
|
+
@zero ||= @base.find{|b| b[:x].zero? }
|
168
|
+
end
|
169
|
+
|
170
|
+
def apply(to: )
|
171
|
+
offset = 0
|
172
|
+
@base.each_index do |i|
|
173
|
+
begin
|
174
|
+
offset += 1 while to[i+offset][:datetime] < @base[i][:datetime]
|
175
|
+
rescue
|
176
|
+
# appending
|
177
|
+
to << @base[i]
|
178
|
+
next
|
179
|
+
end
|
180
|
+
if to[i+offset][:datetime] > @base[i][:datetime]
|
181
|
+
# skipping
|
182
|
+
offset -= 1
|
183
|
+
next
|
184
|
+
end
|
185
|
+
# merging
|
186
|
+
to[i+offset][:x] = @base[i][:x]
|
187
|
+
to[i+offset][:type] = @base[i][:type]
|
188
|
+
end
|
189
|
+
# finally remove all bars that do not belong to the stencil (i.e. holidays)
|
190
|
+
to.reject!{|x| x[:x].nil? }
|
191
|
+
end
|
192
|
+
|
193
|
+
def use(with:, sym:, zero:, grace: -2)
|
194
|
+
# todo: validate with (check if vslid swap
|
195
|
+
# sym (check keys)
|
196
|
+
# zero (ohlc with x.zero?)
|
197
|
+
# side ( upper or lower)
|
198
|
+
swap = with.dup
|
199
|
+
high = swap[:side] == :upper
|
200
|
+
ohlc = high ? :high : :low
|
201
|
+
start = base.find{|x| swap[:datetime] == x[:datetime]}
|
202
|
+
swap[:current_change] = (swap[:tpi] * start[:x]).round(8)
|
203
|
+
swap[:current_value] = swap[:members].last[ ohlc ] + swap[:current_change] * sym[:ticksize]
|
204
|
+
swap[:current_diff] = (swap[:current_value] - zero[ohlc]) * (high ? 1 : -1 )
|
205
|
+
swap[:current_dist] = (swap[:current_diff] / sym[:ticksize]).to_i
|
206
|
+
swap[:exceeded] = zero[:datetime] if swap[:current_dist] < grace
|
207
|
+
swap
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|