cotcube-level 0.3.2 → 0.3.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81e95a5e617ba6dd8fdf5df13c0395455ff19a337e2e72e79f485a37489049c0
4
- data.tar.gz: d592daecc1be2e4686fd3a6807d82c5f5429de6b451b96e4aee30430fe0fbe9d
3
+ metadata.gz: 4bbf4fc1b2edf64ffee60a29ffe7d72a71a4c0f8fb99b889026dce9dbca7399f
4
+ data.tar.gz: 59469c2431a20e9e1f07a99e0af61a4fed59ca276a02d654a2f2bd28a8543c04
5
5
  SHA512:
6
- metadata.gz: 78459e2694a2c1f97357307d550df8c9920b18be785b1c28d712b8d61c074ba4326c867b107b62985030fac412805c548858ee086f13802dec13b01e75c10f9c
7
- data.tar.gz: 0a7ccd8153cda7309f70fbffb2fc2fe7f2595194ddf4688bcc13d28c86418fbe0f23322f5c47a2fba7b4aca0832e95b877747158bd17ad1da33a634bbc3eb529
6
+ metadata.gz: 78a4cb1a3bc80de5086befc5e927cafc86c38ef62410c4fc9d61e716e929f3847ad4a6ac870d30b94168e10dd72d22267aba8d4d4ae2f52a5e7170454a6864f5
7
+ data.tar.gz: 79ed8087a1936cef86257e24c9909f8923902be09e43868ce29deb5b178d03c2d3bbb2700f2bb76f67b2a58a51df930b3bdd41c4134922ed90bd6b2a1f0990ad
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## 0.3.4.2 (November 28, 2021)
2
+ - added 2 executables to bin to display eod- and intraday-swaps
3
+ - helpers: adding ignorance to load_swaps and other, adding .mark_ignored
4
+ - intraday_stencil: allowing absence of :zero in #use
5
+
6
+ ## 0.3.4.1 (October 09, 2021)
7
+ - intraday_stencil: fixing @index, that did not work when used outside active hours
8
+
9
+ ## 0.3.4 (October 06, 2021)
10
+ - intraday_stencil: major rework resp. rebuild ... now beta-ready
11
+ - helpers: preparing puts_swap and get_jsonl_name to work intraday
12
+ - puts_swap: changed output scheme to provide exceedance as NOTE
13
+
14
+ ## 0.3.3 (October 05, 2021)
15
+ - helpers::load_swaps: added :exceed to allow 1(sic) swap to be exceeded while loading
16
+ - tritangulate: added :manual for feature of manual swap creation with base of 2 members
17
+ - helpers::load_swap added :digest to filter for swaps starting with pattern
18
+ - helpers: minor readability improvements
19
+ - eod_stencil: minor readability improvements
20
+ - helpers: few optimizations
21
+
1
22
  ## 0.3.2 (August 29, 2021)
2
23
  - tritangulate: fixing 'finalize', as Integer zero won't comparte to Float zero
