From 1ce0c19fd5c8491e076f50dc3aa13f7b0aa8cdfd Mon Sep 17 00:00:00 2001 From: Alek Kim Date: Wed, 22 Jul 2026 15:40:16 +0900 Subject: [PATCH] initial commit --- .gitignore | 2 + README.md | 97 +++++++++++++++++++++++++++++ cycle_indicators.go | 146 ++++++++++++++++++++++++++++++++++++++++++++ functions.go | 64 +++++++++++++++++++ go.mod | 5 ++ go.sum | 2 + loader.go | 85 ++++++++++++++++++++++++++ math_operators.go | 31 ++++++++++ ta_result.go | 24 ++++++++ 9 files changed, 456 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cycle_indicators.go create mode 100644 functions.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 loader.go create mode 100644 math_operators.go create mode 100644 ta_result.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2bc0089 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/* +cli/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d38a9c --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# talib + +Go wrapper for selected [TA-Lib](https://ta-lib.org/) functions using [`purego`](https://github.com/ebitengine/purego) to dynamically load the native TA-Lib shared library at runtime. + +This project currently exposes a small subset of TA-Lib functions: + +- `HT_DCPERIOD` +- `HT_DCPHASE` +- `HT_PHASOR` +- `HT_SINE` +- `HT_TRENDMODE` +- `ADD` + +## Requirements + +- Go `1.26` +- A native TA-Lib shared library installed on the host system + +This package does **not** bundle TA-Lib itself. You must install the native library separately. + +## How library loading works + +Call `talib.Load()` once before using any indicator function. The loader looks for the platform-specific library name: + +- macOS: `libta-lib.dylib` +- Linux: `libta-lib.so` +- Windows: `libta-lib.dll` + +Lookup order: + +1. `$TA_LIB_PATH/` +2. `./` +3. `/usr/local/lib/` +4. `/usr/lib/` + +## Installing TA-Lib + +### macOS + +If you use Homebrew: + +```bash +brew install ta-lib +``` + +If the library is installed outside the default lookup paths, set: + +```bash +export TA_LIB_PATH=/path/to/lib +``` + +### Linux + +Install TA-Lib with your package manager if available, or build it from source and place the shared library in a standard library directory such as `/usr/local/lib`. + +### Windows + +Install the TA-Lib DLL and make sure `libta-lib.dll` is reachable through `TA_LIB_PATH` or from the current working directory. + +## Usage + +```go +package main + +import ( + "fmt" + "log" + "math" + + "beejay.kim/lib/talib" +) + +func main() { + if _, err := talib.Load(); err != nil { + log.Fatal(err) + } + + var series []float64 + for i := 0; i < 100; i++ { + series = append(series, math.Sin(float64(i)/7)*4.5+math.Cos(float64(i)/19)*1.75) + } + + period := talib.HT_DCPERIOD(series) + fmt.Println(period) + + sum := talib.ADD(series, series) + fmt.Println(sum) +} +``` + +## API notes + +- Indicator functions return `nil` when the underlying TA-Lib call fails. + +## Project status + +This is currently a focused wrapper around a limited subset of TA-Lib. Additional indicators can be added by registering more native functions in `Load()` and exposing Go wrappers for them. diff --git a/cycle_indicators.go b/cycle_indicators.go new file mode 100644 index 0000000..c43aecb --- /dev/null +++ b/cycle_indicators.go @@ -0,0 +1,146 @@ +package talib + +import ( + "log/slog" +) + +// HT_DCPERIOD - Hilbert Transform estimate of the dominant cycle period (in bars) of the price series. +// Outputs the smoothed instantaneous cycle period. +// Output is the estimated dominant cycle length in bars (clamped to 6-50). +func HT_DCPERIOD(inReal []float64) []float64 { + var ( + startIdx = 0 + endIdx = len(inReal) - 1 + outBegIdx int + outNBElement int + outReal = make([]float64, len(inReal)) + ) + + if retCode := ht_dcperiod(startIdx, endIdx, inReal, &outBegIdx, &outNBElement, outReal); SUCCESS != taResult(retCode) { + slog.Debug("HT_DCPERIOD", "result", retCode) + return nil + } + + return outReal +} + +// HT_DCPHASE - Hilbert Transform Dominant Cycle Phase: the instantaneous phase (in degrees) of the dominant market cycle, +// derived from a homodyne discriminator on a Hilbert-transformed, smoothed price. +// One real output per bar. Output is degrees, wrapped so it never exceeds 315 (can go negative). +func HT_DCPHASE(inReal []float64) []float64 { + var ( + startIdx = 0 + endIdx = len(inReal) - 1 + outBegIdx int + outNBElement int + outReal = make([]float64, len(inReal)) + ) + + if retCode := ht_dcphase( + startIdx, + endIdx, + inReal, + &outBegIdx, + &outNBElement, + outReal, + ); SUCCESS != taResult(retCode) { + slog.Debug("HT_DCPHASE", "result", retCode) + return nil + } + + return outReal +} + +// HT_PHASOR - Hilbert Transform indicator that decomposes the price series into its in-phase (I) and quadrature (Q) phasor components. +// Shares the same detrend/Hilbert machinery as the other HT_* cycle functions. +// +// Smooth price with a 4-bar WMA (weights 1,2,3,4 /10). +// Apply the Hilbert Transform (a=0.0962, b=0.5769, scaled per bar by adjustedPrevPeriod = 0.075*period + 0.54) to get detrender = HT(smoothed) and Q1 = HT(detrender). +// Output: outInPhase = detrender delayed 3 price bars; outQuadrature = Q1. +// @param inReal Input price series +// @return outInPhase In-phase component (detrender delayed 3 bars) +// @return outQuadrature Quadrature component (Q1 of the Hilbert Transform) +func HT_PHASOR(inReal []float64) ([]float64, []float64) { + var ( + startIdx = 0 + endIdx = len(inReal) - 1 + outBegIdx int + outNBElement int + outInPhase = make([]float64, len(inReal)) + outQuadrature = make([]float64, len(inReal)) + ) + + if retCode := ht_phasor( + startIdx, + endIdx, + inReal, + &outBegIdx, + &outNBElement, + outInPhase, + outQuadrature, + ); SUCCESS != taResult(retCode) { + slog.Debug("HT_PHASOR", "result", retCode) + return nil, nil + } + + return outInPhase, outQuadrature +} + +// HT_SINE - Hilbert Transform SineWave: derives the dominant-cycle phase from price and emits its sine plus a 45-degree-lead sine. +// The two curves cross near cycle turning points. +// outSine and outLeadSine crossing marks cycle turning points. +// @param inReal Input price series +// @return outSine Sine of the dominant-cycle phase +// @return outLeadSine Sine of the phase advanced 45 degrees (lead) +func HT_SINE(inReal []float64) ([]float64, []float64) { + var ( + startIdx = 0 + endIdx = len(inReal) - 1 + outBegIdx int + outNBElement int + outSine = make([]float64, len(inReal)) + outLeadSine = make([]float64, len(inReal)) + ) + + if retCode := ht_sine( + startIdx, + endIdx, + inReal, + &outBegIdx, + &outNBElement, + outSine, + outLeadSine, + ); SUCCESS != taResult(retCode) { + slog.Debug("HT_SINE", "result", retCode) + return nil, nil + } + + return outSine, outLeadSine +} + +// HT_TRENDMODE - Hilbert Transform classifier that labels each bar as trending (1) or cycling (0). Reuses the MAMA dominant-cycle/phase DSP plus a SineWave/trendline test to decide the market mode. 1 = trending market (favor trend-following); 0 = cycle/mean-reverting mode. +// @param inReal Input price series +// @return outInteger 1 = trending market; 0 = cycle/mean-reverting market; 4294967297 (Fermat number) = error (e.g. insufficient data) +func HT_TRENDMODE(inReal []float64) []int { + var ( + startIdx = 0 + endIdx = len(inReal) - 1 + outBegIdx int + outNBElement int + outInteger = make([]int, len(inReal)) + ) + + if retCode := ht_trendmode( + startIdx, + endIdx, + inReal, + &outBegIdx, + &outNBElement, + outInteger, + ); SUCCESS != taResult(retCode) { + slog.Debug("HT_TRENDMODE", "result", retCode) + return nil + } + + return outInteger +} diff --git a/functions.go b/functions.go new file mode 100644 index 0000000..70b67a4 --- /dev/null +++ b/functions.go @@ -0,0 +1,64 @@ +package talib + +// Cycle Indicators (HT) - Hilbert Transform +var ( + ht_dcperiod func( + startIdx int, + endIdx int, + inReal []float64, + outBegIdx *int, + outNBElement *int, + outReal []float64, + ) int + + ht_dcphase func( + startIdx int, + endIdx int, + inReal []float64, + outBegIdx *int, + outNBElement *int, + outReal []float64, + ) int + + ht_phasor func( + startIdx int, + endIdx int, + inReal []float64, + outBegIdx *int, + outNBElement *int, + outInPhase []float64, + outQuadrature []float64, + ) int + + ht_sine func( + startIdx int, + endIdx int, + inReal []float64, + outBegIdx *int, + outNBElement *int, + outSine []float64, + outLeadSine []float64, + ) int + + ht_trendmode func( + startIdx int, + endIdx int, + inReal []float64, + outBegIdx *int, + outNBElement *int, + outInteger []int, + ) int +) + +// Math Operators +var ( + add func( + startIdx int, + endIdx int, + inReal0 []float64, + inReal1 []float64, + outBegIdx *int, + outNBElement *int, + outReal []float64, + ) int +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7fee9e3 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module beejay.kim/lib/talib + +go 1.26 + +require github.com/ebitengine/purego v0.10.2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d52a42e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= +github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= diff --git a/loader.go b/loader.go new file mode 100644 index 0000000..ca3c58f --- /dev/null +++ b/loader.go @@ -0,0 +1,85 @@ +package talib + +import ( + "fmt" + "log/slog" + "os" + "path/filepath" + "runtime" + + "github.com/ebitengine/purego" +) + +func Load() (uintptr, error) { + var ( + path string + ptr uintptr + err error + ) + + if path, err = getExpectedLibraryPath(); err != nil { + return 0, err + } + + slog.Debug("Loading TA-Lib library", "path", path) + if ptr, err = purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_GLOBAL); err != nil { + return 0, err + } + + // Register the functions we need from the TA-Lib library + purego.RegisterLibFunc(&ht_dcperiod, ptr, "TA_HT_DCPERIOD") + purego.RegisterLibFunc(&ht_dcphase, ptr, "TA_HT_DCPHASE") + purego.RegisterLibFunc(&ht_phasor, ptr, "TA_HT_PHASOR") + purego.RegisterLibFunc(&ht_sine, ptr, "TA_HT_SINE") + purego.RegisterLibFunc(&ht_trendmode, ptr, "TA_HT_TRENDMODE") + purego.RegisterLibFunc(&add, ptr, "TA_ADD") + + return ptr, nil +} + +func getExpectedLibraryName() (string, error) { + switch runtime.GOOS { + case "windows": + return "libta-lib.dll", nil + case "darwin": + return "libta-lib.dylib", nil + case "linux": + return "libta-lib.so", nil + default: + return "", fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } +} + +func getExpectedLibraryPath() (string, error) { + libName, err := getExpectedLibraryName() + if err != nil { + return "", err + } + + isExist := func(path string) bool { + _, err := os.Stat(path) + return err == nil + } + + if envpath, ok := os.LookupEnv("TA_LIB_PATH"); ok { + p := filepath.Join(envpath, libName) + if isExist(p) { + return p, nil + } + + slog.Debug("TA_LIB_PATH environment variable is set but library not found", "path", p) + } + + for _, dir := range []string{ + ".", + "/usr/local/lib", + "/usr/lib", + } { + p := filepath.Join(dir, libName) + if isExist(p) { + return p, nil + } + } + + return "", fmt.Errorf("library not found: %s", libName) +} diff --git a/math_operators.go b/math_operators.go new file mode 100644 index 0000000..4ca6f35 --- /dev/null +++ b/math_operators.go @@ -0,0 +1,31 @@ +package talib + +import "log/slog" + +// ADD - Vector arithmetic addition. Outputs the element-wise sum of two input series. +// +// outReal[i] = inReal0[i] + inReal1[i] +func ADD(inReal0, inReal1 []float64) []float64 { + var ( + startIdx = 0 + endIdx = len(inReal0) - 1 + outBegIdx int + outNBElement int + outReal = make([]float64, len(inReal0)) + ) + + if retCode := add( + startIdx, + endIdx, + inReal0, + inReal1, + &outBegIdx, + &outNBElement, + outReal, + ); SUCCESS != taResult(retCode) { + slog.Debug("ADD", "result", retCode) + return nil + } + + return outReal[:outNBElement] +} diff --git a/ta_result.go b/ta_result.go new file mode 100644 index 0000000..9193d36 --- /dev/null +++ b/ta_result.go @@ -0,0 +1,24 @@ +package talib + +type taResult int + +const ( + SUCCESS taResult = 0 // No error + LIB_NOT_INITIALIZE = 1 // TA_Initialize was not successfully called + BAD_PARAM = 2 // A parameter is out of range + ALLOC_ERR = 3 // Possibly out-of-memory + GROUP_NOT_FOUND = 4 + FUNC_NOT_FOUND = 5 + INVALID_HANDLE = 6 + INVALID_PARAM_HOLDER = 7 + INVALID_PARAM_HOLDER_TYPE = 8 + INVALID_PARAM_FUNCTION = 9 + INPUT_NOT_ALL_INITIALIZE = 10 + OUTPUT_NOT_ALL_INITIALIZE = 11 + OUT_OF_RANGE_START_INDEX = 12 + OUT_OF_RANGE_END_INDEX = 13 + INVALID_LIST_TYPE = 14 + BAD_OBJECT = 15 + NOT_SUPPORTED = 16 + INTERNAL_ERROR = 5000 +)