cotcube-level 0.3.3 → 0.3.4.3

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: 9a349cee7bdea128658af11ee026dcc2ff995d711c930c5ee188396b1cca045a
4
- data.tar.gz: edc2b404b579336d585efeba20b12d8e2572ae3242a939aa060c286b405eee4e
3
+ metadata.gz: 7a77d2be53fcf299f94403671818d6b31a8f0a3bf5bf59e24ac2ab55986fca91
4
+ data.tar.gz: 84f71776307891edfee1a60fed2c8ae20def673f698becf7b6ad64b817f19971
5
5
  SHA512:
6
- metadata.gz: e395bcc4db6052b95b08a1daad0d3ed1b344f533aa779d257de64ba205daf23ccc40853e284ff55dace36c8d3e4e6053e2694a37cee75def861c29e8cab28efe
7
- data.tar.gz: c6e059bd6dfbac4a4a4e3da069ff557f9ebecf46596983f056c78c6675447f6a3cd3eedde2b0ad2f48f9552c1e5bc01ba3fc2057a5f81f4129db3360d501ce50
6
+ metadata.gz: 0f8620c5666ace4d7cea949f0b3cb28a73acfc9ba3167f4d14742e8637daccb5edf50e37aa62008f966941c36ddd7c3167dd88cb9937e7d4500980affcdd77e4
7
+ data.tar.gz: e63bb887f9b6616aa3f43b7704541094334f79e2398f76c95d7751df58961a844378f5462d2ff3096bc7bf061a3dc98b34355c608d8784f215e0b08b436c2415
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 0.3.4.3 (December 23, 2021)
2
+ - gemspec: raised activesupport to v7
3
+ - intraday_stencil: changed naming scheme for cachefile
4
+ - eod_stencil: added :timezone to self.provide_eod_stencil
5
+
6
+ ## 0.3.4.2 (November 28, 2021)
7
+ - added 2 executables to bin to display eod- and intraday-swaps
8
+ - helpers: adding ignorance to load_swaps and other, adding .mark_ignored
9
+ - intraday_stencil: allowing absence of :zero in #use
10
+
11
+ ## 0.3.4.1 (October 09, 2021)
12
+ - intraday_stencil: fixing @index, that did not work when used outside active hours
13
+
14
+ ## 0.3.4 (October 06, 2021)
15
+ - intraday_stencil: major rework resp. rebuild ... now beta-ready
16
+ - helpers: preparing puts_swap and get_jsonl_name to work intraday
17
+ - puts_swap: changed output scheme to provide exceedance as NOTE
18
+
1
19
  ## 0.3.3 (October 05, 2021)
2
20
  - helpers::load_swaps: added :exceed to allow 1(sic) swap to be exceeded while loading
3
21
  - tritangulate: added :manual for feature of manual swap creation with base of 2 members
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.3
1
+ 0.3.4.3
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
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
27
  spec.require_paths = ['lib']
28
28
 
29
- spec.add_dependency 'activesupport', '~> 6'
29
+ spec.add_dependency 'activesupport', '~> 7'
30
30
  spec.add_dependency 'colorize', '~> 0.8'
31
31
  spec.add_dependency 'cotcube-helpers', '~> 0.1'
32
32
  spec.add_dependency 'yaml', '~> 0.1'
@@ -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?)
@@ -147,14 +152,16 @@ module Cotcube
147
152
  start = base.find{|x| swap[:datetime] == x[:datetime]}
148
153
  swap[:current_change] = (swap[:tpi] * start[:x]).round(8)
149
154
  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
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,7 +23,7 @@ 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)
26
+ def member_to_human(member,side: ,format:, daily: false, tws: false)
27
27
  high = (side == :upper)
28
28
  "#{ member[:datetime].strftime("%a, %Y-%m-%d#{daily ? "" :" %I:%M%p"}")
29
29
  } x: #{format '%-4d', member[:x]
@@ -40,37 +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: true, notice: nil, hash: 3)
44
- return if swap[:empty]
45
- daily = %i[ continuous daily ].include?(swap[:interval].to_sym)
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 (hash ? "#{swap[:digest][...hash]} ".colorize(:light_white) : '') +
51
- "S: #{swap[:side]
52
- } L: #{ format '%4d', swap[:length]
53
- } R: #{ format '%4d', swap[:rating]
54
- } D: #{ format '%4d', swap[:depth]
55
- } P: #{ format '%10s', (format '%5.2f', swap[:ppi])
56
- } F: #{ format format, swap[:members].last[ ohlc ]
57
- } S: #{ swap[:members].first[:datetime].strftime(datetime_format)
58
- } - #{ swap[:members].last[:datetime].strftime(datetime_format)
59
- }#{ format('%20s', (swap[:exceeded] ? " XXX: #{swap[:exceeded].strftime(datetime_format)}" : ''))
60
- }#{" 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
61
101
  else
62
- puts "side: #{swap[:side] }\tlen: #{swap[:length]} \trating: #{swap[:rating]}".colorize(swap[:color] || :white )
63
- puts "diff: #{swap[:ticks]}\tdif: #{swap[:diff].round(7)}\tdepth: #{swap[:depth]}".colorize(swap[:color] || :white )
64
- puts "tpi: #{swap[:tpi] }\tppi: #{swap[:ppi]}".colorize(swap[:color] || :white )
65
- puts "NOTE: #{notice}".colorize(:light_white) unless notice.nil?
66
- 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
67
109
  end
