File size: 5,996 Bytes
66b7a93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c34a96e
 
 
 
66b7a93
 
c34a96e
 
66b7a93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86afb93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66b7a93
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/**
 * @module orderBlocks
 * Order block detection with validation and mitigation tracking.
 * Operates on arrays of candle objects: { time, open, high, low, close, volume }
 */

import { atr } from './indicators.js';

/**
 * @typedef {Object} OrderBlock
 * @property {'bullish'|'bearish'} type
 * @property {number} top          - High of the OB candle.
 * @property {number} bottom       - Low of the OB candle.
 * @property {number} index        - Candle index of the OB.
 * @property {number} time         - Timestamp of the OB candle.
 * @property {number} strength     - Quality score 0-100.
 * @property {boolean} mitigated
 * @property {number|null} mitigatedAt - Index where mitigated.
 */

/**
 * Detect order blocks in the candle array.
 *
 * Detection:
 * 1. Calculate ATR.
 * 2. For each candle i (1 ≤ i < length-1):
 *    - Bullish OB: candle[i] is bearish AND candle[i+1] has a bullish body > 1.5×ATR.
 *    - Bearish OB: candle[i] is bullish AND candle[i+1] has a bearish body > 1.5×ATR.
 * 3. Strength scored from impulse/ATR ratio, volume, and FVG presence.
 *
 * @param {{ time: number, open: number, high: number, low: number, close: number, volume: number }[]} candles
 * @param {number} [atrPeriod=14]
 * @returns {OrderBlock[]}
 */
export function detectOrderBlocks(candles, atrPeriod = 14) {
  if (!candles || candles.length < atrPeriod + 2) return [];

  const atrValues = atr(candles, atrPeriod);
  const orderBlocks = [];

  // Pre-compute average volume for strength scoring
  let volumeSum = 0;
  let volumeCount = 0;
  for (const c of candles) {
    if (c.volume > 0) {
      volumeSum += c.volume;
      volumeCount++;
    }
  }
  const avgVolume = volumeCount > 0 ? volumeSum / volumeCount : 1;

  for (let i = 1; i < candles.length - 1; i++) {
    const currentATR = atrValues[i];
    if (isNaN(currentATR) || currentATR <= 0) continue;

    const curr = candles[i];
    const next = candles[i + 1];

    const currBody = Math.abs(curr.close - curr.open);
    const nextBody = Math.abs(next.close - next.open);

    const currIsBearish = curr.close < curr.open;
    const currIsBullish = curr.close > curr.open;
    const nextIsBullish = next.close > next.open;
    const nextIsBearish = next.close < next.open;

    let obType = /** @type {'bullish'|'bearish'|null} */ (null);

    const isVolumeValid = (volumeCount === 0) || (next.volume > avgVolume * 1.5);

    // Bullish OB: bearish candle followed by strong bullish impulse + high volume
    if (currIsBearish && nextIsBullish && nextBody > 1.5 * currentATR && isVolumeValid) {
      obType = 'bullish';
    }
    // Bearish OB: bullish candle followed by strong bearish impulse + high volume
    else if (currIsBullish && nextIsBearish && nextBody > 1.5 * currentATR && isVolumeValid) {
      obType = 'bearish';
    }

    if (!obType) continue;

    // --- Strength scoring ---
    let strength = 0;

    // 1. Impulse body / ATR ratio → up to 40 points
    const impulseRatio = nextBody / currentATR;
    // 1.5× is minimum; scale from 1.5 (0 pts) up to ~4× (40 pts)
    strength += Math.min(40, Math.round(((impulseRatio - 1.5) / 2.5) * 40));

    // 2. Volume of impulse vs average → up to 30 points
    if (next.volume > 0 && avgVolume > 0) {
      const volRatio = next.volume / avgVolume;
      // 1× = 0 pts, 3× = 30 pts
      strength += Math.min(30, Math.round(Math.max(0, (volRatio - 1) / 2) * 30));
    }

    // 3. FVG presence → up to 30 points
    const hasFVG = checkFVG(candles, i, obType);
    if (hasFVG) strength += 30;

    // Clamp 0-100
    strength = Math.max(0, Math.min(100, strength));

    orderBlocks.push({
      type: obType,
      top: curr.high,
      bottom: curr.low,
      index: i,
      time: curr.time,
      strength,
      mitigated: false,
      mitigatedAt: null,
    });
  }

  return orderBlocks;
}

/**
 * Check if a Fair Value Gap exists around the order block candle.
 *
 * Bullish FVG: candle[i-1].high < candle[i+1].low  (gap up)
 * Bearish FVG: candle[i-1].low  > candle[i+1].high (gap down)
 *
 * @param {{ high: number, low: number }[]} candles
 * @param {number} i - Index of the OB candle.
 * @param {'bullish'|'bearish'} obType
 * @returns {boolean}
 */
function checkFVG(candles, i, obType) {
  if (i < 0 || i >= candles.length - 2) return false;

  const obCandle = candles[i];
  const postImpulse = candles[i + 2];

  if (obType === 'bullish') {
    return obCandle.high < postImpulse.low;
  } else {
    return obCandle.low > postImpulse.high;
  }
}

/**
 * Update mitigation status for order blocks.
 * A bullish OB is mitigated when any subsequent candle closes below OB.bottom.
 * A bearish OB is mitigated when any subsequent candle closes above OB.top.
 *
 * @param {OrderBlock[]} orderBlocks
 * @param {{ time: number, open: number, high: number, low: number, close: number, volume: number }[]} candles
 * @returns {OrderBlock[]} Updated array with mitigated flags.
 */
export function updateMitigation(orderBlocks, candles) {
  if (!orderBlocks || orderBlocks.length === 0 || !candles || candles.length === 0) {
    return orderBlocks || [];
  }

  return orderBlocks.map(ob => {
    if (ob.mitigated) return ob; // Already mitigated, skip

    const updated = { ...ob };

    // Only check candles after the OB breakout ( breakout candle is ob.index + 1, so check from +2 )
    for (let c = ob.index + 2; c < candles.length; c++) {
      if (ob.type === 'bullish') {
        const touches = candles[c].low <= ob.top;
        const breaks = candles[c].close < ob.bottom;
        if (touches || breaks) {
          updated.mitigated = true;
          updated.mitigatedAt = c;
          break;
        }
      } else if (ob.type === 'bearish') {
        const touches = candles[c].high >= ob.bottom;
        const breaks = candles[c].close > ob.top;
        if (touches || breaks) {
          updated.mitigated = true;
          updated.mitigatedAt = c;
          break;
        }
      }
    }

    return updated;
  });
}