Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
quantitative (0.3.3)
quantitative (0.3.4)
oj (~> 3.10)
zeitwerk (~> 2.6)

Expand Down
2 changes: 1 addition & 1 deletion lib/quant/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def self.default!
end

def self.config
@config ||= Config.new
@config ||= default!
end
end

Expand Down
8 changes: 4 additions & 4 deletions lib/quant/dominant_cycles_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ module Quant
# indicators that would otherwise be setting an arbitrary lookback period. This makes the
# indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
class DominantCyclesSource
def initialize(indicator_source:)
@indicator_source = indicator_source
indicator_source.define_indicator_accessors(indicator_source: self)
def initialize(indicators_source:)
@indicators_source = indicators_source
indicators_source.define_indicator_accessors(indicators_source: self)
end

private

def indicator(indicator_class)
@indicator_source[indicator_class]
@indicators_source[indicator_class]
end
end
end
175 changes: 136 additions & 39 deletions lib/quant/indicators/adx.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,168 @@ module Indicators
class AdxPoint < IndicatorPoint
attribute :dmu, default: 0.0
attribute :dmd, default: 0.0
attribute :dmu_ema, default: 0.0
attribute :dmd_ema, default: 0.0
attribute :diu, default: 0.0
attribute :did, default: 0.0
attribute :di, default: 0.0
attribute :di_ema, default: 0.0

attribute :adaptive_dmu, default: 0.0
attribute :adaptive_dmd, default: 0.0
attribute :adaptive_diu, default: 0.0
attribute :adaptive_did, default: 0.0
attribute :adaptive_di, default: 0.0
attribute :value, default: 0.0
attribute :inst_stoch, default: 0.0
attribute :stoch, default: 0.0
attribute :stoch_up, default: false
attribute :stoch_turned, default: false
attribute :ssf, default: 0.0
attribute :hp, default: 0.0

attribute :full_dmu, default: 0.0
attribute :full_dmd, default: 0.0
attribute :full_diu, default: 0.0
attribute :full_did, default: 0.0
attribute :full_di, default: 0.0
attribute :full, default: 0.0
attribute :inst_full_stoch, default: 0.0
attribute :full_stoch, default: 0.0

attribute :slow_dmu, default: 0.0
attribute :slow_dmd, default: 0.0
attribute :slow_diu, default: 0.0
attribute :slow_did, default: 0.0
attribute :slow_di, default: 0.0
attribute :slow, default: 0.0
attribute :inst_slow_stoch, default: 0.0
attribute :slow_stoch, default: 0.0

attribute :traditional_dmu, default: 0.0
attribute :traditional_dmd, default: 0.0
attribute :traditional_diu, default: 0.0
attribute :traditional_did, default: 0.0
attribute :traditional_di, default: 0.0
attribute :traditional, default: 0.0
attribute :inst_traditional_stoch, default: 0.0
attribute :traditional_stoch, default: 0.0
end

# The Average Directional Index (ADX) is a technical indicator that measures
# the strength of a trend in the market. It's calculated using a moving
# average of price fluctuations over a specific period of time, and is
# based on two other indicators: the Positive Directional Indicator (+DI)
# and the Negative Directional Indicator (-DI):
#
# 1. Calculate the period's True Range (TR), +DI, and -DI
# 2. Calculate the Smoothed Moving Average (SMA) of the TR, +DI, and -DI
# 3. Compute the Directional Movement Index (DX) using the +DI and -DI SMA
# 4. Calculate the ADX by taking the SMA of the DX
#
# The formula for ADX is:
# ADX = 100 × ( +DI minus -DI) / (+DI plus -DI) / ATR
#
# Welles usually used 14 periods and this indicator takes that to be functionally
# equivalent to half the dominant cycle period.
class Adx < Indicator
register name: :adx
depends_on Indicators::Atr

def scale
1.0
def traditional_period
atr_indicator.traditional_period
end

def full_period
atr_indicator.full_period
end

