From b5544d120944662c49264a79817b121889514937 Mon Sep 17 00:00:00 2001 From: Alek Kim Date: Tue, 28 Jul 2026 14:20:41 +0900 Subject: [PATCH] feat: add volatility and volume indicator wrappers via purego - add TA-Lib function pointer signatures for ATR/NATR/TRANGE and AD/ADOSC/CMF/NVI/OBV/PVI/PVO in functions.go - register new native symbols in loader.go - add new wrapper sources: volatility_indicators.go and volume_indicators.go - update README function matrix with new volatility and volume categories - add ta-lib upstream as a git submodule (.gitmodules + ta-lib gitlink) --- .gitmodules | 3 + functions.go | 116 ++++++++++++++++ loader.go | 12 ++ ta-lib | 1 + volatility_indicators.go | 96 +++++++++++++ volume_indicators.go | 292 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 520 insertions(+) create mode 100644 .gitmodules create mode 160000 ta-lib create mode 100644 volatility_indicators.go create mode 100644 volume_indicators.go diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3dcd24b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ta-lib"] + path = ta-lib + url = https://github.com/TA-Lib/ta-lib.git diff --git a/functions.go b/functions.go index c7bd302..a37aa29 100644 --- a/functions.go +++ b/functions.go @@ -1024,4 +1024,120 @@ var ( outNBElement *int32, outReal []float64, ) int32 + + atr func( + startIdx int32, + endIdx int32, + inHigh []float64, + inLow []float64, + inClose []float64, + optInTimePeriod int32, + outBegIdx *int32, + outNBElement *int32, + outReal []float64, + ) int32 + + natr func( + startIdx int32, + endIdx int32, + inHigh []float64, + inLow []float64, + inClose []float64, + optInTimePeriod int32, + outBegIdx *int32, + outNBElement *int32, + outReal []float64, + ) int32 + + trange func( + startIdx int32, + endIdx int32, + inHigh []float64, + inLow []float64, + inClose []float64, + outBegIdx *int32, + outNBElement *int32, + outReal []float64, + ) int32 + + ad func( + startIdx int32, + endIdx int32, + inHigh []float64, + inLow []float64, + inClose []float64, + inVolume []float64, + outBegIdx *int32, + outNBElement *int32, + outReal []float64, + ) int32 + + adosc func( + startIdx int32, + endIdx int32, + inHigh []float64, + inLow []float64, + inClose []float64, + inVolume []float64, + optInFastPeriod int32, + optInSlowPeriod int32, + outBegIdx *int32, + outNBElement *int32, + outReal []float64, + ) int32 + + cmf func( + startIdx int32, + endIdx int32, + inHigh []float64, + inLow []float64, + inClose []float64, + inVolume []float64, + optInTimePeriod int32, + outBegIdx *int32, + outNBElement *int32, + outReal []float64, + ) int32 + + nvi func( + startIdx int32, + endIdx int32, + inReal []float64, + inVolume []float64, + outBegIdx *int32, + outNBElement *int32, + outReal []float64, + ) int32 + + obv func( + startIdx int32, + endIdx int32, + inReal []float64, + inVolume []float64, + outBegIdx *int32, + outNBElement *int32, + outReal []float64, + ) int32 + + pvi func( + startIdx int32, + endIdx int32, + inReal []float64, + inVolume []float64, + outBegIdx *int32, + outNBElement *int32, + outReal []float64, + ) int32 + + pvo func( + startIdx int32, + endIdx int32, + inVolume []float64, + optInFastPeriod int32, + optInSlowPeriod int32, + optInMAType int32, + outBegIdx *int32, + outNBElement *int32, + outReal []float64, + ) int32 ) diff --git a/loader.go b/loader.go index ccf8d7e..113f9a8 100644 --- a/loader.go +++ b/loader.go @@ -156,6 +156,18 @@ func internalLoad() (uintptr, error) { purego.RegisterLibFunc(&tsf, ptr, "TA_TSF") purego.RegisterLibFunc(&variance, ptr, "TA_VAR") + purego.RegisterLibFunc(&atr, ptr, "TA_ATR") + purego.RegisterLibFunc(&natr, ptr, "TA_NATR") + purego.RegisterLibFunc(&trange, ptr, "TA_TRANGE") + + purego.RegisterLibFunc(&ad, ptr, "TA_AD") + purego.RegisterLibFunc(&adosc, ptr, "TA_ADOSC") + purego.RegisterLibFunc(&cmf, ptr, "TA_CMF") + purego.RegisterLibFunc(&nvi, ptr, "TA_NVI") + purego.RegisterLibFunc(&obv, ptr, "TA_OBV") + purego.RegisterLibFunc(&pvi, ptr, "TA_PVI") + purego.RegisterLibFunc(&pvo, ptr, "TA_PVO") + return ptr, nil } diff --git a/ta-lib b/ta-lib new file mode 160000 index 0000000..144c635 --- /dev/null +++ b/ta-lib @@ -0,0 +1 @@ +Subproject commit 144c635bb429759235696d9f95e1734169205f3a diff --git a/volatility_indicators.go b/volatility_indicators.go new file mode 100644 index 0000000..896591d --- /dev/null +++ b/volatility_indicators.go @@ -0,0 +1,96 @@ +package talib + +// ATR - Wilder-smoothed average of the True Range over a period, measuring price volatility regardless of direction. +// Higher ATR means greater volatility; no directional bias. +// +// TR_t = max(high-low, |prevClose-high|, |prevClose-low|) +// ATR seed = simple average of first period TR values +// ATR_t = (ATR_{t-1} * (period-1) + TR_t) / period +func ATR(inHigh, inLow, inClose []float64, optInTimePeriod int) []float64 { + var ( + startIdx int32 + endIdx = int32(len(inClose) - 1) + outBegIdx int32 + outNBElement int32 + outReal = make([]float64, len(inClose)) + ) + + if retCode := atr( + startIdx, + endIdx, + inHigh, + inLow, + inClose, + int32(optInTimePeriod), + &outBegIdx, + &outNBElement, + outReal, + ); retCode != 0 { + return nil + } + + return outReal +} + +// NATR - Average True Range expressed as a percentage of the current close, making volatility comparable across price levels and securities. +// Same computation as ATR, then normalized by close. Higher values mean greater relative volatility; unit is percent of price. +// +// NATR = (ATR / Close) * 100 +// ATR: first value = SMA of TRANGE over period; then Wilder smoothing ATR_t = (ATR_{t-1}*(period-1) + TR_t) / period +func NATR(inHigh, inLow, inClose []float64, optInTimePeriod int) []float64 { + var ( + startIdx int32 + endIdx = int32(len(inClose) - 1) + outBegIdx int32 + outNBElement int32 + outReal = make([]float64, len(inClose)) + ) + + if retCode := natr( + startIdx, + endIdx, + inHigh, + inLow, + inClose, + int32(optInTimePeriod), + &outBegIdx, + &outNBElement, + outReal, + ); retCode != 0 { + return nil + } + + return outReal +} + +// TRANGE - True Range: the greatest of today's high-low span and the two gaps between yesterday's close and today's high/low. +// Base volatility measure used to build ATR/NATR. Larger values mean wider or gappier bars (higher volatility). +// +// TR = max( high - low, |prevClose - high|, |prevClose - low| ) +// +// Note: The first bar produces no value because it has no prior close; +// unlike some definitions, it does not fall back to the high-low range for that bar. +func TRANGE(inHigh, inLow, inClose []float64) []float64 { + var ( + startIdx int32 + endIdx = int32(len(inClose) - 1) + outBegIdx int32 + outNBElement int32 + outReal = make([]float64, len(inClose)) + ) + + if retCode := trange( + startIdx, + endIdx, + inHigh, + inLow, + inClose, + &outBegIdx, + &outNBElement, + outReal, + ); retCode != 0 { + return nil + } + + return outReal +} diff --git a/volume_indicators.go b/volume_indicators.go new file mode 100644 index 0000000..3cb4775 --- /dev/null +++ b/volume_indicators.go @@ -0,0 +1,292 @@ +package talib + +// AD - Chaikin Accumulation/Distribution Line, a cumulative volume-flow indicator. +// Sums a volume-weighted money-flow multiplier per bar to gauge buying vs. selling pressure. +// Rising line = accumulation (buying pressure); falling = distribution. +// +// MFM = ((close-low) - (high-close)) / (high-low); +// AD_t = AD_{t-1} + MFM_t * volume_t (running sum, seeded at 0) +func AD(inHigh, inLow, inClose, inVolume []float64) []float64 { + var ( + startIdx int32 + endIdx = int32(len(inClose) - 1) + outBegIdx int32 + outNBElement int32 + outReal = make([]float64, len(inClose)) + ) + + if retCode := ad( + startIdx, + endIdx, + inHigh, + inLow, + inClose, + inVolume, + &outBegIdx, + &outNBElement, + outReal, + ); retCode != 0 { + return nil + } + + return outReal +} + +// ADOsc - Chaikin A/D Oscillator: the difference between a fast and a slow EMA of the Accumulation/Distribution line. +// Highlights momentum in accumulation/distribution volume flow. +// Positive/rising suggests accumulation; negative/falling suggests distribution. +// +// ad += ((close-low)-(high-close))/(high-low) * volume (only when high>low) +// fastEMA = fastk*ad + (1-fastk)fastEMA, fastk = 2/(optInFastPeriod+1) +// slowEMA = slowkad + (1-slowk)*slowEMA, slowk = 2/(optInSlowPeriod+1) +// ADOSC = fastEMA - slowEMA +// +// @param inHigh []float64 +// @param inLow []float64 +// @param inClose []float64 +// @param inVolume []float64 +// @param fastPeriod int Period of the fast A/D EMA; default is 3 (2–100000) +// @param slowPeriod int Period of the slow A/D EMA; default is 10 (2–100000) +// @return []float64 Fast-EMA minus slow-EMA of the A/D line +func ADOsc(inHigh, inLow, inClose, inVolume []float64, fastPeriod, slowPeriod int) []float64 { + var ( + startIdx int32 + endIdx = int32(len(inClose) - 1) + outBegIdx int32 + outNBElement int32 + outReal = make([]float64, len(inClose)) + ) + + if retCode := adosc( + startIdx, + endIdx, + inHigh, + inLow, + inClose, + inVolume, + int32(fastPeriod), + int32(slowPeriod), + &outBegIdx, + &outNBElement, + outReal, + ); retCode != 0 { + return nil + } + + return outReal +} + +// CMF - Chaikin Money Flow: over a trailing window of optInTimePeriod bars, the sum of each bar's money flow volume divided by the sum of its volume. +// The result is a ratio in [-1, +1]. +// A bar's money flow volume is its volume scaled by where the close sat inside the bar's range: a close at the high contributes the full volume, +// a close at the low contributes minus the full volume, and a close at the midpoint contributes nothing. +// Summing that over a window and dividing by the window's volume answers "over these N bars, what share of the traded volume closed near the top of its range?" +// +// Above zero is accumulation, below zero is distribution, and the distance from zero measures conviction. +// Because the divisor is the window's own volume, the output is comparable across instruments and across time in a way a raw accumulation total is not. +// +// Created by Marc Chaikin, who also authored the AD line this shares its per-bar multiplier with. +// CMF is that same multiplier summed over a fixed window and normalised, where AD accumulates it from the start of the series without bound. +// +// t = high[i] - low[i] +// mfv[i] = ((close[i] - low[i]) - (high[i] - close[i])) / t * volume[i], or 0 when t is not positive +// CMF[i] = ( sum_{k=i-N+1..i} mfv[k] ) / ( sum_{k=i-N+1..i} volume[k] ), N = optInTimePeriod +// There is no seeding and no recursion, hence no unstable period. Each output depends only on the N bars in its own window. +// +// Note: The output is the raw ratio in [-1, +1], matching every published definition. +// Some retail platforms display it multiplied by 100; that is a presentation choice, not a different indicator. +// +// Each bar's close is expected to lie within its own [low, high], and its volume to be finite and non-negative. +// A close outside its bar makes the multiplier exceed ±1 and is passed through unclamped, exactly as AD does. +// +// A bar whose high equals its low has no range for the close to sit inside, so it contributes exactly zero money flow volume rather than dividing by zero. +// Its volume still counts toward the divisor. +// +// A window whose volume is entirely zero has no money flow to distribute and reports 0.0. +// Published references are silent here and other implementations divide by zero; TA-Lib does not return NaN from a successful call. +// +// Bars where the low exceeds the high are malformed rather than degenerate, and also contribute zero. +// The default period of 20 follows the original write-up, which describes 20 or 21 bars. +// +// @param inHigh []float64 +// @param inLow []float64 +// @param inClose []float64 +// @param inVolume []float64 +// @param period int Lookback window; default is 20 (2–100000) +// @return []float64 +// +// since TA-Lib 0.8.1 +func CMF(inHigh, inLow, inClose, inVolume []float64, period int) []float64 { + var ( + startIdx int32 + endIdx = int32(len(inClose) - 1) + outBegIdx int32 + outNBElement int32 + outReal = make([]float64, len(inClose)) + ) + + if retCode := cmf( + startIdx, + endIdx, + inHigh, + inLow, + inClose, + inVolume, + int32(period), + &outBegIdx, + &outNBElement, + outReal, + ); retCode != 0 { + return nil + } + + return outReal +} + +// NVI - Negative Volume Index: a running cumulative index that changes only on days when +// volume falls versus the prior day, compounding that day's percentage price change. +// The premise is that quiet, low-volume days reflect the actions of well-informed +// "smart money", so NVI is read as a proxy for that cohort's positioning. +// +// NVI[startIdx] = 1000 +// +// For each subsequent bar i: +// NVI[i] = NVI[i-1] + ( inVolume[i] < inVolume[i-1] ? ((inClose[i] - inClose[i-1]) / inClose[i-1]) * NVI[i-1] : 0 ) +// +// The index carries forward unchanged on bars whose volume did not fall (and on the +// degenerate case of a zero previous close, which would otherwise divide by zero). +// +// since TA-Lib 0.8.1 +func NVI(inReal, inVolume []float64) []float64 { + var ( + startIdx int32 + endIdx = int32(len(inReal) - 1) + outBegIdx int32 + outNBElement int32 + outReal = make([]float64, len(inReal)) + ) + + if retCode := nvi( + startIdx, + endIdx, + inReal, + inVolume, + &outBegIdx, + &outNBElement, + outReal, + ); retCode != 0 { + return nil + } + + return outReal +} + +// OBV - On Balance Volume: a running cumulative total of volume, added on up-price bars and subtracted on down-price bars. +// Relates volume flow to price direction. +func OBV(inReal, inVolume []float64) []float64 { + var ( + startIdx int32 + endIdx = int32(len(inReal) - 1) + outBegIdx int32 + outNBElement int32 + outReal = make([]float64, len(inReal)) + ) + + if retCode := obv( + startIdx, + endIdx, + inReal, + inVolume, + &outBegIdx, + &outNBElement, + outReal, + ); retCode != 0 { + return nil + } + + return outReal +} + +// PVI - Positive Volume Index: a running cumulative index that changes only on days when +// volume rises versus the prior day, compounding that day's percentage price change. +// The premise is that active, high-volume days reflect the actions of the +// less-informed "crowd", so PVI is read as a proxy for that cohort's positioning. +// +// PVI[startIdx] = 1000 +// +// For each subsequent bar i: +// PVI[i] = PVI[i-1] + ( inVolume[i] > inVolume[i-1] ? ((inClose[i] - inClose[i-1]) / inClose[i-1]) * PVI[i-1] : 0 ) +// +// The index carries forward unchanged on bars whose volume did not rise (and on the +// degenerate case of a zero previous close, which would otherwise divide by zero). +// +// since TA-Lib 0.8.1 +func PVI(inReal, inVolume []float64) []float64 { + var ( + startIdx int32 + endIdx = int32(len(inReal) - 1) + outBegIdx int32 + outNBElement int32 + outReal = make([]float64, len(inReal)) + ) + + if retCode := pvi( + startIdx, + endIdx, + inReal, + inVolume, + &outBegIdx, + &outNBElement, + outReal, + ); retCode != 0 { + return nil + } + + return outReal +} + +// PVO - Percentage Volume Oscillator: a variation of the Percentage Price Oscillator (PPO, created by Gerald Appel) +// applied to the volume series instead of price. It is the difference between a fast and slow moving average of volume, +// expressed as a percentage of the slow talib.MAType. +// Positive when short-term volume is above its longer-term average (rising participation), negative when below. +// The default periods (12, 26) match MACD and PPO. +// +// PVO = ((fastMA(inVolume) - slowMA(inVolume)) / slowMA(inVolume)) * 100, both MAs of type optInMAType; output = 0 when slowMA == 0 +// +// The standard form is exponential with periods 12 and 26 — ((12-day EMA of Volume - 26-day EMA of Volume) / 26-day EMA of Volume) * 100, +// i.e. the talib.PPO/talib.MACD oscillator computed on volume. +// optInMAType therefore defaults to talib.EMA — the moving average Gerald Appel used for the original talib.PPO/talib.MACD; +// pass another type (e.g. talib.MA_SMA) to override. +// +// @param inVolume []float64 +// @param fastPeriod int Period of the fast moving average; default is 12 (2–100000) +// @param slowPeriod int Period of the slow moving average; default is 26 (2–100000) +// @param ma MAType Type of moving average to use; default is talib.EMA +// @return []float64 Percentage Volume Oscillator +// +// since TA-Lib 0.8.1 +func PVO(inVolume []float64, fastPeriod, slowPeriod int, ma MAType) []float64 { + var ( + startIdx int32 + endIdx = int32(len(inVolume) - 1) + outBegIdx int32 + outNBElement int32 + outReal = make([]float64, len(inVolume)) + ) + + if retCode := pvo( + startIdx, + endIdx, + inVolume, + int32(fastPeriod), + int32(slowPeriod), + int32(ma), + &outBegIdx, + &outNBElement, + outReal, + ); retCode != 0 { + return nil + } + + return outReal +}