Class: Cotcube::Level::Intraday_Stencil

Inherits:
Object
  • Object
show all
Defined in:
lib/cotcube-level/intraday_stencil.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(asset:, interval: 30.minutes, swap_type: :full, datetime: nil, debug: false, weeks: 6, future: 2, measuring: false, version: nil, stencil: nil, warnings: true) ⇒ Intraday_Stencil

Returns a new instance of Intraday_Stencil.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/cotcube-level/intraday_stencil.rb', line 30

def initialize(
  asset:,
  interval: 30.minutes,
  swap_type: :full,
  datetime: nil,
  debug: false,
  weeks: 6,
  future: 2,
  measuring: false,
  version: nil,               # when referring to a specicic version of the stencil
  stencil: nil,               # instead of preparing, use this one if set
  warnings: true              # be more quiet
)
  @shiftset   = Intraday_Stencil.shiftset(asset: asset)
  @timezone   = Cotcube::Level::TIMEZONES[@shiftset[:tz]]
  @debug      = debug
  @interval   = interval
  @swap_type  = swap_type
  @warnings   = warnings
  datetime  ||= DateTime.now
  datetime    = @timezone.at(datetime.to_i) unless datetime.is_a? ActiveSupport::TimeWithZone
  @datetime   = datetime.beginning_of_day
  @datetime  += interval while @datetime <= datetime - interval
  @datetime  -= interval

  now         = DateTime.now
  measure = lambda {|x| puts "\nMeasured #{(Time.now - now).to_f.round(2)}: ".colorize(:light_yellow) +  x + "\n\n" if measuring }

  measure.call "Starting initialization for asset '#{asset}' "

  const = "RAW_INTRA_STENCIL_#{@shiftset[:nr]}_#{interval.in_minutes.to_i}_#{weeks}_#{future}".to_sym
  cachefile = "/var/cotcube/level/stencils/cache/#{asset.to_s}_#{swap_type.to_s}_#{interval}_#{@datetime.strftime('%Y-%m-%d-%H-%M')}.json"

  if Object.const_defined? const
    measure.call 'getting cached base from memory'
    @base = (Object.const_get const).map{|z| z.dup}
  elsif File.exist? cachefile
    measure.call 'getting cached base from file'
    @base = JSON.parse(File.read(cachefile), symbolize_names: true).map{|z| z[:datetime] = DateTime.parse(z[:datetime]); z[:type] = z[:type].to_sym; z }
  else
    measure.call 'creating base from shiftset'
    start_time    = lambda {|x| @shiftset[x].split('-').first rescue '' }
    start_hours   = lambda {|x| @shiftset[x].split('-').first[ 0.. 1].to_i.send(:hours)   rescue 0 }
    start_minutes = lambda {|x| @shiftset[x].split('-').first[-2..-1].to_i.send(:minutes) rescue 0 }
    end_time      = lambda {|x| @shiftset[x].split('-').last  rescue '' }
    end_hours     = lambda {|x| @shiftset[x].split('-').last [ 0.. 1].to_i.send(:hours)   rescue 0 }
    end_minutes   = lambda {|x| @shiftset[x].split('-').last [-2..-1].to_i.send(:minutes) rescue 0 }

    runner = (@datetime -
              weeks * 7.days).beginning_of_week(:sunday)
    tm_runner = lambda { runner.strftime('%H%M') }
    @base = []
    (weeks+future).times do
      while tm_runner.call < GLOBAL_SOW[@shiftset[:tz]].split('-').last
        # if daylight is switched, this phase will be shorter or longer
        @base << { datetime: runner, type: :sow }
        runner += interval
      end
      end_of_week = runner + 6.days + 7.hours

      5.times do |i|
        # TODO: mark holidays as such
        [:sod, :pre, :mpre, (i<4 ? :rth : :rth5), :post, (i<4 ? :mpost : :mpost5)].each do |phase|
          yet_rth = false
          unless start_time.call(phase).empty?
            eophase = end_time.call(phase)
            sophase = start_time.call(phase)
            phase = :rth   if phase == :rth5
            phase = :mpost if phase == :mpost5
            if %i[ pre rth ].include? phase and tm_runner.call > sophase
              # fix previous interval
              @base.last[:type] = phase
              if phase == :rth and not yet_rth
                @base.last[:block] = true
                yet_rth = true
              end
            end
            while ((sophase > eophase) ? (tm_runner.call >= sophase or tm_runner.call < eophase) : (tm_runner.call < eophase))
              current = { datetime: runner, type: phase }
              if phase == :rth and not yet_rth
                current[:block] = true
                yet_rth = true
              end
              @base << current
              runner += interval
            end
          end
        end
        while tm_runner.call < GLOBAL_EOD[@shiftset[:tz]].split('-').last
          @base << { datetime: runner, type: :eod }
          runner += interval
        end
      end # 5.times
      while runner < end_of_week
        @base << { datetime: runner, type: :eow }
        runner += interval
      end
    end
    Object.const_set(const, @base.map{|z| z.dup})
    File.open(cachefile, 'w'){|f| f.write(@base.to_json)}
  end
  measure.call "base created with #{@base.size} records"

  case swap_type
  when :full
    @base.select!{|x| %i[ pre rth post ].include?(x[:type]) }
  when :rth
    @base.select!{|x| x[:type] == :rth  }
  when :flow
    @base.reject!{|x| %i[ sow eow mpost mpost ].include?(x[:type]) }
    @base.
      map{ |x|
      [:high, :low, :volume].map{|z| x[z] = nil} unless x[:type] == :rth
    }
  when :run
    @base.select!{|x| %i[ pre rth post ].include? x[:type]}
  else
    raise ArgumentError, "Unknown stencil/swap type '#{swap_type}'"
  end
  measure.call "swaptype #{swap_type} applied"
  @base.map!{|z| z.dup}
  measure.call 'base dupe\'d'

  # zero is, were either x[:datetime] == @datetime (when we are intraday)
  #         or otherwise {x[:datetime] <= @datetime}.last (when on maintenance)
  selector =  @base.select{|y| y[:datetime] <= @datetime }.last
  @index = @base.index{|x| x == selector }
  measure.call 'index selected'
  @index -= 1 while %i[sow sod mpre mpost eod eow].include? @base[@index][:type]
  measure.call 'index adjusted'
  @datetime = @base[@index][:datetime]
  @zero  = @base[@index]
  counter = 0
  measure.call "Applying counter to past"
  while @base[@index - counter] and @index - counter >= 0
    @base[@index - counter][:x] = counter
    counter += 1
  end
  counter = 0
  measure.call "Applying counter to future"
  while @base[@index + counter] and @index + counter < @base.length
    @base[@index + counter][:x] = -counter
    counter += 1
  end
  measure.call 'initialization finished'
