cotcube-level 0.3.2 → 0.3.4.2

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 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