diff --git a/docs/CMO.md b/docs/CMO.md new file mode 100644 index 0000000..bfc516c --- /dev/null +++ b/docs/CMO.md @@ -0,0 +1,23 @@ +# Chande Momentum Oscillator (CMO) + +The Chande Momentum Oscillator (CMO) is a momentum indicator that measures the difference between the sum of recent gains and losses over a specified period. + +## Formula +CMO = 100 × (Sum of Gains − Sum of Losses) / (Sum of Gains + Sum of Losses) +- Gains: sum of positive changes over period +- Losses: sum of absolute value of negative changes over period + +## Parameters +- `period` (default: 14): Period for calculation + +## Usage Example +```ts +import { CMO } from '../src/cmo'; + +const cmo = new CMO(); +const result = cmo.nextValue(close); +// result: CMO value +``` + +## Returns +A single number: the CMO value for the current input. \ No newline at end of file diff --git a/docs/DMI.md b/docs/DMI.md new file mode 100644 index 0000000..da4b435 --- /dev/null +++ b/docs/DMI.md @@ -0,0 +1,27 @@ +# Directional Movement Index (DMI) + +The Directional Movement Index (DMI) is a trend indicator that consists of two lines: +DI and -DI. Optionally, the Average Directional Index (ADX) can be included to measure trend strength. + +## Lines +- **+DI:** Smoothed positive directional movement +- **-DI:** Smoothed negative directional movement +- **ADX (optional):** Average Directional Index, measures trend strength + +## Parameters +- `period` (default: 14): Period for smoothing +- `withADX` (default: false): Whether to calculate ADX as well + +## Usage Example +```ts +import { DMI } from '../src/dmi'; + +const dmi = new DMI(); +const result = dmi.nextValue(high, low, close); +// result: { plusDI, minusDI } or { plusDI, minusDI, adx } +``` + +## Returns +An object with the following properties: +- `plusDI`: Positive Directional Indicator (+DI) +- `minusDI`: Negative Directional Indicator (-DI) +- `adx`: Average Directional Index (if withADX is true) \ No newline at end of file diff --git a/docs/DPO.md b/docs/DPO.md new file mode 100644 index 0000000..cf93ce4 --- /dev/null +++ b/docs/DPO.md @@ -0,0 +1,22 @@ +# Detrended Price Oscillator (DPO) + +The Detrended Price Oscillator (DPO) is used to eliminate the long-term trends in prices by comparing the price to a shifted moving average. + +## Formula +DPO = Price − SMA(shifted) +- The SMA is shifted back by (period / 2 + 1) bars + +## Parameters +- `period` (default: 20): Period for SMA + +## Usage Example +```ts +import { DPO } from '../src/dpo'; + +const dpo = new DPO(); +const result = dpo.nextValue(close); +// result: DPO value +``` + +## Returns +A single number: the DPO value for the current input. \ No newline at end of file diff --git a/docs/ElderRay.md b/docs/ElderRay.md new file mode 100644 index 0000000..a984bca --- /dev/null +++ b/docs/ElderRay.md @@ -0,0 +1,24 @@ +# Elder Ray Index (Bull Power / Bear Power) + +The Elder Ray Index is a trend-following indicator that measures the strength of bulls and bears in the market. + +## Formula +- Bull Power = High − EMA +- Bear Power = Low − EMA + +## Parameters +- `period` (default: 13): Period for EMA + +## Usage Example +```ts +import { ElderRay } from '../src/elder-ray'; + +const elderRay = new ElderRay(); +const result = elderRay.nextValue(high, low, close); +// result: { bull, bear } +``` + +## Returns +An object with the following properties: +- `bull`: Bull Power value +- `bear`: Bear Power value \ No newline at end of file diff --git a/docs/Envelopes.md b/docs/Envelopes.md new file mode 100644 index 0000000..c71d39b --- /dev/null +++ b/docs/Envelopes.md @@ -0,0 +1,27 @@ +# Moving Average Envelopes + +Moving Average Envelopes are lines plotted at a fixed percentage above and below a moving average (usually SMA). They help identify overbought and oversold conditions, as well as trend direction. + +## Lines +- **Upper Envelope:** SMA + (SMA * percent / 100) +- **Middle Line:** Simple Moving Average (SMA) +- **Lower Envelope:** SMA - (SMA * percent / 100) + +## Parameters +- `period` (default: 20): Period for the moving average +- `percent` (default: 2): Envelope distance in percent + +## Usage Example +```ts +import { Envelopes } from '../src/envelopes'; + +const envelopes = new Envelopes(); +const result = envelopes.nextValue(close); +// result: { lower, middle, upper } +``` + +## Returns +An object with the following properties: +- `lower`: Lower envelope value +- `middle`: Middle line (SMA) value +- `upper`: Upper envelope value \ No newline at end of file diff --git a/docs/ForceIndex.md b/docs/ForceIndex.md new file mode 100644 index 0000000..1ec6bbd --- /dev/null +++ b/docs/ForceIndex.md @@ -0,0 +1,21 @@ +# Force Index + +The Force Index is a volume-based oscillator that measures the strength of bulls and bears by combining price and volume. + +## Formula +Force Index = (Current Close − Previous Close) × Volume + +## Parameters +- None (uses raw calculation) + +## Usage Example +```ts +import { ForceIndex } from '../src/force-index'; + +const fi = new ForceIndex(); +const result = fi.nextValue(close, volume); +// result: Force Index value +``` + +## Returns +A single number: the Force Index value for the current input. \ No newline at end of file diff --git a/docs/Fractal.md b/docs/Fractal.md new file mode 100644 index 0000000..22ecb95 --- /dev/null +++ b/docs/Fractal.md @@ -0,0 +1,25 @@ +# Fractal Indicator (Bill Williams Fractals) + +The Fractal Indicator identifies local highs and lows (fractals) in price data. + +## Definition +- A fractal up is a high that is higher than two bars to the left and right. +- A fractal down is a low that is lower than two bars to the left and right. + +## Parameters +- `left` (default: 2): Number of bars to the left +- `right` (default: 2): Number of bars to the right + +## Usage Example +```ts +import { Fractal } from '../src/fractal'; + +const fractal = new Fractal(); +const result = fractal.nextValue(high, low); +// result: { up, down } +``` + +## Returns +An object with the following properties: +- `up`: Fractal up value (if found) +- `down`: Fractal down value (if found) \ No newline at end of file diff --git a/docs/IchimokuCloud.md b/docs/IchimokuCloud.md new file mode 100644 index 0000000..738be33 --- /dev/null +++ b/docs/IchimokuCloud.md @@ -0,0 +1,33 @@ +# Ichimoku Cloud (Ichimoku Kinko Hyo) + +Ichimoku Cloud is a comprehensive indicator that defines support and resistance, identifies trend direction, gauges momentum, and provides trading signals. + +## Lines +- **Tenkan-sen (Conversion Line):** (highest high + lowest low) / 2 for the last 9 periods (default) +- **Kijun-sen (Base Line):** (highest high + lowest low) / 2 for the last 26 periods (default) +- **Senkou Span A (Leading Span A):** (Tenkan-sen + Kijun-sen) / 2, plotted 26 periods ahead +- **Senkou Span B (Leading Span B):** (highest high + lowest low) / 2 for the last 52 periods (default), plotted 26 periods ahead +- **Chikou Span (Lagging Span):** Closing price, plotted 26 periods back + +## Parameters +- `periodTenkan` (default: 9): Period for Tenkan-sen +- `periodKijun` (default: 26): Period for Kijun-sen +- `periodSenkouB` (default: 52): Period for Senkou Span B +- `displacement` (default: 26): Displacement for Senkou and Chikou lines + +## Usage Example +```ts +import { Ichimoku } from '../src/ichimoku'; + +const ichimoku = new Ichimoku(); +const result = ichimoku.nextValue(high, low, close); +// result: { tenkan, kijun, senkouA, senkouB, chikou } +``` + +## Returns +An object with the following properties: +- `tenkan`: Tenkan-sen value +- `kijun`: Kijun-sen value +- `senkouA`: Senkou Span A value (current, forward shift is handled on chart) +- `senkouB`: Senkou Span B value (current, forward shift is handled on chart) +- `chikou`: Chikou Span value (close, shifted backward) \ No newline at end of file diff --git a/docs/KeltnerChannel.md b/docs/KeltnerChannel.md new file mode 100644 index 0000000..c09f01c --- /dev/null +++ b/docs/KeltnerChannel.md @@ -0,0 +1,27 @@ +# Keltner Channel + +Keltner Channel is a volatility-based envelope set above and below an exponential moving average (EMA). The channel uses the Average True Range (ATR) to set the distance of the bands. + +## Lines +- **Upper Band:** EMA + (ATR * multiplier) +- **Middle Band:** Exponential Moving Average (EMA) +- **Lower Band:** EMA - (ATR * multiplier) + +## Parameters +- `period` (default: 20): Period for EMA and ATR +- `multiplier` (default: 2): ATR multiplier for channel width + +## Usage Example +```ts +import { KeltnerChannel } from '../src/keltner'; + +const keltner = new KeltnerChannel(); +const result = keltner.nextValue(high, low, close); +// result: { lower, middle, upper } +``` + +## Returns +An object with the following properties: +- `lower`: Lower band value +- `middle`: Middle band (EMA) value +- `upper`: Upper band value \ No newline at end of file diff --git a/docs/TEMA.md b/docs/TEMA.md new file mode 100644 index 0000000..da262de --- /dev/null +++ b/docs/TEMA.md @@ -0,0 +1,24 @@ +# Triple Exponential Moving Average (TEMA) + +TEMA is a moving average that reduces lag by combining a single, double, and triple EMA. It is more responsive to price changes than a traditional EMA. + +## Formula +TEMA = 3 × EMA1 − 3 × EMA2 + EMA3 +- EMA1 = EMA of price +- EMA2 = EMA of EMA1 +- EMA3 = EMA of EMA2 + +## Parameters +- `period` (default: 20): Period for all EMAs + +## Usage Example +```ts +import { TEMA } from '../src/tema'; + +const tema = new TEMA(); +const result = tema.nextValue(close); +// result: TEMA value +``` + +## Returns +A single number: the TEMA value for the current input. \ No newline at end of file diff --git a/docs/TRIX.md b/docs/TRIX.md new file mode 100644 index 0000000..6e32c77 --- /dev/null +++ b/docs/TRIX.md @@ -0,0 +1,22 @@ +# TRIX (Triple Exponential Average Oscillator) + +TRIX is a momentum oscillator that displays the percent rate of change of a triple exponentially smoothed moving average. It is used to identify oversold and overbought markets, as well as momentum. + +## Formula +TRIX = [(TEMA - previous TEMA) / previous TEMA] × 100 +- TEMA is the triple EMA of price + +## Parameters +- `period` (default: 15): Period for all EMAs + +## Usage Example +```ts +import { TRIX } from '../src/trix'; + +const trix = new TRIX(); +const result = trix.nextValue(close); +// result: TRIX value +``` + +## Returns +A single number: the TRIX value for the current input. \ No newline at end of file diff --git a/docs/UltimateOscillator.md b/docs/UltimateOscillator.md new file mode 100644 index 0000000..6046fb3 --- /dev/null +++ b/docs/UltimateOscillator.md @@ -0,0 +1,26 @@ +# Ultimate Oscillator + +The Ultimate Oscillator is a momentum oscillator that combines short, intermediate, and long-term price action into one value. + +## Formula +UO = 100 × (4 × avg7 + 2 × avg14 + avg28) / (4 + 2 + 1) +- avgN = sum(BP, N) / sum(TR, N) +- BP = Close − min(Low, PrevClose) +- TR = max(High, PrevClose) − min(Low, PrevClose) + +## Parameters +- `period1` (default: 7): Short period +- `period2` (default: 14): Medium period +- `period3` (default: 28): Long period + +## Usage Example +```ts +import { UltimateOscillator } from '../src/ultimate-oscillator'; + +const uo = new UltimateOscillator(); +const result = uo.nextValue(high, low, close); +// result: Ultimate Oscillator value +``` + +## Returns +A single number: the Ultimate Oscillator value for the current input. \ No newline at end of file diff --git a/docs/VolumeOscillator.md b/docs/VolumeOscillator.md new file mode 100644 index 0000000..f164605 --- /dev/null +++ b/docs/VolumeOscillator.md @@ -0,0 +1,22 @@ +# Volume Oscillator + +The Volume Oscillator measures the difference between two moving averages of volume. + +## Formula +Volume Oscillator = SMA(short) − SMA(long) + +## Parameters +- `shortPeriod` (default: 14): Short period for SMA +- `longPeriod` (default: 28): Long period for SMA + +## Usage Example +```ts +import { VolumeOscillator } from '../src/volume-oscillator'; + +const vo = new VolumeOscillator(); +const result = vo.nextValue(volume); +// result: Volume Oscillator value +``` + +## Returns +A single number: the Volume Oscillator value for the current input. \ No newline at end of file diff --git a/index.ts b/index.ts index 4e63fbe..32b0996 100644 --- a/index.ts +++ b/index.ts @@ -39,4 +39,17 @@ export { Sampler } from './src/providers/sampler'; export { VolumeProfile } from './src/volume-profile'; /** BETA UNSTABLE */ export { ChaikinOscillator } from './src/chaikin'; export { AMA } from './src/ama'; +export { Ichimoku } from './src/ichimoku'; +export { Envelopes } from './src/envelopes'; +export { KeltnerChannel } from './src/keltner'; +export { DMI } from './src/dmi'; +export { TEMA } from './src/tema'; +export { TRIX } from './src/trix'; +export { CMO } from './src/cmo'; +export { DPO } from './src/dpo'; // export { OrderBlock } from './src/order-block'; +export { UltimateOscillator } from './src/ultimate-oscillator'; +export { ElderRay } from './src/elder-ray'; +export { ForceIndex } from './src/force-index'; +export { Fractal } from './src/fractal'; +export { VolumeOscillator } from './src/volume-oscillator'; diff --git a/src/cmo.ts b/src/cmo.ts new file mode 100644 index 0000000..3042760 --- /dev/null +++ b/src/cmo.ts @@ -0,0 +1,79 @@ +import { CircularBuffer } from './providers/circular-buffer'; + +/** + * Chande Momentum Oscillator (CMO) + * + * CMO = 100 * (Sum of Gains - Sum of Losses) / (Sum of Gains + Sum of Losses) + * + * - Gains: sum of positive changes over period + * - Losses: sum of absolute value of negative changes over period + */ +export class CMO { + private buffer: CircularBuffer; + private fill = 0; + + /** + * @param period Period for calculation (default: 14) + */ + constructor(private period = 14) { + this.buffer = new CircularBuffer(period + 1); + } + + /** + * Adds a new value and returns the CMO + * @param value Input value (e.g., close price) + */ + nextValue(value: number) { + this.buffer.push(value); + this.fill++; + if (this.fill < this.period + 1) { + return; + } + const arr = this.buffer.toArray().slice(-this.period - 1); + let gains = 0; + let losses = 0; + for (let i = 1; i < arr.length; i++) { + const diff = arr[i] - arr[i - 1]; + if (diff > 0) gains += diff; + else losses -= diff; + } + const denom = gains + losses; + if (denom === 0) return 0; + const cmo = 100 * (gains - losses) / denom; + this.nextValue = (value: number) => { + this.buffer.push(value); + const arr = this.buffer.toArray().slice(-this.period - 1); + let gains = 0; + let losses = 0; + for (let i = 1; i < arr.length; i++) { + const diff = arr[i] - arr[i - 1]; + if (diff > 0) gains += diff; + else losses -= diff; + } + const denom = gains + losses; + if (denom === 0) return 0; + return 100 * (gains - losses) / denom; + }; + return cmo; + } + + /** + * Calculates CMO for the current (not closed) bar without changing the internal state + * @param value Input value (e.g., close price) + */ + momentValue(value: number) { + const arr = this.buffer.toArray().slice(-this.period); + arr.push(value); + if (arr.length < 2) return; + let gains = 0; + let losses = 0; + for (let i = 1; i < arr.length; i++) { + const diff = arr[i] - arr[i - 1]; + if (diff > 0) gains += diff; + else losses -= diff; + } + const denom = gains + losses; + if (denom === 0) return 0; + return 100 * (gains - losses) / denom; + } +} \ No newline at end of file diff --git a/src/dmi.ts b/src/dmi.ts new file mode 100644 index 0000000..05c94d2 --- /dev/null +++ b/src/dmi.ts @@ -0,0 +1,117 @@ +import { getTrueRange } from './providers/true-range'; +import { WEMA } from './wema'; + +/** + * Directional Movement Index (DMI) + * + * DMI consists of two lines: +DI and -DI, which help identify trend direction and strength. + * Optionally, ADX can be included to measure trend strength. + * + * - +DI: Smoothed positive directional movement + * - -DI: Smoothed negative directional movement + * - ADX: Average Directional Index (optional) + */ +export class DMI { + private prevHigh: number; + private prevLow: number; + private prevClose: number; + private wemaP: WEMA; + private wemaN: WEMA; + private wemaADX: WEMA; + + /** + * @param period Period for smoothing (default: 14) + * @param withADX Whether to calculate ADX as well (default: false) + */ + constructor(public period: number = 14, private withADX = false) { + this.wemaP = new WEMA(period); + this.wemaN = new WEMA(period); + this.wemaADX = new WEMA(period); + } + + /** + * Adds a new value and returns DMI (+DI, -DI, optionally ADX) + * @param h High price of the current bar + * @param l Low price of the current bar + * @param c Close price of the current bar + */ + nextValue(h: number, l: number, c: number) { + if (this.prevClose === undefined) { + this.prevHigh = h; + this.prevLow = l; + this.prevClose = c; + return; + } + let pDM = 0; + let nDM = 0; + const hDiff = h - this.prevHigh; + const lDiff = this.prevLow - l; + if (hDiff > lDiff && hDiff > 0) { + pDM = hDiff; + } + if (lDiff > hDiff && lDiff > 0) { + nDM = lDiff; + } + if (pDM > nDM || nDM < 0) { + nDM = 0; + } + const atr = getTrueRange(h, l, this.prevClose); + const avgPDM = this.wemaP.nextValue(pDM); + const avgNDM = this.wemaN.nextValue(nDM); + this.prevHigh = h; + this.prevLow = l; + this.prevClose = c; + if (avgPDM === undefined || avgNDM === undefined || atr === 0) { + return; + } + const plusDI = (avgPDM * 100) / atr; + const minusDI = (avgNDM * 100) / atr; + if (!this.withADX) { + return { plusDI, minusDI }; + } + const diDiff = Math.abs(plusDI - minusDI); + const diSum = plusDI + minusDI; + const adx = this.wemaADX.nextValue(100 * (diDiff / diSum)); + return { plusDI, minusDI, adx }; + } + + /** + * Calculates DMI for the current (not closed) bar without changing the internal state + * @param h High price of the current bar + * @param l Low price of the current bar + * @param c Close price of the current bar + */ + momentValue(h: number, l: number, c: number) { + if (this.prevClose === undefined) { + return; + } + let pDM = 0; + let nDM = 0; + const hDiff = h - this.prevHigh; + const lDiff = this.prevLow - l; + if (hDiff > lDiff && hDiff > 0) { + pDM = hDiff; + } + if (lDiff > hDiff && lDiff > 0) { + nDM = lDiff; + } + if (pDM > nDM || nDM < 0) { + nDM = 0; + } + const atr = getTrueRange(h, l, this.prevClose); + const avgPDM = this.wemaP.momentValue(pDM); + const avgNDM = this.wemaN.momentValue(nDM); + if (avgPDM === undefined || avgNDM === undefined || atr === 0) { + return; + } + const plusDI = (avgPDM * 100) / atr; + const minusDI = (avgNDM * 100) / atr; + if (!this.withADX) { + return { plusDI, minusDI }; + } + const diDiff = Math.abs(plusDI - minusDI); + const diSum = plusDI + minusDI; + const adx = this.wemaADX.momentValue(100 * (diDiff / diSum)); + return { plusDI, minusDI, adx }; + } +} \ No newline at end of file diff --git a/src/dpo.ts b/src/dpo.ts new file mode 100644 index 0000000..7e9abb9 --- /dev/null +++ b/src/dpo.ts @@ -0,0 +1,63 @@ +import { SMA } from './sma'; +import { CircularBuffer } from './providers/circular-buffer'; + +/** + * Detrended Price Oscillator (DPO) + * + * DPO = Price - SMA(shifted) + * + * The SMA is shifted back by (period / 2 + 1) bars. + */ +export class DPO { + private sma: SMA; + private buffer: CircularBuffer; + private shift: number; + private fill = 0; + + /** + * @param period Period for SMA (default: 20) + */ + constructor(private period = 20) { + this.sma = new SMA(period); + this.shift = Math.floor(period / 2 + 1); + this.buffer = new CircularBuffer(this.shift + 1); + } + + /** + * Adds a new value and returns the DPO + * @param value Input value (e.g., close price) + */ + nextValue(value: number) { + const sma = this.sma.nextValue(value); + this.buffer.push(value); + this.fill++; + if (this.fill < this.period + this.shift) { + return; + } + const arr = this.buffer.toArray(); + const shiftedPrice = arr[0]; + const dpo = shiftedPrice - sma; + this.nextValue = (value: number) => { + const sma = this.sma.nextValue(value); + this.buffer.push(value); + const arr = this.buffer.toArray(); + const shiftedPrice = arr[0]; + return shiftedPrice - sma; + }; + return dpo; + } + + /** + * Calculates DPO for the current (not closed) bar without changing the internal state + * @param value Input value (e.g., close price) + */ + momentValue(value: number) { + const arr = this.buffer.toArray().slice(); + arr.push(value); + if (arr.length > this.shift + 1) arr.shift(); + if (arr.length < this.shift + 1) return; + const sma = this.sma.momentValue(value); + const shiftedPrice = arr[0]; + return shiftedPrice - sma; + } +} \ No newline at end of file diff --git a/src/elder-ray.ts b/src/elder-ray.ts new file mode 100644 index 0000000..a11ff5e --- /dev/null +++ b/src/elder-ray.ts @@ -0,0 +1,49 @@ +import { EMA } from './ema'; + +/** + * Elder Ray Index (Bull Power / Bear Power) + * + * Bull Power = High - EMA + * Bear Power = Low - EMA + */ +export class ElderRay { + private ema: EMA; + private fill = 0; + + /** + * @param period Period for EMA (default: 13) + */ + constructor(private period = 13) { + this.ema = new EMA(period); + } + + /** + * Adds a new value and returns Bull Power and Bear Power + * @param high High price of the current bar + * @param low Low price of the current bar + * @param close Close price of the current bar + */ + nextValue(high: number, low: number, close: number) { + const ema = this.ema.nextValue(close); + this.fill++; + if (this.fill < this.period) return; + const bull = high - ema; + const bear = low - ema; + this.nextValue = (high: number, low: number, close: number) => { + const ema = this.ema.nextValue(close); + return { bull, bear }; + }; + return { bull, bear }; + } + + /** + * Calculates Bull Power and Bear Power for the current (not closed) bar without changing the internal state + * @param high High price of the current bar + * @param low Low price of the current bar + * @param close Close price of the current bar + */ + momentValue(high: number, low: number, close: number) { + const ema = this.ema.momentValue(close); + return { bull: high - ema, bear: low - ema }; + } +} \ No newline at end of file diff --git a/src/envelopes.ts b/src/envelopes.ts new file mode 100644 index 0000000..f1ab110 --- /dev/null +++ b/src/envelopes.ts @@ -0,0 +1,58 @@ +import { SMA } from './sma'; + +/** + * Moving Average Envelopes + * + * Envelopes are plotted at a fixed percentage above and below a moving average (usually SMA). + * + * - Upper Envelope: SMA + (SMA * percent / 100) + * - Lower Envelope: SMA - (SMA * percent / 100) + */ +export class Envelopes { + private sma: SMA; + private fill = 0; + + /** + * @param period Period for the moving average (default: 20) + * @param percent Envelope distance in percent (default: 2) + */ + constructor(private period = 20, private percent = 2) { + this.sma = new SMA(period); + } + + /** + * Adds a new value and returns the envelopes + * @param close Close price of the current bar + */ + nextValue(close: number) { + const middle = this.sma.nextValue(close); + this.fill++; + if (this.fill !== this.period) { + return; + } + const deviation = middle * this.percent / 100; + const upper = middle + deviation; + const lower = middle - deviation; + + this.nextValue = (close: number) => { + const middle = this.sma.nextValue(close); + const deviation = middle * this.percent / 100; + const upper = middle + deviation; + const lower = middle - deviation; + return { lower, middle, upper }; + }; + return { lower, middle, upper }; + } + + /** + * Calculates envelopes for the current (not closed) bar without changing the internal state + * @param close Close price of the current bar + */ + momentValue(close: number) { + const middle = this.sma.momentValue(close); + const deviation = middle * this.percent / 100; + const upper = middle + deviation; + const lower = middle - deviation; + return { lower, middle, upper }; + } +} \ No newline at end of file diff --git a/src/force-index.ts b/src/force-index.ts new file mode 100644 index 0000000..4bda2ec --- /dev/null +++ b/src/force-index.ts @@ -0,0 +1,45 @@ +/** + * Force Index + * + * Force Index = (Current Close - Previous Close) * Volume + */ +export class ForceIndex { + private prevClose: number; + private fill = 0; + + /** + * No parameters (uses raw calculation) + */ + constructor() {} + + /** + * Adds a new value and returns the Force Index + * @param close Close price of the current bar + * @param volume Volume of the current bar + */ + nextValue(close: number, volume: number) { + if (this.prevClose === undefined) { + this.prevClose = close; + return; + } + const force = (close - this.prevClose) * volume; + this.prevClose = close; + this.fill++; + this.nextValue = (close: number, volume: number) => { + const force = (close - this.prevClose) * volume; + this.prevClose = close; + return force; + }; + return force; + } + + /** + * Calculates Force Index for the current (not closed) bar without changing the internal state + * @param close Close price of the current bar + * @param volume Volume of the current bar + */ + momentValue(close: number, volume: number) { + if (this.prevClose === undefined) return; + return (close - this.prevClose) * volume; + } +} \ No newline at end of file diff --git a/src/fractal.ts b/src/fractal.ts new file mode 100644 index 0000000..5135eb0 --- /dev/null +++ b/src/fractal.ts @@ -0,0 +1,65 @@ +import { CircularBuffer } from './providers/circular-buffer'; + +/** + * Fractal Indicator (Bill Williams Fractals) + * + * A fractal up is a high that is higher than two bars to the left and right. + * A fractal down is a low that is lower than two bars to the left and right. + */ +export class Fractal { + private highs: CircularBuffer; + private lows: CircularBuffer; + private fill = 0; + + /** + * @param left Number of bars to the left (default: 2) + * @param right Number of bars to the right (default: 2) + */ + constructor(private left = 2, private right = 2) { + this.highs = new CircularBuffer(left + right + 1); + this.lows = new CircularBuffer(left + right + 1); + } + + /** + * Adds a new value and returns fractal up/down if found + * @param high High price of the current bar + * @param low Low price of the current bar + */ + nextValue(high: number, low: number) { + this.highs.push(high); + this.lows.push(low); + this.fill++; + if (this.fill < this.left + this.right + 1) return; + const arrHighs = this.highs.toArray(); + const arrLows = this.lows.toArray(); + const center = this.left; + const isFractalUp = arrHighs[center] === Math.max(...arrHighs) && arrHighs[center] > Math.max(...arrHighs.slice(0, center), ...arrHighs.slice(center + 1)); + const isFractalDown = arrLows[center] === Math.min(...arrLows) && arrLows[center] < Math.min(...arrLows.slice(0, center), ...arrLows.slice(center + 1)); + return { + up: isFractalUp ? arrHighs[center] : undefined, + down: isFractalDown ? arrLows[center] : undefined + }; + } + + /** + * Calculates fractal up/down for the current (not closed) bar without changing the internal state + * @param high High price of the current bar + * @param low Low price of the current bar + */ + momentValue(high: number, low: number) { + const arrHighs = this.highs.toArray().slice(); + const arrLows = this.lows.toArray().slice(); + arrHighs.push(high); + arrLows.push(low); + if (arrHighs.length > this.left + this.right + 1) arrHighs.shift(); + if (arrLows.length > this.left + this.right + 1) arrLows.shift(); + if (arrHighs.length < this.left + this.right + 1) return; + const center = this.left; + const isFractalUp = arrHighs[center] === Math.max(...arrHighs) && arrHighs[center] > Math.max(...arrHighs.slice(0, center), ...arrHighs.slice(center + 1)); + const isFractalDown = arrLows[center] === Math.min(...arrLows) && arrLows[center] < Math.min(...arrLows.slice(0, center), ...arrLows.slice(center + 1)); + return { + up: isFractalUp ? arrHighs[center] : undefined, + down: isFractalDown ? arrLows[center] : undefined + }; + } +} \ No newline at end of file diff --git a/src/ichimoku.ts b/src/ichimoku.ts new file mode 100644 index 0000000..278ae64 --- /dev/null +++ b/src/ichimoku.ts @@ -0,0 +1,144 @@ +import { getMax, getMin } from './utils'; +import { CircularBuffer } from './providers/circular-buffer'; + +/** + * Ichimoku Cloud (Ichimoku Kinko Hyo) + * + * Lines: + * - Tenkan-sen (Conversion Line): (max(high, 9) + min(low, 9)) / 2 + * - Kijun-sen (Base Line): (max(high, 26) + min(low, 26)) / 2 + * - Senkou Span A: (Tenkan-sen + Kijun-sen) / 2, shifted forward by 26 + * - Senkou Span B: (max(high, 52) + min(low, 52)) / 2, shifted forward by 26 + * - Chikou Span: close, shifted backward by 26 + */ +export class Ichimoku { + private highs9: CircularBuffer; + private lows9: CircularBuffer; + private highs26: CircularBuffer; + private lows26: CircularBuffer; + private highs52: CircularBuffer; + private lows52: CircularBuffer; + private closes: CircularBuffer; + + private tenkan: number; + private kijun: number; + private senkouA: number[] = []; + private senkouB: number[] = []; + private chikou: number[] = []; + + constructor( + private periodTenkan = 9, + private periodKijun = 26, + private periodSenkouB = 52, + private displacement = 26 + ) { + this.highs9 = new CircularBuffer(periodTenkan); + this.lows9 = new CircularBuffer(periodTenkan); + this.highs26 = new CircularBuffer(periodKijun); + this.lows26 = new CircularBuffer(periodKijun); + this.highs52 = new CircularBuffer(periodSenkouB); + this.lows52 = new CircularBuffer(periodSenkouB); + this.closes = new CircularBuffer(displacement + 1); // for Chikou Span + } + + /** + * Adds a new value and returns an object with Ichimoku lines + * @param high High price of the current bar + * @param low Low price of the current bar + * @param close Close price of the current bar + */ + nextValue(high: number, low: number, close: number) { + this.highs9.push(high); + this.lows9.push(low); + this.highs26.push(high); + this.lows26.push(low); + this.highs52.push(high); + this.lows52.push(low); + this.closes.push(close); + + if (!this.highs9.filled || !this.highs26.filled || !this.highs52.filled) { + return; + } + + this.tenkan = (getMax(this.highs9.toArray()) + getMin(this.lows9.toArray())) / 2; + this.kijun = (getMax(this.highs26.toArray()) + getMin(this.lows26.toArray())) / 2; + const senkouA = (this.tenkan + this.kijun) / 2; + const senkouB = (getMax(this.highs52.toArray()) + getMin(this.lows52.toArray())) / 2; + + this.senkouA.push(senkouA); + this.senkouB.push(senkouB); + if (this.senkouA.length > this.displacement) this.senkouA.shift(); + if (this.senkouB.length > this.displacement) this.senkouB.shift(); + + // Chikou Span (close, shifted backward by displacement) + this.chikou.unshift(close); + if (this.chikou.length > this.displacement) this.chikou.length = this.displacement; + + return { + tenkan: this.tenkan, + kijun: this.kijun, + senkouA: this.senkouA[0], // current Senkou A (forward shift is handled on chart) + senkouB: this.senkouB[0], // current Senkou B + chikou: this.chikou[this.displacement - 1] // close, shifted backward + }; + } + + /** + * Calculates Ichimoku values for the current (not closed) bar without changing the internal state + * @param high High price of the current bar + * @param low Low price of the current bar + * @param close Close price of the current bar + */ + momentValue(high: number, low: number, close: number) { + // Copy buffer contents and add the current value + const highs9 = this.highs9.toArray().slice(); + const lows9 = this.lows9.toArray().slice(); + const highs26 = this.highs26.toArray().slice(); + const lows26 = this.lows26.toArray().slice(); + const highs52 = this.highs52.toArray().slice(); + const lows52 = this.lows52.toArray().slice(); + const closes = this.closes.toArray().slice(); + + if (highs9.length === this.periodTenkan) highs9.shift(); + if (lows9.length === this.periodTenkan) lows9.shift(); + if (highs26.length === this.periodKijun) highs26.shift(); + if (lows26.length === this.periodKijun) lows26.shift(); + if (highs52.length === this.periodSenkouB) highs52.shift(); + if (lows52.length === this.periodSenkouB) lows52.shift(); + if (closes.length === this.displacement + 1) closes.shift(); + + highs9.push(high); + lows9.push(low); + highs26.push(high); + lows26.push(low); + highs52.push(high); + lows52.push(low); + closes.push(close); + + if ( + highs9.length < this.periodTenkan || + highs26.length < this.periodKijun || + highs52.length < this.periodSenkouB + ) { + return; + } + + const tenkan = (getMax(highs9) + getMin(lows9)) / 2; + const kijun = (getMax(highs26) + getMin(lows26)) / 2; + const senkouA = (tenkan + kijun) / 2; + const senkouB = (getMax(highs52) + getMin(lows52)) / 2; + + // For moment calculation, we can't shift Senkou lines forward, but we return the current values + // Chikou Span: close, shifted backward by displacement + const chikouIdx = closes.length - 1 - this.displacement; + const chikou = chikouIdx >= 0 ? closes[chikouIdx] : undefined; + + return { + tenkan, + kijun, + senkouA, + senkouB, + chikou + }; + } +} \ No newline at end of file diff --git a/src/keltner.ts b/src/keltner.ts new file mode 100644 index 0000000..d4874b1 --- /dev/null +++ b/src/keltner.ts @@ -0,0 +1,65 @@ +import { EMA } from './ema'; +import { ATR } from './atr'; + +/** + * Keltner Channel + * + * Channels are based on an EMA (middle) and ATR (for band width). + * + * - Upper Band: EMA + (ATR * multiplier) + * - Middle Band: EMA + * - Lower Band: EMA - (ATR * multiplier) + */ +export class KeltnerChannel { + private ema: EMA; + private atr: ATR; + private fill = 0; + + /** + * @param period Period for EMA and ATR (default: 20) + * @param multiplier ATR multiplier (default: 2) + */ + constructor(private period = 20, private multiplier = 2) { + this.ema = new EMA(period); + this.atr = new ATR(period); + } + + /** + * Adds a new value and returns the Keltner Channel bands + * @param high High price of the current bar + * @param low Low price of the current bar + * @param close Close price of the current bar + */ + nextValue(high: number, low: number, close: number) { + const middle = this.ema.nextValue(close); + const atr = this.atr.nextValue(high, low, close); + this.fill++; + if (this.fill !== this.period) { + return; + } + const upper = middle + this.multiplier * atr; + const lower = middle - this.multiplier * atr; + this.nextValue = (high: number, low: number, close: number) => { + const middle = this.ema.nextValue(close); + const atr = this.atr.nextValue(high, low, close); + const upper = middle + this.multiplier * atr; + const lower = middle - this.multiplier * atr; + return { lower, middle, upper }; + }; + return { lower, middle, upper }; + } + + /** + * Calculates Keltner Channel bands for the current (not closed) bar without changing the internal state + * @param high High price of the current bar + * @param low Low price of the current bar + * @param close Close price of the current bar + */ + momentValue(high: number, low: number, close: number) { + const middle = this.ema.momentValue(close); + const atr = this.atr.momentValue(high, low); + const upper = middle + this.multiplier * atr; + const lower = middle - this.multiplier * atr; + return { lower, middle, upper }; + } +} \ No newline at end of file diff --git a/src/tema.ts b/src/tema.ts new file mode 100644 index 0000000..a96bc99 --- /dev/null +++ b/src/tema.ts @@ -0,0 +1,59 @@ +import { EMA } from './ema'; + +/** + * Triple Exponential Moving Average (TEMA) + * + * TEMA = 3 * EMA1 - 3 * EMA2 + EMA3 + * where: + * EMA1 = EMA of price + * EMA2 = EMA of EMA1 + * EMA3 = EMA of EMA2 + */ +export class TEMA { + private ema1: EMA; + private ema2: EMA; + private ema3: EMA; + private fill = 0; + + /** + * @param period Period for all EMAs (default: 20) + */ + constructor(private period = 20) { + this.ema1 = new EMA(period); + this.ema2 = new EMA(period); + this.ema3 = new EMA(period); + } + + /** + * Adds a new value and returns the TEMA + * @param value Input value (e.g., close price) + */ + nextValue(value: number) { + const ema1 = this.ema1.nextValue(value); + if (ema1 === undefined) return; + const ema2 = this.ema2.nextValue(ema1); + if (ema2 === undefined) return; + const ema3 = this.ema3.nextValue(ema2); + this.fill++; + if (this.fill < this.period * 2) return; + const tema = 3 * ema1 - 3 * ema2 + ema3; + this.nextValue = (value: number) => { + const ema1 = this.ema1.nextValue(value); + const ema2 = this.ema2.nextValue(ema1); + const ema3 = this.ema3.nextValue(ema2); + return 3 * ema1 - 3 * ema2 + ema3; + }; + return tema; + } + + /** + * Calculates TEMA for the current (not closed) bar without changing the internal state + * @param value Input value (e.g., close price) + */ + momentValue(value: number) { + const ema1 = this.ema1.momentValue(value); + const ema2 = this.ema2.momentValue(ema1); + const ema3 = this.ema3.momentValue(ema2); + return 3 * ema1 - 3 * ema2 + ema3; + } +} \ No newline at end of file diff --git a/src/trix.ts b/src/trix.ts new file mode 100644 index 0000000..f72150f --- /dev/null +++ b/src/trix.ts @@ -0,0 +1,66 @@ +import { EMA } from './ema'; + +/** + * TRIX (Triple Exponential Average Oscillator) + * + * TRIX = (TEMA - previous TEMA) / previous TEMA * 100 + * where TEMA is the triple EMA of price + */ +export class TRIX { + private ema1: EMA; + private ema2: EMA; + private ema3: EMA; + private prevTrix: number; + private prevTema: number; + private fill = 0; + + /** + * @param period Period for all EMAs (default: 15) + */ + constructor(private period = 15) { + this.ema1 = new EMA(period); + this.ema2 = new EMA(period); + this.ema3 = new EMA(period); + } + + /** + * Adds a new value and returns the TRIX oscillator + * @param value Input value (e.g., close price) + */ + nextValue(value: number) { + const ema1 = this.ema1.nextValue(value); + if (ema1 === undefined) return; + const ema2 = this.ema2.nextValue(ema1); + if (ema2 === undefined) return; + const ema3 = this.ema3.nextValue(ema2); + this.fill++; + if (this.fill < this.period * 2) return; + if (this.prevTema === undefined) { + this.prevTema = ema3; + return; + } + const trix = ((ema3 - this.prevTema) / this.prevTema) * 100; + this.prevTema = ema3; + this.nextValue = (value: number) => { + const ema1 = this.ema1.nextValue(value); + const ema2 = this.ema2.nextValue(ema1); + const ema3 = this.ema3.nextValue(ema2); + const trix = ((ema3 - this.prevTema) / this.prevTema) * 100; + this.prevTema = ema3; + return trix; + }; + return trix; + } + + /** + * Calculates TRIX for the current (not closed) bar without changing the internal state + * @param value Input value (e.g., close price) + */ + momentValue(value: number) { + const ema1 = this.ema1.momentValue(value); + const ema2 = this.ema2.momentValue(ema1); + const ema3 = this.ema3.momentValue(ema2); + if (this.prevTema === undefined) return; + return ((ema3 - this.prevTema) / this.prevTema) * 100; + } +} \ No newline at end of file diff --git a/src/ultimate-oscillator.ts b/src/ultimate-oscillator.ts new file mode 100644 index 0000000..8d44cf1 --- /dev/null +++ b/src/ultimate-oscillator.ts @@ -0,0 +1,92 @@ +import { CircularBuffer } from './providers/circular-buffer'; + +/** + * Ultimate Oscillator + * + * Combines short, intermediate, and long-term price action into one oscillator. + * + * UO = 100 * (4 * avg7 + 2 * avg14 + avg28) / (4 + 2 + 1) + * Where avgN = sum(BP, N) / sum(TR, N) + * BP = Close - min(Low, PrevClose) + * TR = max(High, PrevClose) - min(Low, PrevClose) + */ +export class UltimateOscillator { + private bp7: CircularBuffer; + private tr7: CircularBuffer; + private bp14: CircularBuffer; + private tr14: CircularBuffer; + private bp28: CircularBuffer; + private tr28: CircularBuffer; + private prevClose: number; + private fill = 0; + + /** + * @param period1 Short period (default: 7) + * @param period2 Medium period (default: 14) + * @param period3 Long period (default: 28) + */ + constructor(private period1 = 7, private period2 = 14, private period3 = 28) { + this.bp7 = new CircularBuffer(period1); + this.tr7 = new CircularBuffer(period1); + this.bp14 = new CircularBuffer(period2); + this.tr14 = new CircularBuffer(period2); + this.bp28 = new CircularBuffer(period3); + this.tr28 = new CircularBuffer(period3); + } + + /** + * Adds a new value and returns the Ultimate Oscillator + * @param high High price of the current bar + * @param low Low price of the current bar + * @param close Close price of the current bar + */ + nextValue(high: number, low: number, close: number) { + if (this.prevClose === undefined) this.prevClose = close; + const bp = close - Math.min(low, this.prevClose); + const tr = Math.max(high, this.prevClose) - Math.min(low, this.prevClose); + this.bp7.push(bp); this.tr7.push(tr); + this.bp14.push(bp); this.tr14.push(tr); + this.bp28.push(bp); this.tr28.push(tr); + this.prevClose = close; + this.fill++; + if (this.fill < this.period3) return; + const avg7 = this.bp7.toArray().reduce((a, b) => a + b, 0) / this.tr7.toArray().reduce((a, b) => a + b, 0); + const avg14 = this.bp14.toArray().reduce((a, b) => a + b, 0) / this.tr14.toArray().reduce((a, b) => a + b, 0); + const avg28 = this.bp28.toArray().reduce((a, b) => a + b, 0) / this.tr28.toArray().reduce((a, b) => a + b, 0); + const uo = 100 * (4 * avg7 + 2 * avg14 + avg28) / 7; + this.nextValue = (high: number, low: number, close: number) => { + const bp = close - Math.min(low, this.prevClose); + const tr = Math.max(high, this.prevClose) - Math.min(low, this.prevClose); + this.bp7.push(bp); this.tr7.push(tr); + this.bp14.push(bp); this.tr14.push(tr); + this.bp28.push(bp); this.tr28.push(tr); + this.prevClose = close; + const avg7 = this.bp7.toArray().reduce((a, b) => a + b, 0) / this.tr7.toArray().reduce((a, b) => a + b, 0); + const avg14 = this.bp14.toArray().reduce((a, b) => a + b, 0) / this.tr14.toArray().reduce((a, b) => a + b, 0); + const avg28 = this.bp28.toArray().reduce((a, b) => a + b, 0) / this.tr28.toArray().reduce((a, b) => a + b, 0); + return 100 * (4 * avg7 + 2 * avg14 + avg28) / 7; + }; + return uo; + } + + /** + * Calculates Ultimate Oscillator for the current (not closed) bar without changing the internal state + * @param high High price of the current bar + * @param low Low price of the current bar + * @param close Close price of the current bar + */ + momentValue(high: number, low: number, close: number) { + const bp = close - Math.min(low, this.prevClose); + const tr = Math.max(high, this.prevClose) - Math.min(low, this.prevClose); + const bp7 = this.bp7.toArray().slice(); bp7.push(bp); if (bp7.length > this.period1) bp7.shift(); + const tr7 = this.tr7.toArray().slice(); tr7.push(tr); if (tr7.length > this.period1) tr7.shift(); + const bp14 = this.bp14.toArray().slice(); bp14.push(bp); if (bp14.length > this.period2) bp14.shift(); + const tr14 = this.tr14.toArray().slice(); tr14.push(tr); if (tr14.length > this.period2) tr14.shift(); + const bp28 = this.bp28.toArray().slice(); bp28.push(bp); if (bp28.length > this.period3) bp28.shift(); + const tr28 = this.tr28.toArray().slice(); tr28.push(tr); if (tr28.length > this.period3) tr28.shift(); + const avg7 = bp7.reduce((a, b) => a + b, 0) / tr7.reduce((a, b) => a + b, 0); + const avg14 = bp14.reduce((a, b) => a + b, 0) / tr14.reduce((a, b) => a + b, 0); + const avg28 = bp28.reduce((a, b) => a + b, 0) / tr28.reduce((a, b) => a + b, 0); + return 100 * (4 * avg7 + 2 * avg14 + avg28) / 7; + } +} \ No newline at end of file diff --git a/src/volume-oscillator.ts b/src/volume-oscillator.ts new file mode 100644 index 0000000..11d6318 --- /dev/null +++ b/src/volume-oscillator.ts @@ -0,0 +1,49 @@ +import { SMA } from './sma'; + +/** + * Volume Oscillator + * + * Volume Oscillator = SMA(short) - SMA(long) + */ +export class VolumeOscillator { + private smaShort: SMA; + private smaLong: SMA; + private fill = 0; + + /** + * @param shortPeriod Short period for SMA (default: 14) + * @param longPeriod Long period for SMA (default: 28) + */ + constructor(private shortPeriod = 14, private longPeriod = 28) { + this.smaShort = new SMA(shortPeriod); + this.smaLong = new SMA(longPeriod); + } + + /** + * Adds a new value and returns the Volume Oscillator + * @param volume Volume of the current bar + */ + nextValue(volume: number) { + const short = this.smaShort.nextValue(volume); + const long = this.smaLong.nextValue(volume); + this.fill++; + if (this.fill < this.longPeriod) return; + const vo = short - long; + this.nextValue = (volume: number) => { + const short = this.smaShort.nextValue(volume); + const long = this.smaLong.nextValue(volume); + return short - long; + }; + return vo; + } + + /** + * Calculates Volume Oscillator for the current (not closed) bar without changing the internal state + * @param volume Volume of the current bar + */ + momentValue(volume: number) { + const short = this.smaShort.momentValue(volume); + const long = this.smaLong.momentValue(volume); + return short - long; + } +} \ No newline at end of file