110
+ res
68
111
  end
69
112
 
70
113
  # create a standardized name for the cache files
71
114
  # and, on-the-fly, create these files plus their directory
72
115
  def get_jsonl_name(interval:, swap_type:, contract:, sym: nil)
73
- 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)
74
117
  raise "Swaptype #{swap_type} is not supported, please choose from #{SWAPTYPES}" unless SWAPTYPES.include? swap_type
75
118
  sym ||= Cotcube::Helpers.get_id_set(contract: contract)
76
119
  root = '/var/cotcube/level'
@@ -79,7 +122,9 @@ module Cotcube
79
122
  `mkdir -p #{dir}` unless File.exist?(dir)
80
123
  `ln -s #{dir} #{symlink}` unless File.exist?(symlink)
81
124
  file = "#{dir}/#{contract}_#{interval.to_s}_#{swap_type.to_s}.jsonl"
82
- `touch #{file}`
125
+ unless File.exist? file
126
+ `touch #{file}`
127
+ end
83
128
  file
84
129
  end
85
130
 
@@ -115,7 +160,9 @@ module Cotcube
115
160
 
116
161
  # loading of swaps is also straight forward
117
162
  # it takes few more efforts to normalize the values to their expected format
118
- def load_swaps(interval:, swap_type:, contract:, sym: nil, datetime: nil, recent: false, digest: nil, quiet: false, exceed: false)
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)
119
166
  file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
120
167
  jsonl = File.read(file)
121
168
  data = jsonl.
@@ -126,12 +173,13 @@ module Cotcube
126
173
  tap do |sw|
127
174
  sw[:datetime] = DateTime.parse(sw[:datetime]) rescue nil
128
175
  (sw[:exceeded] = DateTime.parse(sw[:exceeded]) rescue nil) if sw[:exceeded]
176
+ (sw[:ignored] = DateTime.parse(sw[:ignored]) rescue nil) if sw[:ignored]
129
177
  sw[:interval] = interval
130
178
  sw[:swap_type] = swap_type
131
179
  sw[:contract] = contract
132
180
  %i[ side ].each {|key| sw[key] = sw[key].to_sym rescue false }
133
- unless sw[:empty] or sw[:exceeded]
134
- sw[:color] = sw[:color].to_sym
181
+ unless sw[:empty] or sw[:exceeded] or sw[:ignored]
182
+ sw[:color] = sw[:color].to_sym
135
183
  sw[:members].map{|mem| mem[:datetime] = DateTime.parse(mem[:datetime]) }
136
184
  end
137
185
  end
@@ -142,13 +190,20 @@ module Cotcube
142
190
  raise RuntimeError, "Inconsistent history for '#{exc}'. Origin not found." if swap.nil?
143
191
  swap[:exceeded] = exc[:exceeded]
144
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
145
199
  # do not return bare exceedance information
146
- data.reject!{|swap| swap[:exceeded] and swap[:members].nil? }
200
+ data.reject!{|swap| (swap[:ignored] or swap[:exceeded]) and swap[:members].nil? }
147
201
  # do not return swaps that are found 'later'
148
202
  data.reject!{|swap| swap[:datetime] > datetime } unless datetime.nil?
149
203
  # do not return exceeded swaps, that are exceeded in the past
150
204
  recent = 7.days if recent.is_a? TrueClass
151
205
  recent += 5.hours if recent
206
+ data.reject!{|swap| swap[:ignored] } unless keep_ignored
152
207
  data.reject!{|swap| swap[:exceeded] and swap[:exceeded] < datetime - (recent ? recent : 0) } unless datetime.nil?
153
208
  # remove exceedance information that is found 'later'
154
209
  data.map{|swap| swap.delete(:exceeded) if swap[:exceeded] and swap[:exceeded] > datetime} unless datetime.nil?
@@ -200,20 +255,31 @@ module Cotcube
200
255
  save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], contract: contract, sym: sym, quiet: (not debug)
201
256
  swap[:exceeded] = update[:exceeded]
202
257
  end
203
- %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] }
204
259
  swap
205
260
  end.compact
206
261
  end
207
262
 
208
- def mark_exceeded(swap:, datetime:, debug: false)
263
+ def mark_exceeded(swap:, datetime:, debug: false, sym: nil)
209
264
  to_save = {
210
265
  datetime: datetime,
211
266
  ref: swap[:digest],
212
267
  side: swap[:side],
213
268
  exceeded: datetime
214
269
  }
215
- save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], sym: Cotcube::Helpers.get_id_set(contract: swap[:contract]), contract: swap[:contract], quiet: (not debug)
216
- swap[:exceeded] = datetime
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)
217
283
  swap
218
284
  end
219
285
 
@@ -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/#{asset.to_s}_#{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
+ index(0)
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
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.3
4
+ version: 0.3.4.3
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-10-05 00:00:00.000000000 Z
11
+ date: 2021-12-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '6'
19
+ version: '7'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '6'
26
+ version: '7'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: colorize
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -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