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 +4 -4
- data/CHANGELOG.md +21 -0
- data/VERSION +1 -1
- data/bin/iswaps.rb +45 -0
- data/bin/swaps.rb +45 -0
- data/lib/cotcube-level/eod_stencil.rb +18 -11
- data/lib/cotcube-level/helpers.rb +152 -44
- data/lib/cotcube-level/intraday_stencil.rb +125 -71
- data/lib/cotcube-level/tritangulate.rb +6 -3
- data/lib/cotcube-level.rb +2 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4bbf4fc1b2edf64ffee60a29ffe7d72a71a4c0f8fb99b889026dce9dbca7399f
|
4
|
+
data.tar.gz: 59469c2431a20e9e1f07a99e0af61a4fed59ca276a02d654a2f2bd28a8543c04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
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
|
-
|
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
|
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] =
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
162
|
+
end
|
156
163
|
|
157
|
-
|
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',
|
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,
|
33
|
-
} i: #{(format '%4d',
|
34
|
-
} d: #{format '%6.2f',
|
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:
|
44
|
-
return if swap[:empty]
|
45
|
-
|
46
|
-
|
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
|
-
|
51
|
-
}
|
52
|
-
}
|
53
|
-
}
|
54
|
-
|
55
|
-
}
|
56
|
-
}
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
swap[:members].each {|x|
|
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
|
-
|
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
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
sw[:
|
134
|
-
|
135
|
-
|
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, "
|
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
|
-
|
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
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
@
|
42
|
-
@
|
43
|
-
|
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
|
-
|
108
|
-
@
|
109
|
-
|
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
|
-
|
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
|
126
|
-
|
181
|
+
def index(offset = 0)
|
182
|
+
@index ||= @base.index{|b| b[:x].zero? }
|
183
|
+
@base[@index + offset]
|
127
184
|
end
|
128
185
|
|
129
|
-
|
130
|
-
def apply(to
|
186
|
+
|
187
|
+
def apply(to: )
|
131
188
|
offset = 0
|
132
|
-
|
189
|
+
@base.each_index do |i|
|
133
190
|
begin
|
134
|
-
offset += 1 while
|
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
|
-
|
139
|
-
@base << to[i]
|
194
|
+
to << @base[i]
|
140
195
|
next
|
141
196
|
end
|
142
|
-
if
|
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
|
-
|
150
|
-
|
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
|
-
|
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
|
-
|
74
|
-
|
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
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-
|
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.
|
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
|