3
24
  - cotcube-level.rb: added :check_exceedance
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.3.4.2
data/bin/iswaps.rb ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/cotcube-level.rb'
4
+
5
+ HELP = <<HEREDOC
6
+ Display current intraday swaps.
7
+ > USAGE: iswaps.rb <contract> [json]
8
+ > contract a contract known to the system
9
+ > json switch to toggle json output instead of human readable
10
+ HEREDOC
11
+ if ARGV.empty?
12
+ puts HELP
13
+ exit
14
+ end
15
+
16
+ contract = ARGV[0].nil? ? nil : ARGV[0].upcase
17
+ json = ARGV.include? 'json'
18
+
19
+ sym = Cotcube::Helpers.get_id_set(contract: contract) rescue "Could not determine contract #{contract}"
20
+ if sym.is_a? Sring; puts sym; puts HELP; exit 1; end
21
+
22
+ swaps = Cotcube::Level::load_swaps(interval: 30.minutes, swap_type: :full, contract: contract, sym: sym).
23
+ select{|swap| not(swap[:empty]) and
24
+ not(swap[:ignored]) and
25
+ not(swap[:exceeded].presence ? (swap[:exceeded] < DateTime.now - 2.days) : false)
26
+ }
27
+ stencil = Cotcube::Level::Intraday_Stencil.new( interval: 30.minutes, swap_type: :full, asset: contract[..1])
28
+ swaps.map!{|swap| stencil.use with: swap, sym: sym}
29
+
30
+ if json
31
+ puts swaps.to_json
32
+ else
33
+ puts '<none>' if swaps.empty?
34
+ swaps.each {|swap|
35
+ notice = if swap[:exceeded]
36
+ "EXCEEDED #{swap[:exceeded]}"
37
+ elsif swap[:ignored]
38
+ 'IGNORED'
39
+ else
40
+ "Current: #{format sym[:format], swap[:current_value]}"
41
+ end
42
+ Cotcube::Level.puts_swap(swap, format: sym[:format], notice: notice)
43
+ }
44
+ end
45
+
data/bin/swaps.rb ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/cotcube-level.rb'
4
+
5
+ HELP = <<HEREDOC
6
+ swaps.rb: Display current eod swaps.
7
+ > USAGE: swaps.rb <contract> [json]
8
+ > contract a contract known to the system
9
+ > json switch to toggle json output instead of human readable
10
+ HEREDOC
11
+ if ARGV.empty?
12
+ puts HELP
13
+ exit
14
+ end
15
+
16
+
17
+ contract = ARGV[0].nil? ? nil : ARGV[0].upcase
18
+ json = ARGV.include? 'json'
19
+
20
+ sym = Cotcube::Helpers.get_id_set(contract: contract) rescue "ERROR: Could not determine contract '#{contract}'."
21
+
22
+ if sym.is_a? String; puts sym; puts HELP; exit 1; end
23
+
24
+ swaps = Cotcube::Level::load_swaps(interval: :daily, swap_type: :full, contract: contract, quiet: true).
25
+ select{|swap| not(swap[:empty]) and
26
+ not(swap[:ignored]) and
27
+ not(swap[:exceeded].presence ? (swap[:exceeded] < DateTime.now - 2.days) : false)
28
+ }
29
+ stencil = Cotcube::Level::EOD_Stencil.new( interval: :daily, swap_type: :full)
30
+ swaps.map!{|swap| stencil.use with: swap, sym: sym}
31
+ if json
32
+ puts swaps.to_json
33
+ else
34
+ puts '<none>' if swaps.empty?
35
+ swaps.each {|swap|
36
+ notice = if swap[:exceeded]
37
+ "EXCEEDED #{swap[:exceeded].strftime('%Y-%m-%d')}"
38
+ elsif swap[:ignored]
39
+ 'IGNORED'
40
+ else
41
+ "Current: #{format sym[:format], swap[:current_value]}"
42
+ end
43
+ Cotcube::Level.puts_swap(swap, format: sym[:format], notice: notice)
44
+ }
45
+ end
@@ -13,7 +13,7 @@ module Cotcube
13
13
  #
14
14
  # Current daily stencils contain dates from 2020-01-01 to 2023-12-31
15
15
  #
16
- def self.provide_raw_stencil(type:, interval: :daily, version: nil)
16
+ def self.provide_raw_stencil(type:, interval: :daily, version: nil, timezone: Cotcube::Helpers::CHICAGO)
17
17
  loading = lambda do |typ|
18
18
  file_base = "/var/cotcube/level/stencils/stencil_#{interval.to_s}_#{typ.to_s}.csv_"
19
19
  if Dir["#{file_base}?*"].empty?
@@ -27,7 +27,7 @@ module Cotcube
27
27
  raise ArgumentError, "Cannot open stencil from non-existant file #{file}."
28
28
  end
29
29
  end
30
- CSV.read(file).map{|x| { datetime: CHICAGO.parse(x.first).freeze, x: x.last.to_i.freeze } }
30
+ CSV.read(file).map{|x| { datetime: timezone.parse(x.first).freeze, x: x.last.to_i.freeze } }
31
31
  end
32
32
  unless const_defined? :RAW_STENCILS
33
33
  const_set :RAW_STENCILS, { daily:
@@ -80,7 +80,7 @@ module Cotcube
80
80
  raise ArgumentError, "Each stencil members should contain at least :datetime and :x" unless stencil.nil? or
81
81
  stencil.map{|x| ([:datetime, :x] - x.keys).empty? and [ActiveSupport::TimeWithZone, Day].include?( x[:datetime] ) and x[:x].is_a?(Integer)}.reduce(:&)
82
82
 
83
- base = stencil || EOD_Stencil.provide_raw_stencil(type: stencil_type, interval: :daily, version: version)
83
+ base = stencil || EOD_Stencil.provide_raw_stencil(type: stencil_type, interval: :daily, version: version, timezone: timezone)
84
84
 
85
85
  # fast rewind to previous trading day
86
86
  date = timezone.parse(date) unless [NilClass, Date, ActiveSupport::TimeWithZone].include? date.class
@@ -111,7 +111,12 @@ module Cotcube
111
111
  end
112
112
 
113
113
  def zero
114
- @zero ||= @base.find{|b| b[:x].zero? }
114
+ index(0)
115
+ end
116
+
117
+ def index(offset = 0)
118
+ @index ||= @base.index{|b| b[:x].zero? }
119
+ @base[@index + offset]
115
120
  end
116
121
 
117
122
  def apply(to: )
@@ -136,7 +141,7 @@ module Cotcube
136
141
  to.reject!{|x| x[:x].nil? }
137
142
  end
138
143
 
139
- def use(with:, sym:, zero:, grace: -2)
144
+ def use(with:, sym:, zero: nil, grace: -2)
140
145
  # todo: validate with (check if vslid swap
141
146
  # sym (check keys)
142
147
  # zero (ohlc with x.zero?)
@@ -146,15 +151,17 @@ module Cotcube
146
151
  ohlc = high ? :high : :low
147
152
  start = base.find{|x| swap[:datetime] == x[:datetime]}
148
153
  swap[:current_change] = (swap[:tpi] * start[:x]).round(8)
149
- swap[:current_value] = swap[:members].last[ ohlc ] + swap[:current_change] * sym[:ticksize]
150
- swap[:current_diff] = (swap[:current_value] - zero[ohlc]) * (high ? 1 : -1 )
151
- swap[:current_dist] = (swap[:current_diff] / sym[:ticksize]).to_i
152
- swap[:exceeded] = zero[:datetime] if swap[:current_dist] < grace
154
+ swap[:current_value] = swap[:members].last[ ohlc ] + swap[:current_change] * sym[:ticksize]
155
+ unless zero.nil?
156
+ swap[:current_diff] = (swap[:current_value] - zero[ohlc]) * (high ? 1 : -1 )
157
+ swap[:current_dist] = (swap[:current_diff] / sym[:ticksize]).to_i
158
+ swap[:exceeded] = zero[:datetime] if swap[:current_dist] < grace
159
+ end
153
160
  swap
154
161
  end
155
- end
162
+ end
156
163
 
157
- end
164
+ end
158
165
 
159
166
  end
160
167
 
@@ -23,15 +23,15 @@ module Cotcube
23
23
 
24
24
  # human readable output
25
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]
26
+ def member_to_human(member,side: ,format:, daily: false, tws: false)
27
+ high = (side == :upper)
28
+ "#{ member[:datetime].strftime("%a, %Y-%m-%d#{daily ? "" :" %I:%M%p"}")
29
+ } x: #{format '%-4d', member[:x]
30
30
  } dx: #{format '%-8.3f', (member[:dx].nil? ? member[:x] : member[:dx].round(3))
