@vulcan-js/indicators 0.0.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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/index.d.ts +485 -0
- package/dist/index.js +683 -0
- package/dist/index.js.map +1 -0
- package/package.json +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { assert, createSignal } from "@vulcan-js/core";
|
|
2
|
+
import { abs, add, div, divide, eq, equal, from, gt, lt, mul, multiply, sub, subtract } from "dnum";
|
|
3
|
+
|
|
4
|
+
//#region src/trend/exponentialMovingAverage.ts
|
|
5
|
+
const defaultExponentialMovingAverageOptions = { period: 12 };
|
|
6
|
+
/**
|
|
7
|
+
* Exponential Moving Average (EMA)
|
|
8
|
+
*
|
|
9
|
+
* EMA = Price * k + PrevEMA * (1 - k)
|
|
10
|
+
* Where k = 2 / (period + 1)
|
|
11
|
+
*/
|
|
12
|
+
const ema = createSignal(({ period }) => {
|
|
13
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
14
|
+
const k = divide(from(2, 18), from(1 + period, 18), 18);
|
|
15
|
+
const m = subtract(from(1, 18), k);
|
|
16
|
+
let prev;
|
|
17
|
+
return (value) => {
|
|
18
|
+
if (prev === void 0) {
|
|
19
|
+
prev = from(value, 18);
|
|
20
|
+
return prev;
|
|
21
|
+
}
|
|
22
|
+
prev = add(mul(value, k, 18), mul(prev, m, 18));
|
|
23
|
+
return prev;
|
|
24
|
+
};
|
|
25
|
+
}, defaultExponentialMovingAverageOptions);
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/momentum/absolutePriceOscillator.ts
|
|
29
|
+
const defaultAbsolutePriceOscillatorOptions = {
|
|
30
|
+
fastPeriod: 12,
|
|
31
|
+
slowPeriod: 26
|
|
32
|
+
};
|
|
33
|
+
const apo = createSignal(({ fastPeriod, slowPeriod }) => {
|
|
34
|
+
assert(Number.isInteger(fastPeriod) && fastPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected fastPeriod to be a positive integer, got ${fastPeriod}`));
|
|
35
|
+
assert(Number.isInteger(slowPeriod) && slowPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected slowPeriod to be a positive integer, got ${slowPeriod}`));
|
|
36
|
+
const fastProc = ema.create({ period: fastPeriod });
|
|
37
|
+
const slowProc = ema.create({ period: slowPeriod });
|
|
38
|
+
return (value) => sub(fastProc(value), slowProc(value));
|
|
39
|
+
}, defaultAbsolutePriceOscillatorOptions);
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/trend/simpleMovingAverage.ts
|
|
43
|
+
const defaultSMAOptions = { period: 2 };
|
|
44
|
+
/**
|
|
45
|
+
* Simple Moving Average (SMA)
|
|
46
|
+
*
|
|
47
|
+
* Calculates the arithmetic mean of a set of values over a specified period.
|
|
48
|
+
* The SMA is calculated by summing all values in the period and dividing by the period length.
|
|
49
|
+
*
|
|
50
|
+
* Formula: SMA = (P1 + P2 + ... + Pn) / n
|
|
51
|
+
* Where: P = Price values, n = Period
|
|
52
|
+
*
|
|
53
|
+
* @param source - Iterable of price values
|
|
54
|
+
* @param options - Configuration options
|
|
55
|
+
* @param options.period - The period for calculating the moving average (default: 2)
|
|
56
|
+
* @returns Generator yielding SMA values
|
|
57
|
+
*/
|
|
58
|
+
const sma = createSignal(({ period }) => {
|
|
59
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
60
|
+
const buffer = Array.from({ length: period });
|
|
61
|
+
let head = 0;
|
|
62
|
+
let count = 0;
|
|
63
|
+
let runningSum = from(0, 18);
|
|
64
|
+
return (value) => {
|
|
65
|
+
const v = from(value, 18);
|
|
66
|
+
if (count < period) {
|
|
67
|
+
buffer[count] = v;
|
|
68
|
+
runningSum = add(runningSum, v);
|
|
69
|
+
count++;
|
|
70
|
+
} else {
|
|
71
|
+
runningSum = subtract(runningSum, buffer[head]);
|
|
72
|
+
runningSum = add(runningSum, v);
|
|
73
|
+
buffer[head] = v;
|
|
74
|
+
head = (head + 1) % period;
|
|
75
|
+
}
|
|
76
|
+
return div(runningSum, count, 18);
|
|
77
|
+
};
|
|
78
|
+
}, defaultSMAOptions);
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/momentum/awesomeOscillator.ts
|
|
82
|
+
const defaultAwesomeOscillatorOptions = {
|
|
83
|
+
fastPeriod: 5,
|
|
84
|
+
slowPeriod: 34
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Awesome Oscillator (AO)
|
|
88
|
+
*
|
|
89
|
+
* AO = SMA(median, fastPeriod) - SMA(median, slowPeriod)
|
|
90
|
+
* Where median = (high + low) / 2
|
|
91
|
+
*/
|
|
92
|
+
const ao = createSignal(({ fastPeriod, slowPeriod }) => {
|
|
93
|
+
assert(Number.isInteger(fastPeriod) && fastPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected fastPeriod to be a positive integer, got ${fastPeriod}`));
|
|
94
|
+
assert(Number.isInteger(slowPeriod) && slowPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected slowPeriod to be a positive integer, got ${slowPeriod}`));
|
|
95
|
+
const fastProc = sma.create({ period: fastPeriod });
|
|
96
|
+
const slowProc = sma.create({ period: slowPeriod });
|
|
97
|
+
return (bar) => {
|
|
98
|
+
const median = div(add(from(bar.h, 18), from(bar.l, 18)), 2, 18);
|
|
99
|
+
return sub(fastProc(median), slowProc(median));
|
|
100
|
+
};
|
|
101
|
+
}, defaultAwesomeOscillatorOptions);
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/volume/accumulationDistribution.ts
|
|
105
|
+
/**
|
|
106
|
+
* Accumulation/Distribution Indicator (A/D). Cumulative indicator
|
|
107
|
+
* that uses volume and price to assess whether a stock is
|
|
108
|
+
* being accumulated or distributed.
|
|
109
|
+
*
|
|
110
|
+
* MFM = ((Closing - Low) - (High - Closing)) / (High - Low)
|
|
111
|
+
* MFV = MFM * Period Volume
|
|
112
|
+
* AD = Previous AD + CMFV
|
|
113
|
+
*/
|
|
114
|
+
const ad = createSignal(() => {
|
|
115
|
+
let prevAD = from(0, 18);
|
|
116
|
+
return (bar) => {
|
|
117
|
+
const h = from(bar.h, 18);
|
|
118
|
+
const l = from(bar.l, 18);
|
|
119
|
+
const c = from(bar.c, 18);
|
|
120
|
+
const v = from(bar.v, 18);
|
|
121
|
+
const range = subtract(h, l);
|
|
122
|
+
prevAD = add(multiply(equal(range, 0) ? from(0, 18) : divide(subtract(subtract(c, l), subtract(h, c)), range, 18), v), prevAD);
|
|
123
|
+
return prevAD;
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/momentum/chaikinOscillator.ts
|
|
129
|
+
const defaultChaikinOscillatorOptions = {
|
|
130
|
+
fastPeriod: 3,
|
|
131
|
+
slowPeriod: 10
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* The ChaikinOscillator function measures the momentum of the
|
|
135
|
+
* Accumulation/Distribution (A/D) using the Moving Average
|
|
136
|
+
* Convergence Divergence (MACD) formula. It takes the
|
|
137
|
+
* difference between fast and slow periods EMA of the A/D.
|
|
138
|
+
* Cross above the A/D line indicates bullish.
|
|
139
|
+
*
|
|
140
|
+
* CO = Ema(fastPeriod, AD) - Ema(slowPeriod, AD)
|
|
141
|
+
*/
|
|
142
|
+
const cmo = createSignal(({ fastPeriod, slowPeriod }) => {
|
|
143
|
+
assert(Number.isInteger(fastPeriod) && fastPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected fastPeriod to be a positive integer, got ${fastPeriod}`));
|
|
144
|
+
assert(Number.isInteger(slowPeriod) && slowPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected slowPeriod to be a positive integer, got ${slowPeriod}`));
|
|
145
|
+
const adProc = ad.create();
|
|
146
|
+
const fastProc = ema.create({ period: fastPeriod });
|
|
147
|
+
const slowProc = ema.create({ period: slowPeriod });
|
|
148
|
+
return (bar) => {
|
|
149
|
+
const adVal = adProc(bar);
|
|
150
|
+
return sub(fastProc(adVal), slowProc(adVal));
|
|
151
|
+
};
|
|
152
|
+
}, defaultChaikinOscillatorOptions);
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/trend/movingMax.ts
|
|
156
|
+
const defaultMovingMaxOptions = { period: 4 };
|
|
157
|
+
/**
|
|
158
|
+
* Moving Maximum (MovingMax)
|
|
159
|
+
*/
|
|
160
|
+
const mmax = createSignal(({ period }) => {
|
|
161
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
162
|
+
const buffer = [];
|
|
163
|
+
return (value) => {
|
|
164
|
+
buffer.push(from(value, 18));
|
|
165
|
+
if (buffer.length > period) buffer.shift();
|
|
166
|
+
return buffer.reduce((max, cur) => gt(max, cur) ? max : cur);
|
|
167
|
+
};
|
|
168
|
+
}, defaultMovingMaxOptions);
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/trend/movingMin.ts
|
|
172
|
+
const defaultMovingMinOptions = { period: 4 };
|
|
173
|
+
/**
|
|
174
|
+
* Moving Minimum (MovingMin)
|
|
175
|
+
*/
|
|
176
|
+
const mmin = createSignal(({ period }) => {
|
|
177
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
178
|
+
const buffer = [];
|
|
179
|
+
return (value) => {
|
|
180
|
+
buffer.push(from(value, 18));
|
|
181
|
+
if (buffer.length > period) buffer.shift();
|
|
182
|
+
return buffer.reduce((min, cur) => lt(min, cur) ? min : cur);
|
|
183
|
+
};
|
|
184
|
+
}, defaultMovingMinOptions);
|
|
185
|
+
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/momentum/ichimokuCloud.ts
|
|
188
|
+
const defaultIchimokuCloudOptions = {
|
|
189
|
+
conversionPeriod: 9,
|
|
190
|
+
basePeriod: 26,
|
|
191
|
+
leadingBPeriod: 52
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Ichimoku Cloud (Ichimoku Kinko Hyo)
|
|
195
|
+
*
|
|
196
|
+
* Computes raw values for each component. Displacement (shifting
|
|
197
|
+
* Leading Spans forward and Lagging Span backward on the chart)
|
|
198
|
+
* is a presentation concern left to the consumer.
|
|
199
|
+
*
|
|
200
|
+
* - Conversion (Tenkan-sen): (highest high + lowest low) / 2 over conversionPeriod
|
|
201
|
+
* - Base (Kijun-sen): (highest high + lowest low) / 2 over basePeriod
|
|
202
|
+
* - Leading Span A (Senkou A): (conversion + base) / 2
|
|
203
|
+
* - Leading Span B (Senkou B): (highest high + lowest low) / 2 over leadingBPeriod
|
|
204
|
+
* - Lagging (Chikou): current close price
|
|
205
|
+
*/
|
|
206
|
+
const ichimokuCloud = createSignal(({ conversionPeriod, basePeriod, leadingBPeriod }) => {
|
|
207
|
+
assert(Number.isInteger(conversionPeriod) && conversionPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected conversionPeriod to be a positive integer, got ${conversionPeriod}`));
|
|
208
|
+
assert(Number.isInteger(basePeriod) && basePeriod >= 1, /* @__PURE__ */ new RangeError(`Expected basePeriod to be a positive integer, got ${basePeriod}`));
|
|
209
|
+
assert(Number.isInteger(leadingBPeriod) && leadingBPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected leadingBPeriod to be a positive integer, got ${leadingBPeriod}`));
|
|
210
|
+
const convHighProc = mmax.create({ period: conversionPeriod });
|
|
211
|
+
const convLowProc = mmin.create({ period: conversionPeriod });
|
|
212
|
+
const baseHighProc = mmax.create({ period: basePeriod });
|
|
213
|
+
const baseLowProc = mmin.create({ period: basePeriod });
|
|
214
|
+
const leadBHighProc = mmax.create({ period: leadingBPeriod });
|
|
215
|
+
const leadBLowProc = mmin.create({ period: leadingBPeriod });
|
|
216
|
+
return (bar) => {
|
|
217
|
+
const h = from(bar.h, 18);
|
|
218
|
+
const l = from(bar.l, 18);
|
|
219
|
+
const conversion = div(add(convHighProc(h), convLowProc(l)), 2, 18);
|
|
220
|
+
const base = div(add(baseHighProc(h), baseLowProc(l)), 2, 18);
|
|
221
|
+
return {
|
|
222
|
+
conversion,
|
|
223
|
+
base,
|
|
224
|
+
leadingA: div(add(conversion, base), 2, 18),
|
|
225
|
+
leadingB: div(add(leadBHighProc(h), leadBLowProc(l)), 2, 18),
|
|
226
|
+
lagging: from(bar.c, 18)
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
}, defaultIchimokuCloudOptions);
|
|
230
|
+
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/momentum/percentagePriceOscillator.ts
|
|
233
|
+
const defaultPercentagePriceOscillatorOptions = {
|
|
234
|
+
fastPeriod: 12,
|
|
235
|
+
slowPeriod: 26,
|
|
236
|
+
signalPeriod: 9
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* Percentage Price Oscillator (PPO)
|
|
240
|
+
*
|
|
241
|
+
* The Percentage Price Oscillator (PPO) is a momentum oscillator that measures the difference
|
|
242
|
+
* between two moving averages as a percentage of the slower moving average. It consists of three components:
|
|
243
|
+
* - PPO Line: ((Fast EMA - Slow EMA) / Slow EMA) * 100
|
|
244
|
+
* - Signal Line: EMA of the PPO line
|
|
245
|
+
* - Histogram: PPO - Signal
|
|
246
|
+
*
|
|
247
|
+
* Formula:
|
|
248
|
+
* - PPO = ((EMA(fastPeriod, prices) - EMA(slowPeriod, prices)) / EMA(slowPeriod, prices)) * 100
|
|
249
|
+
* - Signal = EMA(signalPeriod, PPO)
|
|
250
|
+
* - Histogram = PPO - Signal
|
|
251
|
+
*
|
|
252
|
+
* @param source - Iterable of price values
|
|
253
|
+
* @param options - Configuration options
|
|
254
|
+
* @param options.fastPeriod - Period for the fast EMA (default: 12)
|
|
255
|
+
* @param options.slowPeriod - Period for the slow EMA (default: 26)
|
|
256
|
+
* @param options.signalPeriod - Period for the signal EMA (default: 9)
|
|
257
|
+
* @returns Generator yielding PPO point objects
|
|
258
|
+
*/
|
|
259
|
+
const ppo = createSignal(({ fastPeriod, slowPeriod, signalPeriod }) => {
|
|
260
|
+
assert(Number.isInteger(fastPeriod) && fastPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected fastPeriod to be a positive integer, got ${fastPeriod}`));
|
|
261
|
+
assert(Number.isInteger(slowPeriod) && slowPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected slowPeriod to be a positive integer, got ${slowPeriod}`));
|
|
262
|
+
assert(Number.isInteger(signalPeriod) && signalPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected signalPeriod to be a positive integer, got ${signalPeriod}`));
|
|
263
|
+
const fastProc = ema.create({ period: fastPeriod });
|
|
264
|
+
const slowProc = ema.create({ period: slowPeriod });
|
|
265
|
+
const signalProc = ema.create({ period: signalPeriod });
|
|
266
|
+
return (value) => {
|
|
267
|
+
const fast = fastProc(from(value, 18));
|
|
268
|
+
const slow = slowProc(from(value, 18));
|
|
269
|
+
const ppoVal = eq(slow, 0) ? from(0, 18) : mul(div(sub(fast, slow), slow, 18), 100, 18);
|
|
270
|
+
const sig = signalProc(ppoVal);
|
|
271
|
+
return {
|
|
272
|
+
ppo: ppoVal,
|
|
273
|
+
signal: sig,
|
|
274
|
+
histogram: sub(ppoVal, sig)
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
}, defaultPercentagePriceOscillatorOptions);
|
|
278
|
+
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/trend/rollingMovingAverage.ts
|
|
281
|
+
const defaultRMAOptions = { period: 4 };
|
|
282
|
+
/**
|
|
283
|
+
* Rolling moving average (RMA).
|
|
284
|
+
*
|
|
285
|
+
* R[0] to R[p-1] is SMA(values)
|
|
286
|
+
*
|
|
287
|
+
* R[p] and after is R[i] = ((R[i-1]*(p-1)) + v[i]) / p
|
|
288
|
+
*/
|
|
289
|
+
const rma = createSignal(({ period }) => {
|
|
290
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
291
|
+
let count = 0;
|
|
292
|
+
let sum = from(0, 18);
|
|
293
|
+
let prev = from(0, 18);
|
|
294
|
+
return (value) => {
|
|
295
|
+
if (count < period) {
|
|
296
|
+
sum = add(sum, value);
|
|
297
|
+
count++;
|
|
298
|
+
prev = div(sum, count, 18);
|
|
299
|
+
return prev;
|
|
300
|
+
}
|
|
301
|
+
prev = div(add(mul(prev, period - 1, 18), value), period, 18);
|
|
302
|
+
return prev;
|
|
303
|
+
};
|
|
304
|
+
}, defaultRMAOptions);
|
|
305
|
+
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/momentum/relativeStrengthIndex.ts
|
|
308
|
+
const defaultRSIOptions = { period: 14 };
|
|
309
|
+
/**
|
|
310
|
+
* Relative Strength Index (RSI). It is a momentum indicator that measures the magnitude of
|
|
311
|
+
* recent price changes to evaluate overbought and oversold conditions
|
|
312
|
+
* using the given window period.
|
|
313
|
+
*
|
|
314
|
+
* RS = Average Gain / Average Loss
|
|
315
|
+
*
|
|
316
|
+
* RSI = 100 - (100 / (1 + RS))
|
|
317
|
+
*/
|
|
318
|
+
const rsi = createSignal(({ period }) => {
|
|
319
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
320
|
+
const gainProc = rma.create({ period });
|
|
321
|
+
const lossProc = rma.create({ period });
|
|
322
|
+
let prev;
|
|
323
|
+
return (value) => {
|
|
324
|
+
const price = from(value, 18);
|
|
325
|
+
if (prev === void 0) {
|
|
326
|
+
prev = price;
|
|
327
|
+
gainProc(from(0, 18));
|
|
328
|
+
lossProc(from(0, 18));
|
|
329
|
+
return from(0, 18);
|
|
330
|
+
}
|
|
331
|
+
const change = sub(price, prev);
|
|
332
|
+
prev = price;
|
|
333
|
+
const gain = gt(change, 0) ? change : from(0, 18);
|
|
334
|
+
const loss = gt(change, 0) ? from(0, 18) : mul(change, -1, 18);
|
|
335
|
+
const avgGain = gainProc(gain);
|
|
336
|
+
const avgLoss = lossProc(loss);
|
|
337
|
+
if (eq(avgLoss, 0)) return from(100, 18);
|
|
338
|
+
return sub(100, div(100, add(1, div(avgGain, avgLoss, 18)), 18));
|
|
339
|
+
};
|
|
340
|
+
}, defaultRSIOptions);
|
|
341
|
+
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/momentum/stochasticOscillator.ts
|
|
344
|
+
const defaultStochasticOscillatorOptions = {
|
|
345
|
+
kPeriod: 14,
|
|
346
|
+
slowingPeriod: 1,
|
|
347
|
+
dPeriod: 3
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Stochastic Oscillator
|
|
351
|
+
*
|
|
352
|
+
* %K = ((Close - Lowest Low) / (Highest High - Lowest Low)) * 100
|
|
353
|
+
* %D = SMA(%K, dPeriod)
|
|
354
|
+
*/
|
|
355
|
+
const stoch = createSignal(({ kPeriod, slowingPeriod, dPeriod }) => {
|
|
356
|
+
assert(Number.isInteger(kPeriod) && kPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected kPeriod to be a positive integer, got ${kPeriod}`));
|
|
357
|
+
assert(Number.isInteger(slowingPeriod) && slowingPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected slowingPeriod to be a positive integer, got ${slowingPeriod}`));
|
|
358
|
+
assert(Number.isInteger(dPeriod) && dPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected dPeriod to be a positive integer, got ${dPeriod}`));
|
|
359
|
+
const mmaxProc = mmax.create({ period: kPeriod });
|
|
360
|
+
const mminProc = mmin.create({ period: kPeriod });
|
|
361
|
+
const slowingProc = slowingPeriod > 1 ? sma.create({ period: slowingPeriod }) : null;
|
|
362
|
+
const dProc = sma.create({ period: dPeriod });
|
|
363
|
+
return (bar) => {
|
|
364
|
+
const h = from(bar.h, 18);
|
|
365
|
+
const l = from(bar.l, 18);
|
|
366
|
+
const c = from(bar.c, 18);
|
|
367
|
+
const highestHigh = mmaxProc(h);
|
|
368
|
+
const lowestLow = mminProc(l);
|
|
369
|
+
const range = sub(highestHigh, lowestLow, 18);
|
|
370
|
+
const rawK = eq(range, 0) ? from(0, 18) : mul(div(sub(c, lowestLow, 18), range, 18), 100, 18);
|
|
371
|
+
const k = slowingProc ? slowingProc(rawK) : rawK;
|
|
372
|
+
return {
|
|
373
|
+
k,
|
|
374
|
+
d: dProc(k)
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
}, defaultStochasticOscillatorOptions);
|
|
378
|
+
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/trend/aroon.ts
|
|
381
|
+
const defaultAroonOptions = { period: 25 };
|
|
382
|
+
/**
|
|
383
|
+
* Aroon Indicator
|
|
384
|
+
*
|
|
385
|
+
* Aroon Up = ((period - days since highest high) / period) * 100
|
|
386
|
+
* Aroon Down = ((period - days since lowest low) / period) * 100
|
|
387
|
+
* Oscillator = Aroon Up - Aroon Down
|
|
388
|
+
*/
|
|
389
|
+
const aroon = createSignal(({ period }) => {
|
|
390
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
391
|
+
const highBuffer = [];
|
|
392
|
+
const lowBuffer = [];
|
|
393
|
+
return (bar) => {
|
|
394
|
+
const h = from(bar.h, 18);
|
|
395
|
+
const l = from(bar.l, 18);
|
|
396
|
+
highBuffer.push(h);
|
|
397
|
+
lowBuffer.push(l);
|
|
398
|
+
if (highBuffer.length > period + 1) highBuffer.shift();
|
|
399
|
+
if (lowBuffer.length > period + 1) lowBuffer.shift();
|
|
400
|
+
let highestIdx = 0;
|
|
401
|
+
let lowestIdx = 0;
|
|
402
|
+
for (let j = 1; j < highBuffer.length; j++) {
|
|
403
|
+
if (!gt(highBuffer[highestIdx], highBuffer[j])) highestIdx = j;
|
|
404
|
+
if (!lt(lowBuffer[lowestIdx], lowBuffer[j])) lowestIdx = j;
|
|
405
|
+
}
|
|
406
|
+
const daysSinceHigh = highBuffer.length - 1 - highestIdx;
|
|
407
|
+
const daysSinceLow = lowBuffer.length - 1 - lowestIdx;
|
|
408
|
+
const periodDnum = from(period, 18);
|
|
409
|
+
const up = divide(multiply(from(period - daysSinceHigh, 18), 100, 18), periodDnum, 18);
|
|
410
|
+
const down = divide(multiply(from(period - daysSinceLow, 18), 100, 18), periodDnum, 18);
|
|
411
|
+
return {
|
|
412
|
+
up,
|
|
413
|
+
down,
|
|
414
|
+
oscillator: subtract(up, down)
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
}, defaultAroonOptions);
|
|
418
|
+
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/trend/balanceOfPower.ts
|
|
421
|
+
const bop = createSignal(() => {
|
|
422
|
+
return (bar) => {
|
|
423
|
+
const o = from(bar.o, 18);
|
|
424
|
+
const h = from(bar.h, 18);
|
|
425
|
+
const l = from(bar.l, 18);
|
|
426
|
+
const c = from(bar.c, 18);
|
|
427
|
+
const range = subtract(h, l);
|
|
428
|
+
if (equal(range, 0)) return from(0, 18);
|
|
429
|
+
return divide(subtract(c, o), range, 18);
|
|
430
|
+
};
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/trend/chandeForecastOscillator.ts
|
|
435
|
+
const defaultCFOOptions = { period: 14 };
|
|
436
|
+
/**
|
|
437
|
+
* Chande Forecast Oscillator (CFO)
|
|
438
|
+
*
|
|
439
|
+
* Measures the percentage difference between the actual close price and the
|
|
440
|
+
* n-period linear regression forecast price. Positive values indicate bullish
|
|
441
|
+
* momentum (price above forecast), negative values indicate bearish momentum.
|
|
442
|
+
*
|
|
443
|
+
* Formula: CFO = ((Close - Forecast) / Close) * 100
|
|
444
|
+
* Where: Forecast = Linear regression value at current point
|
|
445
|
+
*
|
|
446
|
+
* @param source - Iterable of price values
|
|
447
|
+
* @param options - Configuration options
|
|
448
|
+
* @param options.period - The period for linear regression (default: 14)
|
|
449
|
+
* @returns Generator yielding CFO values as percentages
|
|
450
|
+
*/
|
|
451
|
+
const cfo = createSignal(({ period }) => {
|
|
452
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
453
|
+
const buffer = [];
|
|
454
|
+
return (value) => {
|
|
455
|
+
buffer.push(from(value, 18));
|
|
456
|
+
if (buffer.length > period) buffer.shift();
|
|
457
|
+
const n = buffer.length;
|
|
458
|
+
if (n < 2) return from(0, 18);
|
|
459
|
+
const xSum = n * (n + 1) / 2;
|
|
460
|
+
const denom = n * (n * (n + 1) * (2 * n + 1) / 6) - xSum * xSum;
|
|
461
|
+
let sumY = from(0, 18);
|
|
462
|
+
let sumXY = from(0, 18);
|
|
463
|
+
for (let i = 0; i < n; i++) {
|
|
464
|
+
sumY = add(sumY, buffer[i]);
|
|
465
|
+
sumXY = add(sumXY, multiply(buffer[i], i + 1));
|
|
466
|
+
}
|
|
467
|
+
const slope = divide(subtract(multiply(sumXY, n), multiply(sumY, xSum)), denom, 18);
|
|
468
|
+
const intercept = divide(subtract(sumY, multiply(slope, xSum)), n, 18);
|
|
469
|
+
const forecast = add(multiply(slope, n), intercept);
|
|
470
|
+
const close = buffer[n - 1];
|
|
471
|
+
if (equal(close, 0)) return from(0, 18);
|
|
472
|
+
return divide(multiply(subtract(close, forecast), 100), close, 18);
|
|
473
|
+
};
|
|
474
|
+
}, defaultCFOOptions);
|
|
475
|
+
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/trend/commodityChannelIndex.ts
|
|
478
|
+
const defaultCCIOptions = { period: 20 };
|
|
479
|
+
/**
|
|
480
|
+
* Commodity Channel Index (CCI)
|
|
481
|
+
*
|
|
482
|
+
* Developed by Donald Lambert in 1980, CCI measures the deviation of the
|
|
483
|
+
* typical price from its simple moving average, normalized by mean deviation.
|
|
484
|
+
* The constant 0.015 ensures approximately 70-80% of CCI values fall between
|
|
485
|
+
* +100 and -100.
|
|
486
|
+
*
|
|
487
|
+
* Formula:
|
|
488
|
+
* TP = (High + Low + Close) / 3
|
|
489
|
+
* CCI = (TP - SMA(TP, period)) / (0.015 * Mean Deviation)
|
|
490
|
+
* Mean Deviation = SUM(|TP_i - SMA|) / period
|
|
491
|
+
*
|
|
492
|
+
* @param source - Iterable of OHLC candle data
|
|
493
|
+
* @param options - Configuration options
|
|
494
|
+
* @param options.period - The period for CCI calculation (default: 20)
|
|
495
|
+
* @returns Generator yielding CCI values
|
|
496
|
+
*/
|
|
497
|
+
const cci = createSignal(({ period }) => {
|
|
498
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
499
|
+
const buffer = [];
|
|
500
|
+
return (bar) => {
|
|
501
|
+
const h = from(bar.h, 18);
|
|
502
|
+
const l = from(bar.l, 18);
|
|
503
|
+
const c = from(bar.c, 18);
|
|
504
|
+
const tp = divide(add(add(h, l), c), 3, 18);
|
|
505
|
+
buffer.push(tp);
|
|
506
|
+
if (buffer.length > period) buffer.shift();
|
|
507
|
+
const n = buffer.length;
|
|
508
|
+
if (n < period) return from(0, 18);
|
|
509
|
+
let sum = from(0, 18);
|
|
510
|
+
for (const v of buffer) sum = add(sum, v);
|
|
511
|
+
const smaVal = divide(sum, n, 18);
|
|
512
|
+
let devSum = from(0, 18);
|
|
513
|
+
for (const v of buffer) devSum = add(devSum, abs(subtract(v, smaVal)));
|
|
514
|
+
const meanDev = divide(devSum, n, 18);
|
|
515
|
+
if (equal(meanDev, 0)) return from(0, 18);
|
|
516
|
+
const currentTP = buffer[n - 1];
|
|
517
|
+
return divide(subtract(currentTP, smaVal), divide([meanDev[0] * 15n, meanDev[1]], [1000n, 0], 18), 18);
|
|
518
|
+
};
|
|
519
|
+
}, defaultCCIOptions);
|
|
520
|
+
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/trend/doubleExponentialMovingAverage.ts
|
|
523
|
+
const defaultDoubleExponentialMovingAverageOptions = { period: 12 };
|
|
524
|
+
/**
|
|
525
|
+
* Double Exponential Moving Average (DEMA)
|
|
526
|
+
*
|
|
527
|
+
* DEMA reduces lag compared to a traditional EMA by applying the formula:
|
|
528
|
+
* DEMA = 2 * EMA(data, period) - EMA(EMA(data, period), period)
|
|
529
|
+
*
|
|
530
|
+
* @param source - Iterable of input values
|
|
531
|
+
* @param options - Configuration options
|
|
532
|
+
* @param options.period - The lookback period (default: 12)
|
|
533
|
+
* @returns Generator yielding DEMA values
|
|
534
|
+
*/
|
|
535
|
+
const dema = createSignal(({ period }) => {
|
|
536
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
537
|
+
const ema1 = ema.create({ period });
|
|
538
|
+
const ema2 = ema.create({ period });
|
|
539
|
+
return (value) => {
|
|
540
|
+
const e1 = ema1(value);
|
|
541
|
+
const e2 = ema2(e1);
|
|
542
|
+
return sub(mul(e1, 2, 18), e2);
|
|
543
|
+
};
|
|
544
|
+
}, defaultDoubleExponentialMovingAverageOptions);
|
|
545
|
+
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/trend/macd.ts
|
|
548
|
+
const defaultMACDOptions = {
|
|
549
|
+
fastPeriod: 12,
|
|
550
|
+
slowPeriod: 26,
|
|
551
|
+
signalPeriod: 9
|
|
552
|
+
};
|
|
553
|
+
/**
|
|
554
|
+
* Moving Average Convergence Divergence (MACD)
|
|
555
|
+
*
|
|
556
|
+
* MACD is a trend-following momentum indicator that shows the relationship
|
|
557
|
+
* between two exponential moving averages of prices. It consists of three components:
|
|
558
|
+
* - MACD Line: Fast EMA - Slow EMA
|
|
559
|
+
* - Signal Line: EMA of the MACD line
|
|
560
|
+
* - Histogram: MACD - Signal
|
|
561
|
+
*
|
|
562
|
+
* Formula:
|
|
563
|
+
* - MACD = EMA(fastPeriod, prices) - EMA(slowPeriod, prices)
|
|
564
|
+
* - Signal = EMA(signalPeriod, MACD)
|
|
565
|
+
* - Histogram = MACD - Signal
|
|
566
|
+
*
|
|
567
|
+
* @param source - Iterable of price values
|
|
568
|
+
* @param options - Configuration options
|
|
569
|
+
* @param options.fastPeriod - Period for the fast EMA (default: 12)
|
|
570
|
+
* @param options.slowPeriod - Period for the slow EMA (default: 26)
|
|
571
|
+
* @param options.signalPeriod - Period for the signal EMA (default: 9)
|
|
572
|
+
* @returns Generator yielding MACDPoint objects
|
|
573
|
+
*/
|
|
574
|
+
const macd = createSignal(({ fastPeriod, slowPeriod, signalPeriod }) => {
|
|
575
|
+
assert(Number.isInteger(fastPeriod) && fastPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected fastPeriod to be a positive integer, got ${fastPeriod}`));
|
|
576
|
+
assert(Number.isInteger(slowPeriod) && slowPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected slowPeriod to be a positive integer, got ${slowPeriod}`));
|
|
577
|
+
assert(Number.isInteger(signalPeriod) && signalPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected signalPeriod to be a positive integer, got ${signalPeriod}`));
|
|
578
|
+
const fastProc = ema.create({ period: fastPeriod });
|
|
579
|
+
const slowProc = ema.create({ period: slowPeriod });
|
|
580
|
+
const signalProc = ema.create({ period: signalPeriod });
|
|
581
|
+
return (value) => {
|
|
582
|
+
const m = sub(fastProc(value), slowProc(value));
|
|
583
|
+
const sig = signalProc(m);
|
|
584
|
+
return {
|
|
585
|
+
macd: m,
|
|
586
|
+
signal: sig,
|
|
587
|
+
histogram: sub(m, sig)
|
|
588
|
+
};
|
|
589
|
+
};
|
|
590
|
+
}, defaultMACDOptions);
|
|
591
|
+
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/trend/movingSum.ts
|
|
594
|
+
const defaultMovingSumOptions = { period: 4 };
|
|
595
|
+
/**
|
|
596
|
+
* Moving Sum
|
|
597
|
+
*
|
|
598
|
+
* Calculates the sum of values in a sliding window of the specified period.
|
|
599
|
+
*/
|
|
600
|
+
const msum = createSignal(({ period }) => {
|
|
601
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
602
|
+
const buffer = Array.from({ length: period });
|
|
603
|
+
let head = 0;
|
|
604
|
+
let count = 0;
|
|
605
|
+
let runningSum = from(0, 18);
|
|
606
|
+
return (value) => {
|
|
607
|
+
const v = from(value, 18);
|
|
608
|
+
if (count < period) {
|
|
609
|
+
buffer[count] = v;
|
|
610
|
+
runningSum = add(runningSum, v);
|
|
611
|
+
count++;
|
|
612
|
+
} else {
|
|
613
|
+
runningSum = subtract(runningSum, buffer[head]);
|
|
614
|
+
runningSum = add(runningSum, v);
|
|
615
|
+
buffer[head] = v;
|
|
616
|
+
head = (head + 1) % period;
|
|
617
|
+
}
|
|
618
|
+
return runningSum;
|
|
619
|
+
};
|
|
620
|
+
}, defaultMovingSumOptions);
|
|
621
|
+
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/trend/massIndex.ts
|
|
624
|
+
const defaultMassIndexOptions = {
|
|
625
|
+
emaPeriod: 9,
|
|
626
|
+
miPeriod: 25
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* Mass Index (MI)
|
|
630
|
+
*
|
|
631
|
+
* Developed by Donald Dorsey, the Mass Index uses the high-low range
|
|
632
|
+
* to identify trend reversals based on range expansions. A "reversal bulge"
|
|
633
|
+
* occurs when the Mass Index rises above 27 and then falls below 26.5.
|
|
634
|
+
*
|
|
635
|
+
* Formula:
|
|
636
|
+
* Range = High - Low
|
|
637
|
+
* EMA1 = EMA(Range, emaPeriod)
|
|
638
|
+
* EMA2 = EMA(EMA1, emaPeriod)
|
|
639
|
+
* Ratio = EMA1 / EMA2
|
|
640
|
+
* MI = MovingSum(Ratio, miPeriod)
|
|
641
|
+
*
|
|
642
|
+
* @param source - Iterable of OHLC candle data (requires high and low)
|
|
643
|
+
* @param options - Configuration options
|
|
644
|
+
* @param options.emaPeriod - The EMA smoothing period (default: 9)
|
|
645
|
+
* @param options.miPeriod - The moving sum period (default: 25)
|
|
646
|
+
* @returns Generator yielding Mass Index values
|
|
647
|
+
*/
|
|
648
|
+
const mi = createSignal(({ emaPeriod, miPeriod }) => {
|
|
649
|
+
assert(Number.isInteger(emaPeriod) && emaPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected emaPeriod to be a positive integer, got ${emaPeriod}`));
|
|
650
|
+
assert(Number.isInteger(miPeriod) && miPeriod >= 1, /* @__PURE__ */ new RangeError(`Expected miPeriod to be a positive integer, got ${miPeriod}`));
|
|
651
|
+
const ema1Proc = ema.create({ period: emaPeriod });
|
|
652
|
+
const ema2Proc = ema.create({ period: emaPeriod });
|
|
653
|
+
const msumProc = msum.create({ period: miPeriod });
|
|
654
|
+
return (bar) => {
|
|
655
|
+
const e1 = ema1Proc(subtract(from(bar.h, 18), from(bar.l, 18)));
|
|
656
|
+
return msumProc(divide(e1, ema2Proc(e1), 18));
|
|
657
|
+
};
|
|
658
|
+
}, defaultMassIndexOptions);
|
|
659
|
+
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region src/trend/triangularMovingAverage.ts
|
|
662
|
+
const defaultTriangularMovingAverageOptions = { period: 4 };
|
|
663
|
+
const trima = createSignal(({ period }) => {
|
|
664
|
+
assert(Number.isInteger(period) && period >= 1, /* @__PURE__ */ new RangeError(`Expected period to be a positive integer, got ${period}`));
|
|
665
|
+
let n1;
|
|
666
|
+
let n2;
|
|
667
|
+
if (period % 2 === 0) {
|
|
668
|
+
n1 = period / 2;
|
|
669
|
+
n2 = n1 + 1;
|
|
670
|
+
} else {
|
|
671
|
+
n1 = (period + 1) / 2;
|
|
672
|
+
n2 = n1;
|
|
673
|
+
}
|
|
674
|
+
const sma1 = sma.create({ period: n2 });
|
|
675
|
+
const sma2 = sma.create({ period: n1 });
|
|
676
|
+
return (value) => {
|
|
677
|
+
return sma2(sma1(value));
|
|
678
|
+
};
|
|
679
|
+
}, defaultTriangularMovingAverageOptions);
|
|
680
|
+
|
|
681
|
+
//#endregion
|
|
682
|
+
export { apo as absolutePriceOscillator, apo, ad as accumulationDistribution, ad, ao, ao as awesomeOscillator, aroon, bop as balanceOfPower, bop, cci, cci as commodityChannelIndex, cfo, cfo as chandeForecastOscillator, cmo as chaikinOscillator, cmo, defaultAbsolutePriceOscillatorOptions, defaultAroonOptions, defaultAwesomeOscillatorOptions, defaultCCIOptions, defaultCFOOptions, defaultChaikinOscillatorOptions, defaultDoubleExponentialMovingAverageOptions, defaultExponentialMovingAverageOptions, defaultIchimokuCloudOptions, defaultMACDOptions, defaultMassIndexOptions, defaultMovingMaxOptions, defaultMovingMinOptions, defaultMovingSumOptions, defaultPercentagePriceOscillatorOptions, defaultRMAOptions, defaultRSIOptions, defaultSMAOptions, defaultStochasticOscillatorOptions, defaultTriangularMovingAverageOptions, dema, dema as doubleExponentialMovingAverage, ema, ema as exponentialMovingAverage, ichimokuCloud, macd, macd as movingAverageConvergenceDivergence, mi as massIndex, mi, mmax, mmax as movingMax, mmin, mmin as movingMin, msum, ppo as percentagePriceOscillator, ppo, rsi as relativeStrengthIndex, rsi, rma, rma as rollingMovingAverage, sma as simpleMovingAverage, sma, stoch, stoch as stochasticOscillator, trima as triangularMovingAverage, trima };
|
|
683
|
+
//# sourceMappingURL=index.js.map
|