From b4735ed809045cfe82d5f683d3ec8bc874cf30d5 Mon Sep 17 00:00:00 2001 From: mwlang Date: Sat, 8 Jun 2024 22:29:00 -0400 Subject: [PATCH 1/6] using traditional means of computing TR on ATR --- Gemfile.lock | 2 +- lib/quant/indicators/atr.rb | 65 ++++++++++++------- lib/quant/version.rb | 2 +- spec/lib/quant/indicators/adx_spec.rb | 6 +- spec/lib/quant/indicators/atr_spec.rb | 5 +- spec/lib/quant/indicators/pivots/atr_spec.rb | 8 +-- .../quant/indicators/pivots/keltner_spec.rb | 8 +-- 7 files changed, 57 insertions(+), 39 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ecee9fa..0d8cbd5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - quantitative (0.3.3) + quantitative (0.3.4) oj (~> 3.10) zeitwerk (~> 2.6) diff --git a/lib/quant/indicators/atr.rb b/lib/quant/indicators/atr.rb index 4cd0cc9..1a4011e 100644 --- a/lib/quant/indicators/atr.rb +++ b/lib/quant/indicators/atr.rb @@ -4,7 +4,6 @@ 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 @@ -24,45 +23,65 @@ 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. class Atr < Indicator register name: :atr attr_reader :points - def period - dc_period / 2 - end - def fast_alpha - period_to_alpha(period) + period_to_alpha(adaptive_half_period) end def slow_alpha - period_to_alpha(2 * period) + period_to_alpha(adaptive_period) 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. + 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 - # Current ATR = [(Prior ATR x 13) + Current TR] / 14 + [high_low, high_prev_close, low_prev_close].max + end - # - Multiply the previous 14-day ATR by 13. - # - Add the most recent day's TR value. - # - Divide the total by 14 + def compute_true_range + p0.tr = traditional_true_range + p0.tr = t0.high_price - (t0.high_price * 0.99) if p0.tr.zero? + p0.value = three_pole_super_smooth :tr, period: adaptive_half_period, previous: :value + end def compute - p0.period = period - p0.tr = (t1.high_price - t0.close_price).abs - - p0.value = three_pole_super_smooth :tr, period:, previous: :value + compute_true_range - p0.slow = (slow_alpha * p0.value) + ((1.0 - slow_alpha) * p1.slow) - p0.fast = (fast_alpha * p0.value) + ((1.0 - fast_alpha) * p1.fast) + p0.slow = ema(:value, previous: :slow, period: slow_alpha) + p0.fast = ema(:value, previous: :fast, period: fast_alpha) - p0.inst_stoch = stochastic :value, period: - p0.stoch = three_pole_super_smooth(:inst_stoch, previous: :stoch, period:).clamp(0, 100) + 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 diff --git a/lib/quant/version.rb b/lib/quant/version.rb index 82adaba..bfcf4fb 100644 --- a/lib/quant/version.rb +++ b/lib/quant/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Quant - VERSION = "0.3.3" + VERSION = "0.3.4" end diff --git a/spec/lib/quant/indicators/adx_spec.rb b/spec/lib/quant/indicators/adx_spec.rb index d18c0ea..9304beb 100644 --- a/spec/lib/quant/indicators/adx_spec.rb +++ b/spec/lib/quant/indicators/adx_spec.rb @@ -30,10 +30,10 @@ it { expect(subject.values.last(5).map{ |v| v.dmu.round(4) }).to eq([0.5096, 0.5966, 0.669, 0.7249, 0.7629]) } it { expect(subject.values.last(5).map{ |v| v.dmd.round(4) }).to eq([0.5096, 0.5966, 0.669, 0.7249, 0.7629]) } - it { expect(subject.values.last(5).map{ |v| v.diu.round(4) }).to eq([1.6077, 15.8702, 29.1715, 38.7976, 45.3035]) } - it { expect(subject.values.last(5).map{ |v| v.did.round(4) }).to eq([1.6077, 15.8702, 29.1715, 38.7976, 45.3035]) } + it { expect(subject.values.last(5).map{ |v| v.diu.round(4) }).to eq([1.8899, 14.0403, 21.9558, 27.6149, 32.4262]) } + it { expect(subject.values.last(5).map{ |v| v.did.round(4) }).to eq([1.8899, 14.0403, 21.9558, 27.6149, 32.4262]) } - it { expect(subject.values.last(5).map{ |v| v.di.round(4) }).to eq([3.186, 0.4493, 0.228, 0.1241, 0.0718]) } + it { expect(subject.values.last(5).map{ |v| v.di.round(4) }).to eq([4.1724, 0.4327, 0.1803, 0.1025, 0.0742]) } it "is roughly half and half" do direction_of_changes = subject.values.each_cons(2).map{ |(a, b)| a.value - b.value > 0 } diff --git a/spec/lib/quant/indicators/atr_spec.rb b/spec/lib/quant/indicators/atr_spec.rb index f87f2ce..0362e9a 100644 --- a/spec/lib/quant/indicators/atr_spec.rb +++ b/spec/lib/quant/indicators/atr_spec.rb @@ -9,9 +9,8 @@ it { is_expected.to be_a(described_class) } it { expect(subject.series.size).to eq(4) } - it { expect(subject.values.map{ |v| v.tr.round(3) }).to eq([0.0, 4.0, 8.0, 16.0]) } - it { expect(subject.values.map{ |v| v.value.round(3) }).to eq([0.0, 0.231, 0.95, 2.57]) } - # it { expect(subject.values.map(&:crossed)).to all be(:unchanged) } + it { expect(subject.values.map{ |v| v.tr.round(3) }).to eq([2.0, 4.0, 8.0, 16.0]) } + it { expect(subject.values.map{ |v| v.value.round(3) }).to eq([0.115, 0.34, 1.045, 2.646]) } context "sine series" do let(:period) { 40 } # period bar sine wave diff --git a/spec/lib/quant/indicators/pivots/atr_spec.rb b/spec/lib/quant/indicators/pivots/atr_spec.rb index fb3b6c9..8a69f62 100644 --- a/spec/lib/quant/indicators/pivots/atr_spec.rb +++ b/spec/lib/quant/indicators/pivots/atr_spec.rb @@ -13,12 +13,12 @@ it { expect(subject.values.map(&:input)).to eq([3.0, 6.0, 12.0, 24.0]) } context "bands" do - it { expect(subject.values.map{ |v| v.h6.round(3) }).to eq([3.0, 4.09, 7.75, 16.4]) } - it { expect(subject.values.map{ |v| v.h1.round(3) }).to eq([3.0, 3.56, 5.572, 10.509]) } + it { expect(subject.values.map{ |v| v.h6.round(3) }).to eq([3.346, 4.416, 8.034, 16.626]) } + it { expect(subject.values.map{ |v| v.h1.round(3) }).to eq([3.082, 3.637, 5.639, 10.562]) } it { expect(subject.values.map{ |v| v.midpoint.round(3) }).to eq([3.0, 3.397, 4.899, 8.689]) } it { expect(subject.values.map{ |v| v.h0.round(3) }).to eq(subject.values.map{ |v| v.midpoint.round(3) }) } - it { expect(subject.values.map{ |v| v.l1.round(3) }).to eq([3.0, 3.234, 4.226, 6.869]) } - it { expect(subject.values.map{ |v| v.l6.round(3) }).to eq([3.0, 2.704, 2.048, 0.978]) } + it { expect(subject.values.map{ |v| v.l1.round(3) }).to eq([2.918, 3.157, 4.159, 6.816]) } + it { expect(subject.values.map{ |v| v.l6.round(3) }).to eq([2.654, 2.378, 1.764, 0.752]) } end context "bands do not intersect each other" do diff --git a/spec/lib/quant/indicators/pivots/keltner_spec.rb b/spec/lib/quant/indicators/pivots/keltner_spec.rb index 9b5f9ea..01ae125 100644 --- a/spec/lib/quant/indicators/pivots/keltner_spec.rb +++ b/spec/lib/quant/indicators/pivots/keltner_spec.rb @@ -13,12 +13,12 @@ it { expect(subject.values.map(&:input)).to eq([3.0, 6.0, 12.0, 24.0]) } context "bands" do - it { expect(subject.values.map{ |v| v.h6.round(3) }).to eq([3.0, 4.238, 7.934, 16.233]) } - it { expect(subject.values.map{ |v| v.h1.round(3) }).to eq([3.0, 3.709, 5.756, 10.342]) } + it { expect(subject.values.map{ |v| v.h6.round(3) }).to eq([3.346, 4.564, 8.218, 16.459]) } + it { expect(subject.values.map{ |v| v.h1.round(3) }).to eq([3.082, 3.786, 5.823, 10.395]) } it { expect(subject.values.map{ |v| v.midpoint.round(3) }).to eq([3.0, 3.545, 5.083, 8.522]) } it { expect(subject.values.map{ |v| v.h0.round(3) }).to eq(subject.values.map{ |v| v.midpoint.round(3) }) } - it { expect(subject.values.map{ |v| v.l1.round(3) }).to eq([3.0, 3.382, 4.41, 6.702]) } - it { expect(subject.values.map{ |v| v.l6.round(3) }).to eq([3.0, 2.853, 2.231, 0.811]) } + it { expect(subject.values.map{ |v| v.l1.round(3) }).to eq([2.918, 3.305, 4.343, 6.649]) } + it { expect(subject.values.map{ |v| v.l6.round(3) }).to eq([2.654, 2.527, 1.947, 0.585]) } end context "bands do not intersect each other" do From 9a0b137e8e4a9bd4812acabcd058a0731cc88075 Mon Sep 17 00:00:00 2001 From: mwlang Date: Sun, 9 Jun 2024 10:54:03 -0400 Subject: [PATCH 2/6] Improved ADX and ATR indicators and aligned with each other --- lib/quant/config.rb | 2 +- lib/quant/indicators/adx.rb | 160 ++++++++++++++++++++------ lib/quant/indicators/atr.rb | 57 ++++++--- lib/quant/indicators/indicator.rb | 2 +- spec/lib/quant/indicators/adx_spec.rb | 16 ++- spec/lib/quant/indicators/atr_spec.rb | 8 +- 6 files changed, 183 insertions(+), 62 deletions(-) diff --git a/lib/quant/config.rb b/lib/quant/config.rb index de3c6d7..4c613ba 100644 --- a/lib/quant/config.rb +++ b/lib/quant/config.rb @@ -19,7 +19,7 @@ def self.default! end def self.config - @config ||= Config.new + @config ||= default! end end diff --git a/lib/quant/indicators/adx.rb b/lib/quant/indicators/adx.rb index 9d4fd28..d857eb5 100644 --- a/lib/quant/indicators/adx.rb +++ b/lib/quant/indicators/adx.rb @@ -5,71 +5,159 @@ 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 :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 :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 :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_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 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 + 14 + end + + def full_period + adaptive_period end - def period - dc_period + def slow_period + adaptive_period * 2 end def atr_point series.indicators[source].atr.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) + + atr_value = atr_point.value + return if atr_value == 0.0 || points.size < adaptive_half_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) + end - p0.dmu_ema = three_pole_super_smooth :dmu, period:, previous: :dmu_ema - p0.dmd_ema = three_pole_super_smooth :dmd, period:, previous: :dmd_ema + 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) - atr_value = atr_point.fast * scale - return if atr_value == 0.0 || @points.size < period + atr_value = atr_point.full + return if atr_value == 0.0 || points.size < 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 + p0.full_diu = (100.0 * p0.full_dmu) / atr_value + p0.full_did = (100.0 * p0.full_dmd) / atr_value - # 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_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) + 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 < slow_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) + 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) - p0.value = p0.di_ema - p0.inst_stoch = stochastic(:di, period:) - p0.stoch = three_pole_super_smooth :inst_stoch, period:, previous: :stoch + atr_value = atr_point.traditional + return if atr_value == 0.0 || points.size < traditional_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) + end + + def compute_stochastic + 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 + + def compute + compute_directional_movement + compute_adaptive_period + compute_full_period + compute_slow_period + compute_traditional_period + compute_stochastic end end end diff --git a/lib/quant/indicators/atr.rb b/lib/quant/indicators/atr.rb index 1a4011e..67a7553 100644 --- a/lib/quant/indicators/atr.rb +++ b/lib/quant/indicators/atr.rb @@ -6,7 +6,8 @@ class AtrPoint < IndicatorPoint attribute :tr, default: 0.0 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 @@ -26,19 +27,19 @@ def crossed_down? # 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 @@ -47,17 +48,28 @@ def crossed_down? # 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 traditional_period + 14 + end - def fast_alpha - period_to_alpha(adaptive_half_period) + def full_period + adaptive_period end - def slow_alpha - period_to_alpha(adaptive_period) + def slow_period + adaptive_period * 2 end def traditional_true_range @@ -71,20 +83,20 @@ def traditional_true_range def compute_true_range p0.tr = traditional_true_range p0.tr = t0.high_price - (t0.high_price * 0.99) if p0.tr.zero? - p0.value = three_pole_super_smooth :tr, period: adaptive_half_period, previous: :value end - def compute - compute_true_range - - p0.slow = ema(:value, previous: :slow, period: slow_alpha) - p0.fast = ema(:value, previous: :fast, period: fast_alpha) + 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: adaptive_half_period + 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 @@ -92,6 +104,13 @@ def compute_oscillator 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 diff --git a/lib/quant/indicators/indicator.rb b/lib/quant/indicators/indicator.rb index eca1231..5db392b 100644 --- a/lib/quant/indicators/indicator.rb +++ b/lib/quant/indicators/indicator.rb @@ -35,7 +35,7 @@ def self.depends_on(*indicator_classes) Array(indicator_classes).each{ |dependency| dependent_indicator_classes << dependency } end - attr_reader :source, :series + attr_reader :source, :series, :points def initialize(series:, source:) @series = series diff --git a/spec/lib/quant/indicators/adx_spec.rb b/spec/lib/quant/indicators/adx_spec.rb index 9304beb..83e3e68 100644 --- a/spec/lib/quant/indicators/adx_spec.rb +++ b/spec/lib/quant/indicators/adx_spec.rb @@ -9,6 +9,12 @@ it { is_expected.to be_a(described_class) } + context "ADX's periods matches ATR's" do + it { expect(subject.traditional_period).to eq(subject.series.indicators[source].atr.traditional_period) } + it { expect(subject.slow_period).to eq(subject.series.indicators[source].atr.slow_period) } + it { expect(subject.full_period).to eq(subject.series.indicators[source].atr.full_period) } + end + context "sine series" do let(:period) { 40 } let(:cycles) { 4 } @@ -30,16 +36,18 @@ it { expect(subject.values.last(5).map{ |v| v.dmu.round(4) }).to eq([0.5096, 0.5966, 0.669, 0.7249, 0.7629]) } it { expect(subject.values.last(5).map{ |v| v.dmd.round(4) }).to eq([0.5096, 0.5966, 0.669, 0.7249, 0.7629]) } - it { expect(subject.values.last(5).map{ |v| v.diu.round(4) }).to eq([1.8899, 14.0403, 21.9558, 27.6149, 32.4262]) } - it { expect(subject.values.last(5).map{ |v| v.did.round(4) }).to eq([1.8899, 14.0403, 21.9558, 27.6149, 32.4262]) } + it { expect(subject.values.last(5).map{ |v| v.value.round(4) }).to eq([0.3686, 0.3418, 0.2907, 0.2254, 0.1574]) } + it { expect(subject.values.last(5).map{ |v| v.stoch.round(4) }).to eq([48.3063, 46.6152, 41.0631, 32.902, 23.7553]) } - it { expect(subject.values.last(5).map{ |v| v.di.round(4) }).to eq([4.1724, 0.4327, 0.1803, 0.1025, 0.0742]) } + it { expect(subject.values.last(5).map{ |v| v.full.round(4) }).to eq([2.3542, 2.1598, 1.9475, 1.7248, 1.4983]) } + it { expect(subject.values.last(5).map{ |v| v.slow.round(4) }).to eq([0.0245, 0.0245, 0.0246, 0.0248, 0.0251]) } + it { expect(subject.values.last(5).map{ |v| v.traditional.round(4) }).to eq([0.3686, 0.3418, 0.2907, 0.2254, 0.1574]) } it "is roughly half and half" do direction_of_changes = subject.values.each_cons(2).map{ |(a, b)| a.value - b.value > 0 } value_counts = direction_of_changes.group_by(&:itself).transform_values(&:count) - expect(value_counts[true].to_f / value_counts[false]).to be > 0.8 + expect(value_counts[true].to_f / value_counts[false]).to be > 0.7 end end end diff --git a/spec/lib/quant/indicators/atr_spec.rb b/spec/lib/quant/indicators/atr_spec.rb index 0362e9a..391a2b1 100644 --- a/spec/lib/quant/indicators/atr_spec.rb +++ b/spec/lib/quant/indicators/atr_spec.rb @@ -27,9 +27,15 @@ end it { expect(subject.series.size).to eq(160) } + + # NOTE: Traditional same as adaptive values in test suite due to setting to static HalfPerod Indicator + it { expect(subject.values.last(5).map{ |v| v.traditional.round(3) }).to eq([0.179, 0.237, 0.321, 0.419, 0.52]) } it { expect(subject.values.last(5).map{ |v| v.value.round(3) }).to eq([0.179, 0.237, 0.321, 0.419, 0.52]) } - it "crosses 4x cycles" do + it { expect(subject.values.last(5).map{ |v| v.full.round(3) }).to eq([0.471, 0.441, 0.418, 0.403, 0.397]) } + it { expect(subject.values.last(5).map{ |v| v.slow.round(3) }).to eq([0.507, 0.503, 0.499, 0.495, 0.491]) } + + it "crosses 4x cycles for adaptive value" do grouped_crossings = subject.values.map(&:crossed).group_by(&:itself).transform_values(&:count) unchanged_count = period * cycles - cycles * 4 expect(grouped_crossings).to eq({ down: cycles * 2, unchanged: unchanged_count, up: cycles * 2 }) From b04903b6587ffa20f72275e984a49ca4ce9018e4 Mon Sep 17 00:00:00 2001 From: mwlang Date: Sun, 9 Jun 2024 10:58:07 -0400 Subject: [PATCH 3/6] references ATR's periods directly in ADX indicator --- lib/quant/indicators/adx.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/quant/indicators/adx.rb b/lib/quant/indicators/adx.rb index d857eb5..04f084d 100644 --- a/lib/quant/indicators/adx.rb +++ b/lib/quant/indicators/adx.rb @@ -61,19 +61,23 @@ class Adx < Indicator depends_on Indicators::Atr def traditional_period - 14 + atr_indicator.traditional_period end def full_period - adaptive_period + atr_indicator.full_period end def slow_period - adaptive_period * 2 + atr_indicator.slow_period + end + + def atr_indicator + @atr_indicator ||= series.indicators[source].atr end def atr_point - series.indicators[source].atr.points[t0] + atr_indicator.points[t0] end # To calculate the ADX, first determine the + and - directional movement, or DM. From e5f71f15e363371488a878f4454c8db5b462776f Mon Sep 17 00:00:00 2001 From: mwlang Date: Sun, 9 Jun 2024 11:22:02 -0400 Subject: [PATCH 4/6] added stoch for all periods of ADX --- lib/quant/indicators/adx.rb | 35 +++++++++++++++------------ spec/lib/quant/indicators/adx_spec.rb | 2 +- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/quant/indicators/adx.rb b/lib/quant/indicators/adx.rb index 04f084d..c58d35b 100644 --- a/lib/quant/indicators/adx.rb +++ b/lib/quant/indicators/adx.rb @@ -12,6 +12,8 @@ class AdxPoint < IndicatorPoint 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 :full_dmu, default: 0.0 attribute :full_dmd, default: 0.0 @@ -19,6 +21,8 @@ class AdxPoint < IndicatorPoint 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 @@ -26,6 +30,8 @@ class AdxPoint < IndicatorPoint 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 @@ -33,11 +39,8 @@ class AdxPoint < IndicatorPoint attribute :traditional_did, default: 0.0 attribute :traditional_di, default: 0.0 attribute :traditional, 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 :inst_traditional_stoch, default: 0.0 + attribute :traditional_stoch, default: 0.0 end # The Average Directional Index (ADX) is a technical indicator that measures @@ -99,13 +102,15 @@ def compute_adaptive_period p0.adaptive_dmd = three_pole_super_smooth(:dmd, previous: :adaptive_dmd, period: adaptive_half_period) atr_value = atr_point.value - return if atr_value == 0.0 || points.size < adaptive_half_period + 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 def compute_full_period @@ -113,13 +118,15 @@ def compute_full_period p0.full_dmd = three_pole_super_smooth(:dmd, previous: :full_dmd, period: full_period) atr_value = atr_point.full - return if atr_value == 0.0 || points.size < full_period + return if atr_value == 0.0 || points.size < min_period p0.full_diu = (100.0 * p0.full_dmu) / atr_value p0.full_did = (100.0 * p0.full_dmd) / atr_value 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 @@ -127,13 +134,15 @@ def compute_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 < slow_period + 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 @@ -141,18 +150,15 @@ def compute_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 < traditional_period + 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) - end - - def compute_stochastic - p0.inst_stoch = stochastic(:adaptive_di, period: adaptive_half_period) - p0.stoch = three_pole_super_smooth(:inst_stoch, previous: :stoch, period: adaptive_half_period) + 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 @@ -161,7 +167,6 @@ def compute compute_full_period compute_slow_period compute_traditional_period - compute_stochastic end end end diff --git a/spec/lib/quant/indicators/adx_spec.rb b/spec/lib/quant/indicators/adx_spec.rb index 83e3e68..c7613a4 100644 --- a/spec/lib/quant/indicators/adx_spec.rb +++ b/spec/lib/quant/indicators/adx_spec.rb @@ -40,7 +40,7 @@ it { expect(subject.values.last(5).map{ |v| v.stoch.round(4) }).to eq([48.3063, 46.6152, 41.0631, 32.902, 23.7553]) } it { expect(subject.values.last(5).map{ |v| v.full.round(4) }).to eq([2.3542, 2.1598, 1.9475, 1.7248, 1.4983]) } - it { expect(subject.values.last(5).map{ |v| v.slow.round(4) }).to eq([0.0245, 0.0245, 0.0246, 0.0248, 0.0251]) } + it { expect(subject.values.last(5).map{ |v| v.slow.round(4) }).to eq([0.0242, 0.0243, 0.0244, 0.0246, 0.0249]) } it { expect(subject.values.last(5).map{ |v| v.traditional.round(4) }).to eq([0.3686, 0.3418, 0.2907, 0.2254, 0.1574]) } it "is roughly half and half" do From 154d9bb2fabf5f87924ce1e45ad552308d8fbb73 Mon Sep 17 00:00:00 2001 From: mwlang Date: Sat, 15 Jun 2024 22:58:37 -0400 Subject: [PATCH 5/6] limited series compute indicators off parent series --- lib/quant/dominant_cycles_source.rb | 8 ++--- lib/quant/indicators/indicator.rb | 32 ++++++++++++++++--- lib/quant/indicators_registry.rb | 17 +++++----- lib/quant/indicators_source.rb | 14 +++++--- lib/quant/indicators_sources.rb | 11 ++++++- lib/quant/pivots_source.rb | 8 ++--- lib/quant/refinements/array.rb | 5 +-- lib/quant/series.rb | 3 +- spec/lib/quant/dominant_cycles_source_spec.rb | 16 ++-------- spec/lib/quant/indicators/adx_spec.rb | 15 +-------- spec/lib/quant/indicators/atr_spec.rb | 11 +------ spec/lib/quant/indicators/cci_spec.rb | 13 +------- spec/lib/quant/indicators/decycler_spec.rb | 12 +------ .../indicators/dominant_cycles/acr_spec.rb | 16 +++------- .../dominant_cycles/band_pass_spec.rb | 16 +++------- .../dominant_cycles/differential_spec.rb | 16 +++------- .../dominant_cycles/half_period_spec.rb | 16 +++------- .../dominant_cycles/homodyne_spec.rb | 16 +++------- .../dominant_cycles/phase_accumulator_spec.rb | 16 +++------- spec/lib/quant/indicators/frama_spec.rb | 12 +------ spec/lib/quant/indicators/indicator_spec.rb | 28 +++++++++------- spec/lib/quant/indicators/mama_spec.rb | 12 +------ spec/lib/quant/indicators/mesa_spec.rb | 12 +------ spec/lib/quant/indicators/rocket_rsi_spec.rb | 12 +------ spec/lib/quant/indicators/roofing_spec.rb | 13 +------- spec/lib/quant/indicators/rsi_spec.rb | 17 +--------- spec/lib/quant/indicators/snr_spec.rb | 15 +-------- spec/lib/quant/indicators_source_spec.rb | 32 ++++++++++++------- spec/lib/quant/indicators_sources_spec.rb | 16 ++++++++++ spec/lib/quant/pivots_source_spec.rb | 4 +-- spec/lib/quant/refinements/array_spec.rb | 15 ++++++++- spec/lib/quant/series_spec.rb | 12 +++++++ spec/spec_helper.rb | 9 ++++-- spec/support/series_factory.rb | 16 ++++++++++ 34 files changed, 211 insertions(+), 275 deletions(-) create mode 100644 spec/support/series_factory.rb diff --git a/lib/quant/dominant_cycles_source.rb b/lib/quant/dominant_cycles_source.rb index c9bf443..2f46df3 100644 --- a/lib/quant/dominant_cycles_source.rb +++ b/lib/quant/dominant_cycles_source.rb @@ -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 diff --git a/lib/quant/indicators/indicator.rb b/lib/quant/indicators/indicator.rb index 5db392b..70f54b4 100644 --- a/lib/quant/indicators/indicator.rb +++ b/lib/quant/indicators/indicator.rb @@ -41,6 +41,7 @@ def initialize(series:, source:) @series = series @source = source @points = {} + series.each { |tick| self << tick } end @@ -135,22 +136,45 @@ def period_points(max_period) attr_reader :p0, :p1, :p2, :p3 attr_reader :t0, :t1, :t2, :t3 - def <<(tick) - @t0 = tick - @p0 = points_class.new(indicator: self, tick:, source:) - @points[tick] = @p0 + def new_point(tick:) + points_class.new(indicator: self, tick:, source:) + end + def from_parent_series?(tick:) + tick.series? && tick.series != series + end + + def parent_series_point(tick:) + return unless from_parent_series?(tick:) + + tick.series.indicators[source][self.class].points[tick] + end + + def advance_to_next_point(tick:) + @p0 = @points[tick] @p1 = values[-2] || @p0 @p2 = values[-3] || @p1 @p3 = values[-4] || @p2 + @t0 = tick @t1 = ticks[-2] || @t0 @t2 = ticks[-3] || @t1 @t3 = ticks[-4] || @t2 + end + + def <<(tick) + @points[tick] = parent_series_point(tick:) || new_point(tick:) + advance_to_next_point(tick:) + return if from_parent_series?(tick:) compute end + def assign(tick:) + @points[tick] = parent_series_point(tick:) + advance_to_next_point(tick:) + end + def each(&block) @points.each_value(&block) end diff --git a/lib/quant/indicators_registry.rb b/lib/quant/indicators_registry.rb index d89f7d7..65a0364 100644 --- a/lib/quant/indicators_registry.rb +++ b/lib/quant/indicators_registry.rb @@ -6,8 +6,8 @@ def self.included(base) base.extend(ClassMethods) end - def define_indicator_accessors(indicator_source:) - self.class.define_indicator_accessors(indicator_source:) + def define_indicator_accessors(indicators_source:) + self.class.define_indicator_accessors(indicators_source:) end module ClassMethods @@ -43,19 +43,18 @@ def dominant_cycle? def register(name:, indicator_class:) entry = RegistryEntry.new(name:, indicator_class:) registry[entry.key] = entry - # registry[name] = indicator_class end - def registry_entries_for(indicator_source:) - return registry.values.select(&:pivot?) if indicator_source.is_a?(PivotsSource) - return registry.values.select(&:dominant_cycle?) if indicator_source.is_a?(DominantCyclesSource) + def registry_entries_for(indicators_source:) + return registry.values.select(&:pivot?) if indicators_source.is_a?(PivotsSource) + return registry.values.select(&:dominant_cycle?) if indicators_source.is_a?(DominantCyclesSource) registry.values.select(&:standard?) end - def define_indicator_accessors(indicator_source:) - registry_entries_for(indicator_source:).each do |entry| - indicator_source.define_singleton_method(entry.name) { indicator(entry.indicator_class) } + def define_indicator_accessors(indicators_source:) + registry_entries_for(indicators_source:).each do |entry| + indicators_source.define_singleton_method(entry.name) { indicator(entry.indicator_class) } end end end diff --git a/lib/quant/indicators_source.rb b/lib/quant/indicators_source.rb index 8f987f4..6e272c2 100644 --- a/lib/quant/indicators_source.rb +++ b/lib/quant/indicators_source.rb @@ -26,10 +26,10 @@ def initialize(series:, source:) @indicators = {} @ordered_indicators = [] - define_indicator_accessors(indicator_source: self) + define_indicator_accessors(indicators_source: self) - @dominant_cycles = DominantCyclesSource.new(indicator_source: self) - @pivots = PivotsSource.new(indicator_source: self) + @dominant_cycles = DominantCyclesSource.new(indicators_source: self) + @pivots = PivotsSource.new(indicators_source: self) end def [](indicator_class) @@ -40,6 +40,10 @@ def <<(tick) @ordered_indicators.each { |indicator| indicator << tick } end + def assign(indicators_source:, tick:) + indicators_source.ordered_indicators.each { |indicator| self[indicator.class].assign(tick:) } + end + # Attaches a given Indicator class and defines the method for # accessing it using the given name. Indicators take care of # computing their values when first attached to a populated @@ -69,10 +73,12 @@ def dominant_cycle indicator(dominant_cycle_indicator_class) end - private + protected attr_reader :indicators, :ordered_indicators + private + def dominant_cycle_indicator_class Quant.config.indicators.dominant_cycle_indicator_class end diff --git a/lib/quant/indicators_sources.rb b/lib/quant/indicators_sources.rb index 46a3e9c..bad1a45 100644 --- a/lib/quant/indicators_sources.rb +++ b/lib/quant/indicators_sources.rb @@ -12,7 +12,7 @@ class IndicatorsSources COMPUTED_SOURCES = %i[oc2 hl2 hlc3 ohlc4].freeze ].flatten.freeze - attr_reader :series + attr_reader :series, :sources def initialize(series:) @series = series @@ -25,7 +25,10 @@ def [](source) @sources[source] ||= IndicatorsSource.new(series:, source:) end + # The indicators always compute off the original series, so we assign instead of appending. def <<(tick) + return assign(tick) if tick.series? + @sources.each_value { |indicator| indicator << tick } end @@ -47,6 +50,12 @@ def method_missing(method_name, *args, &block) private + def assign(tick) + tick.series.indicators.sources.each do |source, indicators_source| + self[source].assign(indicators_source:, tick:) + end + end + def invalid_source_error(source:) raise Errors::InvalidIndicatorSource, "Invalid indicator source: #{source.inspect}" end diff --git a/lib/quant/pivots_source.rb b/lib/quant/pivots_source.rb index a5ccdb6..c656389 100644 --- a/lib/quant/pivots_source.rb +++ b/lib/quant/pivots_source.rb @@ -2,15 +2,15 @@ module Quant class PivotsSource - 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 diff --git a/lib/quant/refinements/array.rb b/lib/quant/refinements/array.rb index f64a44c..23966ce 100644 --- a/lib/quant/refinements/array.rb +++ b/lib/quant/refinements/array.rb @@ -45,10 +45,11 @@ def <<(value) def push(*objects) Array(objects).each do |object| super(object) + if @max_size && size > @max_size voted_off = shift - @minimum = min if voted_off == @minimum - @maximum = max if voted_off == @maximum + @minimum = min if voted_off == @minimum || object < @minimum + @maximum = max if voted_off == @maximum || object > @maximum else @maximum = object if @maximum.nil? || object > @maximum @minimum = object if @minimum.nil? || object < @minimum diff --git a/lib/quant/series.rb b/lib/quant/series.rb index e0fc60e..be38fbb 100644 --- a/lib/quant/series.rb +++ b/lib/quant/series.rb @@ -106,7 +106,8 @@ def inspect def <<(tick) tick = Ticks::Spot.new(price: tick) if tick.is_a?(Numeric) - indicators << tick unless tick.series? + indicators << tick + @ticks << tick.assign_series(self) self end diff --git a/spec/lib/quant/dominant_cycles_source_spec.rb b/spec/lib/quant/dominant_cycles_source_spec.rb index 38746c9..ea137e9 100644 --- a/spec/lib/quant/dominant_cycles_source_spec.rb +++ b/spec/lib/quant/dominant_cycles_source_spec.rb @@ -1,21 +1,11 @@ # frozen_string_literal: true RSpec.describe Quant::DominantCyclesSource do - let(:series) do - # 40 bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - 5.times do - (0..39).each do |degree| - radians = degree * 2 * Math::PI / 40 - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end let(:source) { :oc2 } - let(:indicator_source) { Quant::IndicatorsSource.new(series:, source:) } + let(:indicators_source) { Quant::IndicatorsSource.new(series:, source:) } + let(:series) { sine_series(period: 40, cycles: 5) } - subject { described_class.new(indicator_source:) } + subject { described_class.new(indicators_source:) } it { expect(subject.acr.values[-1].period).to eq(40) } it { expect(subject.band_pass.values[-1].period).to eq(40) } diff --git a/spec/lib/quant/indicators/adx_spec.rb b/spec/lib/quant/indicators/adx_spec.rb index c7613a4..a7ec470 100644 --- a/spec/lib/quant/indicators/adx_spec.rb +++ b/spec/lib/quant/indicators/adx_spec.rb @@ -16,20 +16,7 @@ end context "sine series" do - let(:period) { 40 } - let(:cycles) { 4 } - let(:uniq_data_points) { cycles * period / cycles } # sine is cyclical, so we expect a few unique data points - let(:series) do - # period bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period: 40, cycles: 4) } it { expect(subject.series.size).to eq(160) } diff --git a/spec/lib/quant/indicators/atr_spec.rb b/spec/lib/quant/indicators/atr_spec.rb index 391a2b1..a0c09eb 100644 --- a/spec/lib/quant/indicators/atr_spec.rb +++ b/spec/lib/quant/indicators/atr_spec.rb @@ -15,16 +15,7 @@ context "sine series" do let(:period) { 40 } # period bar sine wave let(:cycles) { 4 } - let(:series) do - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period:, cycles:) } it { expect(subject.series.size).to eq(160) } diff --git a/spec/lib/quant/indicators/cci_spec.rb b/spec/lib/quant/indicators/cci_spec.rb index bd49171..26c500a 100644 --- a/spec/lib/quant/indicators/cci_spec.rb +++ b/spec/lib/quant/indicators/cci_spec.rb @@ -7,18 +7,7 @@ let(:source) { :oc2 } let(:period) { 40 } let(:cycles) { 5 } - let(:uniq_data_points) { cycles * period / cycles } # sine is cyclical, so we expect a few unique data points - let(:series) do - # period bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period:, cycles:) } it { expect(subject.series.size).to eq(cycles * period) } diff --git a/spec/lib/quant/indicators/decycler_spec.rb b/spec/lib/quant/indicators/decycler_spec.rb index b40f592..a49561a 100644 --- a/spec/lib/quant/indicators/decycler_spec.rb +++ b/spec/lib/quant/indicators/decycler_spec.rb @@ -23,17 +23,7 @@ let(:period) { 40 } let(:cycles) { 5 } let(:uniq_data_points) { cycles * period / cycles } # sine is cyclical, so we expect a few unique data points - let(:series) do - # period bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period:, cycles:) } it { expect(subject.series.size).to eq(cycles * period) } diff --git a/spec/lib/quant/indicators/dominant_cycles/acr_spec.rb b/spec/lib/quant/indicators/dominant_cycles/acr_spec.rb index 3eb06ea..166d617 100644 --- a/spec/lib/quant/indicators/dominant_cycles/acr_spec.rb +++ b/spec/lib/quant/indicators/dominant_cycles/acr_spec.rb @@ -14,19 +14,11 @@ it { expect(subject.t0).to eq subject.ticks[-1] } context "sine series" do - let(:series) do - # 40 bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - 3.times do - (0..39).each do |degree| - radians = degree * 2 * Math::PI / 40 - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:period) { 40 } + let(:cycles) { 5 } + let(:series) { sine_series(period:, cycles:) } - it { expect(subject.series.size).to eq(120) } + it { expect(subject.series.size).to eq(period * cycles) } it { expect(subject.p0.period).to eq 40 } end end diff --git a/spec/lib/quant/indicators/dominant_cycles/band_pass_spec.rb b/spec/lib/quant/indicators/dominant_cycles/band_pass_spec.rb index 3369961..35978f0 100644 --- a/spec/lib/quant/indicators/dominant_cycles/band_pass_spec.rb +++ b/spec/lib/quant/indicators/dominant_cycles/band_pass_spec.rb @@ -14,19 +14,11 @@ it { expect(subject.t0).to eq subject.ticks[-1] } context "sine series" do - let(:series) do - # 40 bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - 3.times do - (0..39).each do |degree| - radians = degree * 2 * Math::PI / 40 - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:period) { 40 } + let(:cycles) { 3 } + let(:series) { sine_series(period: 40, cycles: 3) } - it { expect(subject.series.size).to eq(120) } + it { expect(subject.series.size).to eq(period * cycles) } it { expect(subject.p0.period).to eq 40 } end end diff --git a/spec/lib/quant/indicators/dominant_cycles/differential_spec.rb b/spec/lib/quant/indicators/dominant_cycles/differential_spec.rb index 6e259fa..53e115a 100644 --- a/spec/lib/quant/indicators/dominant_cycles/differential_spec.rb +++ b/spec/lib/quant/indicators/dominant_cycles/differential_spec.rb @@ -14,19 +14,11 @@ it { expect(subject.t0).to eq subject.ticks[-1] } context "sine series" do - let(:series) do - # 40 bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - 2.times do - (0..39).each do |degree| - radians = degree * 2 * Math::PI / 40 - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:period) { 40 } + let(:cycles) { 2 } + let(:series) { sine_series(period:, cycles:) } - it { expect(subject.series.size).to eq(80) } + it { expect(subject.series.size).to eq(period * cycles) } it { expect(subject.p0.period).to eq 40 } end end diff --git a/spec/lib/quant/indicators/dominant_cycles/half_period_spec.rb b/spec/lib/quant/indicators/dominant_cycles/half_period_spec.rb index ca8e0c0..d454f6a 100644 --- a/spec/lib/quant/indicators/dominant_cycles/half_period_spec.rb +++ b/spec/lib/quant/indicators/dominant_cycles/half_period_spec.rb @@ -14,19 +14,11 @@ it { expect(subject.t0).to eq subject.ticks[-1] } context "sine series" do - let(:series) do - # 40 bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - 2.times do - (0..39).each do |degree| - radians = degree * 2 * Math::PI / 40 - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:period) { 40 } + let(:cycles) { 2 } + let(:series) { sine_series(period:, cycles:) } - it { expect(subject.series.size).to eq(80) } + it { expect(subject.series.size).to eq(period * cycles) } it { expect(subject.p0.period).to eq 29 } context "with an alternate configuration" do diff --git a/spec/lib/quant/indicators/dominant_cycles/homodyne_spec.rb b/spec/lib/quant/indicators/dominant_cycles/homodyne_spec.rb index 638487d..e031d36 100644 --- a/spec/lib/quant/indicators/dominant_cycles/homodyne_spec.rb +++ b/spec/lib/quant/indicators/dominant_cycles/homodyne_spec.rb @@ -14,19 +14,11 @@ it { expect(subject.t0).to eq subject.ticks[-1] } context "sine series" do - let(:series) do - # 40 bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - 2.times do - (0..39).each do |degree| - radians = degree * 2 * Math::PI / 40 - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:period) { 40 } + let(:cycles) { 2 } + let(:series) { sine_series(period:, cycles:) } - it { expect(subject.series.size).to eq(80) } + it { expect(subject.series.size).to eq(period * cycles) } it { expect(subject.p0.period).to eq 40 } end end diff --git a/spec/lib/quant/indicators/dominant_cycles/phase_accumulator_spec.rb b/spec/lib/quant/indicators/dominant_cycles/phase_accumulator_spec.rb index 59e428c..0392b3f 100644 --- a/spec/lib/quant/indicators/dominant_cycles/phase_accumulator_spec.rb +++ b/spec/lib/quant/indicators/dominant_cycles/phase_accumulator_spec.rb @@ -14,19 +14,11 @@ it { expect(subject.t0).to eq subject.ticks[-1] } context "sine series" do - let(:series) do - # 40 bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - 5.times do - (0..39).each do |degree| - radians = degree * 2 * Math::PI / 40 - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:period) { 40 } + let(:cycles) { 5 } + let(:series) { sine_series(period:, cycles:) } - it { expect(subject.series.size).to eq(200) } + it { expect(subject.series.size).to eq(period * cycles) } it { expect(subject.p0.period).to eq 41 } end end diff --git a/spec/lib/quant/indicators/frama_spec.rb b/spec/lib/quant/indicators/frama_spec.rb index 0f00b63..2954ca2 100644 --- a/spec/lib/quant/indicators/frama_spec.rb +++ b/spec/lib/quant/indicators/frama_spec.rb @@ -17,17 +17,7 @@ let(:period) { 40 } let(:cycles) { 5 } let(:uniq_data_points) { cycles * period / cycles } # sine is cyclical, so we expect a few unique data points - let(:series) do - # period bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period:, cycles:) } it { expect(subject.series.size).to eq(cycles * period) } diff --git a/spec/lib/quant/indicators/indicator_spec.rb b/spec/lib/quant/indicators/indicator_spec.rb index fe99d10..a0e3b91 100644 --- a/spec/lib/quant/indicators/indicator_spec.rb +++ b/spec/lib/quant/indicators/indicator_spec.rb @@ -74,17 +74,7 @@ def points_class it { expect(subject.periods.uniq).to eq [29] } describe "dominant_cycle" do - let(:series) do - # 40 bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - 3.times do - (0..39).each do |degree| - radians = degree * 2 * Math::PI / 40 - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period: 40, cycles: 3) } before do Quant.configure_indicators(dominant_cycle_kind: :band_pass) @@ -116,5 +106,21 @@ def points_class it { is_expected.to eq([100, 200, 300, 400]) } end end + + context "#assign" do + let(:filename) { fixture_filename("DEUCES-sample.txt", :series) } + let(:series1) { Quant::Series.from_file(filename:, symbol: "DEUCES", interval: "1d") } + + let(:period) { (series1.ticks[1].open_timestamp..series1.ticks[2].close_timestamp) } + let(:series2) { series1.limit(period) } + + it "subset first still matches superset of ticks" do + expect(series1.indicators.oc2.ping.map(&:pong)).to eq [3.0, 6.0, 12.0, 24.0] + expect(series2.indicators.oc2.ping.map(&:pong)).to eq [6.0, 12.0] + + expect(series1.indicators.oc2.pivots.bollinger.map(&:h0)).to eq [3.0, 3.2, 3.706666666666667, 4.712444444444445] + expect(series2.indicators.oc2.pivots.bollinger.map(&:h0)).to eq [3.2, 3.706666666666667] + end + end end end diff --git a/spec/lib/quant/indicators/mama_spec.rb b/spec/lib/quant/indicators/mama_spec.rb index c2d9381..c591589 100644 --- a/spec/lib/quant/indicators/mama_spec.rb +++ b/spec/lib/quant/indicators/mama_spec.rb @@ -22,17 +22,7 @@ let(:period) { 40 } let(:cycles) { 4 } let(:uniq_data_points) { cycles * period / cycles } # sine is cyclical, so we expect a few unique data points - let(:series) do - # period bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period:, cycles:) } it { expect(subject.series.size).to eq(160) } it { expect(subject.values.map{ |m| m.mama.round(1) }.uniq.size).to be_within(10).of(uniq_data_points) } diff --git a/spec/lib/quant/indicators/mesa_spec.rb b/spec/lib/quant/indicators/mesa_spec.rb index d64d5c2..1237f74 100644 --- a/spec/lib/quant/indicators/mesa_spec.rb +++ b/spec/lib/quant/indicators/mesa_spec.rb @@ -22,17 +22,7 @@ let(:period) { 40 } let(:cycles) { 4 } let(:uniq_data_points) { cycles * period / (cycles - 1) } # sine is cyclical, so we expect a few unique data points - let(:series) do - # period bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period:, cycles:) } it { expect(subject.series.size).to eq(160) } it { expect(subject.values.map{ |m| m.mama.round(1) }.uniq.size).to be_within(10).of(uniq_data_points) } diff --git a/spec/lib/quant/indicators/rocket_rsi_spec.rb b/spec/lib/quant/indicators/rocket_rsi_spec.rb index 767ab4c..940ecf6 100644 --- a/spec/lib/quant/indicators/rocket_rsi_spec.rb +++ b/spec/lib/quant/indicators/rocket_rsi_spec.rb @@ -17,17 +17,7 @@ let(:period) { 40 } let(:cycles) { 5 } let(:uniq_data_points) { cycles * period / cycles } # sine is cyclical, so we expect a few unique data points - let(:series) do - # period bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period:, cycles:) } it { expect(subject.series.size).to eq(cycles * period) } diff --git a/spec/lib/quant/indicators/roofing_spec.rb b/spec/lib/quant/indicators/roofing_spec.rb index 4d50b59..bae052d 100644 --- a/spec/lib/quant/indicators/roofing_spec.rb +++ b/spec/lib/quant/indicators/roofing_spec.rb @@ -19,18 +19,7 @@ let(:source) { :oc2 } let(:period) { 40 } let(:cycles) { 1 } - let(:uniq_data_points) { cycles * period / cycles } # sine is cyclical, so we expect a few unique data points - let(:series) do - # period bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period:, cycles:) } it { expect(subject.series.size).to eq(cycles * period) } diff --git a/spec/lib/quant/indicators/rsi_spec.rb b/spec/lib/quant/indicators/rsi_spec.rb index ffbe4e2..b0b9353 100644 --- a/spec/lib/quant/indicators/rsi_spec.rb +++ b/spec/lib/quant/indicators/rsi_spec.rb @@ -14,22 +14,7 @@ context "sine series" do let(:source) { :oc2 } - let(:period) { 40 } - let(:cycles) { 5 } - let(:uniq_data_points) { cycles * period / cycles } # sine is cyclical, so we expect a few unique data points - let(:series) do - # period bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end - - it { expect(subject.series.size).to eq(cycles * period) } + let(:series) { sine_series(period: 40, cycles: 5) } context "when price is climbing" do it { expect(subject.values[-10, 5].map{ |v| v.input.round(3) }).to eq([5.0, 5.062, 5.245, 5.545, 5.955]) } diff --git a/spec/lib/quant/indicators/snr_spec.rb b/spec/lib/quant/indicators/snr_spec.rb index f6b176f..cf10fad 100644 --- a/spec/lib/quant/indicators/snr_spec.rb +++ b/spec/lib/quant/indicators/snr_spec.rb @@ -18,20 +18,7 @@ let(:source) { :oc2 } let(:period) { 40 } let(:cycles) { 5 } - let(:uniq_data_points) { cycles * period / cycles } # sine is cyclical, so we expect a few unique data points - let(:series) do - # period bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - cycles.times do - (0...period).each do |degree| - radians = degree * 2 * Math::PI / period - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end - - it { expect(subject.series.size).to eq(cycles * period) } + let(:series) { sine_series(period:, cycles:) } context "expect signal to follow sine wave cycles" do it { expect(subject.values.last(5).map{ |v| v.signal.round(3) }).to eq([19.914, 19.55, 19.082, 18.569, 18.064]) } diff --git a/spec/lib/quant/indicators_source_spec.rb b/spec/lib/quant/indicators_source_spec.rb index e73beb1..8dc3c2a 100644 --- a/spec/lib/quant/indicators_source_spec.rb +++ b/spec/lib/quant/indicators_source_spec.rb @@ -36,17 +36,7 @@ end describe "Dominant Cycle Indicators" do - let(:series) do - # 40 bar sine wave - Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| - 5.times do - (0..39).each do |degree| - radians = degree * 2 * Math::PI / 40 - series << 5.0 * Math.sin(radians) + 10.0 - end - end - end - end + let(:series) { sine_series(period: 40, cycles: 5) } let(:source) { :oc2 } it { expect(subject.dominant_cycle.values[-1].period).to eq(29) } @@ -198,4 +188,24 @@ end end end + + context "#assign" do + let(:filename) { fixture_filename("DEUCES-sample.txt", :series) } + let(:series1) { Quant::Series.from_file(filename:, symbol: "DEUCES", interval: "1d") } + + let(:period) { (series1.ticks[1].open_timestamp..series1.ticks[2].close_timestamp) } + let(:series2) { series1.limit(period) } + + it "subset first still matches superset of ticks" do + expect(series1.indicators.oc2.ping.map(&:pong)).to eq [3.0, 6.0, 12.0, 24.0] + expect(series2.indicators.oc2.ping.map(&:pong)).to eq [6.0, 12.0] + + expect(series1.indicators.oc2.pivots.bollinger.map(&:h0)).to eq [3.0, 3.2, 3.706666666666667, 4.712444444444445] + + # bollinger doesn't exist because we hadn't accessed it, yet! + # so we're creating series2 before that, then when we attempt to access it + # we expect NOT to compute limited bollinger numbers off the new series instead of the parent series. + expect(series2.indicators.oc2.pivots.bollinger.map(&:h0)).to eq [3.2, 3.706666666666667] + end + end end diff --git a/spec/lib/quant/indicators_sources_spec.rb b/spec/lib/quant/indicators_sources_spec.rb index 5070a31..d9102b8 100644 --- a/spec/lib/quant/indicators_sources_spec.rb +++ b/spec/lib/quant/indicators_sources_spec.rb @@ -23,4 +23,20 @@ it { expect(subject.ping.map(&:pong)).to eq [3.0, 6.0, 12.0, 24.0] } it { expect(subject.ping.source).to eq :oc2 } end + + context "#assign" do + let(:filename) { fixture_filename("DEUCES-sample.txt", :series) } + let(:series1) { Quant::Series.from_file(filename:, symbol: "DEUCES", interval: "1d") } + + let(:period) { (series1.ticks[1].open_timestamp..series1.ticks[2].close_timestamp) } + let(:series2) { series1.limit(period) } + + it "subset first still matches superset of ticks" do + expect(series1.indicators.oc2.ping.map(&:pong)).to eq [3.0, 6.0, 12.0, 24.0] + expect(series2.indicators.oc2.ping.map(&:pong)).to eq [6.0, 12.0] + + expect(series1.indicators.oc2.pivots.bollinger.map(&:h0)).to eq [3.0, 3.2, 3.706666666666667, 4.712444444444445] + expect(series2.indicators.oc2.pivots.bollinger.map(&:h0)).to eq [3.2, 3.706666666666667] + end + end end diff --git a/spec/lib/quant/pivots_source_spec.rb b/spec/lib/quant/pivots_source_spec.rb index 442aa7e..f6dc1a8 100644 --- a/spec/lib/quant/pivots_source_spec.rb +++ b/spec/lib/quant/pivots_source_spec.rb @@ -5,9 +5,9 @@ let(:filename) { fixture_filename("DEUCES-sample.txt", :series) } let(:series) { Quant::Series.from_file(filename:, symbol: "DEUCES", interval: "1d") } let(:source) { :oc2 } - let(:indicator_source) { Quant::IndicatorsSource.new(series:, source:) } + let(:indicators_source) { Quant::IndicatorsSource.new(series:, source:) } - subject { described_class.new(indicator_source:) } + subject { described_class.new(indicators_source:) } it { expect(subject.atr.band?(6)).to be_truthy } it { expect(subject.bollinger.band?(8)).to be_truthy } diff --git a/spec/lib/quant/refinements/array_spec.rb b/spec/lib/quant/refinements/array_spec.rb index 5123270..52e54fc 100644 --- a/spec/lib/quant/refinements/array_spec.rb +++ b/spec/lib/quant/refinements/array_spec.rb @@ -48,7 +48,7 @@ end end - describe "#maximum" do + describe "#maximum/#minimum" do let(:unbounded) { [] } let(:bounded) { [].max_size!(5) } @@ -57,6 +57,19 @@ def preload(array, n) array end + it "new max pushed" do + q = [].max_size!(3).push(3, 4, 5) + q << 6 # becomes the new maximum + expect(q.maximum).to eq q.max + end + + it "new min pushed" do + q = [].max_size!(3).push(6, 5, 4) + q << 3 # becomes the new minimum + expect(q.maximum).to eq q.max + expect(q.minimum).to eq q.min + end + it "is bounded" do q = preload(bounded, 5) expect(q.to_a).to eq [0, 1, 2, 3, 4] diff --git a/spec/lib/quant/series_spec.rb b/spec/lib/quant/series_spec.rb index 8e8476b..183d761 100644 --- a/spec/lib/quant/series_spec.rb +++ b/spec/lib/quant/series_spec.rb @@ -231,6 +231,18 @@ def match_hash_recursive(expected, actual, path = []) expect(series2.indicators.oc2.ping.map(&:pong)).to eq [6.0, 12.0] end + it "subset first still matches superset of ticks" do + expect(series1.indicators.oc2.ping.map(&:pong)).to eq [3.0, 6.0, 12.0, 24.0] + expect(series2.indicators.oc2.ping.map(&:pong)).to eq [6.0, 12.0] + + expect(series1.indicators.oc2.atr.map(&:value)).to eq [0.11541829240170143, 0.339594186094642, 1.0450554008859616, 2.6455812034908734] + expect(series2.indicators.oc2.atr.map(&:value)).to eq [0.339594186094642, 1.0450554008859616] + + # ensures we're computing in context of the larger series1 + expect(series2.indicators.oc2.pivots.bollinger.map(&:h0)).to eq [3.2, 3.706666666666667] + expect(series1.indicators.oc2.pivots.bollinger.map(&:h0)).to eq [3.0, 3.2, 3.706666666666667, 4.712444444444445] + end + it "has shorter date range for series2" do expect(series2.ticks.count).to eq 2 expect(series2.ticks.first.close_timestamp).to eq Time.utc(1999, 1, 5, 21) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 77d30a2..f004a5e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,7 +4,7 @@ require "test-prof" TestProf.configure do |config| - # the directory to put artifacts (reports) in ('tmp/test_prof' by default) + # the directory to put artifacts (reports) in ("tmp/test_prof" by default) config.output_dir = "tmp/test_prof" # use unique filenames for reports (by simply appending current timestamp) @@ -18,7 +18,7 @@ end require "simplecov" -require 'simplecov-cobertura' +require "simplecov-cobertura" SimpleCov.start do add_filter "/spec/" @@ -44,9 +44,12 @@ end def fixture_path(sub_folder) - File.join(File.expand_path(File.join(File.dirname(__FILE__), "fixtures")), sub_folder.to_s) + File.join(File.expand_path(__dir__), "fixtures", sub_folder.to_s) end def fixture_filename(filename, sub_folder = nil) File.join fixture_path(sub_folder), filename end + +support_folder = File.expand_path(__dir__) +Dir[File.join(support_folder, "support", "**", "*.rb")].each { |f| require f } \ No newline at end of file diff --git a/spec/support/series_factory.rb b/spec/support/series_factory.rb new file mode 100644 index 0000000..eca922e --- /dev/null +++ b/spec/support/series_factory.rb @@ -0,0 +1,16 @@ +module SeriesFactory + def sine_series(period:, cycles:) + Quant::Series.new(symbol: "SINE", interval: "1d").tap do |series| + cycles.times do + (0...period).each do |degree| + radians = degree * 2 * Math::PI / period + series << 5.0 * Math.sin(radians) + 10.0 + end + end + end + end +end + +RSpec.configure do |config| + config.include SeriesFactory +end \ No newline at end of file From fc76fb1a37a49d10ebc49cc0985f3b8747f6ceff Mon Sep 17 00:00:00 2001 From: mwlang Date: Sun, 16 Jun 2024 11:42:10 -0400 Subject: [PATCH 6/6] ATR's midepoint is the :input instead of EMA --- lib/quant/indicators/pivots/atr.rb | 2 +- lib/quant/indicators/pivots/pivot.rb | 4 ++++ spec/lib/quant/indicators/pivots/atr_spec.rb | 10 +++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/quant/indicators/pivots/atr.rb b/lib/quant/indicators/pivots/atr.rb index 53169b9..deabc37 100644 --- a/lib/quant/indicators/pivots/atr.rb +++ b/lib/quant/indicators/pivots/atr.rb @@ -18,7 +18,7 @@ def atr_value end def compute_midpoint - p0.midpoint = smoothed_average_midpoint + p0.midpoint = midpoint_at_input end ATR_SERIES = [0.236, 0.382, 0.500, 0.618, 0.786, 1.0].freeze diff --git a/lib/quant/indicators/pivots/pivot.rb b/lib/quant/indicators/pivots/pivot.rb index 66200d6..7e46a6f 100644 --- a/lib/quant/indicators/pivots/pivot.rb +++ b/lib/quant/indicators/pivots/pivot.rb @@ -70,6 +70,10 @@ def period_midpoints period_points(period).map(&:midpoint) end + def midpoint_at_input + p0.input + end + def smoothed_average_midpoint three_pole_super_smooth :input, previous: :midpoint, period: averaging_period end diff --git a/spec/lib/quant/indicators/pivots/atr_spec.rb b/spec/lib/quant/indicators/pivots/atr_spec.rb index 8a69f62..45f723b 100644 --- a/spec/lib/quant/indicators/pivots/atr_spec.rb +++ b/spec/lib/quant/indicators/pivots/atr_spec.rb @@ -13,12 +13,12 @@ it { expect(subject.values.map(&:input)).to eq([3.0, 6.0, 12.0, 24.0]) } context "bands" do - it { expect(subject.values.map{ |v| v.h6.round(3) }).to eq([3.346, 4.416, 8.034, 16.626]) } - it { expect(subject.values.map{ |v| v.h1.round(3) }).to eq([3.082, 3.637, 5.639, 10.562]) } - it { expect(subject.values.map{ |v| v.midpoint.round(3) }).to eq([3.0, 3.397, 4.899, 8.689]) } + it { expect(subject.values.map{ |v| v.h6.round(3) }).to eq([3.346, 7.019, 15.135, 31.937]) } + it { expect(subject.values.map{ |v| v.h1.round(3) }).to eq([3.082, 6.24, 12.74, 25.873]) } + it { expect(subject.values.map{ |v| v.midpoint.round(3) }).to eq([3.0, 6.0, 12.0, 24.0]) } it { expect(subject.values.map{ |v| v.h0.round(3) }).to eq(subject.values.map{ |v| v.midpoint.round(3) }) } - it { expect(subject.values.map{ |v| v.l1.round(3) }).to eq([2.918, 3.157, 4.159, 6.816]) } - it { expect(subject.values.map{ |v| v.l6.round(3) }).to eq([2.654, 2.378, 1.764, 0.752]) } + it { expect(subject.values.map{ |v| v.l1.round(3) }).to eq([2.918, 5.76, 11.26, 22.127]) } + it { expect(subject.values.map{ |v| v.l6.round(3) }).to eq([2.654, 4.981, 8.865, 16.063]) } end context "bands do not intersect each other" do