trading-signals icon indicating copy to clipboard operation
trading-signals copied to clipboard

Trend Determination (Dow Theory Style)

Open bennycode opened this issue 4 months ago • 1 comments

Core definition of trends in Dow Theory:

  • Uptrend = Higher Highs (HH) + Higher Lows (HL)
  • Downtrend = Lower Highs (LH) + Lower Lows (LL)

Once a sequence like this is detected:

  • HH → HL → HH → HL → confirms uptrend
  • LL → LH → LL → LH → confirms downtrend

Dow Theory in the real world is not always clean — price action can be noisy. One or two candles might spike out of sequence, but the overall trend structure remains valid. To handle this, we can introduce a buffer or tolerance, often called:

  • Deviation
  • Threshold
  • Leniency
  • Noise buffer

Requirements:

  • Maintain a rolling window of price data (maxCandles)
  • Calculate pivot highs/lows based on a lookback period
  • Using buffer logic to determine the trend
  • Support real-time candle streaming

bennycode avatar Aug 02 '25 09:08 bennycode

Zero-Lag Dow Theory Trend Detector

type PriceData = { high: number; low: number };
type Trend = 'Uptrend' | 'Downtrend' | 'Neutral';

class RealTimeDowTrend {
    private data: PriceData[] = [];
    private highs: number[] = [];
    private lows: number[] = [];
    private maxCandles: number;
    private lookback: number;
    private buffer: number;

    constructor(maxCandles: number = 100, lookback: number = 5, buffer: number = 0.01) {
        this.maxCandles = maxCandles;
        this.lookback = lookback;
        this.buffer = buffer;
    }

    private isHigher(a: number, b: number): boolean {
        return a >= b * (1 - this.buffer);
    }

    private isLower(a: number, b: number): boolean {
        return a <= b * (1 + this.buffer);
    }

    private detectPivotHigh(index: number): boolean {
        if (index < this.lookback) return false;
        const window = this.data.slice(index - this.lookback, index);
        return this.data[index].high > Math.max(...window.map(d => d.high));
    }

    private detectPivotLow(index: number): boolean {
        if (index < this.lookback) return false;
        const window = this.data.slice(index - this.lookback, index);
        return this.data[index].low < Math.min(...window.map(d => d.low));
    }

    public addCandle(candle: PriceData): Trend {
        this.data.push(candle);
        if (this.data.length > this.maxCandles) {
            this.data.shift();
            this.highs = this.highs.filter(i => i > 0).map(i => i - 1);
            this.lows = this.lows.filter(i => i > 0).map(i => i - 1);
        }

        const idx = this.data.length - 1;

        // Detect pivots using past candles only
        if (this.detectPivotHigh(idx)) this.highs.push(idx);
        if (this.detectPivotLow(idx)) this.lows.push(idx);

        return this.determineTrend();
    }

    private determineTrend(): Trend {
        if (this.highs.length < 2 || this.lows.length < 2) return 'Neutral';

        const lastHigh = this.data[this.highs[this.highs.length - 1]].high;
        const prevHigh = this.data[this.highs[this.highs.length - 2]].high;

        const lastLow = this.data[this.lows[this.lows.length - 1]].low;
        const prevLow = this.data[this.lows[this.lows.length - 2]].low;

        const higherHigh = this.isHigher(lastHigh, prevHigh);
        const higherLow = this.isHigher(lastLow, prevLow);
        const lowerHigh = this.isLower(lastHigh, prevHigh);
        const lowerLow = this.isLower(lastLow, prevLow);

        if (higherHigh && higherLow) return 'Uptrend';
        if (lowerHigh && lowerLow) return 'Downtrend';

        return 'Neutral';
    }
}

bennycode avatar Aug 02 '25 09:08 bennycode