@lacspace/patterns 0.0.0-stage → 1.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/dist/index.cjs ADDED
@@ -0,0 +1,664 @@
1
+ 'use strict';
2
+
3
+ // src/core.ts
4
+ var nulls = (n) => new Array(n).fill(null);
5
+ function rma(values, n) {
6
+ const out = nulls(values.length);
7
+ if (n < 1) return out;
8
+ let prev = null;
9
+ let seed = 0;
10
+ let seen = 0;
11
+ for (let i = 0; i < values.length; i++) {
12
+ const v = values[i];
13
+ if (v == null) continue;
14
+ if (prev == null) {
15
+ seed += v;
16
+ seen++;
17
+ if (seen === n) {
18
+ prev = seed / n;
19
+ out[i] = prev;
20
+ }
21
+ continue;
22
+ }
23
+ prev = (prev * (n - 1) + v) / n;
24
+ out[i] = prev;
25
+ }
26
+ return out;
27
+ }
28
+ function trueRange(bars2) {
29
+ return bars2.map((b, i) => {
30
+ if (i === 0) return b.high - b.low;
31
+ const pc = bars2[i - 1].close;
32
+ return Math.max(b.high - b.low, Math.abs(b.high - pc), Math.abs(b.low - pc));
33
+ });
34
+ }
35
+ function atr(bars2, n = 14) {
36
+ return rma(trueRange(bars2), n);
37
+ }
38
+ function volumeProfile(bars2, rows = 24) {
39
+ if (!bars2.length) return null;
40
+ let lo = Infinity, hi = -Infinity;
41
+ for (const b of bars2) {
42
+ lo = Math.min(lo, b.low);
43
+ hi = Math.max(hi, b.high);
44
+ }
45
+ if (!(hi > lo)) return null;
46
+ const step = (hi - lo) / rows;
47
+ const buckets = Array.from({ length: rows }, (_, k) => ({ lo: lo + k * step, hi: lo + (k + 1) * step, up: 0, down: 0 }));
48
+ for (const b of bars2) {
49
+ if (!b.volume) continue;
50
+ const a2 = Math.max(0, Math.floor((b.low - lo) / step));
51
+ const z2 = Math.min(rows - 1, Math.floor((b.high - lo) / step));
52
+ const share = b.volume / (z2 - a2 + 1);
53
+ for (let k = a2; k <= z2; k++) {
54
+ if (b.close >= b.open) buckets[k].up += share;
55
+ else buckets[k].down += share;
56
+ }
57
+ }
58
+ let poc = 0;
59
+ buckets.forEach((x, k) => {
60
+ if (x.up + x.down > buckets[poc].up + buckets[poc].down) poc = k;
61
+ });
62
+ const total = buckets.reduce((s, x) => s + x.up + x.down, 0);
63
+ let inVa = buckets[poc].up + buckets[poc].down, a = poc, z = poc;
64
+ while (inVa < total * 0.7 && (a > 0 || z < rows - 1)) {
65
+ const below = a > 0 ? buckets[a - 1].up + buckets[a - 1].down : -1;
66
+ const above = z < rows - 1 ? buckets[z + 1].up + buckets[z + 1].down : -1;
67
+ if (above >= below) {
68
+ z++;
69
+ inVa += above;
70
+ } else {
71
+ a--;
72
+ inVa += below;
73
+ }
74
+ }
75
+ return { buckets, poc, vaLow: buckets[a].lo, vaHigh: buckets[z].hi, max: Math.max(...buckets.map((x) => x.up + x.down)) };
76
+ }
77
+
78
+ // src/patterns.ts
79
+ function swings(bars2, k = 2) {
80
+ const n = bars2.length;
81
+ const a = atr(bars2, 14);
82
+ const at = new Array(n);
83
+ let rs = 0;
84
+ for (let i = 0; i < n; i++) {
85
+ rs += bars2[i].high - bars2[i].low;
86
+ at[i] = a[i] ?? rs / (i + 1);
87
+ }
88
+ const piv = [];
89
+ if (n < 3) return { piv, tail: null, atr: at };
90
+ let trend = 0;
91
+ let hiI = 0;
92
+ let loI = 0;
93
+ for (let i = 1; i < n; i++) {
94
+ const t = at[i] * k;
95
+ const b = bars2[i];
96
+ if (trend === 0) {
97
+ if (b.high >= bars2[hiI].high) hiI = i;
98
+ if (b.low <= bars2[loI].low) loI = i;
99
+ if (bars2[hiI].high - bars2[loI].low >= t) {
100
+ if (hiI > loI) {
101
+ piv.push({ i: loI, p: bars2[loI].low, hi: false });
102
+ trend = 1;
103
+ } else {
104
+ piv.push({ i: hiI, p: bars2[hiI].high, hi: true });
105
+ trend = -1;
106
+ }
107
+ }
108
+ } else if (trend === 1) {
109
+ if (b.high >= bars2[hiI].high) hiI = i;
110
+ else if (bars2[hiI].high - b.low >= t) {
111
+ piv.push({ i: hiI, p: bars2[hiI].high, hi: true });
112
+ trend = -1;
113
+ loI = i;
114
+ }
115
+ } else {
116
+ if (b.low <= bars2[loI].low) loI = i;
117
+ else if (b.high - bars2[loI].low >= t) {
118
+ piv.push({ i: loI, p: bars2[loI].low, hi: false });
119
+ trend = 1;
120
+ hiI = i;
121
+ }
122
+ }
123
+ }
124
+ const tail = trend === 1 ? { i: hiI, p: bars2[hiI].high, hi: true } : trend === -1 ? { i: loI, p: bars2[loI].low, hi: false } : null;
125
+ return { piv, tail, atr: at };
126
+ }
127
+ var r2 = (n) => Math.round(n * 100) / 100;
128
+ var fmt = (n) => Math.abs(n) >= 1e3 ? n.toFixed(0) : n.toFixed(2);
129
+ var HARMONICS = [
130
+ { name: "Gartley", ab: [0.618, 0.618], bc: [0.382, 0.886], cd: [1.13, 1.618], xd: [0.786, 0.786] },
131
+ { name: "Bat", ab: [0.382, 0.5], bc: [0.382, 0.886], cd: [1.618, 2.618], xd: [0.886, 0.886] },
132
+ { name: "Alt Bat", ab: [0.382, 0.382], bc: [0.382, 0.886], cd: [2, 3.618], xd: [1.13, 1.13] },
133
+ { name: "Butterfly", ab: [0.786, 0.786], bc: [0.382, 0.886], cd: [1.618, 2.618], xd: [1.27, 1.618] },
134
+ { name: "Crab", ab: [0.382, 0.618], bc: [0.382, 0.886], cd: [2.24, 3.618], xd: [1.618, 1.618] },
135
+ { name: "Deep Crab", ab: [0.886, 0.886], bc: [0.382, 0.886], cd: [2, 3.618], xd: [1.618, 1.618] }
136
+ ];
137
+ var inR = (v, r, tol) => v >= r[0] * (1 - tol) && v <= r[1] * (1 + tol);
138
+ function harmonics(bars2, k = 2, tolPct = 6, show = 3, up = "#16A34A", down = "#DC2626") {
139
+ const { piv, tail } = swings(bars2, k);
140
+ const all = tail ? [...piv, tail] : piv;
141
+ const tol = tolPct / 100;
142
+ const hits = [];
143
+ const legLen = (a, b) => Math.abs(b.p - a.p);
144
+ for (let s = 0; s + 4 < all.length; s++) {
145
+ const [X, A, B, C, D] = all.slice(s, s + 5);
146
+ const XA = legLen(X, A), AB = legLen(A, B), BC = legLen(B, C), CD = legLen(C, D);
147
+ if (!XA || !AB || !BC) continue;
148
+ const bull = !X.hi;
149
+ const ab = AB / XA, bc = BC / AB, cd = CD / BC, xd = Math.abs(A.p - D.p) / XA;
150
+ let name = null;
151
+ for (const h of HARMONICS) {
152
+ if (bc > 1 * (1 + tol)) break;
153
+ if (inR(ab, h.ab, tol) && inR(bc, h.bc, tol) && inR(cd, h.cd, tol) && inR(xd, h.xd, tol)) {
154
+ name = h.name;
155
+ break;
156
+ }
157
+ }
158
+ if (!name) {
159
+ const xc = Math.abs(C.p - X.p) / XA;
160
+ const xcD = Math.abs(C.p - D.p) / Math.abs(C.p - X.p);
161
+ const beyondA = bull ? C.p > A.p : C.p < A.p;
162
+ if (beyondA && inR(ab, [0.382, 0.618], tol) && inR(xc, [1.13, 1.414], tol) && inR(xcD, [0.786, 0.786], tol)) name = "Cypher";
163
+ }
164
+ if (name) hits.push({ name, bull, pts: [X, A, B, C, D], forming: D === tail });
165
+ }
166
+ for (let s = Math.max(0, all.length - 6); s + 3 < all.length; s++) {
167
+ const [A, B, C, D] = all.slice(s, s + 4);
168
+ const AB = legLen(A, B), BC = legLen(B, C), CD = legLen(C, D);
169
+ if (!AB || !BC) continue;
170
+ const bc = BC / AB;
171
+ if (inR(bc, [0.382, 0.886], tol) && Math.abs(CD / AB - 1) <= tol * 2) {
172
+ if (!hits.some((h) => h.pts[4] === D)) hits.push({ name: "AB=CD", bull: A.hi, pts: [A, B, C, D], forming: D === tail });
173
+ }
174
+ }
175
+ const paths = [];
176
+ const zones = [];
177
+ const shown = hits.slice(-show);
178
+ for (const h of shown) {
179
+ const color = h.bull ? up : down;
180
+ const tag = `${h.bull ? "Bullish" : "Bearish"} ${h.name}${h.forming ? " (forming)" : ""}`;
181
+ if (h.pts.length === 5) {
182
+ const [X, A, B, C, D] = h.pts;
183
+ paths.push({ pts: [{ i: X.i, p: X.p, label: "X" }, { i: A.i, p: A.p, label: "A" }, { i: B.i, p: B.p, label: "B" }], color, fill: true, width: 1.5 });
184
+ paths.push({ pts: [{ i: B.i, p: B.p }, { i: C.i, p: C.p, label: "C" }, { i: D.i, p: D.p, label: "D" }], color, fill: true, width: 1.5, label: tag });
185
+ paths.push({ pts: [{ i: X.i, p: X.p }, { i: B.i, p: B.p }], color, dashed: true, width: 1, label: (Math.abs(A.p - B.p) / Math.abs(A.p - X.p)).toFixed(3), labelAt: 0.5 });
186
+ paths.push({ pts: [{ i: B.i, p: B.p }, { i: D.i, p: D.p }], color, dashed: true, width: 1, label: (Math.abs(C.p - D.p) / Math.abs(C.p - B.p)).toFixed(3), labelAt: 0.5 });
187
+ paths.push({ pts: [{ i: X.i, p: X.p }, { i: D.i, p: D.p }], color, dashed: true, width: 1, label: (Math.abs(A.p - D.p) / Math.abs(A.p - X.p)).toFixed(3), labelAt: 0.5 });
188
+ } else {
189
+ const [A, B, C, D] = h.pts;
190
+ paths.push({ pts: [{ i: A.i, p: A.p, label: "A" }, { i: B.i, p: B.p, label: "B" }, { i: C.i, p: C.p, label: "C" }, { i: D.i, p: D.p, label: "D" }], color, width: 1.5, label: tag });
191
+ }
192
+ }
193
+ if (piv.length >= 4) {
194
+ const [X, A, B, C] = piv.slice(-4);
195
+ const XA = legLen(X, A), AB = legLen(A, B), BC = legLen(B, C);
196
+ if (XA && AB && BC) {
197
+ const bull = !X.hi;
198
+ const dir = bull ? -1 : 1;
199
+ const ab = AB / XA, bc = BC / AB;
200
+ for (const h of HARMONICS) {
201
+ if (!inR(ab, h.ab, tol) || !inR(bc, h.bc, tol)) continue;
202
+ const xd1 = A.p + dir * h.xd[0] * XA, xd2 = A.p + dir * h.xd[1] * XA;
203
+ const cd1 = C.p + dir * h.cd[0] * BC, cd2 = C.p + dir * h.cd[1] * BC;
204
+ const loX = Math.min(xd1, xd2) - XA * tol * 0.5, hiX = Math.max(xd1, xd2) + XA * tol * 0.5;
205
+ const lo = Math.max(loX, Math.min(cd1, cd2)), hi = Math.min(hiX, Math.max(cd1, cd2));
206
+ if (!(hi >= lo)) continue;
207
+ const done = tail && tail.i > C.i && (bull ? tail.p <= hi : tail.p >= lo);
208
+ zones.push({ from: C.i, to: null, top: r2(hi + (hi === lo ? XA * 0.01 : 0)), bottom: r2(lo), color: bull ? up : down, dashed: true, label: `${bull ? "Bullish" : "Bearish"} ${h.name} D zone ${fmt(lo)}\u2013${fmt(hi)}${done ? " \xB7 reached" : ""}` });
209
+ paths.push({ pts: [{ i: X.i, p: X.p, label: "X" }, { i: A.i, p: A.p, label: "A" }, { i: B.i, p: B.p, label: "B" }, { i: C.i, p: C.p, label: "C" }], color: bull ? up : down, dashed: true, width: 1 });
210
+ break;
211
+ }
212
+ }
213
+ }
214
+ return { paths, zones, hits };
215
+ }
216
+ function elliott(bars2, k = 2, up = "#16A34A", down = "#DC2626", neutral = "#94A3B8") {
217
+ const { piv, tail } = swings(bars2, k);
218
+ const all = tail ? [...piv, tail] : piv;
219
+ const paths = [];
220
+ const zones = [];
221
+ const valid = (w, upTrend, upTo = 5) => {
222
+ const s = upTrend ? 1 : -1;
223
+ const v = w.map((x) => s * x.p);
224
+ const w1 = v[1] - v[0], w3 = upTo >= 3 ? v[3] - v[2] : 0;
225
+ if (!(w1 > 0)) return false;
226
+ if (!(v[2] > v[0])) return false;
227
+ if (upTo >= 3 && !(v[3] > v[1])) return false;
228
+ if (upTo >= 4 && !(v[4] > v[1])) return false;
229
+ if (upTo >= 5) {
230
+ const w5 = v[5] - v[4];
231
+ if (!(w5 > 0)) return false;
232
+ if (w3 < w1 && w3 < w5) return false;
233
+ }
234
+ return true;
235
+ };
236
+ let found = null;
237
+ for (let s = all.length - 6; s >= 0; s--) {
238
+ const w = all.slice(s, s + 6);
239
+ const upTrend = !w[0].hi;
240
+ if (valid(w, upTrend)) {
241
+ found = { start: s, upTrend };
242
+ break;
243
+ }
244
+ }
245
+ if (found) {
246
+ const w = all.slice(found.start, found.start + 6);
247
+ const color = found.upTrend ? up : down;
248
+ paths.push({ pts: w.map((x, j) => ({ i: x.i, p: x.p, label: j === 0 ? "0" : `(${j})` })), color, width: 2, label: `Impulse ${found.upTrend ? "up" : "down"}${w[5] === tail ? " \xB7 wave 5 forming" : ""}` });
249
+ const after = all.slice(found.start + 5, found.start + 9);
250
+ if (after.length >= 2) {
251
+ const lab = ["", "(a)", "(b)", "(c)"];
252
+ paths.push({ pts: after.map((x, j) => ({ i: x.i, p: x.p, label: lab[j] || void 0 })), color: neutral, width: 1.5, dashed: true, label: "Correction" });
253
+ }
254
+ }
255
+ const last5 = all.slice(-5);
256
+ if (last5.length === 5 && (!found || found.start + 5 < all.length - 5)) {
257
+ const upTrend = !last5[0].hi;
258
+ if (valid(last5, upTrend, 4) && (last5[4] !== tail || piv.length >= 5)) {
259
+ const s = upTrend ? 1 : -1;
260
+ const [p0, p1, , p3, p4] = last5;
261
+ const w1 = Math.abs(p1.p - p0.p);
262
+ const t1 = p4.p + s * w1;
263
+ const t2 = p4.p + s * 0.618 * Math.abs(p3.p - p0.p);
264
+ const color = upTrend ? up : down;
265
+ paths.push({ pts: last5.map((x, j) => ({ i: x.i, p: x.p, label: j === 0 ? "0" : `(${j})` })), color, width: 1.5, dashed: true, label: "Waves 1\u20134 (possible)" });
266
+ zones.push({ from: p4.i, to: null, top: r2(Math.max(t1, t2)), bottom: r2(Math.min(t1, t2)), color, dashed: true, label: `Wave 5 target ${fmt(Math.min(t1, t2))}\u2013${fmt(Math.max(t1, t2))}` });
267
+ }
268
+ }
269
+ return { paths, zones };
270
+ }
271
+ function fit(pts) {
272
+ const n = pts.length;
273
+ const mx = pts.reduce((s, q) => s + q.i, 0) / n;
274
+ const my = pts.reduce((s, q) => s + q.p, 0) / n;
275
+ let sxy = 0, sxx = 0;
276
+ for (const q of pts) {
277
+ sxy += (q.i - mx) * (q.p - my);
278
+ sxx += (q.i - mx) ** 2;
279
+ }
280
+ const m = sxx ? sxy / sxx : 0;
281
+ const b = my - m * mx;
282
+ const err = Math.max(...pts.map((q) => Math.abs(q.p - (m * q.i + b))));
283
+ return { m, b, at: (i) => m * i + b, err };
284
+ }
285
+ function chartPatterns(bars2, k = 1.5, look = 200, up = "#16A34A", down = "#DC2626", neutral = "#94A3B8", show = 2) {
286
+ const n = bars2.length;
287
+ const { piv, tail, atr: at } = swings(bars2, k);
288
+ const paths = [];
289
+ const zones = [];
290
+ if (n < 20 || piv.length < 3) return { paths, zones, found: [] };
291
+ const found = [];
292
+ const from = n - look;
293
+ const A = (i) => at[Math.min(n - 1, Math.max(0, i))];
294
+ const lastClose = bars2[n - 1].close;
295
+ const firstClose = (i, test) => {
296
+ for (let j = i + 1; j < n; j++) if (test(j)) return j;
297
+ return -1;
298
+ };
299
+ const liveFrom = piv.length >= 3 ? piv[piv.length - 3].i : 0;
300
+ const cands = [];
301
+ for (let s = 0; s + 2 < piv.length; s++) {
302
+ const [p1, mid, p2] = piv.slice(s, s + 3);
303
+ if (p2.i < from) continue;
304
+ const a = A(p2.i);
305
+ if (Math.abs(p1.p - p2.p) > 0.6 * a) continue;
306
+ const top = p1.hi;
307
+ const sg = top ? 1 : -1;
308
+ const peak = top ? Math.max(p1.p, p2.p) : Math.min(p1.p, p2.p);
309
+ const h = Math.abs(peak - mid.p);
310
+ if (h < 2 * a) continue;
311
+ const brk = firstClose(p2.i, (j) => sg * (mid.p - bars2[j].close) > 0);
312
+ const fail = firstClose(p2.i, (j) => sg * (bars2[j].close - peak) > 0);
313
+ if (fail >= 0 && (brk < 0 || fail < brk)) continue;
314
+ if (brk < 0 && p2.i < liveFrom) continue;
315
+ const color = top ? down : up;
316
+ const name = top ? "Double top" : "Double bottom";
317
+ const target = mid.p - sg * h;
318
+ const c = { start: p1.i, end: brk >= 0 ? brk : p2.i, name, paths: [], zones: [] };
319
+ c.paths.push({ pts: [{ i: p1.i, p: p1.p, label: top ? "T1" : "B1" }, { i: mid.i, p: mid.p }, { i: p2.i, p: p2.p, label: top ? "T2" : "B2" }], color, width: 1.5, label: `${name}${brk >= 0 ? " \xB7 confirmed" : " \xB7 watching neckline"}`, labelAt: 1 });
320
+ c.zones.push({ from: p1.i, to: brk >= 0 ? brk : null, top: mid.p, bottom: mid.p, color: neutral, dashed: true, label: "Neckline" });
321
+ if (brk >= 0) c.zones.push({ from: brk, to: Math.min(n - 1, brk + (p2.i - p1.i)), top: target, bottom: target, color, dashed: true, label: `Measured move ${fmt(target)}` });
322
+ cands.push(c);
323
+ }
324
+ for (let s = 0; s + 4 < piv.length; s++) {
325
+ const [l, t1, h, t2, r] = piv.slice(s, s + 5);
326
+ if (r.i < from) continue;
327
+ const top = h.hi;
328
+ const sg = top ? 1 : -1;
329
+ const a = A(r.i);
330
+ if (!(sg * (h.p - l.p) > 0.5 * a && sg * (h.p - r.p) > 0.5 * a)) continue;
331
+ if (Math.abs(l.p - r.p) > 1.25 * a) continue;
332
+ const neck = fit([t1, t2]);
333
+ const depth = Math.abs(h.p - neck.at(h.i));
334
+ if (depth < 2 * a) continue;
335
+ const brk = firstClose(r.i, (j) => sg * (neck.at(j) - bars2[j].close) > 0);
336
+ const fail = firstClose(r.i, (j) => sg * (bars2[j].close - h.p) > 0);
337
+ if (fail >= 0 && (brk < 0 || fail < brk)) continue;
338
+ if (brk < 0 && r.i < liveFrom) continue;
339
+ const color = top ? down : up;
340
+ const name = top ? "Head & shoulders" : "Inverse head & shoulders";
341
+ const c = { start: l.i, end: brk >= 0 ? brk : r.i, name, paths: [], zones: [] };
342
+ c.paths.push({ pts: [{ i: l.i, p: l.p, label: "LS" }, { i: t1.i, p: t1.p }, { i: h.i, p: h.p, label: "H" }, { i: t2.i, p: t2.p }, { i: r.i, p: r.p, label: "RS" }], color, width: 1.5, label: `${name}${brk >= 0 ? " \xB7 confirmed" : " \xB7 watching neckline"}`, labelAt: 2 });
343
+ const e = brk >= 0 ? brk : n - 1;
344
+ c.paths.push({ pts: [{ i: t1.i, p: neck.at(t1.i) }, { i: e, p: neck.at(e) }], color: neutral, dashed: true, width: 1, label: "Neckline", labelAt: 0 });
345
+ if (brk >= 0) {
346
+ const target = neck.at(brk) - sg * depth;
347
+ c.zones.push({ from: brk, to: Math.min(n - 1, brk + (r.i - l.i)), top: target, bottom: target, color, dashed: true, label: `Measured move ${fmt(target)}` });
348
+ }
349
+ cands.push(c);
350
+ }
351
+ cands.sort((x, y) => y.end - x.end || (y.name.includes("shoulders") ? 1 : 0) - (x.name.includes("shoulders") ? 1 : 0));
352
+ const kept = [];
353
+ for (const c of cands) {
354
+ if (kept.length >= show) break;
355
+ if (kept.some((q) => c.start < q.end && q.start < c.end)) continue;
356
+ kept.push(c);
357
+ }
358
+ for (const c of kept) {
359
+ paths.push(...c.paths);
360
+ zones.push(...c.zones);
361
+ found.push(c.name);
362
+ }
363
+ const conf = tail ? [...piv, tail] : piv;
364
+ for (const m of [6, 5, 4]) {
365
+ if (conf.length < m) continue;
366
+ const w = conf.slice(-m);
367
+ if (w[0].i < from) continue;
368
+ const hs = w.filter((q) => q.hi), ls = w.filter((q) => !q.hi);
369
+ if (hs.length < 2 || ls.length < 2) continue;
370
+ const U = fit(hs), Lo = fit(ls);
371
+ const a = A(w[w.length - 1].i);
372
+ if (U.err > 0.6 * a || Lo.err > 0.6 * a) continue;
373
+ const i0 = w[0].i, i1 = n - 1;
374
+ const span = i1 - i0;
375
+ const du = U.m * span, dl = Lo.m * span;
376
+ const flat = (d) => Math.abs(d) <= 1 * a;
377
+ const w0 = U.at(i0) - Lo.at(i0), w1 = U.at(i1) - Lo.at(i1);
378
+ if (!(w0 > 0)) continue;
379
+ const conv = w1 < w0 * 0.75, par = Math.abs(w1 - w0) <= w0 * 0.25;
380
+ let name = "";
381
+ if (flat(du) && dl > a && conv) name = "Ascending triangle";
382
+ else if (flat(dl) && du < -a && conv) name = "Descending triangle";
383
+ else if (du < -a && dl > a) name = "Symmetrical triangle";
384
+ else if (du > 0 && dl > 0 && conv && !flat(dl)) name = "Rising wedge";
385
+ else if (du < 0 && dl < 0 && conv && !flat(du)) name = "Falling wedge";
386
+ else if (flat(du) && flat(dl) && par) name = "Range (rectangle)";
387
+ else if (du > a && dl > a && par) name = "Rising channel";
388
+ else if (du < -a && dl < -a && par) name = "Falling channel";
389
+ else if (w1 > w0 * 1.25) name = "Broadening formation";
390
+ if (!name) continue;
391
+ const brokeUp = lastClose > U.at(i1);
392
+ const brokeDn = lastClose < Lo.at(i1);
393
+ const color = brokeUp ? up : brokeDn ? down : neutral;
394
+ const st = brokeUp ? " \xB7 broke up" : brokeDn ? " \xB7 broke down" : "";
395
+ paths.push({ pts: [{ i: i0, p: U.at(i0) }, { i: i1, p: U.at(i1) }], color, width: 1.5, label: `${name}${st}`, labelAt: 1 });
396
+ paths.push({ pts: [{ i: i0, p: Lo.at(i0) }, { i: i1, p: Lo.at(i1) }], color, width: 1.5 });
397
+ found.push(name);
398
+ break;
399
+ }
400
+ return { paths, zones, found };
401
+ }
402
+ var LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
403
+ function niceStep(v) {
404
+ if (!(v > 0)) return 0;
405
+ const mag = Math.pow(10, Math.floor(Math.log10(v)));
406
+ const f = v / mag;
407
+ return (f < 1.5 ? 1 : f < 3.5 ? 2 : f < 7.5 ? 5 : 10) * mag;
408
+ }
409
+ function sessions(bars2) {
410
+ const out = [];
411
+ let cur = -1;
412
+ bars2.forEach((b, i) => {
413
+ const d = Math.floor(b.time / 86400);
414
+ if (d !== cur) {
415
+ out.push({ from: i, to: i });
416
+ cur = d;
417
+ } else out[out.length - 1].to = i;
418
+ });
419
+ return out;
420
+ }
421
+ function valueArea(counts, poc, share = 0.7) {
422
+ const total = counts.reduce((s, x) => s + x, 0);
423
+ let inVa = counts[poc], a = poc, z = poc;
424
+ while (inVa < total * share && (a > 0 || z < counts.length - 1)) {
425
+ const below = a > 0 ? counts[a - 1] : -1;
426
+ const above = z < counts.length - 1 ? counts[z + 1] : -1;
427
+ if (above >= below) {
428
+ z++;
429
+ inVa += above;
430
+ } else {
431
+ a--;
432
+ inVa += below;
433
+ }
434
+ }
435
+ return { a, z };
436
+ }
437
+ function tpoProfiles(bars2, rowsWanted = 24, sessionsShown = 5, color = "#6366F1") {
438
+ const ss = sessions(bars2).slice(-sessionsShown);
439
+ if (!ss.length) return [];
440
+ const ranges = ss.map((s) => {
441
+ let hi = -Infinity, lo = Infinity;
442
+ for (let i = s.from; i <= s.to; i++) {
443
+ hi = Math.max(hi, bars2[i].high);
444
+ lo = Math.min(lo, bars2[i].low);
445
+ }
446
+ return hi - lo;
447
+ }).filter((r) => r > 0).sort((x, y) => x - y);
448
+ const step = niceStep((ranges[Math.floor(ranges.length / 2)] || 0) / rowsWanted);
449
+ if (!step) return [];
450
+ const out = [];
451
+ for (const s of ss) {
452
+ const t0 = bars2[s.from].time;
453
+ const period = bars2.length > 1 && bars2[s.from + 1] && bars2[s.from + 1].time - t0 >= 3600 ? 3600 : 1800;
454
+ let lo = Infinity, hi = -Infinity;
455
+ for (let i = s.from; i <= s.to; i++) {
456
+ hi = Math.max(hi, bars2[i].high);
457
+ lo = Math.min(lo, bars2[i].low);
458
+ }
459
+ if (!(hi >= lo)) continue;
460
+ const base = Math.floor(lo / step + 1e-9);
461
+ const top = Math.floor(hi / step + 1e-9);
462
+ const nRows = top - base + 1;
463
+ if (nRows > 200) continue;
464
+ const letters = new Array(nRows).fill("");
465
+ const marked = Array.from({ length: nRows }, () => /* @__PURE__ */ new Set());
466
+ let ibLo = Infinity, ibHi = -Infinity;
467
+ for (let i = s.from; i <= s.to; i++) {
468
+ const b = bars2[i];
469
+ const per = Math.min(LETTERS.length - 1, Math.floor((b.time - t0) / period));
470
+ if (b.time - t0 < 3600) {
471
+ ibLo = Math.min(ibLo, b.low);
472
+ ibHi = Math.max(ibHi, b.high);
473
+ }
474
+ const a2 = Math.floor(b.low / step + 1e-9) - base, z2 = Math.floor(b.high / step + 1e-9) - base;
475
+ for (let r = Math.max(0, a2); r <= Math.min(nRows - 1, z2); r++) {
476
+ if (!marked[r].has(per)) {
477
+ marked[r].add(per);
478
+ letters[r] += LETTERS[per];
479
+ }
480
+ }
481
+ }
482
+ const counts = marked.map((m) => m.size);
483
+ let poc = 0;
484
+ const mid = (nRows - 1) / 2;
485
+ counts.forEach((c, r) => {
486
+ if (c > counts[poc] || c === counts[poc] && Math.abs(r - mid) < Math.abs(poc - mid)) poc = r;
487
+ });
488
+ const { a, z } = valueArea(counts, poc);
489
+ out.push({
490
+ from: s.from,
491
+ to: s.to,
492
+ kind: "tpo",
493
+ color,
494
+ rows: counts.map((v, r) => ({ lo: (base + r) * step, hi: (base + r + 1) * step, v, letters: letters[r] })),
495
+ poc,
496
+ vaLow: (base + a) * step,
497
+ vaHigh: (base + z + 1) * step,
498
+ ib: ibHi >= ibLo ? { lo: ibLo, hi: ibHi } : void 0
499
+ });
500
+ }
501
+ return out;
502
+ }
503
+ function sessionVolumeProfiles(bars2, rows = 20, sessionsShown = 5, color = "#0EA5E9") {
504
+ const out = [];
505
+ for (const s of sessions(bars2).slice(-sessionsShown)) {
506
+ const vp = volumeProfile(bars2.slice(s.from, s.to + 1), rows);
507
+ if (!vp || !vp.max) continue;
508
+ out.push({
509
+ from: s.from,
510
+ to: s.to,
511
+ kind: "vol",
512
+ color,
513
+ rows: vp.buckets.map((b) => ({ lo: b.lo, hi: b.hi, v: b.up + b.down, up: b.up })),
514
+ poc: vp.poc,
515
+ vaLow: vp.vaLow,
516
+ vaHigh: vp.vaHigh
517
+ });
518
+ }
519
+ return out;
520
+ }
521
+
522
+ // src/altCharts.ts
523
+ function kagi(bars2, rev) {
524
+ const out = [];
525
+ const lines = [];
526
+ if (bars2.length < 2 || !(rev > 0)) return { bars: out, lines };
527
+ let dir = 0;
528
+ let start = bars2[0].close;
529
+ let end = start;
530
+ let t = bars2[0].time;
531
+ let vol = 0;
532
+ let lastT = -Infinity;
533
+ let shoulder = null;
534
+ let waist = null;
535
+ let yang = true;
536
+ const close = () => {
537
+ const time = Math.max(t, lastT + 1);
538
+ lastT = time;
539
+ let flipAt = null;
540
+ const startYang = yang;
541
+ if (dir > 0 && !yang && shoulder != null && end > shoulder) {
542
+ flipAt = shoulder;
543
+ yang = true;
544
+ }
545
+ if (dir < 0 && yang && waist != null && end < waist) {
546
+ flipAt = waist;
547
+ yang = false;
548
+ }
549
+ out.push({ time, open: start, high: Math.max(start, end), low: Math.min(start, end), close: end, volume: vol });
550
+ lines.push({ yang: startYang, flipAt });
551
+ if (dir > 0) shoulder = end;
552
+ else waist = end;
553
+ vol = 0;
554
+ };
555
+ for (let i = 0; i < bars2.length; i++) {
556
+ const b = bars2[i];
557
+ const c = b.close;
558
+ vol += b.volume || 0;
559
+ if (dir === 0) {
560
+ if (c >= start + rev || c <= start - rev) {
561
+ dir = c > start ? 1 : -1;
562
+ yang = dir > 0;
563
+ end = c;
564
+ }
565
+ continue;
566
+ }
567
+ if (dir * (c - end) > 0) {
568
+ end = c;
569
+ } else if (dir * (end - c) >= rev) {
570
+ close();
571
+ start = end;
572
+ end = c;
573
+ dir = -dir;
574
+ t = b.time;
575
+ }
576
+ }
577
+ if (dir !== 0) close();
578
+ return { bars: out, lines };
579
+ }
580
+ function pointFigure(bars2, box, rev = 3) {
581
+ const out = [];
582
+ const cols = [];
583
+ if (bars2.length < 2 || !(box > 0)) return { bars: out, cols };
584
+ const times = [];
585
+ const vols = [];
586
+ let lastT = -Infinity;
587
+ let vol = 0;
588
+ const k0 = Math.round(bars2[0].close / box);
589
+ let cur = null;
590
+ const open = (c, time) => {
591
+ const t = Math.max(time, lastT + 1);
592
+ lastT = t;
593
+ cols.push(c);
594
+ times.push(t);
595
+ vols.push(0);
596
+ cur = c;
597
+ };
598
+ for (const b of bars2) {
599
+ const c = b.close;
600
+ vol += b.volume || 0;
601
+ const up = Math.floor(c / box + 1e-9);
602
+ const dn = Math.ceil(c / box - 1e-9);
603
+ if (!cur) {
604
+ if (up >= k0 + 1) open({ up: true, lo: k0, hi: up }, b.time);
605
+ else if (dn <= k0 - 1) open({ up: false, lo: dn, hi: k0 }, b.time);
606
+ } else {
607
+ const col = cur;
608
+ if (col.up) {
609
+ if (up > col.hi) col.hi = up;
610
+ else if (dn <= col.hi - rev) open({ up: false, lo: dn, hi: col.hi - 1 }, b.time);
611
+ } else {
612
+ if (dn < col.lo) col.lo = dn;
613
+ else if (up >= col.lo + rev) open({ up: true, lo: col.lo + 1, hi: up }, b.time);
614
+ }
615
+ }
616
+ if (cols.length) {
617
+ vols[cols.length - 1] += vol;
618
+ vol = 0;
619
+ }
620
+ }
621
+ cols.forEach((c, k) => {
622
+ const lo = c.lo * box - box / 2;
623
+ const hi = c.hi * box + box / 2;
624
+ out.push({ time: times[k], open: c.up ? lo : hi, high: hi, low: lo, close: c.up ? hi : lo, volume: vols[k] });
625
+ });
626
+ return { bars: out, cols };
627
+ }
628
+
629
+ // src/index.ts
630
+ var VERSION = "1.0.0";
631
+ var bars = { type: "array", description: "OHLCV bars: { time (unix s), open, high, low, close, volume }[]" };
632
+ function describe() {
633
+ return {
634
+ name: "@lacspace/patterns",
635
+ version: VERSION,
636
+ summary: "Rule-based chart-pattern detection on OHLCV: ATR swings, harmonics, Elliott impulse count, double tops/bottoms, head & shoulders, triangles/wedges/channels, TPO and session volume profiles, Kagi and Point & Figure. Descriptive only.",
637
+ commands: [
638
+ { name: "swings", input: { type: "object", properties: { bars, k: { type: "number", default: 2 } }, required: ["bars"] }, output: "{ piv: Pivot[], tail: Pivot|null, atr: number[] }" },
639
+ { name: "harmonics", input: { type: "object", properties: { bars, k: { type: "number", default: 2 }, tolPct: { type: "number", default: 6 }, show: { type: "integer", default: 3 } }, required: ["bars"] }, output: "{ paths, zones, hits: HarmonicHit[] }" },
640
+ { name: "elliott", input: { type: "object", properties: { bars, k: { type: "number", default: 2 } }, required: ["bars"] }, output: "{ paths, zones }" },
641
+ { name: "chartPatterns", input: { type: "object", properties: { bars, k: { type: "number", default: 1.5 }, look: { type: "integer", default: 200 }, show: { type: "integer", default: 2 } }, required: ["bars"] }, output: "{ paths, zones, found: string[] }" },
642
+ { name: "tpoProfiles", input: { type: "object", properties: { bars, rowsWanted: { type: "integer", default: 24 }, sessionsShown: { type: "integer", default: 5 } }, required: ["bars"] }, output: "StudyProfile[]" },
643
+ { name: "sessionVolumeProfiles", input: { type: "object", properties: { bars, rows: { type: "integer", default: 20 }, sessionsShown: { type: "integer", default: 5 } }, required: ["bars"] }, output: "StudyProfile[]" },
644
+ { name: "kagi", input: { type: "object", properties: { bars, rev: { type: "number" } }, required: ["bars", "rev"] }, output: "{ bars, lines }" },
645
+ { name: "pointFigure", input: { type: "object", properties: { bars, box: { type: "number" }, rev: { type: "integer", default: 3 } }, required: ["bars", "box"] }, output: "{ bars, cols }" }
646
+ ]
647
+ };
648
+ }
649
+
650
+ exports.atr = atr;
651
+ exports.chartPatterns = chartPatterns;
652
+ exports.describe = describe;
653
+ exports.elliott = elliott;
654
+ exports.harmonics = harmonics;
655
+ exports.kagi = kagi;
656
+ exports.pointFigure = pointFigure;
657
+ exports.rma = rma;
658
+ exports.sessionVolumeProfiles = sessionVolumeProfiles;
659
+ exports.swings = swings;
660
+ exports.tpoProfiles = tpoProfiles;
661
+ exports.trueRange = trueRange;
662
+ exports.volumeProfile = volumeProfile;
663
+ //# sourceMappingURL=index.cjs.map
664
+ //# sourceMappingURL=index.cjs.map