31
31
  } #{high ? "high" : "low"
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?
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
35
  } #{member[:near].nil? ? '' : "near: #{member[:near]}"
36
36
  }"
37
37
  end
@@ -40,36 +40,80 @@ module Cotcube
40
40
  # format: e.g. sym[:format]
41
41
  # short: print one line / less verbose
42
42
  # notice: add this to output as well
43
- def puts_swap(swap, format: , short: false, notice: nil)
44
- return if swap[:empty]
45
- daily = %i[ continuous daily ].include?(swap[:interval])
46
- datetime_format = daily ? '%Y-%m-%d' : '%Y-%m-%d %H:%M'
43
+ def puts_swap(swap, format: , short: true, notice: nil, hash: 3, tws: false)
44
+ return '' if swap[:empty]
45
+
46
+ # if presenting swaps from json, the datetimes need to be parsed first
47
+ swap[:datetime] = DateTime.parse(swap[:datetime]) if swap[:datetime].is_a? String
48
+ swap[:exceeded] = DateTime.parse(swap[:exceeded]) if swap[:exceeded].is_a? String
49
+ swap[:ignored] = DateTime.parse(swap[:ignored]) if swap[:ignored ].is_a? String
50
+ swap[:side] = swap[:side].to_sym
51
+ swap[:members].each do |mem|
52
+ mem[:datetime] = DateTime.parse(mem[:datetime]) if mem[:datetime].is_a? String
53
+ end
54
+
55
+ # TODO: create config-entry to contain 1.hour -- set of contracts ; 7.hours -- set of contracts [...]
56
+ # instead of hard-coding in here
57
+ # TODO: add also to :member_to_human
58
+ # then commit
59
+ if tws
60
+ case swap[:contract][0...2]
61
+ when *%w[ GC SI PL PA HG NG CL HO RB ]
62
+ delta_datetime = 1.hour
63
+ when *%w[ GG DX ]
64
+ delta_datetime = 7.hours
65
+ else
66
+ delta_datetime = 0
67
+ end
68
+ else
69
+ delta_datetime = 0
70
+ end
71
+ daily = %i[ continuous daily ].include?(swap[:interval].to_sym) rescue false
72
+ datetime_format = daily ? '%Y-%m-%d' : '%Y-%m-%d %I:%M %p'
47
73
  high = swap[:side] == :high
48
74
  ohlc = high ? :high : :low
75
+ if notice.nil? and swap[:exceeded]
76
+ notice = "exceeded #{(swap[:exceeded] + delta_datetime).strftime(datetime_format)}"
77
+ end
78
+ if swap[:ignored]
79
+ notice += " IGNORED"
80
+ end
49
81
  if short