def slow_period
atr_indicator.slow_period
end

def period
dc_period
def atr_indicator
@atr_indicator ||= series.indicators[source].atr
end

def atr_point
series.indicators[source].atr.points[t0]
atr_indicator.points[t0]
end

def compute
# To calculate the ADX, first determine the + and - directional movement, or DM.
# The +DM and -DM are found by calculating the "up-move," or current high minus
# the previous high, and "down-move," or current low minus the previous low.
# If the up-move is greater than the down-move and greater than zero, the +DM equals the up-move;
# otherwise, it equals zero. If the down-move is greater than the up-move and greater than zero,
# the -DM equals the down-move; otherwise, it equals zero.
# To calculate the ADX, first determine the + and - directional movement, or DM.
# The +DM and -DM are found by calculating the "up-move," or current high minus
# the previous high, and "down-move," or current low minus the previous low.
# If the up-move is greater than the down-move and greater than zero, the +DM equals the up-move;
# otherwise, it equals zero. If the down-move is greater than the up-move and greater than zero,
# the -DM equals the down-move; otherwise, it equals zero.
def compute_directional_movement
dm_highs = [t0.high_price - t1.high_price, 0.0].max
dm_lows = [t0.low_price - t1.low_price, 0.0].max

p0.dmu = dm_highs > dm_lows ? 0.0 : dm_highs
p0.dmd = dm_lows > dm_highs ? 0.0 : dm_lows
end

def compute_adaptive_period
p0.adaptive_dmu = three_pole_super_smooth(:dmu, previous: :adaptive_dmu, period: adaptive_half_period)
p0.adaptive_dmd = three_pole_super_smooth(:dmd, previous: :adaptive_dmd, period: adaptive_half_period)

p0.dmu_ema = three_pole_super_smooth :dmu, period:, previous: :dmu_ema
p0.dmd_ema = three_pole_super_smooth :dmd, period:, previous: :dmd_ema
atr_value = atr_point.value
return if atr_value == 0.0 || points.size < min_period

p0.adaptive_diu = (100.0 * p0.adaptive_dmu) / atr_value
p0.adaptive_did = (100.0 * p0.adaptive_dmd) / atr_value

p0.adaptive_di = (p0.adaptive_diu - p1.adaptive_did).abs / (p0.adaptive_diu + p0.adaptive_did)
p0.value = three_pole_super_smooth(:adaptive_di, previous: :value, period: adaptive_half_period).clamp(-10.0, 10.0)
p0.inst_stoch = stochastic(:adaptive_di, period: adaptive_half_period)
p0.stoch = three_pole_super_smooth(:inst_stoch, previous: :stoch, period: adaptive_half_period)
end

atr_value = atr_point.fast * scale
return if atr_value == 0.0 || @points.size < period
def compute_full_period
p0.full_dmu = three_pole_super_smooth(:dmu, previous: :full_dmu, period: full_period)
p0.full_dmd = three_pole_super_smooth(:dmd, previous: :full_dmd, period: full_period)

# The positive directional indicator, or +DI, equals 100 times the EMA of +DM divided by the ATR
# over a given number of time periods. Welles usually used 14 periods.
# The negative directional indicator, or -DI, equals 100 times the EMA of -DM divided by the ATR.
p0.diu = (100.0 * p0.dmu_ema) / atr_value
p0.did = (100.0 * p0.dmd_ema) / atr_value
atr_value = atr_point.full
return if atr_value == 0.0 || points.size < min_period

# The ADX indicator itself equals 100 times the EMA of the absolute value of (+DI minus -DI)
# divided by (+DI plus -DI).
delta = p0.diu + p0.did
p0.di = (p0.diu - p1.did).abs / delta
p0.di_ema = three_pole_super_smooth(:di, period:, previous: :di_ema).clamp(-10.0, 10.0)
p0.full_diu = (100.0 * p0.full_dmu) / atr_value
p0.full_did = (100.0 * p0.full_dmd) / atr_value