end

Instance Attribute Details

#baseObject (readonly)

Returns the value of attribute base.



26
27
28
# File 'lib/cotcube-level/intraday_stencil.rb', line 26

def base
  @base
end

#datetimeObject (readonly)

Returns the value of attribute datetime.



26
27
28
# File 'lib/cotcube-level/intraday_stencil.rb', line 26

def datetime
  @datetime
end

#index(offset = 0) ⇒ Object (readonly)

Returns the value of attribute index.



26
27
28
# File 'lib/cotcube-level/intraday_stencil.rb', line 26

def index
  @index
end

#shiftsetObject (readonly)

Returns the value of attribute shiftset.



26
27
28
# File 'lib/cotcube-level/intraday_stencil.rb', line 26

def shiftset
  @shiftset
end

#timezoneObject (readonly)

Returns the value of attribute timezone.



26
27
28
# File 'lib/cotcube-level/intraday_stencil.rb', line 26

def timezone
  @timezone
end

#zeroObject (readonly)

Returns the value of attribute zero.



26
27
28
# File 'lib/cotcube-level/intraday_stencil.rb', line 26

def zero
  @zero
end

Class Method Details

.shiftset(asset:, sym: nil) ⇒ Object

Class method that loads the (latest) shiftset for given asset These raw stencils are located in /var/cotcube/level/stencils/shiftsets.csv



13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/cotcube-level/intraday_stencil.rb', line 13

def self.shiftset(asset:, sym: nil)
  shiftset_file = '/var/cotcube/level/stencils/shiftsets.csv'
  headers =  %i[nr tz sod pre mpre rth post mpost rth5 mpost5 symbols]
  shiftsets = CSV.read(shiftset_file, headers: headers).
    map{|x| x.to_h}
  current_set = shiftsets.find{|s| s[:symbols] =~ /#{asset}/ }
  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?
  sym ||= Cotcube::Helpers.get_id_set(symbol: asset)
  current_set = shiftsets.find{|s| s[:symbols] =~ /#{sym[:type]}/ }
  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?
  raise "Cannot get shiftset for #{sym[:type]}: #{asset}, please prepare #{shiftset_file} before!"
end

Instance Method Details

#apply(to:) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/cotcube-level/intraday_stencil.rb', line 187

def apply(to: )
  offset = 0
  @base.each_index do |i|
    begin
      offset += 1 while to[i+offset][:datetime] < @base[i][:datetime]
    rescue
      # appending
      to << @base[i]
      next
    end
    if to[i+offset][:datetime] > @base[i][:datetime]
      # skipping
      offset -= 1
      next
    end
    # merging
    to[i+offset][:x] = @base[i][:x]
    to[i+offset][:type] = @base[i][:type]
  end
  # finally remove all bars that do not belong to the stencil (i.e. holidays)
  to.reject!{|x| x[:x].nil? }
end

#use(with:, sym:, zero: nil, grace: -2)) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/cotcube-level/intraday_stencil.rb', line 210

def use(with:, sym:, zero: nil, grace: -2)
  # todo: validate with (check if vslid swap
  #                sym  (check keys)
  #                zero (ohlc with x.zero?)
  #                side ( upper or lower)
  swap  = with.dup
  high  = swap[:side] == :upper
  ohlc  = high ? :high : :low
  start = base.find{|x| swap[:datetime] == x[:datetime]}
  swap[:current_change] = (swap[:tpi] * start[:x]).round(8)
  swap[:current_value]  =  swap[:members].last[ ohlc ] + swap[:current_change] * sym[:ticksize]
  unless zero.nil?
    swap[:current_diff]   = (swap[:current_value] - zero[ohlc]) * (high ? 1 : -1 )
    swap[:current_dist]   = (swap[:current_diff] / sym[:ticksize]).to_i
    swap[:alert]          = (swap[:current_diff] / zero[:atr5]).round(2)
    swap[:exceeded]       =  zero[:datetime] if swap[:current_dist] < grace
  end
  swap
end