50
- puts "S: #{swap[:side]
51
- } L: #{format '%4d', swap[:length]
52
- } R: #{format '%4d', swap[:rating]
53
- } D: #{format '%4d', swap[:depth]
54
- } P: #{format '%10s', (format '%5.2f', swap[:ppi])
55
- } F: #{format format, swap[:members].last[ ohlc ]
56
- } S: #{swap[:members].first[:datetime].strftime(datetime_format)
57
- } - #{swap[:members].last[:datetime].strftime(datetime_format)
58
- }#{format('%20s', (swap[:exceeded] ? " XXX: #{swap[:exceeded].strftime(datetime_format)}" : ''))
59
- }#{" NOTE: #{notice}" unless notice.nil?}".colorize(swap[:color] || :white )
82
+ res ="#{format '%7s', swap[:digest][...hash]
83
+ } #{ swap[:contract]
84
+ } #{ swap[:side].to_s
85
+ }".colorize( swap[:side] == :upper ? :light_green : :light_red ) +
86
+ " (#{ format '%4d', swap[:length]
87
+ },#{ format '%4d', swap[:rating]
88
+ },#{ format '%4d', swap[:depth]
89
+ }) P: #{ format '%6s', (format '%4.2f', swap[:ppi])
90
+ } #{
91
+ if swap[:current_value].nil?
92
+ "I: #{ format '%8s', (format format, swap[:members].last[ ohlc ]) }"
93
+ else
94
+ "C: #{ format '%8s', (format format, swap[:current_value]) } "
95
+ end
96
+ } [#{ (swap[:members].first[:datetime] + delta_datetime).strftime(datetime_format)
97
+ } - #{ (swap[:members].last[:datetime] + delta_datetime).strftime(datetime_format)
98
+ }]#{" NOTE: #{notice}" unless notice.nil?
99
+ }".colorize(swap[:color] || :white )
100
+ puts res
60
101
  else
61
- puts "side: #{swap[:side] }\tlen: #{swap[:length]} \trating: #{swap[:rating]}".colorize(swap[:color] || :white )
62
- puts "diff: #{swap[:ticks]}\tdif: #{swap[:diff].round(7)}\tdepth: #{swap[:depth]}".colorize(swap[:color] || :white )
63
- puts "tpi: #{swap[:tpi] }\tppi: #{swap[:ppi]}".colorize(swap[:color] || :white )
64
- puts "NOTE: #{notice}".colorize(:light_white) unless notice.nil?
65
- swap[:members].each {|x| puts member_to_human(x, side: swap[:side], format: format, daily: daily) }
102
+ res = ["side: #{swap[:side] }\tlen: #{swap[:length]} \trating: #{swap[:rating]}".colorize(swap[:color] || :white )]
103
+ res << "diff: #{swap[:ticks]}\tdif: #{swap[:diff].round(7)}\tdepth: #{swap[:depth]}".colorize(swap[:color] || :white )
104
+ res << "tpi: #{swap[:tpi] }\tppi: #{swap[:ppi]}".colorize(swap[:color] || :white )
105
+ res << "NOTE: #{notice}".colorize(:light_white) unless notice.nil?
106
+ swap[:members].each {|x| res << member_to_human(x, side: swap[:side], format: format, daily: daily) }
107
+ res = res.join("\n")
108
+ puts res
66
109
  end
110
+ res
67
111
  end
68
112
 
69
113
  # create a standardized name for the cache files
70
114
  # and, on-the-fly, create these files plus their directory
71
115
  def get_jsonl_name(interval:, swap_type:, contract:, sym: nil)
72
- raise "Interval #{interval } is not supported, please choose from #{INTERVALS}" unless INTERVALS.include? interval
116
+ raise "Interval #{interval } is not supported, please choose from #{INTERVALS}" unless INTERVALS.include?(interval) || interval.is_a?(Integer)
73
117
  raise "Swaptype #{swap_type} is not supported, please choose from #{SWAPTYPES}" unless SWAPTYPES.include? swap_type
74
118
  sym ||= Cotcube::Helpers.get_id_set(contract: contract)
75
119
  root = '/var/cotcube/level'
@@ -78,7 +122,9 @@ module Cotcube
78
122
  `mkdir -p #{dir}` unless File.exist?(dir)
79
123
  `ln -s #{dir} #{symlink}` unless File.exist?(symlink)
80
124
  file = "#{dir}/#{contract}_#{interval.to_s}_#{swap_type.to_s}.jsonl"
81
- `touch #{file}`
125
+ unless File.exist? file
126
+ `touch #{file}`
127
+ end
82
128
  file
83
129
  end
84
130
 
@@ -114,7 +160,9 @@ module Cotcube
114
160
 
115
161
  # loading of swaps is also straight forward
116
162
  # it takes few more efforts to normalize the values to their expected format
117
- def load_swaps(interval:, swap_type:, contract:, sym: nil, datetime: nil)
163
+ #
164
+ # it is not too nice that some actual interactive process is done here in the load section
165
+ def load_swaps(interval:, swap_type:, contract:, sym: nil, datetime: nil, recent: false, digest: nil, quiet: false, exceed: false, keep_ignored: false)
118
166
  file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
119
167
  jsonl = File.read(file)
120
168
  data = jsonl.
@@ -123,32 +171,69 @@ module Cotcube
123
171
  JSON.parse(x).
124
172
  deep_transform_keys(&:to_sym).
125
173
  tap do |sw|
