Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ const vs = vwapSession(high, low, close, volume, [1, 1]);
## Stateful API (streaming)

```ts
import { createRSI, createVWAPSession } from "ta-crypto";
import { createSMA, createEMA, createRSI, createVWAPSession } from "ta-crypto";

const sma14 = createSMA(14);
const ema14 = createEMA(14);
const rsi14 = createRSI(14);
const nextSma = sma14.next(101.25);
const nextEma = ema14.next(101.25);
const nextRsi = rsi14.next(101.25);

const vwap = createVWAPSession();
Expand All @@ -48,6 +52,24 @@ const nextVwap = vwap.next({
});
```

Websocket-like streaming loop:

```ts
import { createEMA, createRSI } from "ta-crypto";

const ema21 = createEMA(21);
const rsi14 = createRSI(14);

ws.on("message", (tick) => {
const price = Number(tick.last);
const e = ema21.next(price);
const r = rsi14.next(price);
if (e !== null && r !== null) {
// strategy signal path
}
});
```

## Examples

Real-world entry points live in `examples/`:
Expand Down Expand Up @@ -208,7 +230,7 @@ Validation:
import { sma } from "ta-crypto/indicators";
import { vwapSession } from "ta-crypto/crypto";
import { toOHLCV } from "ta-crypto/candles";
import { createRSI } from "ta-crypto/stateful";
import { createSMA, createEMA, createRSI } from "ta-crypto/stateful";
```

## Bench (internal baseline, 10k candles)
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export {
orderflowImbalance
} from "./api.js";
export { pluckOpen, pluckHigh, pluckLow, pluckClose, pluckVolume, toOHLCV } from "./candles.js";
export { createRSI, createVWAPSession } from "./stateful.js";
export { createSMA, createEMA, createRSI, createVWAPSession } from "./stateful.js";
export * from "./types.js";

import * as api from "./api.js";
Expand Down
69 changes: 69 additions & 0 deletions src/stateful.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,75 @@ export type StatefulIndicator<TIn, TOut> = {
reset(): void;
};

export function createSMA(period = 14): StatefulIndicator<number, number | null> {
if (period <= 0) {
throw new Error("period must be > 0");
}

const window: number[] = [];
let sum = 0;

return {
next(value: number): number | null {
if (!Number.isFinite(value)) {
throw new Error("value must be a finite number");
}

window.push(value);
sum += value;

if (window.length < period) {
return null;
}
if (window.length > period) {
sum -= window.shift() as number;
}
return sum / period;
},
reset(): void {
window.length = 0;
sum = 0;
}
};
}

export function createEMA(period = 14): StatefulIndicator<number, number | null> {
if (period <= 0) {
throw new Error("period must be > 0");
}

const k = 2 / (period + 1);
let seedSum = 0;
let seedCount = 0;
let prev: number | null = null;

return {
next(value: number): number | null {
if (!Number.isFinite(value)) {
throw new Error("value must be a finite number");
}

if (prev === null) {
seedSum += value;
seedCount += 1;
if (seedCount < period) {
return null;
}
prev = seedSum / period;
return prev;
}

prev = (value - prev) * k + prev;
return prev;
},
reset(): void {
seedSum = 0;
seedCount = 0;
prev = null;
}
};
}

export function createRSI(period = 14): StatefulIndicator<number, number | null> {
if (period <= 0) {
throw new Error("period must be > 0");
Expand Down
42 changes: 41 additions & 1 deletion test/golden.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
bbands,
atr,
adx,
createSMA,
createEMA,
vwapSession,
createRSI,
createVWAPSession
Expand Down Expand Up @@ -55,11 +57,28 @@ test("golden parity: overlap/momentum/trend/volatility/crypto", () => {
approxSeries(actualADX.minusDI, golden.adx14.minusDI);
});

test("stateful parity: RSI and VWAPSession", () => {
test("stateful parity: SMA/EMA/RSI and VWAPSession", () => {
const smaState = createSMA(14);
const actualStatefulSMA = close.map(v => smaState.next(v));
approxSeries(actualStatefulSMA, sma(close, 14));
smaState.reset();
const actualStatefulSMAAfterReset = close.map(v => smaState.next(v));
approxSeries(actualStatefulSMAAfterReset, sma(close, 14));

const emaState = createEMA(14);
const actualStatefulEMA = close.map(v => emaState.next(v));
approxSeries(actualStatefulEMA, ema(close, 14));
emaState.reset();
const actualStatefulEMAAfterReset = close.map(v => emaState.next(v));
approxSeries(actualStatefulEMAAfterReset, ema(close, 14));

const rsiState = createRSI(14);
const actualStatefulRSI = close.map(v => rsiState.next(v));
approxSeries(actualStatefulRSI, golden.statefulRSI14);
approxSeries(actualStatefulRSI, rsi(close, 14));
rsiState.reset();
const actualStatefulRSIAfterReset = close.map(v => rsiState.next(v));
approxSeries(actualStatefulRSIAfterReset, rsi(close, 14));

const vwapState = createVWAPSession();
const actualStatefulVWAP = high.map((_, i) =>
Expand All @@ -74,4 +93,25 @@ test("stateful parity: RSI and VWAPSession", () => {

approxSeries(actualStatefulVWAP, golden.statefulVWAPSession);
approxSeries(actualStatefulVWAP, vwapSession(high, low, close, volume, session));
vwapState.reset();
const actualStatefulVWAPAfterReset = high.map((_, i) =>
vwapState.next({
high: high[i],
low: low[i],
close: close[i],
volume: volume[i],
sessionId: session[i]
})
);
approxSeries(actualStatefulVWAPAfterReset, vwapSession(high, low, close, volume, session));
});

test("stateful API validates invalid inputs", () => {
const smaState = createSMA(14);
const emaState = createEMA(14);
const rsiState = createRSI(14);

assert.throws(() => smaState.next(Number.NaN), /value must be a finite number/);
assert.throws(() => emaState.next(Number.POSITIVE_INFINITY), /value must be a finite number/);
assert.throws(() => rsiState.next(Number.NaN), /price must be a finite number/);
});