cotcube-level 0.2.0 → 0.3.1
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 +17 -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} +10 -13
- data/lib/cotcube-level/helpers.rb +84 -75
- data/lib/cotcube-level/intraday_stencil.rb +179 -0
- data/lib/cotcube-level/tritangulate.rb +323 -0
- data/lib/cotcube-level.rb +15 -13
- metadata +6 -5
- data/lib/cotcube-level/triangulate.rb +0 -238
@@ -0,0 +1,179 @@
|
|
1
|
+
module Cotcube
|
2
|
+
module Level
|
3
|
+
# TODO: Missing documentation of shiftsets, swap_types and stencil_types
|
4
|
+
class Intraday_Stencil
|
5
|
+
|
6
|
+
GLOBAL_SOW = { 'CT' => '0000-1700' }
|
7
|
+
GLOBAL_EOW = { 'CT' => '1700-0000' }
|
8
|
+
GLOBAL_EOD = { 'CT' => '1600-1700' }
|
9
|
+
|
10
|
+
|
11
|
+
def self.shiftset(asset:, sym: nil)
|
12
|
+
shiftset_file = '/var/cotcube/level/stencils/shiftsets.csv'
|
13
|
+
headers = %i[nr tz sod pre mpre rth post mpost rth5 mpost5 symbols]
|
14
|
+
shiftsets = CSV.read(shiftset_file, headers: headers).
|
15
|
+
map{|x| x.to_h}
|
16
|
+
current_set = shiftsets.find{|s| s[:symbols] =~ /#{asset}/ }
|
17
|
+
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?
|
18
|
+
sym ||= Cotcube::Helpers.get_id_set(symbol: asset)
|
19
|
+
current_set = shiftsets.find{|s| s[:symbols] =~ /#{sym[:type]}/ }
|
20
|
+
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?
|
21
|
+
raise "Cannot get shiftset for #{sym[:type]}: #{asset}, please prepare #{shiftset_file} before!"
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :base, :shiftset, :timezone, :datetime, :zero, :index
|
25
|
+
|
26
|
+
# asset: the asset the stencil will be applied to--or :full, if default stencil is desired
|
27
|
+
# datetime: the datetime that will become 'zero'.
|
28
|
+
# it will be calculated to the beginning of the previous interval
|
29
|
+
# it must match the timezone of the asset
|
30
|
+
# interval: the interval as minutes
|
31
|
+
# weeks: the amount of weeks before the beginning of the current week
|
32
|
+
# future: the amount of weeks after the beginning of the current week
|
33
|
+
def initialize(asset:, sym: nil, datetime:, interval:, weeks:, future: 1, debug: false, type:, base: )
|
34
|
+
@shiftset = Intraday_Stencils.shiftset(asset: asset)
|
35
|
+
@timezone = TIMEZONES[@shiftset[:tz]]
|
36
|
+
@debug = debug
|
37
|
+
datetime = @timezone.at(datetime.to_i) unless datetime.is_a? ActiveSupport::TimeWithZone
|
38
|
+
# slight flaw, as datetime does not carry the actuall timezone information but just the abbr. timezone name (like CDT or CEST)
|
39
|
+
raise "Zone mismatch: Timezone of asset is #{@timezone.now.zone} but datetime given is #{dateime.zone}" unless @timezone.now.zone == datetime.zone
|
40
|
+
@datetime = datetime.beginning_of_day
|
41
|
+
@datetime += interval while @datetime <= datetime - interval
|
42
|
+
@datetime -= interval
|
43
|
+
const = "RAW_INTRA_STENCIL_#{@shiftset[:nr]}_#{interval.in_minutes.to_i}".to_sym
|
44
|
+
if Object.const_defined? const
|
45
|
+
@base = (Object.const_get const).map{|z| z.dup}
|
46
|
+
else
|
47
|
+
|
48
|
+
start_time = lambda {|x| @shiftset[x].split('-').first rescue '' }
|
49
|
+
start_hours = lambda {|x| @shiftset[x].split('-').first[ 0.. 1].to_i.send(:hours) rescue 0 }
|
50
|
+
start_minutes = lambda {|x| @shiftset[x].split('-').first[-2..-1].to_i.send(:minutes) rescue 0 }
|
51
|
+
end_time = lambda {|x| @shiftset[x].split('-').last rescue '' }
|
52
|
+
end_hours = lambda {|x| @shiftset[x].split('-').last [ 0.. 1].to_i.send(:hours) rescue 0 }
|
53
|
+
end_minutes = lambda {|x| @shiftset[x].split('-').last [-2..-1].to_i.send(:minutes) rescue 0 }
|
54
|
+
|
55
|
+
runner = (@datetime -
|
56
|
+
weeks * 7.days).beginning_of_week(:sunday)
|
57
|
+
tm_runner = lambda { runner.strftime('%H%M') }
|
58
|
+
@base = []
|
59
|
+
(weeks+future).times do
|
60
|
+
while tm_runner.call < GLOBAL_SOW[@shiftset[:tz]].split('-').last
|
61
|
+
# if daylight is switched, this phase will be shorter or longer
|
62
|
+
@base << { datetime: runner, type: :sow }
|
63
|
+
runner += interval
|
64
|
+
end
|
65
|
+
end_of_week = runner + 6.days + 7.hours
|
66
|
+
|
67
|
+
5.times do |i|
|
68
|
+
# TODO: mark holidays as such
|
69
|
+
[:sod, :pre, :mpre, (i<4 ? :rth : :rth5), :post, (i<4 ? :mpost : :mpost5)].each do |phase|
|
70
|
+
yet_rth = false
|
71
|
+
unless start_time.call(phase).empty?
|
72
|
+
eophase = end_time.call(phase)
|
73
|
+
sophase = start_time.call(phase)
|
74
|
+
phase = :rth if phase == :rth5
|
75
|
+
phase = :mpost if phase == :mpost5
|
76
|
+
if %i[ pre rth ].include? phase and tm_runner.call > sophase
|
77
|
+
# fix previous interval
|
78
|
+
@base.last[:type] = phase
|
79
|
+
if phase == :rth and not yet_rth
|
80
|
+
@base.last[:block] = true
|
81
|
+
yet_rth = true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
while ((sophase > eophase) ? (tm_runner.call >= sophase or tm_runner.call < eophase) : (tm_runner.call < eophase))
|
85
|
+
current = { datetime: runner, type: phase }
|
86
|
+
if phase == :rth and not yet_rth
|
87
|
+
current[:block] = true
|
88
|
+
yet_rth = true
|
89
|
+
end
|
90
|
+
@base << current
|
91
|
+
runner += interval
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
while tm_runner.call < GLOBAL_EOD[@shiftset[:tz]].split('-').last
|
96
|
+
@base << { datetime: runner, type: :eod }
|
97
|
+
runner += interval
|
98
|
+
end
|
99
|
+
end # 5.times
|
100
|
+
while runner < end_of_week
|
101
|
+
@base << { datetime: runner, type: :eow }
|
102
|
+
runner += interval
|
103
|
+
end
|
104
|
+
end
|
105
|
+
Object.const_set(const, @base.map{|z| z.dup})
|
106
|
+
end
|
107
|
+
self.apply to: base, type: type
|
108
|
+
@index = @base.index{|x| x[:datetime] == @datetime }
|
109
|
+
@index -= 1 while %i[sow sod mpre mpost eod eow].include? @base[@index][:type]
|
110
|
+
@datetime = @base[@index][:datetime]
|
111
|
+
@zero = @base[@index]
|
112
|
+
counter = 0
|
113
|
+
while @base[@index - counter] and @index - counter >= 0
|
114
|
+
@base[@index - counter][:x] = counter
|
115
|
+
counter += 1
|
116
|
+
end
|
117
|
+
counter = 0
|
118
|
+
while @base[@index + counter] and @index + counter < @base.length
|
119
|
+
@base[@index + counter][:x] = -counter
|
120
|
+
counter += 1
|
121
|
+
end
|
122
|
+
@base.select!{|z| z[:x] <= 0 or z[:high]}
|
123
|
+
end
|
124
|
+
|
125
|
+
def apply!(to:, type:)
|
126
|
+
apply(to: to, type: type, force: true)
|
127
|
+
end
|
128
|
+
|
129
|
+
# :force will apply values to each bar regardless of existing ones
|
130
|
+
def apply(to:, type:, force: false, debug: false)
|
131
|
+
offset = 0
|
132
|
+
to.each_index do |i|
|
133
|
+
begin
|
134
|
+
offset += 1 while @base[i+offset][:datetime] < to[i][:datetime]
|
135
|
+
puts "#{i}\t#{offset}\t#{@base[i+offset][:datetime]} < #{to[i][:datetime]}" if debug
|
136
|
+
rescue
|
137
|
+
# appending
|
138
|
+
puts "appending #{i}\t#{offset}\t#{@base[i+offset][:datetime]} < #{to[i][:datetime]}" if debug
|
139
|
+
@base << to[i]
|
140
|
+
next
|
141
|
+
end
|
142
|
+
if @base[i+offset][:datetime] > to[i][:datetime]
|
143
|
+
# skipping
|
144
|
+
puts "skipping #{i}\t#{offset}\t#{@base[i+offset][:datetime]} < #{to[i][:datetime]}" if debug
|
145
|
+
offset -= 1
|
146
|
+
next
|
147
|
+
end
|
148
|
+
# merging
|
149
|
+
j = i + offset
|
150
|
+
@base[j]=@base[j].merge(to[i]) if force or (@base[j][:high].nil? and @base[j][:low].nil?)
|
151
|
+
puts "MERGED:\t#{i}\t#{offset}\t#{@base[j]}" if debug
|
152
|
+
end
|
153
|
+
# finally remove all bars that do not belong to the stencil (i.e. holidays)
|
154
|
+
case type
|
155
|
+
when :full
|
156
|
+
@base.select!{|x| %i[ pre rth post ].include?(x[:type]) }
|
157
|
+
when :rth
|
158
|
+
@base.select!{|x| x[:type] == :rth }
|
159
|
+
# to.map{ |x| [:high, :low, :volume].map{|z| x[z] = nil} if x[:block] }
|
160
|
+
when :flow
|
161
|
+
@base.reject!{|x| %i[ meow postmm postmm5 ].include?(x[:type]) }
|
162
|
+
@base.
|
163
|
+
map{ |x|
|
164
|
+
[:high, :low, :volume].map{|z| x[z] = nil} unless x[:type] == :rth
|
165
|
+
# [:high, :low, :volume].map{|z| x[z] = nil} if x[:block]
|
166
|
+
}
|
167
|
+
when :run
|
168
|
+
@base.select!{|x| %i[ premarket rth postmarket ].include? x[:type]}
|
169
|
+
else
|
170
|
+
raise ArgumentError, "Unknown stencil/swap type '#{type}'"
|
171
|
+
end
|
172
|
+
@base.map!{|z| z.dup}
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
Intraday_Stencils = Intraday_Stencil
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,323 @@
|
|
1
|
+
module Cotcube
|
2
|
+
module Level
|
3
|
+
def tritangulate(
|
4
|
+
contract: nil, # contract actually isnt needed for tritangulation, but allows much more convenient output
|
5
|
+
# on some occasion here can also be given a :symbol, but this requires :sym to be set
|
6
|
+
sym: nil, # sym is the id set provided by Cotcube::Helper.get_id_set
|
7
|
+
side:, # :upper or :lower
|
8
|
+
base:, # the base of a readily injected stencil
|
9
|
+
range: (0..-1), # range is relative to base
|
10
|
+
max: 90, # the range which to scan for swaps goes from deg 0 to max
|
11
|
+
debug: false,
|
12
|
+
min_rating: 3, # swaps having a lower rating are discarded
|
13
|
+
min_length: 8, # shorter swaps are discared
|
14
|
+
save: true, # allow saving of results
|
15
|
+
cached: true, # allow loading of cached results
|
16
|
+
interval: , # interval (currently) is one of %i[ daily continuous halfs ]
|
17
|
+
swap_type: nil, # if not given, a warning is printed and swaps won't be saved or loaded
|
18
|
+
with_flaws: 0, # the maximum amount of consecutive bars that would actually break the current swap
|
19
|
+
# should be set to 0 for dailies and I suggest no more than 3 for intraday
|
20
|
+
deviation: 2) # the maximum shift of :x-values of found members
|
21
|
+
|
22
|
+
raise ArgumentError, "'0 < max < 90, but got '#{max}'" unless max.is_a? Numeric and 0 < max and max <= 90
|
23
|
+
raise ArgumentError, 'need :side either :upper or :lower for dots' unless [:upper, :lower].include? side
|
24
|
+
|
25
|
+
###########################################################################################################################
|
26
|
+
# init some helpers
|
27
|
+
#
|
28
|
+
high = side == :upper
|
29
|
+
# DELETE first = base.to_a.find{|x| not x[:high].nil? }
|
30
|
+
zero = base.select{|x| x[:x].zero? }
|
31
|
+
raise ArgumentError, "Inappropriate base, it should contain ONE :x.zero, but contains #{zero.size}." unless zero.size==1
|
32
|
+
zero = zero.first
|
33
|
+
|
34
|
+
contract ||= zero[:contract]
|
35
|
+
sym ||= Cotcube::Helpers.get_id_set(contract: contract)
|
36
|
+
|
37
|
+
if cached
|
38
|
+
if interval.nil? or swap_type.nil?
|
39
|
+
puts "Warning: Cannot use cache as both :interval and :swap_type must be given".light_yellow
|
40
|
+
else
|
41
|
+
cache = load_swaps(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
|
42
|
+
# if the current datetime has been yet processed but nothing has been found,
|
43
|
+
# an 'empty' value is saved.
|
44
|
+
# that means, if neither an array of swaps nor :empty is found, the datetime has not been processed yet
|
45
|
+
selected = cache.select{|sw| sw[:datetime] == zero[:datetime] and sw[:side] == side}
|
46
|
+
unless selected.empty?
|
47
|
+
puts 'cache_hit'.light_white if debug
|
48
|
+
return (selected.first[:empty] ? [] : selected )
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
###########################################################################################################################
|
54
|
+
# prepare base (i.e. dupe the original, create proper :y, and reject unneeded items)
|
55
|
+
#
|
56
|
+
base = base.
|
57
|
+
map { |x|
|
58
|
+
y = x.dup
|
59
|
+
y[:y] = (high ?
|
60
|
+
(y[:high] - zero[:high]).round(8) :
|
61
|
+
(zero[:low] - y[:low]).round(8)
|
62
|
+
) unless y[:high].nil?
|
63
|
+
y
|
64
|
+
}.
|
65
|
+
reject{|b| b.nil? or b[:datetime] < first[:datetime] or b[:x] < 0 or b[:y].nil?}[range]
|
66
|
+
|
67
|
+
# abs_peak is the absolute high / low of the base. the shearing operation ends there,
|
68
|
+
# but results might be influenced when abs_peak becomes affected by :with_flaws
|
69
|
+
abs_peak = base.send(high ? :max_by : :min_by){|x| x[high ? :high : :low] }[:datetime]
|
70
|
+
base.reject!{|x| x[:datetime] < abs_peak}
|
71
|
+
|
72
|
+
###########################################################################################################################z
|
73
|
+
# only if (and only if) the range portion above change the underlying base
|
74
|
+
# the offset has to be fixed for :x and :y
|
75
|
+
|
76
|
+
unless range == (0..-1)
|
77
|
+
puts "adjusting range to '#{range}'".light_yellow if debug
|
78
|
+
offset_x = base.last[:x]
|
79
|
+
offset_y = base.last[:y]
|
80
|
+
base.map!{|b| b[:x] -= offset_x; b[:y] -= offset_y ; b}
|
81
|
+
end
|
82
|
+
|
83
|
+
###########################################################################################################################
|
84
|
+
# introducing :i to the base, which provides the negative index of the :base Array of the current element
|
85
|
+
# this simplifies handling during the, where I can use the members array,
|
86
|
+
# that will carry just the index of the original base, regardless how many array_members have be already dropped
|
87
|
+
base.each_index.map{|i| base[i][:i] = -base.size + i }
|
88
|
+
|
89
|
+
|
90
|
+
###########################################################################################################################
|
91
|
+
# LAMBDA no1: simplifying DEBUG output
|
92
|
+
#
|
93
|
+
present = lambda {|z| z.slice(*%i[datetime high low x y i yy dx dev near miss dev]) }
|
94
|
+
|
95
|
+
|
96
|
+
###########################################################################################################################
|
97
|
+
# LAMBDA no2: all members except the pivot itself now most probably are too far to the left
|
98
|
+
# finalizing tries to get the proper dx value for them
|
99
|
+
#
|
100
|
+
finalize = lambda do |results|
|
101
|
+
results.map do |result|
|
102
|
+
result[:members].each do |member|
|
103
|
+
next if member[:yy].nil? or member[:yy].zero?
|
104
|
+
|
105
|
+
diff = (member[:x] - member[:dx]).abs / 2.0
|
106
|
+
member[:dx] = member[:x] + diff
|
107
|
+
# it employs another binary-search
|
108
|
+
while member[:yy].round(PRECISION) != 0
|
109
|
+
print '.' if debug
|
110
|
+
member[:yy] = shear_to_deg(deg: result[:deg], base: [ member ] ).first[:yy]
|
111
|
+
diff /= 2.0
|
112
|
+
if member[:yy] > 0
|
113
|
+
member[:dx] += diff
|
114
|
+
else
|
115
|
+
member[:dx] -= diff
|
116
|
+
end
|
117
|
+
end
|
118
|
+
member[:yy] = member[:yy].abs.round(8)
|
119
|
+
end
|
120
|
+
|
121
|
+
puts 'done!'.magenta if debug
|
122
|
+
result[:members].each {|member| puts "finalizing #{member}".magenta } if debug
|
123
|
+
result
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
###########################################################################################################################
|
128
|
+
# LAMDBA no3: the actual 'function' to retrieve the slope
|
129
|
+
#
|
130
|
+
# the idea implemented is based on the fact, that we don't know in which exact time of the interval the value
|
131
|
+
# was created. even further we know that the stencil might be incorrect. so after shearing the :x value of the
|
132
|
+
# recently found new member(s) is shifted by :deviation and shearing is repeated. this is done as long as new members
|
133
|
+
# are found.
|
134
|
+
get_slope = lambda do |b|
|
135
|
+
if debug
|
136
|
+
puts "in get_slope ... SETTING BASE: ".light_green
|
137
|
+
puts "Last: \t#{present.call b.last }".light_green
|
138
|
+
puts "First:\t#{present.call b.first}".light_green
|
139
|
+
end
|
140
|
+
members = [ b.last[:i] ]
|
141
|
+
loop do
|
142
|
+
current_slope = detect_slope(base: b, ticksize: sym[:ticksize], format: sym[:format], debug: debug)
|
143
|
+
if debug
|
144
|
+
puts "CURR: #{current_slope[:deg]} "
|
145
|
+
current_slope[:members].each {|x| puts "CURR: #{present.call(x)}" }
|
146
|
+
end
|
147
|
+
current_members = current_slope[:members].map{|dot| dot[:i]}
|
148
|
+
new_members = current_members - members
|
149
|
+
puts "New members: #{new_members} (as of #{current_members} - #{members})" if debug
|
150
|
+
# the return condition is if no new members are found in slope
|
151
|
+
# except lowest members are neighbours, what (recursively) causes re-run until the
|
152
|
+
# first member is solitary
|
153
|
+
if new_members.empty?
|
154
|
+
mem_sorted=members.sort
|
155
|
+
if mem_sorted[1] == mem_sorted[0] + 1
|
156
|
+
b2 = b[mem_sorted[1]..mem_sorted[-1]].map{|x| x.dup; x[:dx] = nil; x}
|
157
|
+
puts 'starting recursive rerun'.light_red if debug
|
158
|
+
alternative_slope = get_slope.call(b2)
|
159
|
+
alternative = alternative_slope[:members].map{|bar| bar[:i]}
|
160
|
+
# the alternative won't be used if it misses out a member that would have
|
161
|
+
# been in the 'original' slope
|
162
|
+
if (mem_sorted[1..-1] - alternative).empty?
|
163
|
+
current_slope = alternative_slope
|
164
|
+
members = alternative
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
current_slope[:raw] = members.map{|i| base[i][:x]}
|
169
|
+
|
170
|
+
members.sort_by{|i| -i}.each_with_index do |i, index|
|
171
|
+
|
172
|
+
puts "#{index}\t#{range}\t#{present.call b[i]}".light_yellow if debug
|
173
|
+
|
174
|
+
current_slope[:members] << b[i] unless current_slope[:members].map{|x| x[:datetime]}.include? b[i][:datetime]
|
175
|
+
current_slope[:members].sort_by!{|x| x[:datetime]}
|
176
|
+
end
|
177
|
+
current_slope
|
178
|
+
|
179
|
+
end
|
180
|
+
# all new members found in current iteration have now receive their new :x value, depending on their distance to
|
181
|
+
# to the origin. when exploring near distance, it is assumned, that the actual :y value might have an
|
182
|
+
# additional distance of 1, further distant points can be distant even :deviation, what defaults to 2
|
183
|
+
# covering e.g. a holiday when using a daily base
|
184
|
+
new_members.each do |mem|
|
185
|
+
current_deviation = (0.1 * b[mem][:x])
|
186
|
+
current_deviation = 1 if current_deviation < 1
|
187
|
+
current_deviation = deviation if current_deviation > deviation
|
188
|
+
b[mem][:dx] = b[mem][:x] + current_deviation
|
189
|
+
end
|
190
|
+
members += new_members
|
191
|
+
end
|
192
|
+
end # of lambda
|
193
|
+
|
194
|
+
###########################################################################################################################
|
195
|
+
# Lambda no. 4: analyzing the slope, adding near misses and characteristics
|
196
|
+
#
|
197
|
+
# near misses are treated as full members, as for example stacked orders within a swap architecture might impede that the
|
198
|
+
# peak runs to the maximum expansion
|
199
|
+
#
|
200
|
+
# first, the swap_base is created by shearing the entire base to current :deg
|
201
|
+
# then all base members are selected that fit the desired :y range.
|
202
|
+
# please note that here also the processing of :with_flaws takes place
|
203
|
+
#
|
204
|
+
# the key :dev is introduced, which is actually a ticksize-based variant of :yy
|
205
|
+
|
206
|
+
analyze = lambda do |swaps|
|
207
|
+
swaps.each do |swap|
|
208
|
+
|
209
|
+
swap_base = base.map{|y|
|
210
|
+
x = y.slice(*%i[ datetime high low dist x y i yy dx ])
|
211
|
+
current_member = swap[:members].find{|z| z[:datetime] == x[:datetime] }
|
212
|
+
x[:dx] = current_member[:dx] if current_member
|
213
|
+
x
|
214
|
+
}
|
215
|
+
swap_base = shear_to_deg(base: swap_base, deg: swap[:deg])
|
216
|
+
swap_base.map!{|x| x[:dev] = (x[:yy] / sym[:ticksize].to_f); x[:dev] = -( x[:dev] > 0 ? x[:dev].floor : x[:dev].ceil); x}
|
217
|
+
invalids = swap_base.select{|x| x[:dev] < 0 }
|
218
|
+
if with_flaws > 0
|
219
|
+
# TODO: this behaves only as expected when with_flaws == 2
|
220
|
+
last_invalid = invalids[(invalids[-2][:i] + 1 == invalids[-1][:i] ) ? -3 : -2] rescue nil
|
221
|
+
else
|
222
|
+
last_invalid = invalids.last
|
223
|
+
end
|
224
|
+
|
225
|
+
# the 'near' members are all base members found, that fit
|
226
|
+
# 1. being positive (as being zero means that they are original members)
|
227
|
+
# 2. match a valid :dev
|
228
|
+
# 3. appeared later than :last_invalid
|
229
|
+
near = swap_base.select{|x|
|
230
|
+
x[:dev] <= [ 5, (x[:x] / 100)+2 ].min and
|
231
|
+
x[:dev].positive? and
|
232
|
+
(last_invalid.nil? ? true : x[:datetime] > last_invalid[:datetime])
|
233
|
+
}.map{|x| x[:near] = x[:dev]; x}
|
234
|
+
|
235
|
+
# these then are added to the swap[:members] and for further processing swap_base is cleaned
|
236
|
+
swap[:members] = (swap[:members] + near).sort_by{|x| x[:datetime] }
|
237
|
+
swap_base.select!{|x| x[:datetime] >= swap[:members].first[:datetime]}
|
238
|
+
|
239
|
+
########################################################################33
|
240
|
+
# now swap characteristics are calculated
|
241
|
+
#
|
242
|
+
# avg_dev: the average distance of high or low to the swap_line
|
243
|
+
swap[:avg_dev] = (swap_base.reject{|x| x[:dev].zero?}.map{|x| x[:dev].abs}.reduce(:+) / (swap_base.size - swap[:members].size).to_f).ceil rescue 0
|
244
|
+
# depth: the maximum distance to the swap line
|
245
|
+
swap[:depth] = swap_base.max_by{|x| x[:dev]}[:dev]
|
246
|
+
swap[:raw] = swap[:members].map{|x| x[:x]}.reverse
|
247
|
+
swap[:size] = swap[:members].size
|
248
|
+
swap[:length] = swap[:raw][-1] - swap[:raw][0]
|
249
|
+
# rating: the maximum distance of the 'most middle' point of the swap to the nearer end
|
250
|
+
swap[:rating] = swap[:raw][1..-2].map{ |dot| [ dot - swap[:raw][0], swap[:raw][-1] - dot].min }.max || 0
|
251
|
+
swap[:datetime] = swap[:members].last[:datetime]
|
252
|
+
swap[:side] = side
|
253
|
+
rat = swap[:rating]
|
254
|
+
# color: to simplify human readability a standard set of colors for intraday and eod based swaps
|
255
|
+
swap[:color] = (rat > 75) ? :light_blue : (rat > 30) ? :magenta : (rat > 15) ? :light_magenta : (rat > 7) ? (high ? :light_green : :light_red) : high ? :green : :red
|
256
|
+
unless %i[ daily continuous ].include? interval
|
257
|
+
swap[:color] = ((rat > 150) ? :light_blue : (rat > 80) ? :magenta : (rat > 30) ? :light_magenta : (rat > 15) ? :light_yellow : high ? :green : :red)
|
258
|
+
end
|
259
|
+
swap[:diff] = swap[:members].last[ high ? :high : :low ] - swap[:members].first[ high ? :high : :low ]
|
260
|
+
swap[:ticks] = (swap[:diff] / sym[:ticksize]).to_i
|
261
|
+
# tpi: ticks per interval, how many ticks are passed each :interval
|
262
|
+
swap[:tpi] = (swap[:ticks].to_f / swap[:length]).round(3)
|
263
|
+
# ppi: power per interval, how many dollar value is passed each :interval
|
264
|
+
swap[:ppi] = (swap[:tpi] * sym[:power]).round(3)
|
265
|
+
end # swap
|
266
|
+
end # lambda
|
267
|
+
|
268
|
+
###########################################################################################################################
|
269
|
+
# after declaring lambdas, the rest is quite few code
|
270
|
+
#
|
271
|
+
# starting with the full range, a valid slope is searched. the found slope defines an interval of the
|
272
|
+
# base array, in which again a (lower) slope can be uncovered.
|
273
|
+
#
|
274
|
+
# this process is repeated while the interval to be processed is large enough (:min_length)
|
275
|
+
current_range = (0..-1) # RANGE set
|
276
|
+
current_slope = { members: [] } # SLOPE reset
|
277
|
+
current_base = base[current_range].map{|z| z.slice(*%i[datetime high low x y i ])} # BASE set
|
278
|
+
current_results = [ ] # RESULTS reset
|
279
|
+
binding.irb if debug
|
280
|
+
while current_base.size >= min_length # LOOP
|
281
|
+
|
282
|
+
puts '-------------------------------------------------------------------------------------' if debug
|
283
|
+
|
284
|
+
while current_base.size >= min_length and current_slope[:members].size < 2
|
285
|
+
|
286
|
+
puts "---- #{current_base.size} #{current_range.to_s.light_yellow} ------" if debug
|
287
|
+
|
288
|
+
# get new slope
|
289
|
+
current_slope = get_slope.call(current_base) # SLOPE call
|
290
|
+
|
291
|
+
# define new range and base
|
292
|
+
next_i = current_slope[:members].select{|z| z[:miss].nil? and z[:near].nil?}[-2]
|
293
|
+
current_range = ((next_i.nil? ? -2 : next_i[:i])+1..-1) # RANGE adjust
|
294
|
+
current_base = base[current_range].map{|z| z.slice(*%i[datetime high low x y i ])} # BASE adjust
|
295
|
+
end
|
296
|
+
puts "Current slope: ".light_yellow + "#{current_slope}" if debug
|
297
|
+
current_results << current_slope if current_slope # RESULTS add
|
298
|
+
current_slope = { members: [] } # SLOPE reset
|
299
|
+
end
|
300
|
+
|
301
|
+
finalize.call(current_results)
|
302
|
+
analyze.call(current_results)
|
303
|
+
binding.irb if debug
|
304
|
+
|
305
|
+
# reject all results that do not suffice
|
306
|
+
current_results.reject!{|swap| swap[:rating] < min_rating or swap[:length] < min_length or swap[:rating] < swap[:length] / 4.to_f}
|
307
|
+
|
308
|
+
#####################################################################################################################3
|
309
|
+
# finally save results for caching and return them
|
310
|
+
if save
|
311
|
+
if interval.nil? or swap_type.nil?
|
312
|
+
puts "WARNING: Cannot save swaps, as both :interval and :swap_type must be given".colorize(:light_yellow)
|
313
|
+
else
|
314
|
+
current_results.map{|sw| mem = sw[:members]; sw[:slope] = (mem.last[:y] - mem.first[:y]) / (mem.last[mem.last[:dx].nil? ? :x : :dx] - mem.first[mem.first[:dx].nil? ? :x : :dx]).to_f }
|
315
|
+
to_save = current_results.empty? ? [ { datetime: zero[:datetime], side: side, empty: true } ] : current_results
|
316
|
+
save_swaps(to_save, interval: interval, swap_type: swap_type, contract: contract, sym: sym)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
current_results
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
data/lib/cotcube-level.rb
CHANGED
@@ -12,34 +12,36 @@ require 'json' unless defined?(JSON)
|
|
12
12
|
require 'digest' unless defined?(Digest)
|
13
13
|
require 'cotcube-helpers'
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
%w[ stencil detect_slope triangulate helpers].each do |part|
|
15
|
+
%w[ eod_stencil intraday_stencil detect_slope tritangulate helpers].each do |part|
|
18
16
|
require_relative "cotcube-level/#{part}"
|
19
17
|
end
|
20
18
|
|
21
|
-
|
22
|
-
|
23
19
|
module Cotcube
|
24
20
|
module Level
|
25
21
|
|
26
|
-
PRECISION =
|
27
|
-
INTERVALS = %i[ daily ]
|
22
|
+
PRECISION = 16
|
23
|
+
INTERVALS = %i[ daily continuous hours halfs ]
|
28
24
|
SWAPTYPES = %i[ full ]
|
25
|
+
TIMEZONES = { 'CT' => Time.find_zone('America/Chicago'),
|
26
|
+
'DE' => Time.find_zone('Europe/Berlin') }
|
27
|
+
GLOBAL_SOW = { 'CT' => '0000-1700' }
|
28
|
+
GLOBAL_EOW = { 'CT' => '1700-0000' }
|
29
|
+
GLOBAL_EOD = { 'CT' => '1600-1700' }
|
30
|
+
|
29
31
|
#module_function :init, # checks whether environment is prepared and returns the config hash
|
30
|
-
module_function :detect_slope,
|
31
|
-
:
|
32
|
-
:
|
32
|
+
module_function :detect_slope, # in detect_slope.rb
|
33
|
+
:tritangulate, # in tritangulate.rb
|
34
|
+
:shear_to_deg, # in helpers.rb
|
35
|
+
:shear_to_rad, # same all below
|
33
36
|
:rad2deg,
|
34
37
|
:deg2rad,
|
35
38
|
:puts_swaps,
|
36
39
|
:save_swaps,
|
37
40
|
:get_jsonl_name,
|
38
41
|
:load_swaps,
|
39
|
-
:member_to_human
|
40
|
-
:triangulate
|
42
|
+
:member_to_human
|
41
43
|
|
42
|
-
# please note that module_functions of sources provided in
|
44
|
+
# please note that module_functions of sources provided in non-public files must slso be published within these
|
43
45
|
end
|
44
46
|
end
|
45
47
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cotcube-level
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin L. Tischendorf
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-08-
|
11
|
+
date: 2021-08-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -108,7 +108,7 @@ dependencies:
|
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0.9'
|
111
|
-
description: A gem to shear a time series, basically serving as a yet
|
111
|
+
description: A gem to shear a time series, basically serving as a yet unseen class
|
112
112
|
of indicators.
|
113
113
|
email:
|
114
114
|
- donkeybridge@jtown.eu
|
@@ -124,9 +124,10 @@ files:
|
|
124
124
|
- cotcube-level.gemspec
|
125
125
|
- lib/cotcube-level.rb
|
126
126
|
- lib/cotcube-level/detect_slope.rb
|
127
|
+
- lib/cotcube-level/eod_stencil.rb
|
127
128
|
- lib/cotcube-level/helpers.rb
|
128
|
-
- lib/cotcube-level/
|
129
|
-
- lib/cotcube-level/
|
129
|
+
- lib/cotcube-level/intraday_stencil.rb
|
130
|
+
- lib/cotcube-level/tritangulate.rb
|
130
131
|
homepage: https://github.com/donkeybridge/cotcube-level
|
131
132
|
licenses:
|
132
133
|
- BSD-3-Clause
|