126
- sw[:datetime] = DateTime.parse(sw[:datetime]) rescue nil
127
- (sw[:exceeded] = DateTime.parse(sw[:exceeded]) rescue nil) if sw[:exceeded]
128
- sw[:interval] = interval
129
- sw[:swap_type] = swap_type
130
- sw[:contract] = contract
131
- %i[ side ].each {|key| sw[key] = sw[key].to_sym rescue false }
132
- unless sw[:empty] or sw[:exceeded]
133
- sw[:color] = sw[:color].to_sym
134
- sw[:members].map{|mem| mem[:datetime] = DateTime.parse(mem[:datetime]) }
135
- end
174
+ sw[:datetime] = DateTime.parse(sw[:datetime]) rescue nil
175
+ (sw[:exceeded] = DateTime.parse(sw[:exceeded]) rescue nil) if sw[:exceeded]
176
+ (sw[:ignored] = DateTime.parse(sw[:ignored]) rescue nil) if sw[:ignored]
177
+ sw[:interval] = interval
178
+ sw[:swap_type] = swap_type
179
+ sw[:contract] = contract
180
+ %i[ side ].each {|key| sw[key] = sw[key].to_sym rescue false }
181
+ unless sw[:empty] or sw[:exceeded] or sw[:ignored]
182
+ sw[:color] = sw[:color].to_sym
183
+ sw[:members].map{|mem| mem[:datetime] = DateTime.parse(mem[:datetime]) }
184
+ end
136
185
  end
137
186
  end
138
187
  # assign exceedance data to actual swaps
139
188
  data.select{|swap| swap[:exceeded] }.each do |exc|
140
189
  swap = data.find{|ref| ref[:digest] == exc[:ref]}
141
- raise RuntimeError, "Consistent history for '#{exc}'. Origin not found." if swap.nil?
190
+ raise RuntimeError, "Inconsistent history for '#{exc}'. Origin not found." if swap.nil?
142
191
  swap[:exceeded] = exc[:exceeded]
143
192
  end
193
+ # assign ignorance data to actual swaps
194
+ data.select{|swap| swap[:ignored] }.each do |ign|
195
+ swap = data.find{|ref| ref[:digest] == ign[:ref]}
196
+ raise RuntimeError, "Inconsistent history for '#{ign}'. Origin not found." if swap.nil?
197
+ swap[:ignored] = ign[:ignored]
198
+ end
144
199
  # do not return bare exceedance information
145
- data.reject!{|swap| swap[:exceeded] and swap[:members].nil? }
200
+ data.reject!{|swap| (swap[:ignored] or swap[:exceeded]) and swap[:members].nil? }
146
201
  # do not return swaps that are found 'later'
147
202
  data.reject!{|swap| swap[:datetime] > datetime } unless datetime.nil?
148
203
  # do not return exceeded swaps, that are exceeded in the past
149
- data.reject!{|swap| swap[:exceeded] and swap[:exceeded] < datetime } unless datetime.nil?
204
+ recent = 7.days if recent.is_a? TrueClass
205
+ recent += 5.hours if recent
206
+ data.reject!{|swap| swap[:ignored] } unless keep_ignored
207
+ data.reject!{|swap| swap[:exceeded] and swap[:exceeded] < datetime - (recent ? recent : 0) } unless datetime.nil?
150
208
  # remove exceedance information that is found 'later'
151
- data.map{|swap| swap.delete(:exceeded) if swap[:exceeded] and swap[:exceeded] > datetime}
209
+ data.map{|swap| swap.delete(:exceeded) if swap[:exceeded] and swap[:exceeded] > datetime} unless datetime.nil?
210
+ unless digest.nil?
211
+ data.select! do |z|
212
+ (Cotcube::Helpers.sub(minimum: digest.length){ z[:digest] } === digest) and
213
+ not z[:empty]
214
+ end
215
+ case data.size
216
+ when 0
217
+ puts "No swaps found for digest '#{digest}'." unless quiet
218
+ when 1
219
+ sym ||= Cotcube::Helpers.get_id_set(contract: contract)
220
+ if not quiet or exceed
221
+ puts "Found 1 digest: "
222
+ data.each {|d| puts_swap( d, format: sym[:format], short: true, hash: digest.size + 2) }
223
+ if exceed
224
+ exceed = DateTime.now if exceed.is_a? TrueClass
225
+ mark_exceeded(swap: data.first, datetime: exceed)
226
+ puts "Swap marked exceeded."
227
+ end
228
+ end
229
+ else
230
+ sym ||= Cotcube::Helpers.get_id_set(contract: contract)
231
+ unless quiet
232
+ puts "Too many digests found for digest '#{digest}', please consider sending more figures: "
233
+ data.each {|d| puts_swap( d, format: sym[:format], short: true, hash: digest.size + 3)}
234
+ end
235
+ end
236
+ end
152
237
  data
153
238
  end
154
239
 
@@ -170,11 +255,34 @@ module Cotcube
170
255
  save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], contract: contract, sym: sym, quiet: (not debug)
171
256
  swap[:exceeded] = update[:exceeded]
