Schaff Trend Cycle (STC)

Parameters: fast_period = 23 | slow_period = 50 | k_period = 10 | d_period = 3 | fast_ma_type = ema | slow_ma_type = ema

Overview

The Schaff Trend Cycle combines the trend detection capabilities of MACD with the responsiveness of stochastic oscillators, creating a fast moving indicator that captures both momentum and cyclical turns within a bounded 0 to 100 range. STC first calculates the difference between fast and slow moving averages like MACD, then applies a double stochastic transformation with exponential smoothing to normalize this difference into an oscillator that reacts more quickly to trend changes than traditional MACD. Values above 75 suggest overbought conditions within an uptrend, while readings below 25 indicate oversold conditions in a downtrend, with the fastest signals occurring when STC crosses these extreme levels. Traders appreciate STC for its ability to identify trend changes earlier than MACD while avoiding the whipsaws common in pure stochastic indicators. The indicator excels in trending markets where its dual nature helps confirm trend direction while timing entries at cycle lows and exits at cycle highs.

Implementation Examples

Compute STC from slices or candles:

use vector_ta::indicators::stc::{stc, StcInput, StcParams};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using a price slice
let price = vec![100.0, 101.0, 100.5, 102.0, 103.5];
let params = StcParams { // defaults: fast=23, slow=50, k=10, d=3, types="ema"
    fast_period: Some(23),
    slow_period: Some(50),
    k_period: Some(10),
    d_period: Some(3),
    fast_ma_type: Some("ema".into()),
    slow_ma_type: Some("ema".into()),
};
let input = StcInput::from_slice(&price, params);
let out = stc(&input)?;

// Using Candles (source defaults to "close") with default parameters
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = StcInput::with_default_candles(&candles);
let out = stc(&input)?;

// Access values
for v in out.values { println!("STC: {}", v); }

API Reference

Input Methods
// From price slice
StcInput::from_slice(&[f64], StcParams) -> StcInput

// From candles with explicit source (e.g., "close")
StcInput::from_candles(&Candles, &str, StcParams) -> StcInput

// From candles with defaults (source="close", fast=23, slow=50, k=10, d=3)
StcInput::with_default_candles(&Candles) -> StcInput
Parameters Structure
#[derive(Debug, Clone)]
pub struct StcParams {
    pub fast_period: Option<usize>,   // Default: 23
    pub slow_period: Option<usize>,   // Default: 50
    pub k_period:   Option<usize>,    // Default: 10
    pub d_period:   Option<usize>,    // Default: 3
    pub fast_ma_type: Option<String>, // Default: "ema"
    pub slow_ma_type: Option<String>, // Default: "ema"
}
Output Structure
#[derive(Debug, Clone)]
pub struct StcOutput {
    pub values: Vec<f64>, // STC values in [0, 100] with NaN warmup
}
Validation, Warmup & NaNs
  • Empty input yields StcError::EmptyData.
  • If all values are NaN: StcError::AllValuesNaN.
  • Requires at least max(fast, slow, k, d) valid points after the first finite value; else StcError::NotEnoughValidData { needed, valid }.
  • Warmup: indices before first + max(fast, slow, k, d) − 1 are set to NaN (via alloc_with_nan_prefix).
  • Flat ranges in stochastic windows seed to 50.0 when inputs are finite.
  • Streaming: StcStream::try_new errors if any period is zero; update returns None until enough data accumulates.
Error Handling
use vector_ta::indicators::stc::{stc, StcError};

match stc(&input) {
    Ok(output) => handle(output.values),
    Err(StcError::EmptyData) => eprintln!("Empty data"),
    Err(StcError::AllValuesNaN) => eprintln!("All values are NaN"),
    Err(StcError::NotEnoughValidData { needed, valid }) =>
        eprintln!("Need {} valid points, have {}", needed, valid),
    Err(StcError::Internal(msg)) => eprintln!("Internal: {}", msg),
}

Python Bindings

Basic Usage

Calculate STC using NumPy arrays (defaults: fast=23, slow=50, k=10, d=3, types="ema"):

import numpy as np
from vector_ta import stc

data = np.array([100.0, 101.0, 100.5, 102.0, 103.5], dtype=float)

# Specify kernel (optional): "auto", "scalar", "avx2", "avx512"
values = stc(data, fast_period=23, slow_period=50, k_period=10, d_period=3,
             fast_ma_type="ema", slow_ma_type="ema", kernel="auto")

