ib-technical-analysis 0.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 +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
|