172
257
  end
173
- %i[ current_change current_value current_diff current_dist ].map{|key| swap[key] = update[key] }
258
+ %i[ current_change current_value current_diff current_dist alert].map{|key| swap[key] = update[key] }
174
259
  swap
175
260
  end.compact
176
261
  end
177
262
 
263
+ def mark_exceeded(swap:, datetime:, debug: false, sym: nil)
264
+ to_save = {
265
+ datetime: datetime,
266
+ ref: swap[:digest],
267
+ side: swap[:side],
268
+ exceeded: datetime
269
+ }
270
+ sym ||= Cotcube::Helpers.get_id_set(contract: swap[:contract])
271
+ save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], sym: sym, contract: swap[:contract], quiet: (not debug)
272
+ swap
273
+ end
274
+
275
+ def mark_ignored(swap:, datetime: DateTime.now, sym: , debug: true)
276
+ to_save = {
277
+ datetime: datetime,
278
+ ref: swap[:digest],
279
+ side: swap[:side],
280
+ ignored: datetime
281
+ }
282
+ save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], sym: sym, contract: swap[:contract], quiet: (not debug)
283
+ swap
284
+ end
285
+
178
286
  end
179
287
  end
180
288
 
@@ -1,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Cotcube
2
4
  module Level
3
- # TODO: Missing documentation of shiftsets, swap_types and stencil_types
5
+
4
6
  class Intraday_Stencil
5
7
 
6
- GLOBAL_SOW = { 'CT' => '0000-1700' }
7
- GLOBAL_EOW = { 'CT' => '1700-0000' }
8
- GLOBAL_EOD = { 'CT' => '1600-1700' }
9
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
+ #
10
12
 
11
13
  def self.shiftset(asset:, sym: nil)
12
14
  shiftset_file = '/var/cotcube/level/stencils/shiftsets.csv'
@@ -18,45 +20,66 @@ module Cotcube
18
20
  sym ||= Cotcube::Helpers.get_id_set(symbol: asset)
19
21
  current_set = shiftsets.find{|s| s[:symbols] =~ /#{sym[:type]}/ }
20
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?
21
- raise "Cannot get shiftset for #{sym[:type]}: #{asset}, please prepare #{shiftset_file} before!"
23
+ raise "Cannot get shiftset for #{sym[:type]}: #{asset}, please prepare #{shiftset_file} before!"
22
24
  end
23
25
 
24
26
  attr_reader :base, :shiftset, :timezone, :datetime, :zero, :index
25
27
 
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
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
+ measuring: false,
39
+ version: nil, # when referring to a specicic version of the stencil
40
+ stencil: nil, # instead of preparing, use this one if set
41
+ warnings: true # be more quiet
42
+ )
43
+ @shiftset = Intraday_Stencil.shiftset(asset: asset)
44
+ @timezone = Cotcube::Level::TIMEZONES[@shiftset[:tz]]
45
+ @debug = debug
46
+ @interval = interval
47
+ @swap_type = swap_type
48
+ @warnings = warnings
49
+ datetime ||= DateTime.now
50
+ datetime = @timezone.at(datetime.to_i) unless datetime.is_a? ActiveSupport::TimeWithZone
51
+ @datetime = datetime.beginning_of_day
52
+ @datetime += interval while @datetime <= datetime - interval
53
+ @datetime -= interval
54
+
55
+ now = DateTime.now
56
+ measure = lambda {|x| puts "\nMeasured #{(Time.now - now).to_f.round(2)}: ".colorize(:light_yellow) + x + "\n\n" if measuring }
57
+
58
+ measure.call "Starting initialization for asset '#{asset}' "
59
+
60
+ const = "RAW_INTRA_STENCIL_#{@shiftset[:nr]}_#{interval.in_minutes.to_i}_#{weeks}_#{future}".to_sym
61
+ cachefile = "/var/cotcube/level/stencils/cache/#{swap_type.to_s}_#{interval}_#{@datetime.strftime('%Y-%m-%d-%H-%M')}.json"
62
+
44
63
  if Object.const_defined? const
64
+ measure.call 'getting cached base from memory'
45
65
  @base = (Object.const_get const).map{|z| z.dup}
66
+ elsif File.exist? cachefile
67
+ measure.call 'getting cached base from file'
68
+ @base = JSON.parse(File.read(cachefile), symbolize_names: true).map{|z| z[:datetime] = DateTime.parse(z[:datetime]); z[:type] = z[:type].to_sym; z }
46
69
  else
47
-
70
+ measure.call 'creating base from shiftset'
48
71
  start_time = lambda {|x| @shiftset[x].split('-').first rescue '' }
49
72
  start_hours = lambda {|x| @shiftset[x].split('-').first[ 0.. 1].to_i.send(:hours) rescue 0 }
50
73
  start_minutes = lambda {|x| @shiftset[x].split('-').first[-2..-1].to_i.send(:minutes) rescue 0 }
51
74
  end_time = lambda {|x| @shiftset[x].split('-').last rescue '' }
