From f05ea1f01e141c46b1fe7d294049912f493d68f8 Mon Sep 17 00:00:00 2001 From: TDamiao Date: Thu, 26 Feb 2026 14:29:32 -0300 Subject: [PATCH] feat(api): standardize typed inputs and candle aliases --- README.md | 39 +++- src/api.ts | 427 ++++++++++++++++++++++++++++++++++++++++ src/candles.ts | 110 ++++++++--- src/crypto.ts | 2 +- src/index.ts | 44 +++-- src/indicators.ts | 28 ++- src/types.ts | 40 +++- test/contracts.test.mjs | 40 +++- 8 files changed, 667 insertions(+), 63 deletions(-) create mode 100644 src/api.ts diff --git a/README.md b/README.md index 262da88..c950144 100644 --- a/README.md +++ b/README.md @@ -158,20 +158,49 @@ Limitations: - Orderflow proxies infer pressure from candle direction and volume; they are not a replacement for L2/L3 order book data. - Different libraries use different warmup conventions; comparisons use overlapping non-null windows. -## Candle Contracts +## Typing and Inputs -Use typed candles plus helpers: +`ta-crypto` public APIs accept two input styles: +- Primitive arrays (`number[]`) for low-level control. +- Candle objects with long keys (`open/high/low/close/volume/time`) or aliases (`o/h/l/c/v/t`). + +Single-series APIs (for example `sma`, `ema`, `rsi`, `macd`, `bbands`) accept: + +```ts +import { rsi } from "ta-crypto"; + +const close = [101, 102, 103, 104]; +const candles = [{ o: 100, h: 102, l: 99, c: 101, v: 10, t: 1 }]; + +rsi(close, 14); +rsi(candles, 14); // uses candle close/c +``` + +OHLC/OHLCV APIs (for example `vwap`, `stoch`, `atr`, `natr`, `mfi`, `adx`) accept: + +```ts +import { atr, vwap } from "ta-crypto"; + +atr([102, 103], [99, 100], [101, 102], 14); +atr([{ open: 100, high: 102, low: 99, close: 101, volume: 10 }], 14); + +vwap([102, 103], [99, 100], [101, 102], [10, 12]); +vwap([{ o: 100, h: 102, l: 99, c: 101, v: 10 }]); +``` + +Helpers: ```ts import { pluckClose, toOHLCV } from "ta-crypto"; const close = pluckClose(candles); -const { open, high, low, close: c, volume } = toOHLCV(candles, 0); +const ohlcv = toOHLCV(candles, 0); +const ohlcvFromArrays = toOHLCV({ o: [1, 2], h: [3, 4], l: [0, 1], c: [2, 3], v: [5, 6] }); ``` Validation: -- Multi-series indicators enforce equal lengths (`assertSameLength`). -- Candle helpers validate finite numeric fields with index-specific error messages. +- Multi-series APIs enforce equal lengths. +- Numeric inputs are validated as finite numbers with index-aware messages when possible. ## Module Imports diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..4628960 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,427 @@ +import type { Candle, OHLCVInput, PriceInput } from "./types.js"; +import { assertFiniteSeries } from "./core/math.js"; +import { normalizePrice, toOHLCV } from "./candles.js"; +import { + sma as coreSma, + ema as coreEma, + rma as coreRma, + hl2 as coreHl2, + hlc3 as coreHlc3, + ohlc4 as coreOhlc4, + vwap as coreVwap, + bbands as coreBbands +} from "./core/overlap.js"; +import { rsi as coreRsi, macd as coreMacd, stoch as coreStoch } from "./core/momentum.js"; +import { trueRange as coreTrueRange, atr as coreAtr, natr as coreNatr } from "./core/volatility.js"; +import { + logReturn as coreLogReturn, + percentReturn as corePercentReturn, + realizedVolatility as coreRealizedVolatility +} from "./core/performance.js"; +import { obv as coreObv, mfi as coreMfi } from "./core/volume.js"; +import { adx as coreAdx } from "./core/trend.js"; +import { + vwapSession as coreVwapSession, + fundingRateCumulative as coreFundingRateCumulative, + fundingRateAPR as coreFundingRateAPR, + volatilityRegime as coreVolatilityRegime, + signedVolume as coreSignedVolume, + volumeDelta as coreVolumeDelta, + orderflowImbalance as coreOrderflowImbalance +} from "./core/crypto.js"; + +type OHLCInput = Candle[] | OHLCVInput; + +function isNumericArray(input: unknown): input is number[] { + return Array.isArray(input) && (input.length === 0 || typeof input[0] === "number"); +} + +function parseHLC(input: number[] | OHLCInput, low?: number[], close?: number[]) { + if (isNumericArray(input)) { + if (!low || !close) { + throw new Error("Expected high, low, close arrays or candles/OHLCV object input"); + } + assertFiniteSeries("high", input); + assertFiniteSeries("low", low); + assertFiniteSeries("close", close); + return { high: input, low, close }; + } + return toOHLCV(input); +} + +function parseOHLC(input: number[] | OHLCInput, high?: number[], low?: number[], close?: number[]) { + if (isNumericArray(input)) { + if (!high || !low || !close) { + throw new Error("Expected open, high, low, close arrays or candles/OHLCV object input"); + } + assertFiniteSeries("open", input); + assertFiniteSeries("high", high); + assertFiniteSeries("low", low); + assertFiniteSeries("close", close); + return { open: input, high, low, close }; + } + return toOHLCV(input); +} + +function parseHLCV( + input: number[] | OHLCInput, + low?: number[], + close?: number[], + volume?: number[] +) { + if (isNumericArray(input)) { + if (!low || !close || !volume) { + throw new Error("Expected high, low, close, volume arrays or candles/OHLCV object input"); + } + assertFiniteSeries("high", input); + assertFiniteSeries("low", low); + assertFiniteSeries("close", close); + assertFiniteSeries("volume", volume); + return { high: input, low, close, volume }; + } + return toOHLCV(input); +} + +function parseOCV(input: number[] | OHLCInput, close?: number[], volume?: number[]) { + if (isNumericArray(input)) { + if (!close || !volume) { + throw new Error("Expected open, close, volume arrays or candles/OHLCV object input"); + } + assertFiniteSeries("open", input); + assertFiniteSeries("close", close); + assertFiniteSeries("volume", volume); + return { open: input, close, volume }; + } + return toOHLCV(input); +} + +export function sma(input: PriceInput, length = 14): Array { + return coreSma(normalizePrice(input), length); +} + +export function ema(input: PriceInput, length = 14): Array { + return coreEma(normalizePrice(input), length); +} + +export function rma(input: PriceInput, length = 14): Array { + return coreRma(normalizePrice(input), length); +} + +export function bbands(input: PriceInput, length = 20, std = 2) { + return coreBbands(normalizePrice(input), length, std); +} + +export function macd(input: PriceInput, fast = 12, slow = 26, signal = 9) { + return coreMacd(normalizePrice(input), fast, slow, signal); +} + +export function rsi(input: PriceInput, length = 14): Array { + return coreRsi(normalizePrice(input), length); +} + +export function logReturn(input: PriceInput, cumulative = false): Array { + return coreLogReturn(normalizePrice(input), cumulative); +} + +export function percentReturn(input: PriceInput, cumulative = false): Array { + return corePercentReturn(normalizePrice(input), cumulative); +} + +export function realizedVolatility(input: PriceInput, length = 30, periodsPerYear = 365): Array { + return coreRealizedVolatility(normalizePrice(input), length, periodsPerYear); +} + +export function hl2(high: number[], low: number[]): Array; +export function hl2(input: OHLCInput): Array; +export function hl2(input: number[] | OHLCInput, low?: number[]): Array { + if (isNumericArray(input)) { + if (!low) throw new Error("Expected high + low arrays or candles/OHLCV object input"); + return coreHl2(input, low); + } + const ohlcv = toOHLCV(input); + return coreHl2(ohlcv.high, ohlcv.low); +} + +export function hlc3(high: number[], low: number[], close: number[]): Array; +export function hlc3(input: OHLCInput): Array; +export function hlc3(input: number[] | OHLCInput, low?: number[], close?: number[]): Array { + const ohlcv = parseHLC(input, low, close); + return coreHlc3(ohlcv.high, ohlcv.low, ohlcv.close); +} + +export function ohlc4(open: number[], high: number[], low: number[], close: number[]): Array; +export function ohlc4(input: OHLCInput): Array; +export function ohlc4( + input: number[] | OHLCInput, + high?: number[], + low?: number[], + close?: number[] +): Array { + const ohlcv = parseOHLC(input, high, low, close); + return coreOhlc4(ohlcv.open, ohlcv.high, ohlcv.low, ohlcv.close); +} + +export function vwap( + high: number[], + low: number[], + close: number[], + volume: number[], + length?: number +): Array; +export function vwap(input: OHLCInput, length?: number): Array; +export function vwap( + input: number[] | OHLCInput, + lowOrLength?: number[] | number, + close?: number[], + volume?: number[], + length?: number +): Array { + if (isNumericArray(input)) { + const low = Array.isArray(lowOrLength) ? lowOrLength : undefined; + const resolvedLength = typeof lowOrLength === "number" ? lowOrLength : length; + const ohlcv = parseHLCV(input, low, close, volume); + return coreVwap(ohlcv.high, ohlcv.low, ohlcv.close, ohlcv.volume, resolvedLength); + } + const ohlcv = toOHLCV(input); + const resolvedLength = typeof lowOrLength === "number" ? lowOrLength : length; + return coreVwap(ohlcv.high, ohlcv.low, ohlcv.close, ohlcv.volume, resolvedLength); +} + +export function stoch( + high: number[], + low: number[], + close: number[], + kLength?: number, + dLength?: number +): { k: Array; d: Array }; +export function stoch(input: OHLCInput, kLength?: number, dLength?: number): { k: Array; d: Array }; +export function stoch( + input: number[] | OHLCInput, + lowOrKLength?: number[] | number, + closeOrDLength?: number[] | number, + kLength = 14, + dLength = 3 +) { + if (isNumericArray(input)) { + const low = Array.isArray(lowOrKLength) ? lowOrKLength : undefined; + const close = Array.isArray(closeOrDLength) ? closeOrDLength : undefined; + const resolvedK = typeof lowOrKLength === "number" ? lowOrKLength : kLength; + const resolvedD = typeof closeOrDLength === "number" ? closeOrDLength : dLength; + const ohlcv = parseHLC(input, low, close); + return coreStoch(ohlcv.high, ohlcv.low, ohlcv.close, resolvedK, resolvedD); + } + const resolvedK = typeof lowOrKLength === "number" ? lowOrKLength : kLength; + const resolvedD = typeof closeOrDLength === "number" ? closeOrDLength : dLength; + const ohlcv = toOHLCV(input); + return coreStoch(ohlcv.high, ohlcv.low, ohlcv.close, resolvedK, resolvedD); +} + +export function trueRange(high: number[], low: number[], close: number[]): Array; +export function trueRange(input: OHLCInput): Array; +export function trueRange(input: number[] | OHLCInput, low?: number[], close?: number[]): Array { + const ohlcv = parseHLC(input, low, close); + return coreTrueRange(ohlcv.high, ohlcv.low, ohlcv.close); +} + +export function atr(high: number[], low: number[], close: number[], length?: number): Array; +export function atr(input: OHLCInput, length?: number): Array; +export function atr( + input: number[] | OHLCInput, + lowOrLength?: number[] | number, + close?: number[], + length = 14 +): Array { + if (isNumericArray(input)) { + const low = Array.isArray(lowOrLength) ? lowOrLength : undefined; + const resolvedLength = typeof lowOrLength === "number" ? lowOrLength : length; + const ohlcv = parseHLC(input, low, close); + return coreAtr(ohlcv.high, ohlcv.low, ohlcv.close, resolvedLength); + } + const ohlcv = toOHLCV(input); + const resolvedLength = typeof lowOrLength === "number" ? lowOrLength : length; + return coreAtr(ohlcv.high, ohlcv.low, ohlcv.close, resolvedLength); +} + +export function natr(high: number[], low: number[], close: number[], length?: number): Array; +export function natr(input: OHLCInput, length?: number): Array; +export function natr( + input: number[] | OHLCInput, + lowOrLength?: number[] | number, + close?: number[], + length = 14 +): Array { + if (isNumericArray(input)) { + const low = Array.isArray(lowOrLength) ? lowOrLength : undefined; + const resolvedLength = typeof lowOrLength === "number" ? lowOrLength : length; + const ohlcv = parseHLC(input, low, close); + return coreNatr(ohlcv.high, ohlcv.low, ohlcv.close, resolvedLength); + } + const ohlcv = toOHLCV(input); + const resolvedLength = typeof lowOrLength === "number" ? lowOrLength : length; + return coreNatr(ohlcv.high, ohlcv.low, ohlcv.close, resolvedLength); +} + +export function obv(close: number[], volume: number[]): Array; +export function obv(input: OHLCInput): Array; +export function obv(input: number[] | OHLCInput, volume?: number[]): Array { + if (isNumericArray(input)) { + if (!volume) throw new Error("Expected close + volume arrays or candles/OHLCV object input"); + return coreObv(input, volume); + } + const ohlcv = toOHLCV(input); + return coreObv(ohlcv.close, ohlcv.volume); +} + +export function mfi( + high: number[], + low: number[], + close: number[], + volume: number[], + length?: number +): Array; +export function mfi(input: OHLCInput, length?: number): Array; +export function mfi( + input: number[] | OHLCInput, + lowOrLength?: number[] | number, + close?: number[], + volume?: number[], + length = 14 +): Array { + if (isNumericArray(input)) { + const low = Array.isArray(lowOrLength) ? lowOrLength : undefined; + const resolvedLength = typeof lowOrLength === "number" ? lowOrLength : length; + const ohlcv = parseHLCV(input, low, close, volume); + return coreMfi(ohlcv.high, ohlcv.low, ohlcv.close, ohlcv.volume, resolvedLength); + } + const ohlcv = toOHLCV(input); + const resolvedLength = typeof lowOrLength === "number" ? lowOrLength : length; + return coreMfi(ohlcv.high, ohlcv.low, ohlcv.close, ohlcv.volume, resolvedLength); +} + +export function adx( + high: number[], + low: number[], + close: number[], + length?: number +): { adx: Array; plusDI: Array; minusDI: Array }; +export function adx( + input: OHLCInput, + length?: number +): { adx: Array; plusDI: Array; minusDI: Array }; +export function adx( + input: number[] | OHLCInput, + lowOrLength?: number[] | number, + close?: number[], + length = 14 +): { adx: Array; plusDI: Array; minusDI: Array } { + if (isNumericArray(input)) { + const low = Array.isArray(lowOrLength) ? lowOrLength : undefined; + const resolvedLength = typeof lowOrLength === "number" ? lowOrLength : length; + const ohlcv = parseHLC(input, low, close); + return coreAdx(ohlcv.high, ohlcv.low, ohlcv.close, resolvedLength); + } + const ohlcv = toOHLCV(input); + const resolvedLength = typeof lowOrLength === "number" ? lowOrLength : length; + return coreAdx(ohlcv.high, ohlcv.low, ohlcv.close, resolvedLength); +} + +export function vwapSession( + high: number[], + low: number[], + close: number[], + volume: number[], + session: Array +): Array; +export function vwapSession(input: OHLCInput, session: Array): Array; +export function vwapSession( + input: number[] | OHLCInput, + lowOrSession: number[] | Array, + close?: number[], + volume?: number[], + session?: Array +): Array { + if (isNumericArray(input)) { + if (!isNumericArray(lowOrSession)) { + throw new Error("Expected high, low, close, volume, session arrays"); + } + if (!session || !close || !volume) { + throw new Error("Expected high, low, close, volume, session arrays"); + } + return coreVwapSession(input, lowOrSession, close, volume, session); + } + if (!Array.isArray(lowOrSession)) { + throw new Error("session must be an array"); + } + const ohlcv = toOHLCV(input); + return coreVwapSession(ohlcv.high, ohlcv.low, ohlcv.close, ohlcv.volume, lowOrSession as Array); +} + +export function signedVolume(open: number[], close: number[], volume: number[]): Array; +export function signedVolume(input: OHLCInput): Array; +export function signedVolume(input: number[] | OHLCInput, close?: number[], volume?: number[]): Array { + const ohlcv = parseOCV(input, close, volume); + return coreSignedVolume(ohlcv.open, ohlcv.close, ohlcv.volume); +} + +export function volumeDelta(open: number[], close: number[], volume: number[], length?: number): Array; +export function volumeDelta(input: OHLCInput, length?: number): Array; +export function volumeDelta( + input: number[] | OHLCInput, + closeOrLength?: number[] | number, + volume?: number[], + length = 14 +): Array { + if (isNumericArray(input)) { + const close = Array.isArray(closeOrLength) ? closeOrLength : undefined; + const resolvedLength = typeof closeOrLength === "number" ? closeOrLength : length; + const ohlcv = parseOCV(input, close, volume); + return coreVolumeDelta(ohlcv.open, ohlcv.close, ohlcv.volume, resolvedLength); + } + const ohlcv = toOHLCV(input); + const resolvedLength = typeof closeOrLength === "number" ? closeOrLength : length; + return coreVolumeDelta(ohlcv.open, ohlcv.close, ohlcv.volume, resolvedLength); +} + +export function orderflowImbalance( + open: number[], + close: number[], + volume: number[], + length?: number +): Array; +export function orderflowImbalance(input: OHLCInput, length?: number): Array; +export function orderflowImbalance( + input: number[] | OHLCInput, + closeOrLength?: number[] | number, + volume?: number[], + length = 14 +): Array { + if (isNumericArray(input)) { + const close = Array.isArray(closeOrLength) ? closeOrLength : undefined; + const resolvedLength = typeof closeOrLength === "number" ? closeOrLength : length; + const ohlcv = parseOCV(input, close, volume); + return coreOrderflowImbalance(ohlcv.open, ohlcv.close, ohlcv.volume, resolvedLength); + } + const ohlcv = toOHLCV(input); + const resolvedLength = typeof closeOrLength === "number" ? closeOrLength : length; + return coreOrderflowImbalance(ohlcv.open, ohlcv.close, ohlcv.volume, resolvedLength); +} + +export function fundingRateCumulative(values: number[]): Array { + assertFiniteSeries("values", values); + return coreFundingRateCumulative(values); +} + +export function fundingRateAPR(values: number[], periodsPerYear = 365 * 3): Array { + assertFiniteSeries("values", values); + return coreFundingRateAPR(values, periodsPerYear); +} + +export function volatilityRegime( + input: PriceInput, + length = 30, + periodsPerYear = 365, + lowZ = -0.5, + highZ = 0.5 +): Array { + return coreVolatilityRegime(normalizePrice(input), length, periodsPerYear, lowZ, highZ); +} diff --git a/src/candles.ts b/src/candles.ts index 7e9293a..bbaddff 100644 --- a/src/candles.ts +++ b/src/candles.ts @@ -1,52 +1,112 @@ -import type { Candle } from "./types.js"; -import { assertFiniteSeries } from "./core/math.js"; - -export type OHLCV = { - open: number[]; - high: number[]; - low: number[]; - close: number[]; - volume: number[]; - time: Array; -}; +import { assertFiniteSeries, assertSameLength, isNum } from "./core/math.js"; +import type { Candle, OHLCV, OHLCVInput, PriceInput, TimeValue } from "./types.js"; + +function readField(candle: Candle, index: number, name: string, alias: string): number { + const value = (candle as Record)[name] ?? (candle as Record)[alias]; + if (!isNum(value)) { + throw new Error(`candles[${index}].${name} (or .${alias}) must be a finite number`); + } + return value; +} + +function readTime(candle: Candle): TimeValue | undefined { + const value = (candle as Record).time ?? (candle as Record).t; + return value as TimeValue | undefined; +} + +function hasOHLCVArrays(input: OHLCVInput): input is OHLCV { + return "open" in input && "high" in input && "low" in input && "close" in input; +} + +export function isCandleArray(input: PriceInput | OHLCVInput): input is Candle[] { + return Array.isArray(input) && input.length > 0 && typeof input[0] !== "number"; +} + +export function normalizePrice(input: PriceInput, name = "values"): number[] { + if (Array.isArray(input) && (input.length === 0 || typeof input[0] === "number")) { + assertFiniteSeries(name, input as number[]); + return input as number[]; + } + return pluckClose(input as Candle[]); +} export function pluckOpen(candles: Candle[]): number[] { - const out = candles.map(c => c.open); + const out = candles.map((candle, index) => readField(candle, index, "open", "o")); assertFiniteSeries("open", out); return out; } export function pluckHigh(candles: Candle[]): number[] { - const out = candles.map(c => c.high); + const out = candles.map((candle, index) => readField(candle, index, "high", "h")); assertFiniteSeries("high", out); return out; } export function pluckLow(candles: Candle[]): number[] { - const out = candles.map(c => c.low); + const out = candles.map((candle, index) => readField(candle, index, "low", "l")); assertFiniteSeries("low", out); return out; } export function pluckClose(candles: Candle[]): number[] { - const out = candles.map(c => c.close); + const out = candles.map((candle, index) => readField(candle, index, "close", "c")); assertFiniteSeries("close", out); return out; } export function pluckVolume(candles: Candle[], fallback = 0): number[] { - const out = candles.map(c => (c.volume === undefined ? fallback : c.volume)); + if (!isNum(fallback)) { + throw new Error("volumeFallback must be a finite number"); + } + const out = candles.map((candle, index) => { + const value = (candle as Record).volume ?? (candle as Record).v; + const normalized = value === undefined ? fallback : value; + if (!isNum(normalized)) { + throw new Error(`candles[${index}].volume (or .v) must be a finite number`); + } + return normalized; + }); assertFiniteSeries("volume", out); return out; } -export function toOHLCV(candles: Candle[], volumeFallback = 0): OHLCV { - return { - open: pluckOpen(candles), - high: pluckHigh(candles), - low: pluckLow(candles), - close: pluckClose(candles), - volume: pluckVolume(candles, volumeFallback), - time: candles.map(c => c.time) - }; +export function toOHLCV(input: Candle[] | OHLCVInput, volumeFallback = 0): OHLCV { + if (Array.isArray(input)) { + return { + open: pluckOpen(input), + high: pluckHigh(input), + low: pluckLow(input), + close: pluckClose(input), + volume: pluckVolume(input, volumeFallback), + time: input.map(c => readTime(c)) + }; + } + + if (hasOHLCVArrays(input)) { + assertFiniteSeries("open", input.open); + assertFiniteSeries("high", input.high); + assertFiniteSeries("low", input.low); + assertFiniteSeries("close", input.close); + const volume = input.volume ?? new Array(input.close.length).fill(volumeFallback); + assertFiniteSeries("volume", volume); + assertSameLength(input.open, input.high, input.low, input.close, volume); + const time = input.time ?? new Array(input.close.length).fill(undefined); + if (time.length !== input.close.length) { + throw new Error(`All series must have the same length (expected ${input.close.length}, got ${time.length})`); + } + return { open: input.open, high: input.high, low: input.low, close: input.close, volume, time }; + } + + assertFiniteSeries("open", input.o); + assertFiniteSeries("high", input.h); + assertFiniteSeries("low", input.l); + assertFiniteSeries("close", input.c); + const volume = input.v ?? new Array(input.c.length).fill(volumeFallback); + assertFiniteSeries("volume", volume); + assertSameLength(input.o, input.h, input.l, input.c, volume); + const time = input.t ?? new Array(input.c.length).fill(undefined); + if (time.length !== input.c.length) { + throw new Error(`All series must have the same length (expected ${input.c.length}, got ${time.length})`); + } + return { open: input.o, high: input.h, low: input.l, close: input.c, volume, time }; } diff --git a/src/crypto.ts b/src/crypto.ts index a88b28e..cc328f8 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -6,5 +6,5 @@ export { signedVolume, volumeDelta, orderflowImbalance -} from "./core/crypto.js"; +} from "./api.js"; export { createVWAPSession } from "./stateful.js"; diff --git a/src/index.ts b/src/index.ts index 559e9e6..9af11c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,24 @@ -export { sma, ema, rma, hl2, hlc3, ohlc4, vwap, bbands } from "./core/overlap.js"; -export { rsi, macd, stoch } from "./core/momentum.js"; -export { trueRange, atr, natr } from "./core/volatility.js"; -export { logReturn, percentReturn, realizedVolatility } from "./core/performance.js"; -export { obv, mfi } from "./core/volume.js"; -export { adx } from "./core/trend.js"; export { + sma, + ema, + rma, + hl2, + hlc3, + ohlc4, + vwap, + bbands, + rsi, + macd, + stoch, + trueRange, + atr, + natr, + logReturn, + percentReturn, + realizedVolatility, + obv, + mfi, + adx, vwapSession, fundingRateCumulative, fundingRateAPR, @@ -12,29 +26,17 @@ export { signedVolume, volumeDelta, orderflowImbalance -} from "./core/crypto.js"; +} from "./api.js"; export { pluckOpen, pluckHigh, pluckLow, pluckClose, pluckVolume, toOHLCV } from "./candles.js"; export { createRSI, createVWAPSession } from "./stateful.js"; export * from "./types.js"; -import * as overlap from "./core/overlap.js"; -import * as momentum from "./core/momentum.js"; -import * as volatility from "./core/volatility.js"; -import * as performance from "./core/performance.js"; -import * as volume from "./core/volume.js"; -import * as trend from "./core/trend.js"; -import * as crypto from "./core/crypto.js"; +import * as api from "./api.js"; import * as candles from "./candles.js"; import * as stateful from "./stateful.js"; export const ta = { - ...overlap, - ...momentum, - ...volatility, - ...performance, - ...volume, - ...trend, - ...crypto, + ...api, ...candles, ...stateful }; diff --git a/src/indicators.ts b/src/indicators.ts index 8c8fb1b..783a937 100644 --- a/src/indicators.ts +++ b/src/indicators.ts @@ -1,6 +1,22 @@ -export { sma, ema, rma, hl2, hlc3, ohlc4, vwap, bbands } from "./core/overlap.js"; -export { rsi, macd, stoch } from "./core/momentum.js"; -export { trueRange, atr, natr } from "./core/volatility.js"; -export { logReturn, percentReturn, realizedVolatility } from "./core/performance.js"; -export { obv, mfi } from "./core/volume.js"; -export { adx } from "./core/trend.js"; +export { + sma, + ema, + rma, + hl2, + hlc3, + ohlc4, + vwap, + bbands, + rsi, + macd, + stoch, + trueRange, + atr, + natr, + logReturn, + percentReturn, + realizedVolatility, + obv, + mfi, + adx +} from "./api.js"; diff --git a/src/types.ts b/src/types.ts index dc828bf..9107936 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,12 +1,44 @@ -export type NumericSeries = number[]; - +export type NumericSeries = number[]; export type Series = Array; +export type TimeValue = number | string | Date; -export type Candle = { - time?: number | string | Date; +export type CandleObject = { open: number; high: number; low: number; close: number; volume?: number; + time?: TimeValue; +}; + +export type CandleAlias = { + o: number; + h: number; + l: number; + c: number; + v?: number; + t?: TimeValue; }; + +export type Candle = CandleObject | CandleAlias; + +export type OHLCV = { + open: number[]; + high: number[]; + low: number[]; + close: number[]; + volume: number[]; + time: Array; +}; + +export type OHLCVAlias = { + o: number[]; + h: number[]; + l: number[]; + c: number[]; + v?: number[]; + t?: Array; +}; + +export type OHLCVInput = OHLCV | OHLCVAlias; +export type PriceInput = NumericSeries | Candle[]; diff --git a/test/contracts.test.mjs b/test/contracts.test.mjs index 284c40e..bcc42d7 100644 --- a/test/contracts.test.mjs +++ b/test/contracts.test.mjs @@ -1,6 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { pluckClose, pluckVolume, toOHLCV, vwap } from "../dist/index.js"; +import { atr, pluckClose, pluckVolume, rsi, sma, toOHLCV, vwap } from "../dist/index.js"; const candles = [ { open: 100, high: 102, low: 99, close: 101, volume: 10, time: 1 }, @@ -8,6 +8,12 @@ const candles = [ { open: 102, high: 104, low: 101, close: 103, time: 3 } ]; +const candlesAlias = [ + { o: 100, h: 102, l: 99, c: 101, v: 10, t: 1 }, + { o: 101, h: 103, l: 100, c: 102, v: 12, t: 2 }, + { o: 102, h: 104, l: 101, c: 103, v: 8, t: 3 } +]; + test("candles helpers produce typed OHLCV arrays", () => { assert.deepEqual(pluckClose(candles), [101, 102, 103]); assert.deepEqual(pluckVolume(candles, 0), [10, 12, 0]); @@ -20,10 +26,42 @@ test("candles helpers produce typed OHLCV arrays", () => { assert.deepEqual(ohlcv.volume, [10, 12, 0]); }); +test("candles helpers accept alias fields and array-based OHLCV inputs", () => { + assert.deepEqual(pluckClose(candlesAlias), [101, 102, 103]); + assert.deepEqual(pluckVolume(candlesAlias), [10, 12, 8]); + + const byAliases = toOHLCV(candlesAlias, 0); + assert.deepEqual(byAliases.open, [100, 101, 102]); + assert.deepEqual(byAliases.high, [102, 103, 104]); + assert.deepEqual(byAliases.low, [99, 100, 101]); + assert.deepEqual(byAliases.close, [101, 102, 103]); + assert.deepEqual(byAliases.volume, [10, 12, 8]); + assert.deepEqual(byAliases.time, [1, 2, 3]); + + const byArrays = toOHLCV({ o: [1, 2], h: [3, 4], l: [0, 1], c: [2, 3], v: [5, 6], t: ["a", "b"] }); + assert.deepEqual(byArrays.open, [1, 2]); + assert.deepEqual(byArrays.volume, [5, 6]); + assert.deepEqual(byArrays.time, ["a", "b"]); +}); + +test("main APIs support both primitive arrays and candle objects", () => { + const close = [101, 102, 103]; + const high = [102, 103, 104]; + const low = [99, 100, 101]; + const volume = [10, 12, 8]; + + assert.deepEqual(sma(close, 2), sma(candlesAlias, 2)); + assert.deepEqual(rsi(close, 2), rsi(candlesAlias, 2)); + assert.deepEqual(vwap(high, low, close, volume), vwap(candlesAlias)); + assert.deepEqual(atr(high, low, close, 2), atr(candlesAlias, 2)); +}); + test("length and numeric validations return actionable messages", () => { assert.throws( () => vwap([1, 2, 3], [1, 2], [1, 2, 3], [10, 20, 30]), /All series must have the same length/ ); + assert.throws(() => vwap([1, 2, 3]), /Expected high, low, close, volume arrays or candles\/OHLCV object input/); assert.throws(() => pluckClose([{ open: 1, high: 2, low: 0, close: Number.NaN }]), /must be a finite number/); + assert.throws(() => pluckClose([{ o: 1, h: 2, l: 0, c: Number.NaN }]), /candles\[0\]\.close \(or \.c\) must be a finite number/); });