( data: ReadonlyArray<OHLCDataPoint>, targetPoints: number, )
| 23 | * - Output shape matches input shape (tuples → tuples, objects → objects) |
| 24 | */ |
| 25 | export function ohlcSample( |
| 26 | data: ReadonlyArray<OHLCDataPoint>, |
| 27 | targetPoints: number, |
| 28 | ): ReadonlyArray<OHLCDataPoint> { |
| 29 | const threshold = Math.floor(targetPoints); |
| 30 | const n = data.length; |
| 31 | |
| 32 | // Return original if already under target or insufficient target. |
| 33 | if (threshold < 2 || n <= threshold) return data; |
| 34 | |
| 35 | const out = new Array<OHLCDataPoint>(threshold); |
| 36 | |
| 37 | // Preserve first and last candles exactly. |
| 38 | out[0] = data[0]!; |
| 39 | out[threshold - 1] = data[n - 1]!; |
| 40 | |
| 41 | if (threshold === 2) return out; |
| 42 | |
| 43 | // Hoist tuple-vs-object detection once (assume homogeneous arrays). |
| 44 | const isTuple = isTupleOHLCDataPoint(data[0]!); |
| 45 | |
| 46 | // Bucket size for interior points: (n - 2) interior input points → (threshold - 2) interior output points. |
| 47 | const bucketSize = (n - 2) / (threshold - 2); |
| 48 | |
| 49 | if (isTuple) { |
| 50 | // Tuple format path: [timestamp, open, close, low, high] |
| 51 | const dataAsTuples = data as ReadonlyArray<OHLCDataPointTuple>; |
| 52 | |
| 53 | for (let bucket = 0; bucket < threshold - 2; bucket++) { |
| 54 | // Bucket range: [rangeStart, rangeEndExclusive) |
| 55 | let rangeStart = Math.floor(bucketSize * bucket) + 1; |
| 56 | let rangeEndExclusive = Math.min(Math.floor(bucketSize * (bucket + 1)) + 1, n - 1); |
| 57 | |
| 58 | // Defensive: ensure at least one candidate point. |
| 59 | if (rangeStart >= rangeEndExclusive) { |
| 60 | rangeStart = Math.min(rangeStart, n - 2); |
| 61 | rangeEndExclusive = Math.min(rangeStart + 1, n - 1); |
| 62 | } |
| 63 | |
| 64 | // Extract first and last candles in bucket. |
| 65 | const firstCandle = dataAsTuples[rangeStart]!; |
| 66 | const lastCandle = dataAsTuples[rangeEndExclusive - 1]!; |
| 67 | |
| 68 | const timestamp = firstCandle[0]; |
| 69 | const open = firstCandle[1]; |
| 70 | const close = lastCandle[2]; |
| 71 | |
| 72 | // Aggregate high and low across the bucket. |
| 73 | let high = -Infinity; |
| 74 | let low = Infinity; |
| 75 | for (let i = rangeStart; i < rangeEndExclusive; i++) { |
| 76 | const candle = dataAsTuples[i]!; |
| 77 | const candleLow = candle[3]; |
| 78 | const candleHigh = candle[4]; |
| 79 | if (candleHigh > high) high = candleHigh; |
| 80 | if (candleLow < low) low = candleLow; |
| 81 | } |
| 82 |
no test coverage detected