52
75
  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 }
76
+ end_minutes = lambda {|x| @shiftset[x].split('-').last [-2..-1].to_i.send(:minutes) rescue 0 }
54
77
 
55
- runner = (@datetime -
78
+ runner = (@datetime -
56
79
  weeks * 7.days).beginning_of_week(:sunday)
57
80
  tm_runner = lambda { runner.strftime('%H%M') }
58
- @base = []
59
- (weeks+future).times do
81
+ @base = []
82
+ (weeks+future).times do
60
83
  while tm_runner.call < GLOBAL_SOW[@shiftset[:tz]].split('-').last
61
84
  # if daylight is switched, this phase will be shorter or longer
62
85
  @base << { datetime: runner, type: :sow }
@@ -81,10 +104,10 @@ module Cotcube
81
104
  yet_rth = true
82
105
  end
83
106
  end
84
- while ((sophase > eophase) ? (tm_runner.call >= sophase or tm_runner.call < eophase) : (tm_runner.call < eophase))
107
+ while ((sophase > eophase) ? (tm_runner.call >= sophase or tm_runner.call < eophase) : (tm_runner.call < eophase))
85
108
  current = { datetime: runner, type: phase }
86
109
  if phase == :rth and not yet_rth
87
- current[:block] = true
110
+ current[:block] = true
88
111
  yet_rth = true
89
112
  end
90
113
  @base << current
@@ -100,80 +123,111 @@ module Cotcube
100
123
  while runner < end_of_week
101
124
  @base << { datetime: runner, type: :eow }
102
125
  runner += interval
103
- end
126
+ end
104
127
  end
105
128
  Object.const_set(const, @base.map{|z| z.dup})
129
+ File.open(cachefile, 'w'){|f| f.write(@base.to_json)}
130
+ end
131
+ measure.call "base created with #{@base.size} records"
132
+
133
+ case swap_type
134
+ when :full
135
+ @base.select!{|x| %i[ pre rth post ].include?(x[:type]) }
136
+ when :rth
137
+ @base.select!{|x| x[:type] == :rth }
138
+ when :flow
139
+ @base.reject!{|x| %i[ sow eow mpost mpost ].include?(x[:type]) }
140
+ @base.
141
+ map{ |x|
142
+ [:high, :low, :volume].map{|z| x[z] = nil} unless x[:type] == :rth
143
+ }
144
+ when :run
145
+ @base.select!{|x| %i[ pre rth post ].include? x[:type]}
146
+ else
147
+ raise ArgumentError, "Unknown stencil/swap type '#{swap_type}'"
106
148
  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]
149
+ measure.call "swaptype #{swap_type} applied"
150
+ @base.map!{|z| z.dup}
151
+ measure.call 'base dupe\'d'
152
+
153
+ # zero is, were either x[:datetime] == @datetime (when we are intraday)
154
+ # or otherwise {x[:datetime] <= @datetime}.last (when on maintenance)
155
+ selector = @base.select{|y| y[:datetime] <= @datetime }.last
156
+ @index = @base.index{|x| x == selector }
157
+ measure.call 'index selected'
158
+ @index -= 1 while %i[sow sod mpre mpost eod eow].include? @base[@index][:type]
159
+ measure.call 'index adjusted'
110
160
  @datetime = @base[@index][:datetime]
111
161
  @zero = @base[@index]
112
- counter = 0
162
+ counter = 0
163
+ measure.call "Applying counter to past"
113
164
  while @base[@index - counter] and @index - counter >= 0
114
165
  @base[@index - counter][:x] = counter
115
166
  counter += 1
116
167
  end
117
- counter = 0
168
+ counter = 0
169
+ measure.call "Applying counter to future"
118
170
  while @base[@index + counter] and @index + counter < @base.length
119
171
  @base[@index + counter][:x] = -counter
120
172
  counter += 1
121
173
  end
122
- @base.select!{|z| z[:x] <= 0 or z[:high]}
174
+ measure.call 'initialization finished'
175
+ end
176
+
177
+ def zero
178
+ @zero ||= @base.find{|b| b[:x].zero? }
123
179
  end
124
180
 
125
- def apply!(to:, type:)
126
- apply(to: to, type: type, force: true)
181
+ def index(offset = 0)
182
+ @index ||= @base.index{|b| b[:x].zero? }
183
+ @base[@index + offset]
127
184
  end
128
185
 
129
- # :force will apply values to each bar regardless of existing ones
130
- def apply(to:, type:, force: false, debug: false)
186
+
187
+ def apply(to: )
131
188
  offset = 0
132
- to.each_index do |i|
189
+ @base.each_index do |i|
133
190
  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
191
+ offset += 1 while to[i+offset][:datetime] < @base[i][:datetime]
136
192
  rescue
137
193
  # appending
138
- puts "appending #{i}\t#{offset}\t#{@base[i+offset][:datetime]} < #{to[i][:datetime]}" if debug
139
- @base << to[i]
194
+ to << @base[i]
140
195
  next