print(values)
Streaming Real-time Updates
from vector_ta import StcStream

stream = StcStream(fast_period=23, slow_period=50, k_period=10, d_period=3)
for x in feed:
    value = stream.update(x)
    if value is not None:
        use(value)
Batch Parameter Optimization

Test multiple parameter combinations efficiently:

import numpy as np
from vector_ta import stc_batch

data = np.array([...], dtype=float)

results = stc_batch(
    data,
    fast_period_range=(20, 30, 5),
    slow_period_range=(40, 60, 10),
    k_period_range=(5, 15, 5),
    d_period_range=(3, 5, 1),
    kernel="auto",
)

print(results['values'].shape)   # (rows, len(data))
print(results['fast_periods'])
print(results['slow_periods'])
print(results['k_periods'])
print(results['d_periods'])
CUDA Acceleration

CUDA helpers are available when the Python package is built with CUDA support. Inputs must be float32; outputs are device arrays (DLPack / __cuda_array_interface__ compatible).

import numpy as np
from vector_ta import stc_cuda_batch_dev, stc_cuda_many_series_one_param_dev

# One series (float32)
data_f32 = np.asarray(load_data(), dtype=np.float32)

dev = stc_cuda_batch_dev(
    data_f32=data_f32,
    fast_period_range=(5, 30, 5),
    slow_period_range=(5, 30, 5),
    k_period_range=(5, 30, 5),
    d_period_range=(5, 30, 5),
    device_id=0,
)

# Many series (time-major)
data_tm_f32 = np.asarray(load_data_time_major_matrix(), dtype=np.float32)
rows, cols = data_tm_f32.shape
data_tm_f32 = data_tm_f32.ravel()

dev_tm = stc_cuda_many_series_one_param_dev(
    data_tm_f32=data_tm_f32,
    cols=cols,
    rows=rows,
    fast_period=12,
    slow_period=26,
    k_period=14,
    d_period=14,
    device_id=0,
)

JavaScript/WASM Bindings

Basic Usage

Calculate STC in JavaScript/TypeScript:

import { stc_js } from 'vectorta-wasm';

const data = new Float64Array([100, 101, 100.5, 102, 103.5]);
const values = stc_js(data, 23, 50, 10, 3, 'ema', 'ema');
console.log('STC values:', values);
Memory-Efficient Operations

Use zero-copy into/alloc/free for large datasets:

import { stc_alloc, stc_free, stc_into, memory } from 'vectorta-wasm';

const data = new Float64Array([...]);
const len = data.length;

const inPtr = stc_alloc(len);
const outPtr = stc_alloc(len);

new Float64Array(memory.buffer, inPtr, len).set(data);

// Args: in_ptr, out_ptr, len, fast, slow, k, d, fast_ma_type, slow_ma_type
stc_into(inPtr, outPtr, len, 23, 50, 10, 3, 'ema', 'ema');

const result = new Float64Array(memory.buffer, outPtr, len).slice();

stc_free(inPtr, len);
stc_free(outPtr, len);

console.log('STC values:', result);
Batch Processing

Test multiple parameter combinations and get a result matrix:

import { stc_batch } from 'vectorta-wasm';

const data = new Float64Array([...]);

// Config uses tuples for ranges: (start, end, step)
const output = stc_batch(data, {
  fast_period_range: [20, 30, 5],
  slow_period_range: [40, 60, 10],
  k_period_range: [5, 15, 5],
  d_period_range: [3, 5, 1],
});

// output = { values: Float64Array(flat), combos: [{ fast_period, slow_period, k_period, d_period }], rows, cols }
const { values, combos, rows, cols } = output;

// Reshape flat values into [rows x cols]
const matrix: Float64Array[] = [];
for (let r = 0; r < rows; r++) {
  const start = r * cols;
  matrix.push(values.slice(start, start + cols));
}

console.log('Fast periods tested:', combos.map(c => c.fast_period));

CUDA Bindings (Rust)

use vector_ta::cuda::CudaStc;
use vector_ta::indicators::stc::StcBatchRange;

let cuda = CudaStc::new(0)?;

let data: [f32] = /* ... */;
let sweep = StcBatchRange::default();

let out = cuda.stc_batch_dev(&data, &sweep)?;
let _ = out;

Performance Analysis

Comparison:
View:
Loading chart...

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

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