HalfTrend

Parameters: amplitude = 2 | channel_deviation = 2 | atr_period = 100

Overview

HalfTrend generates crystal clear trend signals by constructing a step function that switches between tracking the highest recent lows during uptrends and the lowest recent highs during downtrends, creating an adaptive support and resistance line that only flips when price decisively breaks through volatility adjusted boundaries. The indicator monitors rolling price extremes within a specified amplitude window, then applies ATR based channels to filter out false breakouts that would trigger premature reversals in choppy conditions. When price closes beyond the channel deviation from the current trend line and confirms with a move past recent extremes, HalfTrend flips direction and generates an explicit buy or sell arrow at the exact reversal point. Traders appreciate HalfTrend for its binary clarity where the market is either definitively long or short with no ambiguous middle ground, eliminating the analysis paralysis that plagues multi signal systems. The visual simplicity of following a single colored line that stays on one side of price during trends makes it particularly effective for newer traders learning trend following concepts. Furthermore, the combination of price structure (highs and lows) with volatility filtering (ATR channels) creates robust signals that adapt to changing market conditions while maintaining consistent risk parameters across different volatility regimes.

Implementation Examples

Calculate HalfTrend from OHLC slices or Candles:

use vectorta::indicators::halftrend::{halftrend, HalfTrendInput, HalfTrendParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using with OHLC slices
let high = vec![1.0, 1.2, 1.1, 1.3, 1.4, 1.35];
let low = vec![0.9, 1.0, 1.0, 1.1, 1.2, 1.25];
let close = vec![1.0, 1.1, 1.05, 1.2, 1.3, 1.28];

let params = HalfTrendParams { amplitude: Some(2), channel_deviation: Some(2.0), atr_period: Some(100) };
let input = HalfTrendInput::from_slices(&high, &low, &close, params);
let output = halftrend(&input)?;

// Or with Candles and default parameters
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = HalfTrendInput::with_default_candles(&candles);
let output = halftrend(&input)?;

// Access results
println!("HT[0] = {:?}", output.halftrend[0]);
println!("Trend[0] = {:?}", output.trend[0]);
println!("ATR_HIGH[0] = {:?}", output.atr_high[0]);
println!("ATR_LOW[0] = {:?}", output.atr_low[0]);
// buy_signal/sell_signal are NaN except on flip bars

API Reference

Input Methods
// From OHLC slices
HalfTrendInput::from_slices(&[f64], &[f64], &[f64], HalfTrendParams) -> HalfTrendInput

// From Candles
HalfTrendInput::from_candles(&Candles, HalfTrendParams) -> HalfTrendInput

// Candles with defaults (amplitude=2, channel_deviation=2.0, atr_period=100)
HalfTrendInput::with_default_candles(&Candles) -> HalfTrendInput
Parameters Structure
pub struct HalfTrendParams {
    pub amplitude: Option<usize>,        // Default: 2
    pub channel_deviation: Option<f64>,  // Default: 2.0 (must be > 0)
    pub atr_period: Option<usize>,       // Default: 100
}
Output Structure
pub struct HalfTrendOutput {
    pub halftrend: Vec<f64>,   // main step line
    pub trend: Vec<f64>,       // 0.0 = uptrend, 1.0 = downtrend
    pub atr_high: Vec<f64>,    // upper volatility band (HT + dev)
    pub atr_low: Vec<f64>,     // lower volatility band (HT - dev)
    pub buy_signal: Vec<f64>,  // price on flip-up bars; NaN otherwise
    pub sell_signal: Vec<f64>, // price on flip-down bars; NaN otherwise
}
Validation, Warmup & NaNs
  • Inputs must be non-empty and equal length; otherwise HalfTrendError::InvalidPeriod.
  • amplitude > 0, atr_period > 0, and channel_deviation > 0 (finite); else InvalidPeriod or InvalidChannelDeviation.
  • First valid index is the earliest bar where any OHLC is finite; if none: AllValuesNaN.
  • Warmup span = max(amplitude, atr_period); if insufficient valid data after first finite bar: NotEnoughValidData.
  • All outputs are NaN before the warm index; values compute from there forward.
Error Handling
use vectorta::indicators::halftrend::{halftrend, HalfTrendError};

match halftrend(&input) {
    Ok(out) => process(out),
    Err(HalfTrendError::EmptyInputData) => eprintln!("Input data is empty"),
    Err(HalfTrendError::AllValuesNaN) => eprintln!("All OHLC values are NaN"),
    Err(HalfTrendError::InvalidPeriod { period, data_len }) =>
        eprintln!("Invalid period {} for data length {}", period, data_len),
    Err(HalfTrendError::NotEnoughValidData { needed, valid }) =>
        eprintln!("Need {} valid points, only {} available", needed, valid),
    Err(HalfTrendError::InvalidChannelDeviation { channel_deviation }) =>
        eprintln!("channel_deviation must be > 0, got {}", channel_deviation),
    Err(HalfTrendError::AtrError(e)) | Err(HalfTrendError::SmaError(e)) =>
        eprintln!("Dependency error: {}", e),
}

Python Bindings

Basic Usage

Compute HalfTrend using NumPy arrays (defaults: amplitude=2, channel_deviation=2.0, atr_period=100):

import numpy as np
from vectorta import halftrend

high = np.array([1.0, 1.2, 1.1, 1.3], dtype=float)
low = np.array([0.9, 1.0, 1.0, 1.1], dtype=float)
close = np.array([1.0, 1.1, 1.05, 1.2], dtype=float)

# Positional args: (high, low, close, amplitude, channel_deviation, atr_period)
out = halftrend(high, low, close, 2, 2.0, 100)

# Output is a dict with series arrays
ht = out['halftrend']; tr = out['trend']
ah = out['atr_high']; al = out['atr_low']
bs = out['buy_signal']; ss = out['sell_signal']
print(ht[:5], tr[:5])
Streaming Real-time Updates

Stream OHLC updates and react to flip markers:

from vectorta import HalfTrendStream

stream = HalfTrendStream(amplitude=2, channel_deviation=2.0, atr_period=100)

for (h, l, c) in ohlc_feed:
    tup = stream.update(h, l, c)
    if tup is not None:
        halftrend, trend, atr_high, atr_low, buy_signal, sell_signal = tup
        # buy/sell are None except on the flip bar
        handle(halftrend, trend, atr_high, atr_low, buy_signal, sell_signal)
Batch Parameter Optimization

Sweep amplitude, channel_deviation, and atr_period:

import numpy as np
from vectorta import halftrend_batch

high = np.array([...], dtype=float)
low = np.array([...], dtype=float)
close = np.array([...], dtype=float)

res = halftrend_batch(
    high, low, close,
    amplitude_start=2, amplitude_end=4, amplitude_step=1,
    channel_deviation_start=1.5, channel_deviation_end=2.5, channel_deviation_step=0.5,
    atr_period_start=50, atr_period_end=150, atr_period_step=50,
    kernel='auto'
)

# Results: dict with 'values' shaped [6*rows, cols] and parameter arrays
vals = res['values']
series = res['series']  # ['halftrend','trend','atr_high','atr_low','buy','sell']
amps = res['amplitudes']; chs = res['channel_deviations']; aps = res['atr_periods']
CUDA Acceleration

CUDA support for HalfTrend is coming soon.

# Coming soon: CUDA-accelerated HalfTrend calculations

JavaScript/WASM Bindings

Basic Usage

Compute HalfTrend in JavaScript/TypeScript:

import { halftrend, halftrend_wasm } from 'vectorta-wasm';

const high = new Float64Array([1.0, 1.2, 1.1, 1.3]);
const low = new Float64Array([0.9, 1.0, 1.0, 1.1]);
const close = new Float64Array([1.0, 1.1, 1.05, 1.2]);

// Explicit parameters
const out = halftrend(high, low, close, 2, 2.0, 100);

// Optional defaults via *_wasm variant (omit parameters)
const out2 = halftrend_wasm(high, low, close, null, null, null);

console.log(out); // structure contains series arrays (implementation-specific)
Memory-Efficient Operations

Use zero-copy operations with manual memory management:

import { halftrend_alloc, halftrend_free, halftrend_into, memory } from 'vectorta-wasm';

const len = high.length;
// Allocate WASM memory for inputs and outputs
const hPtr = halftrend_alloc(len);
const lPtr = halftrend_alloc(len);
const cPtr = halftrend_alloc(len);
// Output layout is implementation-defined; allocate one or more buffers as required
const outPtr = halftrend_alloc(len); // example allocation

// Copy inputs into WASM memory
new Float64Array(memory.buffer, hPtr, len).set(high);
new Float64Array(memory.buffer, lPtr, len).set(low);
new Float64Array(memory.buffer, cPtr, len).set(close);

// Compute into pre-allocated memory
halftrend_into(hPtr, lPtr, cPtr, outPtr, len, 2, 2.0, 100);

// Read back results as needed
const result = new Float64Array(memory.buffer, outPtr, len).slice();

// Free memory
halftrend_free(hPtr, len);
halftrend_free(lPtr, len);
halftrend_free(cPtr, len);
halftrend_free(outPtr, len);
Batch Processing

Test multiple combinations efficiently:

import { halftrend_batch } from 'vectorta-wasm';

const cfg = {
  amplitude_range: [2, 4, 1],
  channel_deviation_range: [1.5, 2.5, 0.5],
  atr_period_range: [50, 150, 50]
};

const res = halftrend_batch(high, low, close, cfg);
console.log(res); // structure with 6*rows × cols flattened values and metadata

Performance Analysis

Comparison:
View:
Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05

CUDA note

In our benchmark workload, the Rust CPU implementation is faster than CUDA for this indicator. Prefer the Rust/CPU path unless your workload differs.

Related Indicators