141
196
  end
142
- if @base[i+offset][:datetime] > to[i][:datetime]
197
+ if to[i+offset][:datetime] > @base[i][:datetime]
143
198
  # skipping
144
- puts "skipping #{i}\t#{offset}\t#{@base[i+offset][:datetime]} < #{to[i][:datetime]}" if debug
145
199
  offset -= 1
146
200
  next
147
201
  end
148
202
  # 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
203
+ to[i+offset][:x] = @base[i][:x]
204
+ to[i+offset][:type] = @base[i][:type]
152
205
  end
153
206
  # 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}
207
+ to.reject!{|x| x[:x].nil? }
173
208
  end
174
209
 
210
+ def use(with:, sym:, zero: nil, grace: -2)
211
+ # todo: validate with (check if vslid swap
212
+ # sym (check keys)
213
+ # zero (ohlc with x.zero?)
214
+ # side ( upper or lower)
215
+ swap = with.dup
216
+ high = swap[:side] == :upper
217
+ ohlc = high ? :high : :low
218
+ start = base.find{|x| swap[:datetime] == x[:datetime]}
219
+ swap[:current_change] = (swap[:tpi] * start[:x]).round(8)
220
+ swap[:current_value] = swap[:members].last[ ohlc ] + swap[:current_change] * sym[:ticksize]
221
+ unless zero.nil?
222
+ swap[:current_diff] = (swap[:current_value] - zero[ohlc]) * (high ? 1 : -1 )
223
+ swap[:current_dist] = (swap[:current_diff] / sym[:ticksize]).to_i
224
+ swap[:alert] = (swap[:current_diff] / zero[:atr5]).round(2)
225
+ swap[:exceeded] = zero[:datetime] if swap[:current_dist] < grace
226
+ end
227
+ swap
228
+ end
175
229
  end
176
230
 
177
- Intraday_Stencils = Intraday_Stencil
178
231
  end
232
+
179
233
  end
@@ -19,6 +19,7 @@ module Cotcube
19
19
  swap_type: nil, # if not given, a warning is printed and swaps won't be saved or loaded
20
20
  with_flaws: 0, # the maximum amount of consecutive bars that would actually break the current swap
21
21
  # should be set to 0 for dailies and I suggest no more than 3 for intraday
22
+ manual: false, # some triggers must be set differently when manual entry is used
22
23
  deviation: 2 # the maximum shift of :x-values of found members
23
24
  )
24
25
 
@@ -70,8 +71,10 @@ module Cotcube
70
71
 
71
72
  # abs_peak is the absolute high / low of the base. the shearing operation ends there,
72
73
  # but results might be influenced when abs_peak becomes affected by :with_flaws
73
- abs_peak = base.send(high ? :max_by : :min_by){|x| x[high ? :high : :low] }[:datetime]
74
- base.reject!{|x| x[:datetime] < abs_peak}
74
+ unless manual
75
+ abs_peak = base.send(high ? :max_by : :min_by){|x| x[high ? :high : :low] }[:datetime]
76
+ base.reject!{|x| x[:datetime] < abs_peak}
77
+ end
75
78
 
76
79
  ###########################################################################################################################z
77
80
  # only if (and only if) the range portion above change the underlying base
@@ -155,7 +158,7 @@ module Cotcube
155
158
  # first member is solitary
156
159
  if new_members.empty?
157
160
  mem_sorted=members.sort
158
- if mem_sorted[1] == mem_sorted[0] + 1
161
+ if mem_sorted[1] == mem_sorted[0] + 1 and not manual
159
162
  b2 = b[mem_sorted[1]..mem_sorted[-1]].map{|x| x.dup; x[:dx] = nil; x}
160
163
  puts 'starting recursive rerun'.light_red if debug
161
164
  alternative_slope = get_slope.call(b2)
data/lib/cotcube-level.rb CHANGED
@@ -40,6 +40,8 @@ module Cotcube
40
40
  :get_jsonl_name,
41
41
  :load_swaps,
42
42
  :check_exceedance,
43
+ :mark_exceeded,
44
+ :mark_ignored,
43
45
  :member_to_human
44
46
 
45
47
  # please note that module_functions of sources provided in non-public files must slso be published within these
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.3.2
4
+ version: 0.3.4.2
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-29 00:00:00.000000000 Z
11
+ date: 2021-12-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -120,6 +120,8 @@ files:
120
120
  - Gemfile
121
121
  - README.md
122
122
  - VERSION
123
+ - bin/iswaps.rb
124
+ - bin/swaps.rb
123
125
  - cotcube-level.gemspec
124
126
  - lib/cotcube-level.rb
125
127
  - lib/cotcube-level/detect_slope.rb
@@ -149,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
151
  - !ruby/object:Gem::Version
150
152
  version: '0'
151
153
  requirements: []
152
- rubygems_version: 3.1.2
154
+ rubygems_version: 3.1.6
153
155
  signing_key:
154
156
  specification_version: 4
155
157
  summary: A gem to shear a time series