ib-technical-analysis 0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +56 -0
- data/Gemfile +6 -0
- data/Guardfile +24 -0
- data/LICENSE +21 -0
- data/README.md +140 -0
- data/bin/console +100 -0
- data/bin/console.yml +3 -0
- data/bin/gateway +113 -0
- data/bin/history.irb +186 -0
- data/ib-technical-analysis.gemspec +38 -0
- data/lib/calculation_helpers.rb +149 -0
- data/lib/ib/bar.rb +23 -0
- data/lib/ta_support.rb +157 -0
- data/lib/technical-analysis.rb +11 -0
- data/lib/technical_analysis/bar_calculation.rb +4 -0
- data/lib/technical_analysis/lane.rb +46 -0
- data/lib/technical_analysis/momentum/lane-stochastic.rb +105 -0
- data/lib/technical_analysis/momentum/rsi.rb +76 -0
- data/lib/technical_analysis/momentum/tsi.rb +79 -0
- data/lib/technical_analysis/moving-average/base.rb +59 -0
- data/lib/technical_analysis/moving-average/exp-ma.rb +53 -0
- data/lib/technical_analysis/moving-average/ka-ma.rb +90 -0
- data/lib/technical_analysis/moving-average/simple-ma.rb +42 -0
- data/lib/technical_analysis/moving-average/weighted-ma.rb +25 -0
- data/lib/technical_analysis/moving_average.rb +85 -0
- data/lib/technical_analysis/version.rb +4 -0
- metadata +183 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'logging.rb'
|
2
|
+
require 'ib/bar.rb'
|
3
|
+
require 'technical_analysis/moving-average/base.rb'
|
4
|
+
require 'technical_analysis/moving-average/simple-ma.rb'
|
5
|
+
require 'technical_analysis/moving-average/weighted-ma.rb'
|
6
|
+
require 'technical_analysis/moving-average/exp-ma.rb'
|
7
|
+
require 'technical_analysis/moving-average/ka-ma.rb'
|
8
|
+
require 'technical_analysis/momentum/tsi.rb'
|
9
|
+
require 'technical_analysis/momentum/lane-stochastic.rb'
|
10
|
+
require 'technical_analysis/momentum/rsi.rb'
|
11
|
+
require 'ta_support'
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
|
3
|
+
module Momentum
|
4
|
+
RSI = Struct.new :time, :value
|
5
|
+
TSI = Struct.new :time, :value
|
6
|
+
|
7
|
+
#
|
8
|
+
# Calculates the true strength index (TSI) for the data over the given period
|
9
|
+
# https://en.wikipedia.org/wiki/True_strength_index
|
10
|
+
|
11
|
+
#
|
12
|
+
# z = Symbols::Futures.mini_dax.eod( duration: '30 d').each
|
13
|
+
# e= nil
|
14
|
+
# ema= z.map{|y| e= TechnicalAnalysis::MovingAverage.ema( y.close, z.map(&:close), 30, e ) }
|
15
|
+
#
|
16
|
+
# or
|
17
|
+
#
|
18
|
+
# EMA = Struct.new :time, :ema
|
19
|
+
# e = nil
|
20
|
+
# ema = z.map do |y|
|
21
|
+
# EMA.new y.time,
|
22
|
+
# e = TechnicalAnalysis::MovingAverage.ema y.close, z.map(&:close), 30, e
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
|
26
|
+
def self.tsi current_value, tsi_source, low_period = 13, high_period = 25
|
27
|
+
|
28
|
+
if current_value.present?
|
29
|
+
tsi_source.push current_value
|
30
|
+
else
|
31
|
+
current_value = tsi_source.last
|
32
|
+
end
|
33
|
+
|
34
|
+
momentum = tsi_source[-1] - tsi_source[-2]
|
35
|
+
|
36
|
+
high_multiplier = (2.0 / (high_period + 1.0))
|
37
|
+
low_multiplier = (2.0 / (low_period + 1.0))
|
38
|
+
|
39
|
+
if prev_tsi.nil?
|
40
|
+
data.sum / data.size.to_f # Average
|
41
|
+
else
|
42
|
+
(current_value - prev_ema) * (2.0 / (period + 1.0)) + prev_ema
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
module Momentum
|
3
|
+
LANE = Struct.new :time, :value
|
4
|
+
|
5
|
+
|
6
|
+
=begin rdoc
|
7
|
+
Defines the Lane-Stochastics
|
8
|
+
price - min(low)[period]
|
9
|
+
%k = -----------------------------------------------
|
10
|
+
max(high)[period] - min(low)[period]
|
11
|
+
|
12
|
+
The Stochastics may depend on
|
13
|
+
* OHLC-Objects (IB::Bar).
|
14
|
+
|
15
|
+
Then <em>"close"</em> is taken as default-value of the current candle.
|
16
|
+
|
17
|
+
By specifiying the parameter <em>"take"</em> this can be customized.
|
18
|
+
|
19
|
+
=end
|
20
|
+
class Lane
|
21
|
+
# attr :oversold, true
|
22
|
+
# attr :overbought, true
|
23
|
+
|
24
|
+
def initialize period: 5, fast: 3, slow: 3, data: [], strict: true, take: :close
|
25
|
+
# note: strict is always true
|
26
|
+
raise "Periods must be greater then one" if period <= 1
|
27
|
+
raise "fast must be smaller then slow" if slow < fast
|
28
|
+
|
29
|
+
@queue = []
|
30
|
+
@buffer = []
|
31
|
+
@take = take
|
32
|
+
@smooth_2 = slow # smooth-Const of StochasticsSlow
|
33
|
+
@smooth_1 = fast # smooth-Const of StochasticsFast
|
34
|
+
@period = period # period of StochasticsLane
|
35
|
+
@stochastics_fast = TechnicalAnalysis::MovingAverage::ExpMA.new( period: @smooth_1, strict: true)
|
36
|
+
@stochastics_slow = TechnicalAnalysis::MovingAverage::ExpMA.new( period: @smooth_2, strict: true)
|
37
|
+
if !data.empty?
|
38
|
+
data.map{|d| add_item d }
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# adds item, calculates the ema, puts value to the buffer and returns the result
|
45
|
+
def add_item value
|
46
|
+
is_ohlc = nil
|
47
|
+
@queue << if value.is_a? IB::Bar
|
48
|
+
is_ohlc = true
|
49
|
+
value
|
50
|
+
else
|
51
|
+
value.to_f
|
52
|
+
end
|
53
|
+
|
54
|
+
if @queue.size < @period
|
55
|
+
@buffer << nil
|
56
|
+
else
|
57
|
+
@queue.shift if @queue.size > @period
|
58
|
+
the_high = is_ohlc ? @queue.map(&:high).max : @queue.max
|
59
|
+
the_low = is_ohlc ? @queue.map(&:low).min : @queue.min
|
60
|
+
# the value is :close by default. But can be :wap, :typical_price aso.
|
61
|
+
the_value = is_ohlc ? value.send( @take ) : value
|
62
|
+
k = ( the_value - the_low )/ ( the_high - the_low )
|
63
|
+
@stochastics_fast.add_item k
|
64
|
+
|
65
|
+
out_1 = if @stochastics_fast.current.present?
|
66
|
+
@stochastics_slow.add_item @stochastics_fast.current
|
67
|
+
@stochastics_fast.current
|
68
|
+
else
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
#puts "momentum: #{@emas[:low].current} <--> #{@emas[:low_abs].current}"
|
73
|
+
out_2 = if @stochastics_slow.current.present? # warmup perioda is over
|
74
|
+
@stochastics_slow.current
|
75
|
+
else
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
@buffer << { fast: out_1, slow: out_2 }
|
80
|
+
|
81
|
+
current # return the last buffer value
|
82
|
+
end
|
83
|
+
|
84
|
+
# returns the ema-buffer
|
85
|
+
def stochastics
|
86
|
+
@buffer
|
87
|
+
end
|
88
|
+
|
89
|
+
alias lane stochastics
|
90
|
+
|
91
|
+
# returns the ema if the last computed item
|
92
|
+
def current
|
93
|
+
obj = @buffer.last
|
94
|
+
if obj.values.compact.empty?
|
95
|
+
nil
|
96
|
+
else
|
97
|
+
obj
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# set alias for the class
|
103
|
+
LaneStochastic = Lane
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
module Momentum
|
3
|
+
RSI = Struct.new :time, :value
|
4
|
+
|
5
|
+
class Rsi
|
6
|
+
=begin
|
7
|
+
|
8
|
+
|
9
|
+
The RSI is calulated by
|
10
|
+
|
11
|
+
ExpMA( Deltas of up-trending Periods )
|
12
|
+
RSI = ----------------------------------------------
|
13
|
+
ExpMA( Deltas of down-trending Periods )
|
14
|
+
|
15
|
+
|
16
|
+
If the average of Delta values is zero, then according to the equation, the RS value will approach infinitiy
|
17
|
+
=end
|
18
|
+
|
19
|
+
def initialize period: 15, data: [], strict: true
|
20
|
+
# strict is always true
|
21
|
+
|
22
|
+
raise "Periods must be greater then one" if period <= 1
|
23
|
+
|
24
|
+
@queue = []
|
25
|
+
@buffer = []
|
26
|
+
@period= period
|
27
|
+
|
28
|
+
@emas = [ TechnicalAnalysis::MovingAverage::ExpMA.new( period: period, strict: true ),
|
29
|
+
TechnicalAnalysis::MovingAverage::ExpMA.new( period: period, strict: true ) ]
|
30
|
+
|
31
|
+
if !data.empty?
|
32
|
+
data.map{|d| add_item d }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# adds item, calculates the ema, puts value to the buffer and returns the result
|
37
|
+
def add_item value
|
38
|
+
@queue << value.to_f
|
39
|
+
|
40
|
+
up_or_down = -> (previous, actual) do
|
41
|
+
if actual >= previous
|
42
|
+
[actual-previous,0]
|
43
|
+
else
|
44
|
+
[0,previous-actual]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
if @queue.size < 2
|
49
|
+
@buffer << nil
|
50
|
+
else
|
51
|
+
# up-trend-data are added to @emas.first, downtrend-data to @emas.last
|
52
|
+
# the lambda up_and_down always returns positve data (or "0")
|
53
|
+
|
54
|
+
@emas.zip( up_or_down[ *@queue[-2,2] ] ).each { | exp_ma, item | exp_ma.add_item item }
|
55
|
+
|
56
|
+
if @emas.first.current.present?
|
57
|
+
@buffer << 100 - ( 100 / (1 + ( @emas.first.current / @emas.last.current rescue 100 ) ) )
|
58
|
+
end
|
59
|
+
|
60
|
+
current # return the last buffer value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# returns the ema-buffer
|
65
|
+
def momentum
|
66
|
+
@buffer
|
67
|
+
end
|
68
|
+
|
69
|
+
# returns the ema if the last computed item
|
70
|
+
def current
|
71
|
+
@buffer.last
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
module Momentum
|
3
|
+
TSI = Struct.new :time, :value
|
4
|
+
|
5
|
+
class Tsi
|
6
|
+
=begin
|
7
|
+
ExpMA(low) ( ExpMA(high ) prices )
|
8
|
+
TSI = ----------------------------------------------
|
9
|
+
ExpMA(low) ( ExpMA(high ) prices.abs )
|
10
|
+
|
11
|
+
|
12
|
+
While the TSI output is bound between +1 and −1, most values fall between +0.25 and −0.25.
|
13
|
+
Blau suggests interpreting these values as overbought and oversold levels, respectively,
|
14
|
+
at which point a trader may anticipate a market turn.
|
15
|
+
|
16
|
+
Trend direction is indicated by the slope of the TSI;
|
17
|
+
a rising TSI suggests an up-trend in the market, and a falling TSI suggests a down-trend
|
18
|
+
|
19
|
+
=end
|
20
|
+
|
21
|
+
def initialize low: 13, high: 25, data: [], strict: true
|
22
|
+
# strict is always true
|
23
|
+
|
24
|
+
raise "Periods must be greater then one" if low <= 1 || high <= 1
|
25
|
+
raise "high must be greater then low" if low > high
|
26
|
+
|
27
|
+
@queue = []
|
28
|
+
@buffer = []
|
29
|
+
@low= low
|
30
|
+
@high = high
|
31
|
+
|
32
|
+
@emas = { high: TechnicalAnalysis::MovingAverage::ExpMA.new( period: high, strict: true ),
|
33
|
+
low: TechnicalAnalysis::MovingAverage::ExpMA.new( period: low, strict: true ),
|
34
|
+
high_abs: TechnicalAnalysis::MovingAverage::ExpMA.new( period: high, strict: true ),
|
35
|
+
low_abs: TechnicalAnalysis::MovingAverage::ExpMA.new( period: low, strict: true ) }
|
36
|
+
|
37
|
+
if !data.empty?
|
38
|
+
data.map{|d| add_item d }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# adds item, calculates the ema, puts value to the buffer and returns the result
|
43
|
+
def add_item value
|
44
|
+
@queue << value.to_f
|
45
|
+
|
46
|
+
if @queue.size < 2
|
47
|
+
@buffer << nil
|
48
|
+
else
|
49
|
+
momentum = @queue.last - @queue[-2]
|
50
|
+
@emas[:high].add_item momentum
|
51
|
+
@emas[:high_abs].add_item momentum.abs
|
52
|
+
|
53
|
+
if @emas[:high].current.present?
|
54
|
+
@emas[:low].add_item @emas[:high].current
|
55
|
+
@emas[:low_abs].add_item @emas[:high_abs].current
|
56
|
+
end
|
57
|
+
|
58
|
+
#puts "momentum: #{@emas[:low].current} <--> #{@emas[:low_abs].current}"
|
59
|
+
if @emas[:low].current.present? # warmup perioda is over
|
60
|
+
@buffer << @emas[:low].current / @emas[:low_abs].current
|
61
|
+
end
|
62
|
+
|
63
|
+
current # return the last buffer value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# returns the ema-buffer
|
68
|
+
def momentum
|
69
|
+
@buffer
|
70
|
+
end
|
71
|
+
|
72
|
+
# returns the ema if the last computed item
|
73
|
+
def current
|
74
|
+
@buffer.last
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
module MovingAverage
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
class Base
|
7
|
+
def initialize period: 15, data: [], strict: true
|
8
|
+
|
9
|
+
raise "Period must be greater then one" if period <= 1
|
10
|
+
@period = period
|
11
|
+
@strict= strict
|
12
|
+
# queue for relevant data to process the calculation
|
13
|
+
@queue = []
|
14
|
+
# buffer contains the calculated indicator values
|
15
|
+
@buffer = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# adds item, calculates the ema, puts value to the buffer and returns the result
|
19
|
+
def add_item value
|
20
|
+
@queue << value
|
21
|
+
@queue.shift if @queue.size > @period
|
22
|
+
if @buffer.empty?
|
23
|
+
@buffer=[value.to_f]
|
24
|
+
else
|
25
|
+
prev_ema = @buffer.last
|
26
|
+
current_value = value.to_f
|
27
|
+
@buffer << (current_value - prev_ema) * @smooth_constant + prev_ema
|
28
|
+
end
|
29
|
+
current # return the last buffer value
|
30
|
+
end
|
31
|
+
|
32
|
+
# returns the moving-average-buffer
|
33
|
+
def ma
|
34
|
+
if @strict
|
35
|
+
@buffer.drop @period-1
|
36
|
+
else
|
37
|
+
@buffer
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# returns the ema of the last computed item
|
43
|
+
def current
|
44
|
+
if @strict && warmup?
|
45
|
+
nil
|
46
|
+
else
|
47
|
+
@buffer.last
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def warmup?
|
54
|
+
@queue.size < @period
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
module MovingAverage
|
3
|
+
EMA = Struct.new :time, :value
|
4
|
+
|
5
|
+
|
6
|
+
# Calculates the exponential moving average (EMA) for the data over the given period
|
7
|
+
# https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
|
8
|
+
#
|
9
|
+
# Takes a block which replaces the _smooth-constant_
|
10
|
+
#
|
11
|
+
# z = Symbols::Futures.mini_dax.eod( duration: '90 d')
|
12
|
+
# TechnicalAnalysis::MovingAverage::ExpMA.new( data=z.map(&:close))
|
13
|
+
#
|
14
|
+
# returns an array with the calculated moving-average data
|
15
|
+
#
|
16
|
+
# ema = TechnicalAnalysis::MovingAverage::ExpMA.new( default_value = z.first[:close])
|
17
|
+
# moving_average = z.map{|x| EMA.new x.time, ema.add_item(x.close)}
|
18
|
+
#
|
19
|
+
# returns an array of EMA-Objects
|
20
|
+
#
|
21
|
+
|
22
|
+
class ExpMA < Base
|
23
|
+
def initialize period: 15, data: [], strict: true
|
24
|
+
super
|
25
|
+
|
26
|
+
@smooth_constant = if block_given?
|
27
|
+
yield period
|
28
|
+
else
|
29
|
+
(2.0 / (period + 1.0))
|
30
|
+
end
|
31
|
+
if !data.empty?
|
32
|
+
data.map{|d| add_item d }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# adds item, calculates the ema, puts value to the buffer and returns the result
|
37
|
+
def add_item value
|
38
|
+
@queue << value
|
39
|
+
@queue.shift if @queue.size > @period
|
40
|
+
if @buffer.empty?
|
41
|
+
@buffer=[value.to_f]
|
42
|
+
else
|
43
|
+
prev_ema = @buffer.last
|
44
|
+
current_value = value.to_f
|
45
|
+
@buffer << (current_value - prev_ema) * @smooth_constant + prev_ema
|
46
|
+
end
|
47
|
+
current # return the last buffer value
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
module MovingAverage
|
3
|
+
KAMA = Struct.new :time, :value
|
4
|
+
|
5
|
+
|
6
|
+
# Calculates the exponential moving average (EMA) for the data over the given period
|
7
|
+
# https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
|
8
|
+
#
|
9
|
+
# Takes a block which replaces the _smooth-constant_
|
10
|
+
#
|
11
|
+
# z = Symbols::Futures.mini_dax.eod( duration: '90 d')
|
12
|
+
# TechnicalAnalysis::MovingAverage::ExpMA.new( data=z.map(&:close))
|
13
|
+
#
|
14
|
+
# returns an array with the calculated moving-average data
|
15
|
+
#
|
16
|
+
# ema = TechnicalAnalysis::MovingAverage::ExpMA.new( default_value = z.first[:close])
|
17
|
+
# moving_average = z.map{|x| EMA.new x.time, ema.add_item(x.close)}
|
18
|
+
#
|
19
|
+
# returns an array of EMA-Objects
|
20
|
+
#
|
21
|
+
|
22
|
+
class KaMA
|
23
|
+
def initialize period: 10, fast: 2, slow: 30, data: []
|
24
|
+
|
25
|
+
raise "Period must be greater then one" if period <= 1
|
26
|
+
|
27
|
+
@smoothConst_fast = 2 / (fast +1).to_f
|
28
|
+
@smoothConst_slow = 2 / (slow +1).to_f
|
29
|
+
|
30
|
+
@period = period
|
31
|
+
@queue = []
|
32
|
+
@buffer = []
|
33
|
+
|
34
|
+
if !data.empty?
|
35
|
+
data.map{|d| add_item d }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# adds item, calculates the kama, puts value to the buffer and returns the result
|
40
|
+
def add_item value
|
41
|
+
begin
|
42
|
+
@queue << value.to_f
|
43
|
+
rescue NoMethodError => e
|
44
|
+
puts "add item only supports single values. Using the :close method"
|
45
|
+
@queue << value.send(:close) || value
|
46
|
+
end
|
47
|
+
if @queue.size < @period
|
48
|
+
@buffer = [ @queue.sum / @queue.size ] # fill buffer with average
|
49
|
+
else
|
50
|
+
@queue.shift if @queue.size > @period
|
51
|
+
@buffer << calculate
|
52
|
+
end
|
53
|
+
current # return the last buffer value
|
54
|
+
end
|
55
|
+
|
56
|
+
# returns the kama-buffer
|
57
|
+
def kaema
|
58
|
+
@buffer
|
59
|
+
end
|
60
|
+
|
61
|
+
# returns the kama of the last computed item
|
62
|
+
def current
|
63
|
+
@buffer.last
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def calculate
|
68
|
+
# define the Effiency Ratio to be used by the "kaufmans Adaptive Moving Average" : kama
|
69
|
+
# ER is calculated by dividing the absolute difference between the
|
70
|
+
# current price and the price at the beginning of the period by the sum
|
71
|
+
# of the absolute difference between each pair of closes during the
|
72
|
+
# period.
|
73
|
+
# | x(t) - x(t-n) |
|
74
|
+
# er = ----------------------------
|
75
|
+
# sum | x(i) - x(i-1) |
|
76
|
+
|
77
|
+
er= (@queue.first - @queue.last).abs /
|
78
|
+
(1..@queue.size-1).map{|x| (@queue[x] - @queue[x-1]).abs }.sum # rescue 1
|
79
|
+
|
80
|
+
alpha = (er * ( @smoothConst_fast - @smoothConst_slow ) + @smoothConst_slow )**2
|
81
|
+
prev_kama = @buffer.last
|
82
|
+
current_value = @queue.last
|
83
|
+
|
84
|
+
@buffer.last + alpha * (current_value - prev_kama)
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|