cotcube-helpers 0.1.10 → 0.2.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/Gemfile +3 -0
- data/VERSION +1 -1
- data/lib/cotcube-helpers/cache_client.rb +28 -0
- data/lib/cotcube-helpers/constants.rb +10 -4
- data/lib/cotcube-helpers/data_client.rb +230 -0
- data/lib/cotcube-helpers/expiration.rb +31 -0
- data/lib/cotcube-helpers/get_id_set.rb +3 -2
- data/lib/cotcube-helpers/ib_contracts.rb +69 -0
- data/lib/cotcube-helpers/orderclient.rb +203 -0
- data/lib/cotcube-helpers/output.rb +20 -0
- data/lib/cotcube-helpers/recognition.rb +509 -0
- data/lib/cotcube-helpers/symbols.rb +50 -18
- data/lib/cotcube-helpers.rb +11 -0
- data/scripts/collect_market_depth +103 -0
- data/scripts/symbols +13 -0
- metadata +12 -3
@@ -0,0 +1,20 @@
|
|
1
|
+
module Cotcube
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
def instance_inspect(obj, keylength: 20, &block)
|
5
|
+
obj.instance_variables.map do |var|
|
6
|
+
if block_given?
|
7
|
+
block.call(var, obj.instance_variable_get(var))
|
8
|
+
else
|
9
|
+
puts "#{format "%-#{keylength}s", var
|
10
|
+
}: #{obj.instance_variable_get(var).inspect.scan(/.{1,120}/).join( "\n" + ' '*(keylength+2))
|
11
|
+
}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module_function :instance_inspect
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,509 @@
|
|
1
|
+
module Cotcube
|
2
|
+
module Helpers
|
3
|
+
module Candlestick_Recognition
|
4
|
+
|
5
|
+
SUPERFLUOUS = %i[wap datetime prev_slope upper lower bar_size body_size lower_wick upper_wick ranges vranges vavg trades type bullish bearish doji contract]
|
6
|
+
COMMON = [:symbol, :timestamp, :size, :rth, :time_end,
|
7
|
+
:open, :high, :vhigh, :vlow, :low, :close,
|
8
|
+
:interval, :offset, :appeared, :datetime, :volume, :dist, :vperd, :vol_i, :contract, :date,
|
9
|
+
:bar_size, :upper_wick, :lower_wick, :body_size, :upper, :lower, :slope, :rel, :tr, :atr, :ranges, :vranges, :vavg]
|
10
|
+
|
11
|
+
# recognize serves as interface
|
12
|
+
def recognize(contract:, interval: :quarters, short: true, base:, return_as_string: false, sym: )
|
13
|
+
s = bas
|
14
|
+
CR::candles s, ticksize: sym[:ticksize]
|
15
|
+
CR::patterns s, contract: contract, ticksize: sym[:ticksize]
|
16
|
+
s.map{|x| x[:datetime] += 7.hours } if s and %w[ GG DY ].include?(contract[..1]) and interval == :quarters
|
17
|
+
s.map{|x| x[:datetime] += 1.hour } if s and %w[ GC PA PL SI HG NG HO RB CL ].include?(contract[..1]) and interval == :quarters
|
18
|
+
make_string = lambda {|c| "#{contract
|
19
|
+
}\t#{c[:datetime].strftime( interval == :quarters ? '%Y-%m-%d %H:%M' : '%Y-%m-%d' )}".colorize(:light_white) +
|
20
|
+
"\t#{print_bar(bar: c, format: sym[:format], power: sym[:power], short: short)
|
21
|
+
} #{"\n\n" if c[:datetime].wday == 5 and interval == :days}" }
|
22
|
+
return_as_string ? s[-count..].map{|candle| make_string.call(candle) }.join("\n") : s
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def print_bar(bar:, format:, power:, short: false)
|
27
|
+
x = bar.dup
|
28
|
+
dir = x[:bullish] ? :bullish : x[:bearish] ? :bearish : x[:doji] ? :doji : :none
|
29
|
+
contract = bar[:contract]
|
30
|
+
SUPERFLUOUS.map{|s| x.delete(s)}
|
31
|
+
%i[ UPPER_PIVOT LOWER_PIVOT UPPER_ISOPIVOT LOWER_ISOPIVOT ].map{|s| x.delete(s)}
|
32
|
+
|
33
|
+
|
34
|
+
vol = x.keys.select{|x| x.to_s =~ /_volume/}[0]
|
35
|
+
x.delete vol
|
36
|
+
|
37
|
+
vol = vol.to_s.split("_")[0]
|
38
|
+
col = case dir
|
39
|
+
when :bullish
|
40
|
+
:light_green
|
41
|
+
when :bearish
|
42
|
+
:light_red
|
43
|
+
when :doji
|
44
|
+
:light_blue
|
45
|
+
else:light_black
|
46
|
+
end
|
47
|
+
|
48
|
+
special = lambda do |s|
|
49
|
+
case s
|
50
|
+
when *%i[ THRUSTING_LINE SHOOTING_STAR HANGING_MAN EVENING_STAR EVENING_DOJI_STAR UPSIDEGAP_TWO_CROWS UMKEHRSTAB_BEARISH ]
|
51
|
+
s.to_s.colorize(:light_red)
|
52
|
+
when *%i[ PIERCING_LINE INVERTED_HAMMER HAMMER MORNING_STAR MORNING_DOJI_STAR DOWNGAP_TWO_RIVERS UMKEHRSTAB_BULLISH ]
|
53
|
+
s.to_s.colorize(:light_green)
|
54
|
+
else
|
55
|
+
s.to_s.colorize(:light_cyan)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
"#{ format '%12s', (format % x[:open ]) }" +
|
62
|
+
" #{format '%12s', (format % x[:high ])}".colorize( (x.keys.include?(:UPPER_PIVOT) or x.keys.include?(:UPPER_ISOPIVOT)) ? :light_blue : :white ) +
|
63
|
+
" #{format '%12s', (format % x[:low ])}".colorize( (x.keys.include?(:LOWER_PIVOT) or x.keys.include?(:LOWER_ISOPIVOT)) ? :light_blue : :white ) +
|
64
|
+
" #{format '%12s', ( format % x[:close ])}" +
|
65
|
+
(short ? "" : " #{"%10s" % ("[ % -4.1f ]" % x[:slope])}".colorize( if x[:slope].abs < 3; :yellow; elsif x[:slope] >= 3; :green; else; :red; end)) +
|
66
|
+
(short ? "" : " #{"% 8d" % x[:volume]}") +
|
67
|
+
" #{vol}".colorize( case vol; when *["BREAKN", "FAINTN"]; :light_blue; when *["RISING","FALLIN"]; :cyan; else; :white; end) +
|
68
|
+
(short ? "" : " D:#{"%5d" % x[:dist]}" ) +
|
69
|
+
(short ? "" : " P:#{"%10.2f" % (x[:dist] * power)} ".cyan ) +
|
70
|
+
format('%10s', dir.to_s).colorize(col) +
|
71
|
+
(short ? "" : format('%-22s', " >>> #{x.keys.map{|v| (COMMON.include?(v) or v.upcase == v)? nil : v}.compact.join(" ")}").colorize( col )) +
|
72
|
+
"\t#{x.keys.map{|v| (COMMON.include?(v) or v.upcase != v)? nil : special.call(v)}.compact.join(" ")}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def candles(candles, debug: false, sym: )
|
76
|
+
ticksize = sym[:ticksize]
|
77
|
+
|
78
|
+
candles.each_with_index do |bar, i|
|
79
|
+
# rel simply sets a grace limit based on the full height of the bar, so we won't need to use the hard limit of zero
|
80
|
+
begin
|
81
|
+
rel = ((bar[:high] - bar[:low]) * 0.05).round(8)
|
82
|
+
rel = 2 * ticksize if rel < 2 * ticksize
|
83
|
+
rescue
|
84
|
+
puts "Warning, found inappropriate bar".light_white + " #{bar}"
|
85
|
+
raise
|
86
|
+
end
|
87
|
+
bar[:rel] = rel
|
88
|
+
bar[:dist] ||= ((bar[:high] - bar[:low])/ticksize).round(8)
|
89
|
+
|
90
|
+
bar[:upper] = [bar[:open], bar[:close]].max
|
91
|
+
bar[:lower] = [bar[:open], bar[:close]].min
|
92
|
+
bar[:bar_size] = (bar[:high] - bar[:low])
|
93
|
+
bar[:body_size] = (bar[:open] - bar[:close]).abs
|
94
|
+
bar[:lower_wick] = (bar[:lower] - bar[:low])
|
95
|
+
bar[:upper_wick] = (bar[:high] - bar[:upper])
|
96
|
+
bar.each{|k,v| bar[k] = v.round(8) if v.is_a? Float}
|
97
|
+
|
98
|
+
# a doji's open and close are same (or only differ by rel)
|
99
|
+
bar[:doji] = true if bar[:body_size] <= rel and bar[:dist] >= 3
|
100
|
+
bar[:tiny] = true if bar[:dist] <= 5
|
101
|
+
|
102
|
+
next if bar[:tiny]
|
103
|
+
|
104
|
+
bar[:bullish] = true if not bar[:doji] and bar[:close] > bar[:open]
|
105
|
+
bar[:bearish] = true if not bar[:doji] and bar[:close] < bar[:open]
|
106
|
+
|
107
|
+
bar[:spinning_top] = true if bar[:body_size] <= bar[:bar_size] / 4 and
|
108
|
+
bar[:lower_wick] >= bar[:bar_size] / 4 and
|
109
|
+
bar[:upper_wick] >= bar[:bar_size] / 4
|
110
|
+
|
111
|
+
# a marubozu open at high or low and closes at low or high
|
112
|
+
bar[:marubozu] = true if bar[:upper_wick] < rel and bar[:lower_wick] < rel
|
113
|
+
|
114
|
+
# a bar is considered bearish if it has at least a dist of 5 ticks and it's close it near high (low resp)
|
115
|
+
bar[:bullish_close] = true if (bar[:high] - bar[:close]) <= rel and not bar[:marubozu]
|
116
|
+
bar[:bearish_close] = true if (bar[:close] - bar[:low]) <= rel and not bar[:marubozu]
|
117
|
+
|
118
|
+
# the distribution of main volume is shown in 5 segments, like [0|0|0|4|5] shows that most volume concentrated at the bottom, [0|0|3|0|0] is heavily centered
|
119
|
+
# TODO
|
120
|
+
|
121
|
+
end
|
122
|
+
candles
|
123
|
+
end
|
124
|
+
|
125
|
+
def comparebars(prev, curr)
|
126
|
+
bullishscore = 0
|
127
|
+
bearishscore = 0
|
128
|
+
bullishscore += 1 if prev[:high] <= curr[:high]
|
129
|
+
bullishscore += 1 if prev[:low] <= curr[:low]
|
130
|
+
bullishscore += 1 if prev[:close] <= curr[:close]
|
131
|
+
bearishscore += 1 if prev[:close] >= curr[:close]
|
132
|
+
bearishscore += 1 if prev[:low] >= curr[:low]
|
133
|
+
bearishscore += 1 if prev[:high] >= curr[:high]
|
134
|
+
r = {}
|
135
|
+
r[:bullish] = true if bullishscore >= 2
|
136
|
+
r[:bearish] = true if bearishscore >= 2
|
137
|
+
return r
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
def patterns(candles, debug: false, size: 5, contract:, ticksize: nil )
|
142
|
+
candles.each_with_index do |bar, i|
|
143
|
+
preceeding = candles.select{|x| x[:datetime] <= bar[:datetime] }
|
144
|
+
if i.zero?
|
145
|
+
bar[:slope] = 0
|
146
|
+
next
|
147
|
+
end
|
148
|
+
ppprev= candles[i-3]
|
149
|
+
pprev= candles[i-2]
|
150
|
+
prev = candles[i-1]
|
151
|
+
succ = candles[i+1]
|
152
|
+
|
153
|
+
bar[:huge] = true if bar[:true_range] >= bar[:atr] * 1.5
|
154
|
+
bar[:small] = true if bar[:true_range] <= bar[:atr5] * 0.6666
|
155
|
+
|
156
|
+
bar[:vavg] = (bar[:vranges].reduce(:+) / bar[:vranges].size.to_f).round if bar[:vranges] and bar[:vranges].compact.size > 0
|
157
|
+
bar[:vranges] = prev[:vranges].nil? ? [ bar[:volume] ] : prev[:vranges] + [ bar[:volume] ]
|
158
|
+
bar[:vranges].shift while bar[:vranges].size > size
|
159
|
+
bar[:vavg] ||= (bar[:vranges].reduce(:+) / bar[:vranges].size.to_f).round if bar[:vranges] and bar[:vranges].compact.size > 0
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
# VOLUME
|
164
|
+
if bar[:volume] > bar[:vavg] * 1.3
|
165
|
+
bar[:BREAKN_volume] = true
|
166
|
+
elsif bar[:volume] >= bar[:vavg] * 1.1
|
167
|
+
bar[:RISING_volume] = true
|
168
|
+
elsif bar[:volume] < bar[:vavg] * 0.7
|
169
|
+
bar[:FAINTN_volume] = true
|
170
|
+
elsif bar[:volume] <= bar[:vavg] * 0.9
|
171
|
+
bar[:FALLIN_volume] = true
|
172
|
+
else
|
173
|
+
bar[:STABLE_volume] = true
|
174
|
+
end
|
175
|
+
|
176
|
+
# GAPS
|
177
|
+
bar[:bodygap] = true if bar[:lower] > prev[:upper] or bar[:upper] < prev[:lower]
|
178
|
+
bar[:gap] = true if bar[:low] > prev[:high] or bar[:high] < prev[:low]
|
179
|
+
|
180
|
+
|
181
|
+
bar[:slope] = slopescore(pprev, prev, bar, debug)
|
182
|
+
bar[:prev_slope] = prev[:slope]
|
183
|
+
|
184
|
+
# UPPER_PIVOTs define by having higher highs and higher lows than their neighor
|
185
|
+
bar[:UPPER_ISOPIVOT] = true if succ and prev[:high] < bar[:high] and prev[:low] <= bar[:low] and succ[:high] < bar[:high] and succ[:low] <= bar[:low] and bar[:lower] >= [prev[:upper], succ[:upper]].max
|
186
|
+
bar[:LOWER_ISOPIVOT] = true if succ and prev[:high] >= bar[:high] and prev[:low] > bar[:low] and succ[:high] >= bar[:high] and succ[:low] > bar[:low] and bar[:upper] <= [prev[:lower], succ[:lower]].min
|
187
|
+
bar[:UPPER_PIVOT] = true if succ and prev[:high] < bar[:high] and prev[:low] <= bar[:low] and succ[:high] < bar[:high] and succ[:low] <= bar[:low] and not bar[:UPPER_ISOPIVOT]
|
188
|
+
bar[:LOWER_PIVOT] = true if succ and prev[:high] >= bar[:high] and prev[:low] > bar[:low] and succ[:high] >= bar[:high] and succ[:low] > bar[:low] and not bar[:LOWER_ISOPIVOT]
|
189
|
+
|
190
|
+
# stopping volume is defined as high volume candle during downtrend then closes above mid candle (i.e. lower_wick > body_size)
|
191
|
+
bar[:stopping_volume] = true if (bar[:BREAKN_volume] or bar[:RISING_volume]) and prev[:slope] < -5 and bar[:lower_wick] >= bar[:body_size]
|
192
|
+
bar[:stopping_volume] = true if (bar[:BREAKN_volume] or bar[:RISING_volume]) and prev[:slope] > 5 and bar[:upper_wick] >= bar[:body_size]
|
193
|
+
bar[:volume_lower_wick] = true if bar[:vhigh] and (bar[:vol_i].nil? or bar[:vol_i] >= 2) and bar[:vhigh] <= bar[:lower] and not bar[:FAINTN_volume] and not bar[:FALLIN_volume]
|
194
|
+
bar[:volume_upper_wick] = true if bar[:vlow] and (bar[:vol_i].nil? or bar[:vol_i] >= 2) and bar[:vlow] >= bar[:upper] and not bar[:FAINTN_volume] and not bar[:FALLIN_volume]
|
195
|
+
|
196
|
+
|
197
|
+
###################################
|
198
|
+
# SINGLE CANDLE PATTERNS
|
199
|
+
###################################
|
200
|
+
|
201
|
+
# a hammer is a bar, whose open or close is at the high and whose body is lte 1/3 of the size, found on falling slope, preferrably gapping away
|
202
|
+
bar[:HAMMER] = true if bar[:upper_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 3 and bar[ :slope] <= -6
|
203
|
+
# same shape, but found at a raising slope without the need to gap away
|
204
|
+
bar[:HANGING_MAN] = true if bar[:upper_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 3 and prev[:slope] >= 6
|
205
|
+
|
206
|
+
# a shooting star is the inverse of the hammer, while the inverted hammer is the inverse of the hanging man
|
207
|
+
bar[:SHOOTING_STAR] = true if bar[:lower_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 2.5 and bar[ :slope] >= 6
|
208
|
+
bar[:INVERTED_HAMMER] = true if bar[:lower_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 3 and prev[:slope] <= -6
|
209
|
+
|
210
|
+
# a star is simply gapping away the preceding slope
|
211
|
+
if ((bar[:lower] > prev[:upper] and bar[:slope] >= 6 and bar[:high] >= prev[:high]) or
|
212
|
+
(bar[:upper] < prev[:lower] and bar[:slope] <= -6) and bar[:low] <= prev[:low])
|
213
|
+
bar[:doji] ? bar[:DOJI_STAR] = true : bar[:STAR] = true
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
# a belthold is has a gap in the open, but reverses strong
|
218
|
+
bar[:BULLISH_BELTHOLD] = true if bar[:lower_wick] <= bar[:rel] and bar[:body_size] >= bar[:bar_size] / 2 and
|
219
|
+
prev[:slope] <= -4 and bar[:lower] <= prev[:low ] and bar[:bullish] and not prev[:bullish] and bar[:bar_size] >= prev[:bar_size]
|
220
|
+
bar[:BEARISH_BELTHOLD] = true if bar[:upper_wick] <= bar[:rel] and bar[:body_size] >= bar[:bar_size] / 2 and
|
221
|
+
prev[:slope] >= -4 and bar[:upper] <= prev[:high] and bar[:bearish] and not prev[:bearish] and bar[:bar_size] >= prev[:bar_size]
|
222
|
+
|
223
|
+
|
224
|
+
###################################
|
225
|
+
# DUAL CANDLE PATTERNS
|
226
|
+
###################################
|
227
|
+
|
228
|
+
|
229
|
+
# ENGULFINGS
|
230
|
+
bar[:BULLISH_ENGULFING] = true if bar[:bullish] and prev[:bearish] and bar[:lower] <= prev[:lower] and bar[:upper] > prev[:upper] and prev[:slope] <= -6
|
231
|
+
bar[:BEARISH_ENGULFING] = true if bar[:bearish] and prev[:bullish] and bar[:lower] < prev[:lower] and bar[:upper] >= prev[:upper] and prev[:slope] >= 6
|
232
|
+
|
233
|
+
|
234
|
+
# DARK-CLOUD-COVER / PIERCING-LINE (on-neck / in-neck / thrusting / piercing / PDF pg 63)
|
235
|
+
bar[:DARK_CLOUD_COVER] = true if bar[:slope] > 5 and prev[:bullish] and bar[:open] > prev[:high] and bar[:close] < prev[:upper] - prev[:body_size] * 0.5 and
|
236
|
+
not bar[:BEARISH_ENGULFING]
|
237
|
+
bar[:PIERCING_LINE] = true if bar[:slope] < -5 and prev[:bearish] and bar[:open] < prev[:low ] and bar[:close] > prev[:lower] + prev[:body_size] * 0.5 and
|
238
|
+
not bar[:BULLISH_ENGULFING]
|
239
|
+
bar[:SMALL_CLOUD_COVER] = true if bar[:slope] > 5 and prev[:bullish] and bar[:open] > prev[:high] and bar[:close] < prev[:upper] - prev[:body_size] * 0.25 and
|
240
|
+
not bar[:BEARISH_ENGULFING] and not bar[:DARK_CLOUD_COVER]
|
241
|
+
bar[:THRUSTING_LINE] = true if bar[:slope] < -5 and prev[:bearish] and bar[:open] < prev[:low ] and bar[:close] > prev[:lower] + prev[:body_size] * 0.25 and
|
242
|
+
not bar[:BULLISH_ENGULFING] and not bar[:PIERCING_LINE]
|
243
|
+
|
244
|
+
|
245
|
+
# COUNTER ATTACKS are like piercings / cloud covers, but insist on a large reverse while only reaching the preceding close
|
246
|
+
bar[:BULLISH_COUNTERATTACK] = true if bar[:slope] < 6 and prev[:bearish] and bar[:bar_size] > bar[:atr] * 0.66 and (bar[:close] - prev[:close]).abs < 2 * bar[:rel] and
|
247
|
+
bar[:body_size] >= bar[:bar_size] * 0.5 and bar[:bullish]
|
248
|
+
bar[:BEARISH_COUNTERATTACK] = true if bar[:slope] > 6 and prev[:bullish] and bar[:bar_size] > bar[:atr] * 0.66 and (bar[:close] - prev[:close]).abs < 2 * bar[:rel] and
|
249
|
+
bar[:body_size] >= bar[:bar_size] * 0.5 and bar[:bearish]
|
250
|
+
|
251
|
+
|
252
|
+
# HARAMIs are an unusual long body embedding the following small body
|
253
|
+
bar[:HARAMI] = true if bar[:body_size] < prev[:body_size] / 2.5 and prev[:bar_size] >= bar[:atr] and
|
254
|
+
prev[:upper] > bar[:upper] and prev[:lower] < bar[:lower] and not bar[:doji]
|
255
|
+
bar[:HARAMI_CROSS] = true if bar[:body_size] < prev[:body_size] / 2.5 and prev[:bar_size] >= bar[:atr] and
|
256
|
+
prev[:upper] > bar[:upper] and prev[:lower] < bar[:lower] and bar[:doji]
|
257
|
+
if bar[:HARAMI] or bar[:HARAMI_CROSS]
|
258
|
+
puts [ :date, :open, :high, :low, :close, :upper, :lower ].map{|x| prev[x]}.join("\t") if debug
|
259
|
+
puts [ :date, :open, :high, :low, :close, :upper, :lower ].map{|x| bar[ x]}.join("\t") if debug
|
260
|
+
puts "" if debug
|
261
|
+
end
|
262
|
+
|
263
|
+
# TODO TWEEZER_TOP and TWEEZER_BOTTOM
|
264
|
+
# actually being a double top / bottom, this dual candle pattern has to be unfolded. It is valid on daily or weekly charts,
|
265
|
+
# and valid if
|
266
|
+
# 1 it has an according
|
267
|
+
|
268
|
+
|
269
|
+
###################################
|
270
|
+
# TRIPLE CANDLE PATTERNS
|
271
|
+
###################################
|
272
|
+
|
273
|
+
# morning star, morning doji star
|
274
|
+
next unless prev and pprev
|
275
|
+
bar[:MORNING_STAR] = true if prev[:STAR] and bar[:bullish] and bar[:close] >= pprev[:lower] and prev[:slope] < -6
|
276
|
+
bar[:MORNING_DOJI_STAR] = true if prev[:DOJI_STAR] and bar[:bullish] and bar[:close] >= pprev[:lower] and prev[:slope] < -6
|
277
|
+
bar[:EVENING_STAR] = true if prev[:STAR] and bar[:bearish] and bar[:close] <= pprev[:upper] and prev[:slope] > 6
|
278
|
+
bar[:EVENING_DOJI_STAR] = true if prev[:DOJI_STAR] and bar[:bearish] and bar[:close] <= pprev[:upper] and prev[:slope] > 6
|
279
|
+
|
280
|
+
# the abandoned baby escalates above stars by gapping the inner star candle to both framing it
|
281
|
+
bar[:ABANDONED_BABY] = true if (bar[:MORNING_STAR] or bar[:MORNING_DOJI_STAR]) and prev[:high] <= [ pprev[:low ], bar[:low ] ].min
|
282
|
+
bar[:ABANDONED_BABY] = true if (bar[:EVENING_STAR] or bar[:EVENING_DOJI_STAR]) and prev[:low ] >= [ pprev[:high], bar[:high] ].max
|
283
|
+
|
284
|
+
# UPSIDEGAP_TWO_CROWS
|
285
|
+
bar[:UPSIDEGAP_TWO_CROWS] = true if (prev[:STAR] or prev[:DOJI_STAR]) and prev[:slope] > 4 and bar[:bearish] and prev[:bearish] and bar[:close] > pprev[:close]
|
286
|
+
bar[:DOWNGAP_TWO_RIVERS] = true if (prev[:STAR] or prev[:DOJI_STAR]) and prev[:slope] < 4 and bar[:bullish] and prev[:bullish] and bar[:close] < pprev[:close]
|
287
|
+
|
288
|
+
# THREE BLACK CROWS / THREE WHITE SOLDIERS
|
289
|
+
bar[:THREE_BLACK_CROWS] = true if [ bar, prev, pprev ].map{|x| x[:bearish] and x[:bar_size] > 0.5 * bar[:atr] }.reduce(:&) and
|
290
|
+
pprev[:close] - prev[ :close] > bar[:atr] * 0.2 and
|
291
|
+
prev[ :close] - bar[ :close] > bar[:atr] * 0.2
|
292
|
+
bar[:THREE_WHITE_SOLDIERS] = true if [ bar, prev, pprev ].map{|x| x[:bullish] and x[:bar_size] > 0.5 * bar[:atr] }.reduce(:&) and
|
293
|
+
prev[:close] - pprev[:close] > bar[:atr] * 0.2 and
|
294
|
+
bar[ :close] - prev[ :close] > bar[:atr] * 0.2
|
295
|
+
|
296
|
+
#### MARKTTECHNIK ####
|
297
|
+
|
298
|
+
# Umkehrstäbe
|
299
|
+
# Ein Umkehrstab bullish ist ein Candle, der zumindest einen Downtrend aus 3 Kerzen beenden könnte.
|
300
|
+
# dazu muss der stab selber ein niedrigers tief haben als seine beiden vorgänger,
|
301
|
+
# der TR muss above average sein und der vorgänger muss einen slopescore von < -5 haben
|
302
|
+
#
|
303
|
+
if pprev[:low] > prev[:low] and prev[:low] > bar[:low] and
|
304
|
+
( bar[:tr] > bar[:atr] * 1.25 or bar[:BREAKN_volume] ) and
|
305
|
+
prev[:slope] <= -5 and
|
306
|
+
bar[:close] - bar[:low] >= (bar[:bar_size]) * 0.7
|
307
|
+
#bar[:close] >= (bar[:high] + bar[:low]) / 2.0
|
308
|
+
bar[:UMKEHRSTAB_BULLISH] = true
|
309
|
+
bar[:CLASSIC] = true
|
310
|
+
end
|
311
|
+
|
312
|
+
if pprev[:high] < prev[:high] and prev[:high] < bar[:high] and
|
313
|
+
( bar[:tr] > bar[:atr] * 1.25 or bar[:BREAKN_volume] ) and
|
314
|
+
prev[:slope] >= 5 and
|
315
|
+
bar[:high] - bar[:close] >= (bar[:bar_size]) * 0.7
|
316
|
+
#bar[:close] <= (bar[:high] + bar[:low]) / 2.0
|
317
|
+
bar[:UMKEHRSTAB_BEARISH] = true
|
318
|
+
bar[:CLASSIC] = true
|
319
|
+
end
|
320
|
+
|
321
|
+
# TINY REVERSALS only work for short periods of time (i.e. lte 5 min)
|
322
|
+
|
323
|
+
if bar[:datetime] and bar[:datetime].respond_to?(:-) and bar[:datetime] - prev[:datetime] <= 5*60
|
324
|
+
if pprev[:low] > prev[:low] and prev[:low] > bar[:low] and
|
325
|
+
((not bar[:tiny] and bar[:bar_size] >= 0.75 * bar[:atr]) or bar[:BREAKN_volume]) and
|
326
|
+
bar[:high] - bar[:close] < bar[:bar_size] / 2.5
|
327
|
+
bar[:UMKEHRSTAB_BULLISH] = true
|
328
|
+
bar[:TINY] = true
|
329
|
+
end
|
330
|
+
if pprev[:high] < prev[:high] and prev[:high] < bar[:high] and
|
331
|
+
((not bar[:tiny] and bar[:bar_size] >= 0.75 * bar[:atr]) or bar[:BREAKN_volume]) and
|
332
|
+
bar[:close] - bar[:low] < bar[:bar_size] / 2.5
|
333
|
+
bar[:UMKEHRSTAB_BEARISH] = true
|
334
|
+
bar[:TINY] = true
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# GEM reversals are just another set of criteria
|
339
|
+
if prev[:low] > bar[:low] and prev[:high] > bar[:high] and
|
340
|
+
(bar[:bar_size] >= bar[:atr] or bar[:BREAKN_volume]) and
|
341
|
+
bar[:high] - bar[:close] < bar[:bar_size] / 4.0
|
342
|
+
bar[:UMKEHRSTAB_BULLISH] = true
|
343
|
+
bar[:GEM] = true
|
344
|
+
end
|
345
|
+
if prev[:low] < bar[:low] and prev[:high] < bar[:high] and
|
346
|
+
(bar[:bar_size] >= bar[:atr] or bar[:BREAKN_volume]) and
|
347
|
+
bar[:close] - bar[:low] < bar[:bar_size] / 4.0
|
348
|
+
bar[:UMKEHRSTAB_BEARISH] = true
|
349
|
+
bar[:GEM] = true
|
350
|
+
end
|
351
|
+
|
352
|
+
# DOUBLEBAR reversals are reversals, that span over 2 bars instead of one
|
353
|
+
unless ppprev.nil? or pprev[:slope].nil?
|
354
|
+
pot = {
|
355
|
+
open: prev[:open],
|
356
|
+
high: [prev[:high], bar[:high]].max,
|
357
|
+
low: [prev[:low], bar[:low]].min,
|
358
|
+
close: bar[:close]
|
359
|
+
}
|
360
|
+
pot[:size] = (pot[:high] - pot[:low]).round(8)
|
361
|
+
if (pprev[:slope] >=8 or (prev[:low] > pprev[:low] and pprev[:low] > ppprev[:low])) and pot[:high] > pprev[:high] and
|
362
|
+
pot[:high] - pot[:close] >= pot[:size] * 0.7
|
363
|
+
bar[:UMKEHRSTAB_BEARISH] = true
|
364
|
+
bar[:DOUBLE] = true
|
365
|
+
end
|
366
|
+
if (pprev[:slope] <=-8 or (prev[:high] < pprev[:high] and pprev[:high] < ppprev[:high])) and
|
367
|
+
pot[:low] < pprev[:low] and
|
368
|
+
pot[:close] - pot[:low] >= pot[:size] * 0.7
|
369
|
+
bar[:UMKEHRSTAB_BULLISH] = true
|
370
|
+
bar[:DOUBLE] = true
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# BULLISH_MOVE und BEARISH_MOVE
|
375
|
+
|
376
|
+
unless pprev[:atr].nil?
|
377
|
+
if pprev[:high] > prev[:high] and prev[:high] > bar[:high] and
|
378
|
+
pprev[:low] > prev[:low] and prev[:low] > bar[:low] and
|
379
|
+
bar[:bar_size] >= pprev[:atr] and pprev[:bar_size] >= pprev[:atr] and prev[:bar_size] >= pprev[:atr] and
|
380
|
+
bar[:close] - bar[:low] < bar[:bar_size] / 3
|
381
|
+
bar[:BEARISH_MOVE] = prev[:BEARISH_MOVE].nil? ? (pprev[:BEARISH_MOVE].nil? ? 1 : pprev[:BEARISH_MOVE] + 2) : prev[:BEARISH_MOVE] + 1
|
382
|
+
end
|
383
|
+
if pprev[:high] < prev[:high] and prev[:high] < bar[:high] and
|
384
|
+
pprev[:low] < prev[:low] and prev[:low] < bar[:low] and
|
385
|
+
bar[:bar_size] >= pprev[:atr] and pprev[:bar_size] >= pprev[:atr] and prev[:bar_size] >= pprev[:atr] and
|
386
|
+
bar[:high] - bar[:close] < bar[:bar_size] / 3
|
387
|
+
bar[:BULLISH_MOVE] = prev[:BULLISH_MOVE].nil? ? (pprev[:BULLISH_MOVE].nil? ? 1 : pprev[:BULLISH_MOVE] + 2) : prev[:BULLISH_MOVE] + 1
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# support and resistance
|
392
|
+
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
|
397
|
+
# SLOPE SCORE
|
398
|
+
def slopescore(pprev, prev, bar, debug = false)
|
399
|
+
# the slope between to bars is considered bullish, if 2 of three points match
|
400
|
+
# - higher high
|
401
|
+
# - higher close
|
402
|
+
# - higher low
|
403
|
+
# the opposite counts for bearish
|
404
|
+
#
|
405
|
+
# this comparison is done between the current bar and previous bar
|
406
|
+
# - if it confirms the score of the previous bar, the new slope score is prev + curr
|
407
|
+
# - otherwise the is compared to score of the pprevious bar
|
408
|
+
# - if it confirms there, the new slope score is pprev + curr
|
409
|
+
# - otherwise the trend is destroyed and tne new score is solely curr
|
410
|
+
|
411
|
+
if bar[:bullish]
|
412
|
+
curr = 1
|
413
|
+
curr += 1 if bar[:bullish_close]
|
414
|
+
elsif bar[:bearish]
|
415
|
+
curr = -1
|
416
|
+
curr -= 1 if bar[:bearish_close]
|
417
|
+
else
|
418
|
+
curr = 0
|
419
|
+
end
|
420
|
+
puts "curr set to #{curr} @ #{bar[:date]}".yellow if debug
|
421
|
+
if prev.nil?
|
422
|
+
puts "no prev found, score == curr: #{curr}" if debug
|
423
|
+
score = curr
|
424
|
+
else
|
425
|
+
comp = comparebars(prev, bar)
|
426
|
+
|
427
|
+
puts prev.select{|k,v| [:high,:low,:close,:score].include?(k)} if debug
|
428
|
+
puts bar if debug
|
429
|
+
puts "COMPARISON 1: #{comp}" if debug
|
430
|
+
|
431
|
+
if prev[:slope] >= 0 and comp[:bullish] # bullish slope confirmed
|
432
|
+
score = prev[:slope]
|
433
|
+
score += curr if curr > 0
|
434
|
+
[ :gap, :bodygap ] .each {|x| score += 0.5 if bar[x] }
|
435
|
+
score += 1 if bar[:RISING_volume]
|
436
|
+
score += 2 if bar[:BREAKN_volume]
|
437
|
+
puts "found bullish slope confirmed, new score #{score}" if debug
|
438
|
+
elsif prev[:slope] <= 0 and comp[:bearish] # bearish slope confirmed
|
439
|
+
score = prev[:slope]
|
440
|
+
score += curr if curr < 0
|
441
|
+
[ :gap, :bodygap ] .each {|x| score -= 0.5 if bar[x] }
|
442
|
+
score -= 1 if bar[:RISING_volume]
|
443
|
+
score -= 2 if bar[:BREAKN_volume]
|
444
|
+
puts "found bearish slope confirmed, new score #{score} (including #{curr} and #{bar[:bodygap]} and #{bar[:gap]}" if debug
|
445
|
+
else #if prev[:slope] > 0 # slopes failed
|
446
|
+
puts "confirmation failed: " if debug
|
447
|
+
if pprev.nil?
|
448
|
+
score = curr
|
449
|
+
else
|
450
|
+
comp2 = comparebars(pprev, bar)
|
451
|
+
puts "\t\tCOMPARISON 2: #{comp2}" if debug
|
452
|
+
if pprev[:slope] >= 0 and comp2[:bullish] # bullish slope confirmed on pprev
|
453
|
+
score = pprev[:slope]
|
454
|
+
score += curr if curr > 0
|
455
|
+
[ :gap, :bodygap ] .each {|x| score += 0.5 if bar[x] }
|
456
|
+
puts "\t\tfound bullish slope confirmed, new score #{score}" if debug
|
457
|
+
score += 1 if bar[:RISING_volume]
|
458
|
+
score += 2 if bar[:BREAKN_volume]
|
459
|
+
elsif pprev[:slope] <= 0 and comp2[:bearish] # bearish slope confirmed
|
460
|
+
score = pprev[:slope]
|
461
|
+
score += curr if curr < 0
|
462
|
+
[ :gap, :bodygap ] .each {|x| score -= 0.5 if bar[x] }
|
463
|
+
score -= 1 if bar[:RISING_volume]
|
464
|
+
score -= 2 if bar[:BREAKN_volume]
|
465
|
+
puts "\t\tfound bearish slope confirmed, new score #{score}" if debug
|
466
|
+
else #slope confirmation finally failed
|
467
|
+
comp3 = comparebars(pprev, prev)
|
468
|
+
if prev[:slope] > 0 # was bullish, turning bearish now
|
469
|
+
score = curr
|
470
|
+
score -= 1 if comp3[:bearish]
|
471
|
+
score -= 1 if comp[:bearish]
|
472
|
+
score -= 1 if prev[:bearish]
|
473
|
+
score -= 1 if prev[:RISING_volume] and comp3[:bearish]
|
474
|
+
score -= 2 if prev[:BREAKN_volume] and comp3[:bearish]
|
475
|
+
score -= 1 if bar[:RISING_volume] and comp[:bearish]
|
476
|
+
score -= 2 if bar[:BREAKN_volume] and comp[:bearish]
|
477
|
+
score -= 1 if bar[:RISING_volume] and comp[:bearish]
|
478
|
+
score -= 2 if bar[:BREAKN_volume] and comp[:bearish]
|
479
|
+
[ :gap, :bodygap ] .each {|x| score += 0.5 if bar[x] }
|
480
|
+
puts "\t\tfinally gave up, turning bearish now, new score #{score}" if debug
|
481
|
+
elsif prev[:slope] < 0
|
482
|
+
score = curr
|
483
|
+
score += 1 if comp3[:bullish]
|
484
|
+
score += 1 if comp[:bullish]
|
485
|
+
score += 1 if prev[:bullish]
|
486
|
+
score += 1 if prev[:RISING_volume] and comp3[:bullish]
|
487
|
+
score += 2 if prev[:BREAKN_volume] and comp3[:bullish]
|
488
|
+
score += 1 if bar[:RISING_volume] and comp[:bullish]
|
489
|
+
score += 2 if bar[:BREAKN_volume] and comp[:bullish]
|
490
|
+
score += 1 if bar[:RISING_volume] and comp[:bullish]
|
491
|
+
score += 2 if bar[:BREAKN_volume] and comp[:bullish]
|
492
|
+
[ :gap, :bodygap ] .each {|x| score -= 0.5 if bar[x] } if curr < 0
|
493
|
+
puts "\t\tfinally gave up, turning bullish now, new score #{score}" if debug
|
494
|
+
else
|
495
|
+
score = 0
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|
501
|
+
puts "" if debug
|
502
|
+
score
|
503
|
+
end
|
504
|
+
|
505
|
+
end
|
506
|
+
|
507
|
+
CR = Candlestick_Recognition
|
508
|
+
end
|
509
|
+
end
|
@@ -4,34 +4,66 @@ module Cotcube
|
|
4
4
|
# Missing top level documentation
|
5
5
|
module Helpers
|
6
6
|
|
7
|
-
|
7
|
+
SYMBOL_HEADERS = %i[ id symbol ib_symbol internal exchange currency ticksize power months type bcf reports format name ]
|
8
|
+
|
9
|
+
def symbols(config: init, **args)
|
8
10
|
if config[:symbols_file].nil?
|
9
11
|
SYMBOL_EXAMPLES
|
10
12
|
else
|
11
13
|
CSV
|
12
|
-
.read(config[:symbols_file], headers:
|
14
|
+
.read(config[:symbols_file], headers: SYMBOL_HEADERS)
|
13
15
|
.map{|row| row.to_h }
|
14
|
-
.map{|row|
|
16
|
+
.map{|row|
|
17
|
+
[ :ticksize, :power, :bcf ].each {|z| row[z] = row[z].to_f}
|
18
|
+
row[:format] = "%#{row[:format]}f"
|
19
|
+
row[:currency] ||= 'USD'
|
20
|
+
row[:multiplier] = (row[:power] / row[:ticksize]).round(8)
|
21
|
+
row
|
22
|
+
}
|
15
23
|
.reject{|row| row[:id].nil? }
|
16
|
-
.tap
|
17
|
-
|
18
|
-
|
24
|
+
.tap{ |all|
|
25
|
+
args.keys.each { |header|
|
26
|
+
unless SYMBOL_HEADERS.include? header
|
27
|
+
puts "WARNING in Cotcube::Helpers.symbols: '#{header}' is not a valid symbol header. Skipping..."
|
28
|
+
next
|
29
|
+
end
|
30
|
+
all.select!{|x| x[header] == args[header]} unless args[header].nil?
|
31
|
+
return all.first if all.size == 1
|
32
|
+
}
|
33
|
+
return all
|
34
|
+
}
|
19
35
|
end
|
20
36
|
end
|
21
37
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
38
|
+
def micros(config: init, **args)
|
39
|
+
if config[:micros_file].nil?
|
40
|
+
MICRO_EXAMPLES
|
41
|
+
else
|
42
|
+
CSV
|
43
|
+
.read(config[:micros_file], headers: SYMBOL_HEADERS)
|
44
|
+
.map{|row| row.to_h }
|
45
|
+
.map{|row|
|
46
|
+
[ :ticksize, :power, :bcf ].each {|z| row[z] = row[z].to_f }
|
47
|
+
row[:format] = "%#{row[:format]}f"
|
48
|
+
row[:currency] ||= 'USD'
|
49
|
+
row[:multiplier] = (row[:power] / row[:ticksize]).round(8)
|
50
|
+
row
|
51
|
+
}
|
52
|
+
.reject{|row| row[:id].nil? }
|
53
|
+
.tap{ |all|
|
54
|
+
args.keys.each { |header|
|
55
|
+
unless SYMBOL_HEADERS.include? header
|
56
|
+
puts "WARNING in Cotcube::Helpers.micros: '#{header}' is not a valid symbol header. Skipping..."
|
57
|
+
next
|
58
|
+
end
|
59
|
+
all.select!{|x| x[header] == args[header]} unless args[header].nil?
|
60
|
+
return all.first if all.size == 1
|
61
|
+
}
|
62
|
+
return all
|
63
|
+
}
|
64
|
+
end
|
33
65
|
end
|
34
|
-
|
66
|
+
|
35
67
|
end
|
36
68
|
|
37
69
|
end
|
data/lib/cotcube-helpers.rb
CHANGED
@@ -7,6 +7,8 @@ require 'active_support/core_ext/time'
|
|
7
7
|
require 'active_support/core_ext/numeric'
|
8
8
|
require 'parallel'
|
9
9
|
require 'csv'
|
10
|
+
require 'yaml'
|
11
|
+
require 'json'
|
10
12
|
|
11
13
|
require_relative 'cotcube-helpers/array_ext'
|
12
14
|
require_relative 'cotcube-helpers/enum_ext'
|
@@ -21,10 +23,13 @@ require_relative 'cotcube-helpers/simple_output'
|
|
21
23
|
require_relative 'cotcube-helpers/simple_series_stats'
|
22
24
|
require_relative 'cotcube-helpers/constants'
|
23
25
|
require_relative 'cotcube-helpers/input'
|
26
|
+
require_relative 'cotcube-helpers/output'
|
24
27
|
require_relative 'cotcube-helpers/reduce'
|
25
28
|
require_relative 'cotcube-helpers/symbols'
|
26
29
|
require_relative 'cotcube-helpers/init'
|
27
30
|
require_relative 'cotcube-helpers/get_id_set'
|
31
|
+
require_relative 'cotcube-helpers/ib_contracts'
|
32
|
+
require_relative 'cotcube-helpers/recognition'
|
28
33
|
|
29
34
|
module Cotcube
|
30
35
|
module Helpers
|
@@ -38,8 +43,14 @@ module Cotcube
|
|
38
43
|
:symbols,
|
39
44
|
:micros,
|
40
45
|
:get_id_set,
|
46
|
+
:get_ib_contract,
|
47
|
+
:update_ib_contracts,
|
48
|
+
:translate_ib_contract,
|
41
49
|
:init
|
42
50
|
|
43
51
|
# please not that module_functions of source provided in private files must be published there
|
44
52
|
end
|
45
53
|
end
|
54
|
+
|
55
|
+
require_relative 'cotcube-helpers/data_client'
|
56
|
+
require_relative 'cotcube-helpers/cache_client'
|