p0.value = p0.di_ema
p0.inst_stoch = stochastic(:di, period:)
p0.stoch = three_pole_super_smooth :inst_stoch, period:, previous: :stoch
p0.full_di = (p0.full_diu - p1.full_did).abs / (p0.full_diu + p0.full_did)
p0.full = three_pole_super_smooth(:full_di, previous: :full, period: full_period).clamp(-10.0, 10.0)
p0.inst_full_stoch = stochastic(:full_di, period: adaptive_half_period)
p0.full_stoch = three_pole_super_smooth(:inst_full_stoch, previous: :full_stoch, period: adaptive_half_period)
end

def compute_slow_period
p0.slow_dmu = three_pole_super_smooth(:dmu, previous: :slow_dmu, period: slow_period)
p0.slow_dmd = three_pole_super_smooth(:dmd, previous: :slow_dmd, period: slow_period)

atr_value = atr_point.slow
return if atr_value == 0.0 || points.size < min_period

p0.slow_diu = (100.0 * p0.slow_dmu) / atr_value
p0.slow_did = (100.0 * p0.slow_dmd) / atr_value

p0.slow_di = (p0.slow_diu - p1.slow_did).abs / (p0.slow_diu + p0.slow_did)
p0.slow = three_pole_super_smooth(:slow_di, previous: :slow, period: slow_period).clamp(-10.0, 10.0)
p0.inst_slow_stoch = stochastic(:slow_di, period: adaptive_half_period)
p0.slow_stoch = three_pole_super_smooth(:inst_slow_stoch, previous: :slow_stoch, period: adaptive_half_period)
end

def compute_traditional_period
p0.traditional_dmu = three_pole_super_smooth(:dmu, previous: :traditional_dmu, period: traditional_period)
p0.traditional_dmd = three_pole_super_smooth(:dmd, previous: :traditional_dmd, period: traditional_period)

atr_value = atr_point.traditional
return if atr_value == 0.0 || points.size < min_period

p0.traditional_diu = (100.0 * p0.traditional_dmu) / atr_value
p0.traditional_did = (100.0 * p0.traditional_dmd) / atr_value

p0.traditional_di = (p0.traditional_diu - p1.traditional_did).abs / (p0.traditional_diu + p0.traditional_did)
p0.traditional = three_pole_super_smooth(:traditional_di, previous: :traditional, period: traditional_period).clamp(-10.0, 10.0)
p0.inst_traditional_stoch = stochastic(:traditional_di, period: adaptive_half_period)
p0.traditional_stoch = three_pole_super_smooth(:inst_traditional_stoch, previous: :traditional_stoch, period: adaptive_half_period)
end

def compute
compute_directional_movement
compute_adaptive_period
compute_full_period
compute_slow_period
compute_traditional_period
end
end
end
Expand Down
96 changes: 67 additions & 29 deletions lib/quant/indicators/atr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ module Quant
module Indicators
class AtrPoint < IndicatorPoint
attribute :tr, default: 0.0
attribute :period, default: :min_period
attribute :value, default: 0.0
attribute :slow, default: 0.0
attribute :fast, default: 0.0
attribute :full, default: 0.0
attribute :traditional, default: 0.0
attribute :inst_stoch, default: 0.0
attribute :stoch, default: 0.0
attribute :stoch_up, default: false
Expand All @@ -24,55 +24,93 @@ def crossed_down?
end
end

