@vesper85/technical-indicators 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # @osiris-ai/technical-indicators
2
+
3
+ A library for calculating technical indicators used in financial analysis and trading.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @osiris-ai/technical-indicators
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Using Individual Functions
14
+
15
+ ```typescript
16
+ import { calculateSMA, calculateRSI, calculateMACD } from '@osiris-ai/technical-indicators';
17
+
18
+ // Simple Moving Average
19
+ const closes = [100, 102, 101, 105, 107, 109, 108];
20
+ const sma = calculateSMA(closes, { period: 5 });
21
+ console.log(sma); // 106.4 (single number)
22
+
23
+ // RSI
24
+ const rsi = calculateRSI(closes, { period: 14 });
25
+ console.log(rsi); // 65.5 (single number)
26
+
27
+ // MACD (returns array)
28
+ const macd = calculateMACD(closes, {
29
+ fastPeriod: 12,
30
+ slowPeriod: 26,
31
+ signalPeriod: 9
32
+ });
33
+ console.log(macd); // [0.5, 0.3, 0.2] - [MACD, signal, histogram]
34
+ ```
35
+
36
+ ### Using TechnicalAnalysisService
37
+
38
+ ```typescript
39
+ import { TechnicalAnalysisService } from '@osiris-ai/technical-indicators';
40
+ import { Logger } from '@osiris-ai/logger';
41
+
42
+ const service = new TechnicalAnalysisService({ logger: new Logger() });
43
+
44
+ // OHLCV data format
45
+ const ohlcvData = [
46
+ {
47
+ timestamp: 1640995200000,
48
+ open: 100,
49
+ high: 105,
50
+ low: 99,
51
+ close: 103,
52
+ volume: 1000000
53
+ },
54
+ // ... more candles
55
+ ];
56
+
57
+ // Calculate RSI
58
+ const rsi = service.calculate('rsi_14', ohlcvData, { period: 14 });
59
+ console.log(rsi); // 65.5
60
+
61
+ // Calculate MACD with component selection
62
+ const macdSignal = service.calculate(
63
+ 'macd_12_26_9',
64
+ ohlcvData,
65
+ { component: 'signal' }
66
+ );
67
+ console.log(macdSignal); // 0.3 (just the signal line)
68
+ ```
69
+
70
+ ## Input/Output Types
71
+
72
+ ### Input Types
73
+
74
+ **Individual Functions:**
75
+ - **Price arrays**: `number[]` - Array of closing prices, highs, lows, etc.
76
+ - **Volume arrays**: `number[]` - Array of volume values
77
+ - **Parameters**: `TAParams` - Object containing indicator-specific parameters
78
+
79
+ **TechnicalAnalysisService:**
80
+ - **Data**: `OHLCV[]` - Array of OHLCV objects
81
+ ```typescript
82
+ interface OHLCV {
83
+ timestamp: number;
84
+ open: number;
85
+ high: number;
86
+ low: number;
87
+ close: number;
88
+ volume: number;
89
+ }
90
+ ```
91
+ - **Indicator**: `string` - Indicator name with parameters (e.g., `"rsi_14"`, `"macd_12_26_9"`)
92
+ - **Params**: `TAParams & { component?: string }` - Additional parameters and optional component selector
93
+
94
+ ### Output Types
95
+
96
+ **Single Value Indicators** (return `number`):
97
+ - RSI, EMA, SMA, ATR, ADX, OBV, CCI, MFI, VWAP, WilliamsR, ROC, PSAR, WMA
98
+
99
+ **Multi-Value Indicators** (return `number[]`):
100
+ - **MACD**: `[MACD, signal, histogram]`
101
+ - **Bollinger Bands**: `[upper, middle, lower]`
102
+ - **Stochastic**: `[%K, %D]`
103
+ - **Stochastic RSI**: `[%K, %D]`
104
+ - **Keltner Channels**: `[upper, lower, middle]`
105
+
106
+ ### Examples
107
+
108
+ ```typescript
109
+ // Single value output
110
+ const rsi: number = calculateRSI([100, 102, 101, 105, 107], { period: 5 });
111
+ // Result: 65.5
112
+
113
+ // Array output - MACD
114
+ const macd: number[] = calculateMACD(closes, {
115
+ fastPeriod: 12,
116
+ slowPeriod: 26,
117
+ signalPeriod: 9
118
+ });
119
+ // Result: [0.5, 0.3, 0.2]
120
+
121
+ // Array output - Bollinger Bands
122
+ const bb: number[] = calculateBollingerBands(closes, {
123
+ period: 20,
124
+ stdDev: 2
125
+ });
126
+ // Result: [110.5, 105.0, 99.5] // [upper, middle, lower]
127
+
128
+ // Using component selector with service
129
+ const upperBand: number = service.calculate(
130
+ 'bb_20_2',
131
+ ohlcvData,
132
+ { component: 'upper' }
133
+ );
134
+ // Result: 110.5 (just the upper band)
135
+ ```
136
+
137
+ ## Available Indicators
138
+
139
+ - **SMA** (Simple Moving Average) - `calculateSMA`
140
+ - **EMA** (Exponential Moving Average) - `calculateEMA`
141
+ - **RSI** (Relative Strength Index) - `calculateRSI`
142
+ - **MACD** (Moving Average Convergence Divergence) - `calculateMACD`
143
+ - **ATR** (Average True Range) - `calculateATR`
144
+ - **Bollinger Bands** - `calculateBollingerBands`
145
+ - **ADX** (Average Directional Index) - `calculateADX`
146
+ - **Stochastic** - `calculateStochastic`
147
+ - **OBV** (On-Balance Volume) - `calculateOBV`
148
+ - **CCI** (Commodity Channel Index) - `calculateCCI`
149
+ - **MFI** (Money Flow Index) - `calculateMFI`
150
+ - **VWAP** (Volume Weighted Average Price) - `calculateVWAP`
151
+ - **Williams %R** - `calculateWilliamsR`
152
+ - **Stochastic RSI** - `calculateStochasticRSI`
153
+ - **ROC** (Rate of Change) - `calculateROC`
154
+ - **Parabolic SAR** - `calculateParabolicSAR`
155
+ - **Keltner Channels** - `calculateKeltnerChannels`
156
+ - **WMA** (Weighted Moving Average) - `calculateWMA`
157
+
158
+ ## Adding New Technical Indicators
159
+
160
+ Follow this checklist when adding a new technical indicator:
161
+
162
+ ### 1. Update Indicator Lists
163
+ - [ ] Add indicator name to `supportedIndicators` array in `src/indicators/index.ts`
164
+ - [ ] If indicator has multiple components (e.g., MACD, Bollinger Bands), add entry to `availableComponents` object
165
+
166
+ ### 2. Implement Calculation Function
167
+ - [ ] Create `calculate[IndicatorName]` function in `src/indicators/index.ts`
168
+ - [ ] Define function signature with appropriate input arrays and `TAParams`
169
+ - [ ] Return type: `number` for single value, `number[]` for multi-value indicators
170
+ - [ ] Add parameter validation (check required params, minimum data length)
171
+ - [ ] Use `technicalindicators` library for calculation
172
+ - [ ] Return the last calculated value (for single) or array of values (for multi-value)
173
+
174
+ ### 3. Integrate with Service
175
+ - [ ] Add case in `calculate()` method switch statement in `src/index.ts`
176
+ - [ ] Handle component selection if indicator has multiple components
177
+ - [ ] Extract appropriate price arrays (closes, highs, lows, volumes) from OHLCV data
178
+
179
+ ### 4. Add Parameter Extraction
180
+ - [ ] Add case in `extractIndicatorParams()` method if indicator uses parameterized naming (e.g., `rsi_14`)
181
+ - [ ] Parse parameters from indicator string format
182
+ - [ ] Return base indicator name and extracted parameters
183
+
184
+ ### 5. Testing & Documentation
185
+ - [ ] Test with sample data
186
+ - [ ] Verify error handling for insufficient data
187
+ - [ ] Update this README with the new indicator
188
+ - [ ] Add example usage in README
189
+
190
+ ### Example: Adding a New Indicator
191
+
192
+ ```typescript
193
+ // 1. Add to supportedIndicators
194
+ export const supportedIndicators = [..., "newindicator"] as const;
195
+
196
+ // 2. Implement function
197
+ export function calculateNewIndicator(
198
+ closes: number[],
199
+ params: TAParams
200
+ ): number {
201
+ const period = params.period;
202
+ if (!period) {
203
+ throw new Error("Period is required for NewIndicator");
204
+ }
205
+ if (closes.length < period) {
206
+ throw new Error(`Insufficient data: need ${period}, got ${closes.length}`);
207
+ }
208
+ const values = TI.NewIndicator.calculate({ values: closes, period });
209
+ const last = values[values.length - 1];
210
+ if (!last) {
211
+ throw new Error("Failed to calculate NewIndicator");
212
+ }
213
+ return last;
214
+ }
215
+
216
+ // 3. Add to service switch
217
+ case "newindicator":
218
+ return calculateNewIndicator(closes, finalParams);
219
+
220
+ // 4. Add parameter extraction if needed
221
+ case "newindicator":
222
+ if (!parts || !parts[0])
223
+ throw new Error(`Period is required for newindicator`);
224
+ return { base, params: { period: parseInt(parts[0]) } };
225
+ ```
226
+
227
+ ## Development
228
+
229
+ ```bash
230
+ # Install dependencies
231
+ pnpm install
232
+
233
+ # Build the package
234
+ pnpm build
235
+
236
+ # Watch mode
237
+ pnpm build:watch
238
+
239
+ # Type check
240
+ pnpm type-check
241
+
242
+ # Lint
243
+ pnpm lint
244
+ ```
245
+
246
+ ## License
247
+
248
+ MIT
249
+
@@ -0,0 +1,15 @@
1
+ import { Logger } from "@osiris-ai/logger";
2
+ import { OHLCV, TAParams } from "./types";
3
+ export declare class TechnicalAnalysisService {
4
+ private logger;
5
+ constructor(config: {
6
+ logger: Logger;
7
+ });
8
+ calculate(indicator: string, data: OHLCV[], params: TAParams & {
9
+ component?: string;
10
+ }): number | number[];
11
+ private extractIndicatorParams;
12
+ }
13
+ export * from "./indicators/index";
14
+ export * from "./types";
15
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,688 @@
1
+ import * as TI from 'technicalindicators';
2
+
3
+ // @osiris-ai/technical-indicators - Technical Indicators Library
4
+
5
+ var supportedIndicators = ["rsi", "ema", "sma", "atr", "macd", "bb", "wma", "adx", "stochastic", "obv", "cci", "mfi", "vwap", "williamsr", "stochasticrsi", "roc", "psar", "keltnerchannels"];
6
+ var availableComponents = {
7
+ rsi: ["rsi"],
8
+ ema: ["ema"],
9
+ sma: ["sma"],
10
+ atr: ["atr"],
11
+ macd: ["macd", "signal", "histogram"],
12
+ bb: ["upper", "middle", "lower"],
13
+ wma: ["wma"],
14
+ adx: ["adx"],
15
+ stochastic: ["stochastic", "k", "d"],
16
+ obv: ["obv"],
17
+ cci: ["cci"],
18
+ mfi: ["mfi"],
19
+ vwap: ["vwap"],
20
+ williamsr: ["williamsr"],
21
+ stochasticrsi: ["stochasticrsi", "k", "d"],
22
+ roc: ["roc"],
23
+ psar: ["psar"],
24
+ keltnerchannels: ["keltnerchannels", "upper", "middle", "lower"]
25
+ };
26
+ function calculateRSI(closes, params) {
27
+ const period = params.period;
28
+ if (!period) {
29
+ throw new Error("Period is required for RSI");
30
+ }
31
+ if (closes.length < period) {
32
+ throw new Error(
33
+ `Insufficient data for RSI: need ${period}, got ${closes.length}`
34
+ );
35
+ }
36
+ const values = TI.RSI.calculate({ values: closes, period });
37
+ const last = values[values.length - 1];
38
+ if (!last) {
39
+ throw new Error("Failed to calculate RSI");
40
+ }
41
+ return last;
42
+ }
43
+ function calculateEMA(closes, params) {
44
+ const period = params.period;
45
+ if (!period) {
46
+ throw new Error("Period is required for EMA");
47
+ }
48
+ if (closes.length < period) {
49
+ throw new Error(
50
+ `Insufficient data for EMA: need ${period}, got ${closes.length}`
51
+ );
52
+ }
53
+ const values = TI.EMA.calculate({ values: closes, period });
54
+ if (!values || values.length === 0) {
55
+ throw new Error(`Failed to calculate EMA: library returned empty array. Input length: ${closes.length}, period: ${period}`);
56
+ }
57
+ const last = values[values.length - 1];
58
+ if (last === void 0 || last === null || isNaN(last)) {
59
+ throw new Error(`Failed to calculate EMA: invalid result. Array length: ${values.length}, last value: ${last}`);
60
+ }
61
+ return last;
62
+ }
63
+ function calculateSMA(closes, params) {
64
+ const period = params.period;
65
+ if (!period) {
66
+ throw new Error("Period is required for SMA");
67
+ }
68
+ if (closes.length < period) {
69
+ throw new Error(
70
+ `Insufficient data for SMA: need ${period}, got ${closes.length}`
71
+ );
72
+ }
73
+ const values = TI.SMA.calculate({ values: closes, period });
74
+ if (!values || values.length === 0) {
75
+ throw new Error(`Failed to calculate SMA: library returned empty array. Input length: ${closes.length}, period: ${period}`);
76
+ }
77
+ const last = values[values.length - 1];
78
+ if (last === void 0 || last === null || isNaN(last)) {
79
+ throw new Error(`Failed to calculate SMA: invalid result. Array length: ${values.length}, last value: ${last}`);
80
+ }
81
+ return last;
82
+ }
83
+ function calculateATR(highs, lows, closes, params) {
84
+ const period = params.period;
85
+ if (!period) {
86
+ throw new Error("Period is required for ATR");
87
+ }
88
+ if (closes.length < period) {
89
+ throw new Error(
90
+ `Insufficient data for ATR: need ${period}, got ${closes.length}`
91
+ );
92
+ }
93
+ const values = TI.ATR.calculate({
94
+ high: highs,
95
+ low: lows,
96
+ close: closes,
97
+ period
98
+ });
99
+ const last = values[values.length - 1];
100
+ if (!last) {
101
+ throw new Error("Failed to calculate ATR");
102
+ }
103
+ return last;
104
+ }
105
+ function calculateMACD(closes, params) {
106
+ const { fastPeriod, slowPeriod, signalPeriod } = params;
107
+ if (!fastPeriod) {
108
+ throw new Error("Fast period is required for MACD");
109
+ }
110
+ if (!slowPeriod) {
111
+ throw new Error("Slow period is required for MACD");
112
+ }
113
+ if (!signalPeriod) {
114
+ throw new Error("Signal period is required for MACD");
115
+ }
116
+ const minBars = Math.max(fastPeriod, slowPeriod) + signalPeriod;
117
+ if (closes.length < minBars) {
118
+ throw new Error(
119
+ `Insufficient data for MACD: need ${minBars}, got ${closes.length}`
120
+ );
121
+ }
122
+ const values = TI.MACD.calculate({
123
+ values: closes,
124
+ fastPeriod,
125
+ slowPeriod,
126
+ signalPeriod,
127
+ SimpleMAOscillator: false,
128
+ SimpleMASignal: false
129
+ });
130
+ if (!values || values.length === 0) {
131
+ throw new Error(`Failed to calculate MACD: library returned empty array. Input length: ${closes.length}, fastPeriod: ${fastPeriod}, slowPeriod: ${slowPeriod}, signalPeriod: ${signalPeriod}`);
132
+ }
133
+ const last = values[values.length - 1];
134
+ if (!last || !last.MACD || !last.signal || !last.histogram) {
135
+ throw new Error(`Failed to calculate MACD: invalid result. Array length: ${values.length}, last value: ${JSON.stringify(last)}`);
136
+ }
137
+ return [last.MACD, last.signal, last.histogram];
138
+ }
139
+ function calculateBollingerBands(closes, params) {
140
+ const period = params.period;
141
+ if (!period) {
142
+ throw new Error("Period is required for Bollinger Bands");
143
+ }
144
+ const stdDev = params.stdDev;
145
+ if (!stdDev) {
146
+ throw new Error("Standard deviation is required for Bollinger Bands");
147
+ }
148
+ if (closes.length < period) {
149
+ throw new Error(
150
+ `Insufficient data for Bollinger Bands: need ${period}, got ${closes.length}`
151
+ );
152
+ }
153
+ const values = TI.BollingerBands.calculate({
154
+ values: closes,
155
+ period,
156
+ stdDev
157
+ });
158
+ if (!values || values.length === 0) {
159
+ throw new Error(`Failed to calculate Bollinger Bands: library returned empty array. Input length: ${closes.length}, period: ${period}, stdDev: ${stdDev}`);
160
+ }
161
+ const last = values[values.length - 1];
162
+ if (!last || !last.upper || !last.middle || !last.lower) {
163
+ throw new Error(`Failed to calculate Bollinger Bands: invalid result. Array length: ${values.length}, last value: ${JSON.stringify(last)}`);
164
+ }
165
+ return [last.upper, last.middle, last.lower];
166
+ }
167
+ function calculateADX(highs, lows, closes, params) {
168
+ const period = params.period;
169
+ if (!period) {
170
+ throw new Error("Period is required for ADX");
171
+ }
172
+ const minBars = period * 2;
173
+ if (closes.length < minBars) {
174
+ throw new Error(
175
+ `Insufficient data for ADX: need ${minBars}, got ${closes.length}`
176
+ );
177
+ }
178
+ const values = TI.ADX.calculate({
179
+ high: highs,
180
+ low: lows,
181
+ close: closes,
182
+ period
183
+ });
184
+ const last = values[values.length - 1];
185
+ if (!last || !last.adx) {
186
+ throw new Error("Failed to calculate ADX");
187
+ }
188
+ return last.adx;
189
+ }
190
+ function calculateStochastic(highs, lows, closes, params) {
191
+ const period = params.period;
192
+ if (!period) {
193
+ throw new Error("Period is required for Stochastic");
194
+ }
195
+ const signalPeriod = params.signalPeriod;
196
+ if (!signalPeriod) {
197
+ throw new Error("Signal period is required for Stochastic");
198
+ }
199
+ const minBars = period + signalPeriod;
200
+ if (closes.length < minBars) {
201
+ throw new Error(
202
+ `Insufficient data for Stochastic: need ${minBars}, got ${closes.length}`
203
+ );
204
+ }
205
+ const values = TI.Stochastic.calculate({
206
+ high: highs,
207
+ low: lows,
208
+ close: closes,
209
+ period,
210
+ signalPeriod
211
+ });
212
+ const last = values[values.length - 1];
213
+ if (!last || !last.k || !last.d) {
214
+ throw new Error("Failed to calculate Stochastic");
215
+ }
216
+ return [last.k, last.d];
217
+ }
218
+ function calculateOBV(closes, volumes, params) {
219
+ if (closes.length < 2) {
220
+ throw new Error(
221
+ `Insufficient data for OBV: need at least 2 candles, got ${closes.length}`
222
+ );
223
+ }
224
+ const values = TI.OBV.calculate({ close: closes, volume: volumes });
225
+ const last = values[values.length - 1];
226
+ if (!last) {
227
+ throw new Error("Failed to calculate OBV");
228
+ }
229
+ return last;
230
+ }
231
+ function calculateCCI(highs, lows, closes, params) {
232
+ const period = params.period;
233
+ if (!period) {
234
+ throw new Error("Period is required for CCI");
235
+ }
236
+ if (closes.length < period) {
237
+ throw new Error(
238
+ `Insufficient data for CCI: need ${period}, got ${closes.length}`
239
+ );
240
+ }
241
+ const values = TI.CCI.calculate({
242
+ high: highs,
243
+ low: lows,
244
+ close: closes,
245
+ period
246
+ });
247
+ const last = values[values.length - 1];
248
+ if (!last) {
249
+ throw new Error("Failed to calculate CCI");
250
+ }
251
+ return last;
252
+ }
253
+ function calculateMFI(highs, lows, closes, volumes, params) {
254
+ const period = params.period;
255
+ if (!period) {
256
+ throw new Error("Period is required for MFI");
257
+ }
258
+ if (closes.length < period) {
259
+ throw new Error(
260
+ `Insufficient data for MFI: need ${period}, got ${closes.length}`
261
+ );
262
+ }
263
+ const values = TI.MFI.calculate({
264
+ high: highs,
265
+ low: lows,
266
+ close: closes,
267
+ volume: volumes,
268
+ period
269
+ });
270
+ const last = values[values.length - 1];
271
+ if (!last) {
272
+ throw new Error("Failed to calculate MFI");
273
+ }
274
+ return last;
275
+ }
276
+ function calculateVWAP(highs, lows, closes, volumes, params) {
277
+ if (closes.length === 0) {
278
+ throw new Error("Insufficient data for VWAP");
279
+ }
280
+ const values = TI.VWAP.calculate({
281
+ high: highs,
282
+ low: lows,
283
+ close: closes,
284
+ volume: volumes
285
+ });
286
+ if (!values || values.length === 0) {
287
+ throw new Error(`Failed to calculate VWAP: library returned empty array. Input length: ${closes.length}`);
288
+ }
289
+ const last = values[values.length - 1];
290
+ if (last === void 0 || last === null || isNaN(last)) {
291
+ throw new Error(`Failed to calculate VWAP: invalid result. Array length: ${values.length}, last value: ${last}`);
292
+ }
293
+ return last;
294
+ }
295
+ function calculateWilliamsR(highs, lows, closes, params) {
296
+ const period = params.period;
297
+ if (!period) {
298
+ throw new Error("Period is required for Williams %R");
299
+ }
300
+ if (closes.length < period) {
301
+ throw new Error(
302
+ `Insufficient data for Williams %R: need ${period}, got ${closes.length}`
303
+ );
304
+ }
305
+ const values = TI.WilliamsR.calculate({
306
+ high: highs,
307
+ low: lows,
308
+ close: closes,
309
+ period
310
+ });
311
+ const last = values[values.length - 1];
312
+ if (!last) {
313
+ throw new Error("Failed to calculate Williams %R");
314
+ }
315
+ return last;
316
+ }
317
+ function calculateStochasticRSI(closes, params) {
318
+ const rsiPeriod = params.rsiPeriod;
319
+ const stochasticPeriod = params.stochasticPeriod;
320
+ const kPeriod = params.kPeriod;
321
+ const dPeriod = params.dPeriod;
322
+ if (!rsiPeriod) {
323
+ throw new Error("RSI period is required for Stochastic RSI");
324
+ }
325
+ if (!stochasticPeriod) {
326
+ throw new Error("Stochastic period is required for Stochastic RSI");
327
+ }
328
+ if (!kPeriod) {
329
+ throw new Error("K period is required for Stochastic RSI");
330
+ }
331
+ if (!dPeriod) {
332
+ throw new Error("D period is required for Stochastic RSI");
333
+ }
334
+ const minBars = Math.max(rsiPeriod, stochasticPeriod) + kPeriod + dPeriod;
335
+ if (closes.length < minBars) {
336
+ throw new Error(
337
+ `Insufficient data for Stochastic RSI: need ${minBars}, got ${closes.length}`
338
+ );
339
+ }
340
+ const values = TI.StochasticRSI.calculate({
341
+ values: closes,
342
+ rsiPeriod,
343
+ stochasticPeriod,
344
+ kPeriod,
345
+ dPeriod
346
+ });
347
+ const last = values[values.length - 1];
348
+ if (!last || !last.k || !last.d) {
349
+ throw new Error("Failed to calculate Stochastic RSI");
350
+ }
351
+ return [last.k, last.d];
352
+ }
353
+ function calculateROC(closes, params) {
354
+ const period = params.period;
355
+ if (!period) {
356
+ throw new Error("Period is required for ROC");
357
+ }
358
+ if (closes.length < period + 1) {
359
+ throw new Error(
360
+ `Insufficient data for ROC: need ${period + 1}, got ${closes.length}`
361
+ );
362
+ }
363
+ const values = TI.ROC.calculate({ values: closes, period });
364
+ const last = values[values.length - 1];
365
+ if (!last) {
366
+ throw new Error("Failed to calculate ROC");
367
+ }
368
+ return last;
369
+ }
370
+ function calculateParabolicSAR(highs, lows, params) {
371
+ const step = params.step;
372
+ const max = params.max;
373
+ if (!step) {
374
+ throw new Error("Step is required for Parabolic SAR");
375
+ }
376
+ if (!max) {
377
+ throw new Error("Max is required for Parabolic SAR");
378
+ }
379
+ if (highs.length < 2) {
380
+ throw new Error(
381
+ `Insufficient data for Parabolic SAR: need at least 2 candles, got ${highs.length}`
382
+ );
383
+ }
384
+ const values = TI.PSAR.calculate({
385
+ high: highs,
386
+ low: lows,
387
+ step,
388
+ max
389
+ });
390
+ if (!values || values.length === 0) {
391
+ throw new Error(`Failed to calculate Parabolic SAR: library returned empty array. Input length: ${highs.length}, step: ${step}, max: ${max}`);
392
+ }
393
+ const last = values[values.length - 1];
394
+ if (last === void 0 || last === null || isNaN(last)) {
395
+ throw new Error(`Failed to calculate Parabolic SAR: invalid result. Array length: ${values.length}, last value: ${last}`);
396
+ }
397
+ return last;
398
+ }
399
+ function calculateKeltnerChannels(highs, lows, closes, params) {
400
+ const maPeriod = params.maPeriod;
401
+ const multiplier = params.multiplier;
402
+ const atrPeriod = params.atrPeriod;
403
+ if (!maPeriod) {
404
+ throw new Error("MA period is required for Keltner Channels");
405
+ }
406
+ if (!multiplier) {
407
+ throw new Error("Multiplier is required for Keltner Channels");
408
+ }
409
+ if (!atrPeriod) {
410
+ throw new Error("ATR period is required for Keltner Channels");
411
+ }
412
+ const minBars = Math.max(maPeriod, atrPeriod);
413
+ if (closes.length < minBars) {
414
+ throw new Error(
415
+ `Insufficient data for Keltner Channels: need ${minBars}, got ${closes.length}`
416
+ );
417
+ }
418
+ const values = TI.KeltnerChannels.calculate({
419
+ high: highs,
420
+ low: lows,
421
+ close: closes,
422
+ maPeriod,
423
+ multiplier,
424
+ atrPeriod,
425
+ useSMA: false
426
+ });
427
+ if (!values || values.length === 0) {
428
+ throw new Error(`Failed to calculate Keltner Channels: library returned empty array. Input length: ${closes.length}, maPeriod: ${maPeriod}, multiplier: ${multiplier}, atrPeriod: ${atrPeriod}`);
429
+ }
430
+ const last = values[values.length - 1];
431
+ if (!last || !last.upper || !last.lower || !last.middle) {
432
+ throw new Error(`Failed to calculate Keltner Channels: invalid result. Array length: ${values.length}, last value: ${JSON.stringify(last)}`);
433
+ }
434
+ return [last.upper, last.lower, last.middle];
435
+ }
436
+ function calculateWMA(closes, params) {
437
+ const period = params.period;
438
+ if (!period) {
439
+ throw new Error("Period is required for WMA");
440
+ }
441
+ if (closes.length < period) {
442
+ throw new Error(
443
+ `Insufficient data for WMA: need ${period}, got ${closes.length}`
444
+ );
445
+ }
446
+ const values = TI.WMA.calculate({ values: closes, period });
447
+ const last = values[values.length - 1];
448
+ if (!last) {
449
+ throw new Error("Failed to calculate WMA");
450
+ }
451
+ return last;
452
+ }
453
+
454
+ // src/index.ts
455
+ var TechnicalAnalysisService = class {
456
+ logger;
457
+ constructor(config) {
458
+ this.logger = config.logger;
459
+ }
460
+ calculate(indicator, data, params) {
461
+ try {
462
+ this.logger.info("Calculating technical indicator", { indicator, params });
463
+ if (!data || data.length === 0) {
464
+ throw new Error("No OHLCV data provided for TA calculation");
465
+ }
466
+ const closes = data.map((d) => Number(d.close));
467
+ const highs = data.map((d) => Number(d.high));
468
+ const lows = data.map((d) => Number(d.low));
469
+ const volumes = data.map((d) => Number(d.volume));
470
+ const component = params.component;
471
+ const paramsWithoutComponent = { ...params };
472
+ delete paramsWithoutComponent.component;
473
+ const { base, params: extractedParams } = this.extractIndicatorParams(indicator);
474
+ const finalParams = { ...extractedParams, ...paramsWithoutComponent };
475
+ switch (base) {
476
+ case "rsi":
477
+ return calculateRSI(closes, finalParams);
478
+ case "ema":
479
+ return calculateEMA(closes, finalParams);
480
+ case "sma":
481
+ return calculateSMA(closes, finalParams);
482
+ case "atr":
483
+ return calculateATR(highs, lows, closes, finalParams);
484
+ case "macd": {
485
+ const result = calculateMACD(closes, finalParams);
486
+ if (component && !availableComponents.macd.includes(component)) {
487
+ throw new Error(`Invalid component for MACD: ${component}`);
488
+ }
489
+ if (component === "signal") {
490
+ return result[1] ?? 0;
491
+ } else if (component === "macd") {
492
+ return result[0] ?? 0;
493
+ } else if (component === "histogram") {
494
+ return result[2] ?? 0;
495
+ }
496
+ return result;
497
+ }
498
+ case "bb": {
499
+ const result = calculateBollingerBands(closes, finalParams);
500
+ if (component && !availableComponents.bb.includes(component)) {
501
+ throw new Error(`Invalid component for Bollinger Bands: ${component}`);
502
+ }
503
+ if (component === "lower") {
504
+ return result[2] ?? 0;
505
+ } else if (component === "upper") {
506
+ return result[0] ?? 0;
507
+ } else if (component === "middle") {
508
+ return result[1] ?? 0;
509
+ }
510
+ return result;
511
+ }
512
+ case "adx":
513
+ return calculateADX(highs, lows, closes, finalParams);
514
+ case "stochastic": {
515
+ const result = calculateStochastic(highs, lows, closes, finalParams);
516
+ if (component && !availableComponents.stochastic.includes(component)) {
517
+ throw new Error(`Invalid component for Stochastic: ${component}`);
518
+ }
519
+ if (component === "d") {
520
+ return result[1] ?? 0;
521
+ } else if (component === "k") {
522
+ return result[0] ?? 0;
523
+ }
524
+ return result;
525
+ }
526
+ case "obv":
527
+ return calculateOBV(closes, volumes, finalParams);
528
+ case "cci":
529
+ return calculateCCI(highs, lows, closes, finalParams);
530
+ case "mfi":
531
+ return calculateMFI(highs, lows, closes, volumes, finalParams);
532
+ case "vwap":
533
+ return calculateVWAP(highs, lows, closes, volumes, finalParams);
534
+ case "williamsr":
535
+ return calculateWilliamsR(highs, lows, closes, finalParams);
536
+ case "stochasticrsi": {
537
+ const result = calculateStochasticRSI(closes, finalParams);
538
+ if (component && !availableComponents.stochasticrsi.includes(component)) {
539
+ throw new Error(`Invalid component for Stochastic RSI: ${component}`);
540
+ }
541
+ if (component === "k") {
542
+ return result[0] ?? 0;
543
+ } else if (component === "d") {
544
+ return result[1] ?? 0;
545
+ }
546
+ return result;
547
+ }
548
+ case "roc":
549
+ return calculateROC(closes, finalParams);
550
+ case "psar":
551
+ return calculateParabolicSAR(highs, lows, finalParams);
552
+ case "keltnerchannels": {
553
+ const result = calculateKeltnerChannels(highs, lows, closes, finalParams);
554
+ if (component && !availableComponents.keltnerchannels.includes(component)) {
555
+ throw new Error(`Invalid component for Keltner Channels: ${component}`);
556
+ }
557
+ if (component === "upper") {
558
+ return result[0] ?? 0;
559
+ } else if (component === "lower") {
560
+ return result[1] ?? 0;
561
+ } else if (component === "middle") {
562
+ return result[2] ?? 0;
563
+ }
564
+ return result;
565
+ }
566
+ case "wma":
567
+ return calculateWMA(closes, finalParams);
568
+ default:
569
+ throw new Error(`Unsupported technical indicator: ${indicator}`);
570
+ }
571
+ } catch (error) {
572
+ const errorMessage = error instanceof Error ? error.message : String(error);
573
+ const errorStack = error instanceof Error ? error.stack : void 0;
574
+ this.logger.error("Error calculating technical indicator", {
575
+ indicator,
576
+ params,
577
+ error: errorMessage,
578
+ stack: errorStack
579
+ });
580
+ throw error;
581
+ }
582
+ }
583
+ extractIndicatorParams(indicator) {
584
+ const cleanIndicator = indicator.startsWith("ta_") ? indicator.substring(3) : indicator;
585
+ if (cleanIndicator === "vwap") {
586
+ return { base: "vwap", params: {} };
587
+ }
588
+ const match = cleanIndicator.match(/^([a-z]+)_(.+)$/);
589
+ if (!match) {
590
+ throw new Error(`Invalid indicator format: ${indicator}`);
591
+ }
592
+ const base = match[1];
593
+ if (!base || !supportedIndicators.includes(base)) {
594
+ throw new Error(`Base indicator not found: ${indicator}`);
595
+ }
596
+ const parts = match[2]?.split("_");
597
+ switch (base) {
598
+ case "rsi":
599
+ case "ema":
600
+ case "sma":
601
+ case "atr":
602
+ if (!parts || !parts[0])
603
+ throw new Error(`Period is required for ${base}`);
604
+ return { base, params: { period: parseInt(parts[0]) } };
605
+ case "macd":
606
+ if (!parts || (!parts[0] || !parts[1] || !parts[2]))
607
+ throw new Error(`MACD requires 3 params: ${indicator}`);
608
+ return {
609
+ base,
610
+ params: {
611
+ fastPeriod: parseInt(parts[0]),
612
+ slowPeriod: parseInt(parts[1]),
613
+ signalPeriod: parseInt(parts[2])
614
+ }
615
+ };
616
+ case "bb":
617
+ if (!parts || !parts[0] || !parts[1])
618
+ throw new Error(`BB requires 2 params: ${indicator}`);
619
+ return {
620
+ base,
621
+ params: {
622
+ period: parseInt(parts[0]),
623
+ stdDev: parseFloat(parts[1])
624
+ }
625
+ };
626
+ case "stochastic":
627
+ if (!parts || !parts[0] || !parts[1])
628
+ throw new Error(`Stoch requires 2 params: ${indicator}`);
629
+ return {
630
+ base,
631
+ params: {
632
+ period: parseInt(parts[0]),
633
+ signalPeriod: parseInt(parts[1])
634
+ }
635
+ };
636
+ case "adx":
637
+ if (!parts || !parts[0])
638
+ throw new Error(`Period is required for ${base}`);
639
+ return { base, params: { period: parseInt(parts[0]) } };
640
+ case "roc":
641
+ case "wma":
642
+ if (!parts || !parts[0])
643
+ throw new Error(`Period is required for ${base}`);
644
+ return { base, params: { period: parseInt(parts[0]) } };
645
+ case "stochasticrsi":
646
+ if (!parts || !parts[0] || !parts[1] || !parts[2] || !parts[3])
647
+ throw new Error(`Stochastic RSI requires 4 params: ${indicator}`);
648
+ return {
649
+ base,
650
+ params: {
651
+ rsiPeriod: parseInt(parts[0]),
652
+ stochasticPeriod: parseInt(parts[1]),
653
+ kPeriod: parseInt(parts[2]),
654
+ dPeriod: parseInt(parts[3])
655
+ }
656
+ };
657
+ case "psar":
658
+ if (!parts || !parts[0] || !parts[1])
659
+ throw new Error(`Parabolic SAR requires 2 params: ${indicator}`);
660
+ return {
661
+ base,
662
+ params: {
663
+ step: parseFloat(parts[0]),
664
+ max: parseFloat(parts[1])
665
+ }
666
+ };
667
+ case "keltnerchannels":
668
+ if (!parts || !parts[0] || !parts[1] || !parts[2])
669
+ throw new Error(`Keltner Channels requires 3 params: ${indicator}`);
670
+ return {
671
+ base,
672
+ params: {
673
+ maPeriod: parseInt(parts[0]),
674
+ multiplier: parseFloat(parts[1]),
675
+ atrPeriod: parseInt(parts[2])
676
+ }
677
+ };
678
+ case "vwap":
679
+ return { base, params: {} };
680
+ default:
681
+ throw new Error(`Unknown indicator: ${base}`);
682
+ }
683
+ }
684
+ };
685
+
686
+ export { TechnicalAnalysisService, availableComponents, calculateADX, calculateATR, calculateBollingerBands, calculateCCI, calculateEMA, calculateKeltnerChannels, calculateMACD, calculateMFI, calculateOBV, calculateParabolicSAR, calculateROC, calculateRSI, calculateSMA, calculateStochastic, calculateStochasticRSI, calculateVWAP, calculateWMA, calculateWilliamsR, supportedIndicators };
687
+ //# sourceMappingURL=index.js.map
688
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,41 @@
1
+ import { TAParams } from "../types";
2
+ export declare const supportedIndicators: readonly ["rsi", "ema", "sma", "atr", "macd", "bb", "wma", "adx", "stochastic", "obv", "cci", "mfi", "vwap", "williamsr", "stochasticrsi", "roc", "psar", "keltnerchannels"];
3
+ export declare const availableComponents: {
4
+ rsi: string[];
5
+ ema: string[];
6
+ sma: string[];
7
+ atr: string[];
8
+ macd: string[];
9
+ bb: string[];
10
+ wma: string[];
11
+ adx: string[];
12
+ stochastic: string[];
13
+ obv: string[];
14
+ cci: string[];
15
+ mfi: string[];
16
+ vwap: string[];
17
+ williamsr: string[];
18
+ stochasticrsi: string[];
19
+ roc: string[];
20
+ psar: string[];
21
+ keltnerchannels: string[];
22
+ };
23
+ export declare function calculateRSI(closes: number[], params: TAParams): number;
24
+ export declare function calculateEMA(closes: number[], params: TAParams): number;
25
+ export declare function calculateSMA(closes: number[], params: TAParams): number;
26
+ export declare function calculateATR(highs: number[], lows: number[], closes: number[], params: TAParams): number;
27
+ export declare function calculateMACD(closes: number[], params: TAParams): number[];
28
+ export declare function calculateBollingerBands(closes: number[], params: TAParams): number[];
29
+ export declare function calculateADX(highs: number[], lows: number[], closes: number[], params: TAParams): number;
30
+ export declare function calculateStochastic(highs: number[], lows: number[], closes: number[], params: TAParams): number[];
31
+ export declare function calculateOBV(closes: number[], volumes: number[], params: TAParams): number;
32
+ export declare function calculateCCI(highs: number[], lows: number[], closes: number[], params: TAParams): number;
33
+ export declare function calculateMFI(highs: number[], lows: number[], closes: number[], volumes: number[], params: TAParams): number;
34
+ export declare function calculateVWAP(highs: number[], lows: number[], closes: number[], volumes: number[], params: TAParams): number;
35
+ export declare function calculateWilliamsR(highs: number[], lows: number[], closes: number[], params: TAParams): number;
36
+ export declare function calculateStochasticRSI(closes: number[], params: TAParams): number[];
37
+ export declare function calculateROC(closes: number[], params: TAParams): number;
38
+ export declare function calculateParabolicSAR(highs: number[], lows: number[], params: TAParams): number;
39
+ export declare function calculateKeltnerChannels(highs: number[], lows: number[], closes: number[], params: TAParams): number[];
40
+ export declare function calculateWMA(closes: number[], params: TAParams): number;
41
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,11 @@
1
+ export interface TAParams extends Record<string, any> {
2
+ }
3
+ export interface OHLCV {
4
+ timestamp: number;
5
+ open: number;
6
+ high: number;
7
+ low: number;
8
+ close: number;
9
+ volume: number;
10
+ }
11
+ //# sourceMappingURL=types.d.ts.map
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@vesper85/technical-indicators",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Technical indicators library for financial analysis",
6
+ "keywords": [
7
+ "technical-indicators",
8
+ "trading",
9
+ "finance",
10
+ "indicators",
11
+ "ta",
12
+ "technical-analysis"
13
+ ],
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "CHANGELOG.md",
27
+ "LICENSE",
28
+ "!**/*.map"
29
+ ],
30
+ "sideEffects": false,
31
+ "license": "MIT",
32
+ "scripts": {
33
+ "build": "tsup && npm run types",
34
+ "types": "tsc --emitDeclarationOnly --declaration --outDir dist",
35
+ "build:watch": "tsup --watch",
36
+ "dev": "tsup --watch",
37
+ "lint": "eslint \"src/**/*.ts*\"",
38
+ "lint:fix": "eslint \"src/**/*.ts*\" --fix",
39
+ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
40
+ "type-check": "tsc --noEmit",
41
+ "test:service": "tsx test-ta-service.ts",
42
+ "prepublishOnly": "npm run build",
43
+ "changeset": "changeset",
44
+ "version": "changeset version",
45
+ "release": "npm run build && changeset publish"
46
+ },
47
+ "devDependencies": {
48
+ "@changesets/cli": "^2.29.8",
49
+ "@osiris-ai/eslint-config": "workspace:*",
50
+ "@osiris-ai/typescript-config": "workspace:*",
51
+ "@types/node": "^24.0.8",
52
+ "eslint": "^8.37.0",
53
+ "tsup": "^8.5.0",
54
+ "tsx": "^4.19.2",
55
+ "typescript": "^5.8.3"
56
+ },
57
+ "dependencies": {
58
+ "@osiris-ai/logger": "workspace:*",
59
+ "technicalindicators": "^3.1.0"
60
+ },
61
+ "engines": {
62
+ "node": ">=18.0.0"
63
+ },
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "https://github.com/FetcchX/legion-sdk.git"
67
+ },
68
+ "bugs": {
69
+ "url": "https://github.com/FetcchX/legion-sdk/issues"
70
+ },
71
+ "homepage": "https://github.com/FetcchX/legion-sdk#readme"
72
+ }