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.
@@ -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
- def shear_to_deg(base:, deg:)
14
- shear_to_rad(base: base, rad: deg2rad(deg))
15
- end
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
- def shear_to_rad(base: , rad:)
18
- tan = Math.tan(rad)
19
- base.map { |bar|
20
- # separating lines for easier debugging
21
- bar[:yy] =
22
- bar[:y] +
23
- (bar[:dx].nil? ? bar[:x] : bar[:dx]) * tan
24
- bar
25
- }
26
- end
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
- def member_to_human(member,side: ,format:)
29
- high = side == :high
30
- "#{member[:datetime].strftime("%a, %Y-%m-%d %H:%M")
31
- } x: #{format '%-4d', member[:x]
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, member[high ? :high : :low]
35
- } i: #{(format '%4d', member[:i]) unless member[:i].nil?
36
- } #{member[:miss].nil? ? '' : "miss: #{member[:miss]}" }"
37
- end
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
- def puts_swaps(swaps, format: )
40
- swaps = [ swaps ] unless swaps.is_a? Array
41
- swaps.each do |swap|
42
- puts "side: #{swap[:side] }\tlen: #{swap[:length]} \trating: #{swap[:rating]}".colorize(swap[:color] || :white )
43
- puts "diff: #{swap[:ticks]}\tdif: #{swap[:diff].round(7)}\tdepth: #{swap[:depth]}".colorize(swap[:color] || :white )
44
- puts "tpi: #{swap[:tpi] }\tppi: #{swap[:ppi]}".colorize(swap[:color] || :white )
45
- swap[:members].each {|x| puts member_to_human(x, side: swap[:side], format: format) }
46
- end
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
- def get_jsonl_name(interval:, swap_type:, contract:, sym: nil)
50
- raise "Interval #{interval } is not supported, please choose from #{INTERVALS}" unless INTERVALS.include? interval
51
- raise "Swaptype #{swap_type} is not supported, please choose from #{SWAPTYPES}" unless SWAPTYPES.include? swap_type
52
- sym ||= Cotcube::Helpers.get_id_set(contract: contract)
53
- root = '/var/cotcube/level'
54
- dir = "#{root}/#{sym[:id]}"
55
- symlink = "#{root}/#{sym[:symbol]}"
56
- `mkdir -p #{dir}` unless File.exist?(dir)
57
- `ln -s #{dir} #{symlink}` unless File.exist?(symlink)
58
- file = "#{dir}/#{contract}_#{interval.to_s}_#{swap_type.to_s}.jsonl"
59
- `touch #{file}`
60
- file
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
- def save_swaps(swaps, interval:, swap_type:, contract:, sym: nil)
64
- file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
65
- swaps.each do |swap|
66
- swap_json = swap.to_json
67
- digest = Digest::SHA256.hexdigest swap_json
68
- res = `cat #{file} | grep #{digest}`.strip
69
- unless res.empty?
70
- puts "Cannot save swap, it is already in #{file}:"
71
- p swap
72
- else
73
- swap[:digest] = digest
74
- File.open(file, 'a+'){|f| f.write(swap.to_json + "\n") }
75
- end
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
- def load_swaps(interval:, swap_type:, contract:, sym: nil)
80
- file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
81
- jsonl = File.read(file)
82
- jsonl.
83
- each_line.
84
- map do |x|
85
- JSON.parse(x).
86
- deep_transform_keys(&:to_sym).
87
- tap do |sw|
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[:side] = sw[:side].to_sym
90
- unless sw[:empty]
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
- end
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