# The Average True Range refers to a technical analysis indicator that measures
# the volatility of an asset’s or security’s price action. The ATR was introduced
# by J. Welles Wilder in his book “New Concepts in Technical Trading Systems” in 1978.
#
# The ATR formula is “[(Prior ATR x(n-1)) + Current TR]/n” where
# TR = ​max [(high − low), abs(high − previous close​), abs(low – previous close)].
#
# ATR values are primarily calculated on 14-day periods. Also, analysts use it
# to measure volatility for any specific duration spanning from intraday time
# frames to larger time frames.
#
# A high value of ATR implies high volatility, and a low value of ATR indicates
# low volatility or market sideways.
#
# Current ATR = [(Prior ATR x 13) + Current TR] / 14
#
# - Multiply the previous 14-day ATR by 13.
# - Add the most recent day's TR value.
# - Divide the total by 14
#
# This ATR is an adaptive version of the tradtional ATR based on half the
# dominant cycle period. It uses a 3-pole super smooth filter to smooth
# the ATR values. It also calculates the stochastic value of the ATR
# and the oscillator value of the ATR.
#
# Welles usually used 14 periods and this indicator takes that to be functionally
# equivalent to half the dominant cycle period for the ATR.
#
# Additional values are calculated:
# * "value" is adaptive and based on half the dominant cycle period
# * "tradtional" is static and uses the traditional 14 period ATR
# * "full" is adaptive and based on the full dominant cycle period
# * "slow" is adaptive and based on twice the dominant cycle period
class Atr < Indicator
register name: :atr

attr_reader :points

def period
dc_period / 2
def traditional_period
14
end

def fast_alpha
period_to_alpha(period)
def full_period
adaptive_period
end

def slow_alpha
period_to_alpha(2 * period)
def slow_period
adaptive_period * 2
end

# Typically, the Average True Range (ATR) is based on 14 periods and can be calculated on an intraday, daily, weekly
# or monthly basis. For this example, the ATR will be based on daily data. Because there must be a beginning, the first
# TR value is simply the High minus the Low, and the first 14-day ATR is the average of the daily TR values for the
# last 14 days. After that, Wilder sought to smooth the data by incorporating the previous period's ATR value.

# Current ATR = [(Prior ATR x 13) + Current TR] / 14

# - Multiply the previous 14-day ATR by 13.
# - Add the most recent day's TR value.
# - Divide the total by 14
def traditional_true_range
high_low = t0.high_price - t0.low_price
high_prev_close = (t0.high_price - t1.close_price).abs
low_prev_close = (t0.low_price - t1.close_price).abs

def compute
p0.period = period
p0.tr = (t1.high_price - t0.close_price).abs
[high_low, high_prev_close, low_prev_close].max
end

p0.value = three_pole_super_smooth :tr, period:, previous: :value
def compute_true_range
p0.tr = traditional_true_range
p0.tr = t0.high_price - (t0.high_price * 0.99) if p0.tr.zero?
end

p0.slow = (slow_alpha * p0.value) + ((1.0 - slow_alpha) * p1.slow)
p0.fast = (fast_alpha * p0.value) + ((1.0 - fast_alpha) * p1.fast)
def compute_average_true_range
p0.value = three_pole_super_smooth(:tr, previous: :value, period: adaptive_half_period)
p0.full = three_pole_super_smooth(:tr, previous: :full, period: full_period)
p0.slow = three_pole_super_smooth(:tr, previous: :slow, period: slow_period)
p0.traditional = three_pole_super_smooth(:tr, previous: :traditional, period: traditional_period)
end

p0.inst_stoch = stochastic :value, period:
p0.stoch = three_pole_super_smooth(:inst_stoch, previous: :stoch, period:).clamp(0, 100)
def compute_stoch
p0.inst_stoch = stochastic(:value, period: adaptive_half_period)
p0.stoch = three_pole_super_smooth(:inst_stoch, previous: :stoch, period: adaptive_half_period).clamp(0, 100)
p0.stoch_up = p0.stoch >= 70
p0.stoch_turned = p0.stoch_up && !p1.stoch_up
compute_oscillator
end

def compute_oscillator
p0.osc = p0.value - wma(:value)
p0.crossed = :up if p0.osc >= 0 && p1.osc < 0
p0.crossed = :down if p0.osc <= 0 && p1.osc > 0
end

def compute
compute_true_range
compute_average_true_range
compute_stoch
compute_oscillator
end
end
end
end
Loading