@tradecanvas/analytics 1.3.0 → 1.4.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.js CHANGED
@@ -1,546 +1,541 @@
1
- //#region src/commission.ts
2
- var e = class {
3
- constructor(e) {
4
- this.perTrade = e;
5
- }
6
- calculate() {
7
- return this.perTrade;
8
- }
9
- }, t = class {
10
- constructor(e) {
11
- this.rate = e;
12
- }
13
- calculate(e, t) {
14
- return Math.abs(e) * t * this.rate;
15
- }
16
- }, n = class {
17
- constructor(e, t = 0) {
18
- this.perShare = e, this.minimum = t;
19
- }
20
- calculate(e) {
21
- return Math.max(this.minimum, Math.abs(e) * this.perShare);
22
- }
23
- }, r = { calculate: () => 0 }, i = { apply: (e) => e }, a = class {
24
- constructor(e) {
25
- this.rate = e;
26
- }
27
- apply(e, t) {
28
- return e * (t === "long" ? 1 + this.rate : 1 - this.rate);
29
- }
30
- }, o = class {
31
- constructor(e) {
32
- this.factor = e;
33
- }
34
- apply(e, t, n) {
35
- let r = (n.high - n.low) * this.factor;
36
- return t === "long" ? e + r : e - r;
37
- }
38
- }, s = class {
39
- cash;
40
- initialCash;
41
- position = null;
42
- fills = [];
43
- trades = [];
44
- equityCurve = [];
45
- realizedPnl = 0;
46
- constructor(e) {
47
- if (e.initialCash <= 0) throw Error("initialCash must be > 0");
48
- this.cash = e.initialCash, this.initialCash = e.initialCash;
49
- }
50
- getCash() {
51
- return this.cash;
52
- }
53
- getPosition() {
54
- return this.position;
55
- }
56
- getFills() {
57
- return this.fills;
58
- }
59
- getTrades() {
60
- return this.trades;
61
- }
62
- getEquityCurve() {
63
- return this.equityCurve;
64
- }
65
- getInitialCash() {
66
- return this.initialCash;
67
- }
68
- getRealizedPnl() {
69
- return this.realizedPnl;
70
- }
71
- unrealizedPnl(e) {
72
- if (!this.position) return 0;
73
- let t = this.position.side === "long" ? 1 : -1;
74
- return (e - this.position.averagePrice) * this.position.quantity * t;
75
- }
76
- equity(e) {
77
- return this.cash + this.positionValue(e);
78
- }
79
- positionValue(e) {
80
- if (!this.position) return 0;
81
- let t = this.position.side === "long" ? 1 : -1;
82
- return this.position.quantity * e * t;
83
- }
84
- applyFill(e) {
85
- this.fills.push(e);
86
- let t = e.side === "long" ? e.quantity : -e.quantity;
87
- if (this.cash -= t * e.price, this.cash -= e.commission, !this.position) {
88
- this.position = {
89
- side: e.side,
90
- quantity: e.quantity,
91
- averagePrice: e.price,
92
- openedAt: e.time,
93
- tag: e.tag
94
- };
95
- return;
96
- }
97
- if (this.position.side === e.side) {
98
- let t = this.position.quantity + e.quantity;
99
- this.position = {
100
- ...this.position,
101
- quantity: t,
102
- averagePrice: (this.position.averagePrice * this.position.quantity + e.price * e.quantity) / t
103
- };
104
- return;
105
- }
106
- let n = Math.min(this.position.quantity, e.quantity), r = this.position.side === "long" ? 1 : -1, i = (e.price - this.position.averagePrice) * n * r;
107
- this.realizedPnl += i, this.trades.push({
108
- entryTime: this.position.openedAt,
109
- exitTime: e.time,
110
- side: this.position.side,
111
- quantity: n,
112
- entryPrice: this.position.averagePrice,
113
- exitPrice: e.price,
114
- pnl: i,
115
- pnlPct: (e.price / this.position.averagePrice - 1) * r,
116
- commission: e.commission,
117
- tag: e.tag ?? this.position.tag
118
- });
119
- let a = this.position.quantity - e.quantity;
120
- a > 0 ? this.position = {
121
- ...this.position,
122
- quantity: a
123
- } : a < 0 ? this.position = {
124
- side: e.side,
125
- quantity: -a,
126
- averagePrice: e.price,
127
- openedAt: e.time,
128
- tag: e.tag
129
- } : this.position = null;
130
- }
131
- mark(e, t) {
132
- let n = this.positionValue(t), r = this.unrealizedPnl(t);
133
- this.equityCurve.push({
134
- time: e,
135
- equity: this.cash + n,
136
- cash: this.cash,
137
- positionValue: n,
138
- unrealizedPnl: r,
139
- realizedPnl: this.realizedPnl
140
- });
141
- }
142
- reverseSide(e) {
143
- return e === "long" ? "short" : "long";
144
- }
145
- }, c = 365 * 24 * 60 * 60 * 1e3;
146
- function l(e, t, n, r = {}) {
147
- if (t.length < 2) return u(e, t, n);
148
- let i = t[t.length - 1].equity, a = i - e, o = a / e, s = r.periodsPerYear ?? _(t), l = (r.riskFreeRate ?? 0) / s, v = d(t), y = f(v), b = p(v, y), x = m(v, l), S = b === 0 ? 0 : (y - l) / b * Math.sqrt(s), C = x === 0 ? 0 : (y - l) / x * Math.sqrt(s), w = t[t.length - 1].time - t[0].time, T = w > 0 ? w / c : 0, E = T > 0 ? (i / e) ** (1 / T) - 1 : 0, { maxDrawdown: D, maxDrawdownPct: O } = h(t);
149
- return {
150
- totalReturn: a,
151
- totalReturnPct: o,
152
- cagr: E,
153
- sharpe: S,
154
- sortino: C,
155
- calmar: O > 0 ? E / O : 0,
156
- maxDrawdown: D,
157
- maxDrawdownPct: O,
158
- ...g(n)
159
- };
1
+ class $ {
2
+ constructor(t) {
3
+ this.perTrade = t;
4
+ }
5
+ calculate() {
6
+ return this.perTrade;
7
+ }
160
8
  }
161
- function u(e, t, n) {
162
- let r = t.length > 0 ? t[t.length - 1].equity : e;
163
- return {
164
- totalReturn: r - e,
165
- totalReturnPct: (r - e) / e,
166
- cagr: 0,
167
- sharpe: 0,
168
- sortino: 0,
169
- calmar: 0,
170
- maxDrawdown: 0,
171
- maxDrawdownPct: 0,
172
- ...g(n)
173
- };
9
+ class G {
10
+ /** rate = 0.001 → 10 bps per trade notional. */
11
+ constructor(t) {
12
+ this.rate = t;
13
+ }
14
+ calculate(t, e) {
15
+ return Math.abs(t) * e * this.rate;
16
+ }
174
17
  }
175
- function d(e) {
176
- let t = [];
177
- for (let n = 1; n < e.length; n++) {
178
- let r = e[n - 1].equity;
179
- if (r <= 0) {
180
- t.push(0);
181
- continue;
182
- }
183
- t.push(e[n].equity / r - 1);
184
- }
185
- return t;
18
+ class Q {
19
+ /** Minimum total commission per trade (optional). */
20
+ constructor(t, e = 0) {
21
+ this.perShare = t, this.minimum = e;
22
+ }
23
+ calculate(t) {
24
+ return Math.max(this.minimum, Math.abs(t) * this.perShare);
25
+ }
186
26
  }
187
- function f(e) {
188
- if (e.length === 0) return 0;
189
- let t = 0;
190
- for (let n of e) t += n;
191
- return t / e.length;
27
+ const A = {
28
+ calculate: () => 0
29
+ }, L = {
30
+ apply: (i) => i
31
+ };
32
+ class U {
33
+ /** rate = 0.0005 → 5bps adverse */
34
+ constructor(t) {
35
+ this.rate = t;
36
+ }
37
+ apply(t, e) {
38
+ const n = e === "long" ? 1 + this.rate : 1 - this.rate;
39
+ return t * n;
40
+ }
192
41
  }
193
- function p(e, t) {
194
- if (e.length < 2) return 0;
195
- let n = 0;
196
- for (let r of e) n += (r - t) ** 2;
197
- return Math.sqrt(n / (e.length - 1));
42
+ class Z {
43
+ /** factor = 0.1 → 10% of the bar's range pushes against the order */
44
+ constructor(t) {
45
+ this.factor = t;
46
+ }
47
+ apply(t, e, n) {
48
+ const o = (n.high - n.low) * this.factor;
49
+ return e === "long" ? t + o : t - o;
50
+ }
198
51
  }
199
- function m(e, t) {
200
- if (e.length < 2) return 0;
201
- let n = 0, r = 0;
202
- for (let i of e) {
203
- let e = i - t;
204
- e < 0 && (n += e ** 2, r++);
205
- }
206
- return r === 0 ? 0 : Math.sqrt(n / r);
52
+ class I {
53
+ cash;
54
+ initialCash;
55
+ position = null;
56
+ fills = [];
57
+ trades = [];
58
+ equityCurve = [];
59
+ realizedPnl = 0;
60
+ constructor(t) {
61
+ if (t.initialCash <= 0) throw new Error("initialCash must be > 0");
62
+ this.cash = t.initialCash, this.initialCash = t.initialCash;
63
+ }
64
+ getCash() {
65
+ return this.cash;
66
+ }
67
+ getPosition() {
68
+ return this.position;
69
+ }
70
+ getFills() {
71
+ return this.fills;
72
+ }
73
+ getTrades() {
74
+ return this.trades;
75
+ }
76
+ getEquityCurve() {
77
+ return this.equityCurve;
78
+ }
79
+ getInitialCash() {
80
+ return this.initialCash;
81
+ }
82
+ getRealizedPnl() {
83
+ return this.realizedPnl;
84
+ }
85
+ unrealizedPnl(t) {
86
+ if (!this.position) return 0;
87
+ const e = this.position.side === "long" ? 1 : -1;
88
+ return (t - this.position.averagePrice) * this.position.quantity * e;
89
+ }
90
+ equity(t) {
91
+ return this.cash + this.positionValue(t);
92
+ }
93
+ positionValue(t) {
94
+ if (!this.position) return 0;
95
+ const e = this.position.side === "long" ? 1 : -1;
96
+ return this.position.quantity * t * e;
97
+ }
98
+ /** Apply a fill: cash flow + position update + realized PnL. */
99
+ applyFill(t) {
100
+ this.fills.push(t);
101
+ const e = t.side === "long" ? t.quantity : -t.quantity;
102
+ if (this.cash -= e * t.price, this.cash -= t.commission, !this.position) {
103
+ this.position = {
104
+ side: t.side,
105
+ quantity: t.quantity,
106
+ averagePrice: t.price,
107
+ openedAt: t.time,
108
+ tag: t.tag
109
+ };
110
+ return;
111
+ }
112
+ if (this.position.side === t.side) {
113
+ const a = this.position.quantity + t.quantity;
114
+ this.position = {
115
+ ...this.position,
116
+ quantity: a,
117
+ averagePrice: (this.position.averagePrice * this.position.quantity + t.price * t.quantity) / a
118
+ };
119
+ return;
120
+ }
121
+ const n = Math.min(this.position.quantity, t.quantity), s = this.position.side === "long" ? 1 : -1, o = (t.price - this.position.averagePrice) * n * s;
122
+ this.realizedPnl += o, this.trades.push({
123
+ entryTime: this.position.openedAt,
124
+ exitTime: t.time,
125
+ side: this.position.side,
126
+ quantity: n,
127
+ entryPrice: this.position.averagePrice,
128
+ exitPrice: t.price,
129
+ pnl: o,
130
+ pnlPct: (t.price / this.position.averagePrice - 1) * s,
131
+ commission: t.commission,
132
+ tag: t.tag ?? this.position.tag
133
+ });
134
+ const r = this.position.quantity - t.quantity;
135
+ r > 0 ? this.position = { ...this.position, quantity: r } : r < 0 ? this.position = {
136
+ side: t.side,
137
+ quantity: -r,
138
+ averagePrice: t.price,
139
+ openedAt: t.time,
140
+ tag: t.tag
141
+ } : this.position = null;
142
+ }
143
+ /** Snapshot equity at the current bar close. */
144
+ mark(t, e) {
145
+ const n = this.positionValue(e), s = this.unrealizedPnl(e);
146
+ this.equityCurve.push({
147
+ time: t,
148
+ equity: this.cash + n,
149
+ cash: this.cash,
150
+ positionValue: n,
151
+ unrealizedPnl: s,
152
+ realizedPnl: this.realizedPnl
153
+ });
154
+ }
155
+ reverseSide(t) {
156
+ return t === "long" ? "short" : "long";
157
+ }
207
158
  }
208
- function h(e) {
209
- let t = e[0].equity, n = 0, r = 0;
210
- for (let i of e) {
211
- i.equity > t && (t = i.equity);
212
- let e = t - i.equity;
213
- e > n && (n = e, r = t > 0 ? e / t : 0);
214
- }
215
- return {
216
- maxDrawdown: n,
217
- maxDrawdownPct: r
218
- };
159
+ const E = 365 * 24 * 60 * 60 * 1e3;
160
+ function T(i, t, e, n = {}) {
161
+ if (t.length < 2)
162
+ return B(i, t, e);
163
+ const s = t[t.length - 1].equity, o = s - i, r = o / i, a = n.periodsPerYear ?? _(t), u = (n.riskFreeRate ?? 0) / a, h = Y(t), d = z(h), m = N(h, d), q = C(h, u), f = m === 0 ? 0 : (d - u) / m * Math.sqrt(a), O = q === 0 ? 0 : (d - u) / q * Math.sqrt(a), M = t[t.length - 1].time - t[0].time, l = M > 0 ? M / E : 0, p = l > 0 ? Math.pow(s / i, 1 / l) - 1 : 0, { maxDrawdown: y, maxDrawdownPct: g } = V(t), v = g > 0 ? p / g : 0, P = b(e);
164
+ return {
165
+ totalReturn: o,
166
+ totalReturnPct: r,
167
+ cagr: p,
168
+ sharpe: f,
169
+ sortino: O,
170
+ calmar: v,
171
+ maxDrawdown: y,
172
+ maxDrawdownPct: g,
173
+ ...P
174
+ };
219
175
  }
220
- function g(e) {
221
- if (e.length === 0) return {
222
- winRate: 0,
223
- profitFactor: 0,
224
- expectancy: 0,
225
- averageWin: 0,
226
- averageLoss: 0,
227
- trades: 0
228
- };
229
- let t = 0, n = 0, r = 0, i = 0;
230
- for (let a of e) a.pnl > 0 ? (t++, r += a.pnl) : a.pnl < 0 && (n++, i += -a.pnl);
231
- let a = t / e.length, o = t > 0 ? r / t : 0, s = n > 0 ? i / n : 0;
232
- return {
233
- winRate: a,
234
- profitFactor: i > 0 ? r / i : r > 0 ? Infinity : 0,
235
- expectancy: a * o - (1 - a) * s,
236
- averageWin: o,
237
- averageLoss: s,
238
- trades: e.length
239
- };
176
+ function B(i, t, e) {
177
+ const n = t.length > 0 ? t[t.length - 1].equity : i;
178
+ return {
179
+ totalReturn: n - i,
180
+ totalReturnPct: (n - i) / i,
181
+ cagr: 0,
182
+ sharpe: 0,
183
+ sortino: 0,
184
+ calmar: 0,
185
+ maxDrawdown: 0,
186
+ maxDrawdownPct: 0,
187
+ ...b(e)
188
+ };
240
189
  }
241
- function _(e) {
242
- if (e.length < 2) return 252;
243
- let t = [];
244
- for (let n = 1; n < e.length && n < 50; n++) t.push(e[n].time - e[n - 1].time);
245
- let n = f(t);
246
- if (n <= 0) return 252;
247
- let r = c / n;
248
- return r > 2e5 ? 365 * 24 * 60 : r > 5e4 ? 365 * 24 * 4 : r > 5e3 ? 365 * 24 : r > 200 ? 252 : r > 40 ? 52 : 12;
190
+ function Y(i) {
191
+ const t = [];
192
+ for (let e = 1; e < i.length; e++) {
193
+ const n = i[e - 1].equity;
194
+ if (n <= 0) {
195
+ t.push(0);
196
+ continue;
197
+ }
198
+ t.push(i[e].equity / n - 1);
199
+ }
200
+ return t;
249
201
  }
250
- //#endregion
251
- //#region src/Backtester.ts
252
- var v = class {
253
- commission;
254
- slippage;
255
- allowShort;
256
- portfolio;
257
- pendingOrders = [];
258
- orderSeq = 0;
259
- constructor(e) {
260
- this.commission = e.commission ?? r, this.slippage = e.slippage ?? i, this.allowShort = e.allowShort ?? !0, this.portfolio = new s({ initialCash: e.initialCash });
261
- }
262
- run(e, t) {
263
- if (e.length < 2) throw Error("Backtester requires at least 2 bars");
264
- for (let n = 0; n < e.length; n++) {
265
- let r = e[n];
266
- this.fillPendingOrders(r), this.portfolio.mark(r.time, r.close), n < e.length - 1 && t(this.makeContext(r, n, e.slice(0, n + 1)));
267
- }
268
- for (let e of this.pendingOrders) e.status === "pending" && (e.status = "cancelled");
269
- let n = this.portfolio.getEquityCurve(), r = this.portfolio.getInitialCash(), i = this.portfolio.getTrades(), a = n.length > 0 ? n[n.length - 1].equity : r;
270
- return {
271
- fills: this.portfolio.getFills(),
272
- trades: i,
273
- equityCurve: n,
274
- initialCash: r,
275
- finalEquity: a,
276
- metrics: l(r, n, i)
277
- };
278
- }
279
- fillPendingOrders(e) {
280
- for (let t of this.pendingOrders) {
281
- if (t.status !== "pending") continue;
282
- let n = this.resolveFillPrice(t, e);
283
- if (n === null) {
284
- (t.timeInForce === "day" || t.timeInForce === "ioc") && (t.status = "cancelled");
285
- continue;
286
- }
287
- let r = this.slippage.apply(n, t.side, e), i = this.commission.calculate(t.quantity, r), a = {
288
- orderId: t.id,
289
- time: e.time,
290
- price: r,
291
- quantity: t.quantity,
292
- side: t.side,
293
- commission: i,
294
- slippage: Math.abs(r - n),
295
- tag: t.tag
296
- };
297
- this.portfolio.applyFill(a), t.status = "filled";
298
- }
299
- this.pendingOrders = this.pendingOrders.filter((e) => e.status === "pending");
300
- }
301
- resolveFillPrice(e, t) {
302
- switch (e.type) {
303
- case "market": return t.open;
304
- case "limit": return e.price === void 0 ? null : e.side === "long" && t.low <= e.price ? Math.min(e.price, t.open) : e.side === "short" && t.high >= e.price ? Math.max(e.price, t.open) : null;
305
- case "stop": return e.price === void 0 ? null : e.side === "long" && t.high >= e.price ? Math.max(e.price, t.open) : e.side === "short" && t.low <= e.price ? Math.min(e.price, t.open) : null;
306
- }
307
- }
308
- makeContext(e, t, n) {
309
- let r = this.portfolio;
310
- return {
311
- bar: e,
312
- index: t,
313
- history: n,
314
- position: r.getPosition(),
315
- cash: r.getCash(),
316
- equity: r.equity(e.close),
317
- placeOrder: (t) => this.placeOrder(t, e.time),
318
- close: (t) => this.closePosition(e.time, t),
319
- cancel: (e) => this.cancelOrder(e)
320
- };
321
- }
322
- placeOrder(e, t) {
323
- if (e.quantity <= 0) throw Error("order quantity must be > 0");
324
- if (!this.allowShort && e.side === "short") {
325
- let t = this.portfolio.getPosition();
326
- if (!(t !== null && t.side === "long" && e.quantity <= t.quantity)) throw Error("shorting is disabled");
327
- }
328
- let n = e.id ?? `o-${++this.orderSeq}`;
329
- return this.pendingOrders.push({
330
- id: n,
331
- side: e.side,
332
- type: e.type,
333
- quantity: e.quantity,
334
- price: e.price,
335
- tag: e.tag,
336
- timeInForce: e.timeInForce ?? "gtc",
337
- status: "pending",
338
- placedAt: t
339
- }), n;
340
- }
341
- closePosition(e, t) {
342
- let n = this.portfolio.getPosition();
343
- if (!n) return null;
344
- let r = n.side === "long" ? "short" : "long";
345
- return this.placeOrder({
346
- side: r,
347
- type: "market",
348
- quantity: n.quantity,
349
- tag: t
350
- }, e);
351
- }
352
- cancelOrder(e) {
353
- let t = this.pendingOrders.find((t) => t.id === e);
354
- return !t || t.status !== "pending" ? !1 : (t.status = "cancelled", !0);
355
- }
356
- };
357
- //#endregion
358
- //#region src/MonteCarlo.ts
359
- function y(e, t, n = {}) {
360
- let r = n.simulations ?? 1e3, i = (n.percentiles ?? [
361
- 5,
362
- 25,
363
- 50,
364
- 75,
365
- 95
366
- ]).slice().sort((e, t) => e - t), a = S(n.seed ?? Date.now() ^ 23100);
367
- if (t.length === 0 || r <= 0) return {
368
- simulations: r,
369
- initialCash: e,
370
- equityBands: [{
371
- step: 0,
372
- p5: e,
373
- p25: e,
374
- p50: e,
375
- p75: e,
376
- p95: e
377
- }],
378
- finalEquityPercentiles: Object.fromEntries(i.map((t) => [`p${t}`, e])),
379
- probabilityProfitable: 0,
380
- worstMaxDrawdownPct: 0
381
- };
382
- let o = t.map((e) => e.pnl), s = t.length + 1, c = o.slice(), l = new Float64Array(r), u = new Float64Array(r * s), d = 0, f = 0;
383
- for (let t = 0; t < r; t++) {
384
- b(c, a);
385
- let n = e, r = e, i = 0, o = t * s;
386
- u[o] = n;
387
- for (let e = 0; e < c.length; e++) {
388
- n += c[e], u[o + e + 1] = n, n > r && (r = n);
389
- let t = r > 0 ? (r - n) / r : 0;
390
- t > i && (i = t);
391
- }
392
- l[t] = n, n > e && f++, i > d && (d = i);
393
- }
394
- let p = [], m = new Float64Array(r);
395
- for (let e = 0; e < s; e++) {
396
- for (let t = 0; t < r; t++) m[t] = u[t * s + e];
397
- m.sort(), p.push({
398
- step: e,
399
- p5: x(m, 5),
400
- p25: x(m, 25),
401
- p50: x(m, 50),
402
- p75: x(m, 75),
403
- p95: x(m, 95)
404
- });
405
- }
406
- let h = new Float64Array(l);
407
- h.sort();
408
- let g = {};
409
- for (let e of i) g[`p${e}`] = x(h, e);
410
- return {
411
- simulations: r,
412
- initialCash: e,
413
- equityBands: p,
414
- finalEquityPercentiles: g,
415
- probabilityProfitable: f / r,
416
- worstMaxDrawdownPct: d * 100
417
- };
202
+ function z(i) {
203
+ if (i.length === 0) return 0;
204
+ let t = 0;
205
+ for (const e of i) t += e;
206
+ return t / i.length;
207
+ }
208
+ function N(i, t) {
209
+ if (i.length < 2) return 0;
210
+ let e = 0;
211
+ for (const n of i) e += (n - t) ** 2;
212
+ return Math.sqrt(e / (i.length - 1));
213
+ }
214
+ function C(i, t) {
215
+ if (i.length < 2) return 0;
216
+ let e = 0, n = 0;
217
+ for (const s of i) {
218
+ const o = s - t;
219
+ o < 0 && (e += o ** 2, n++);
220
+ }
221
+ return n === 0 ? 0 : Math.sqrt(e / n);
222
+ }
223
+ function V(i) {
224
+ let t = i[0].equity, e = 0, n = 0;
225
+ for (const s of i) {
226
+ s.equity > t && (t = s.equity);
227
+ const o = t - s.equity;
228
+ o > e && (e = o, n = t > 0 ? o / t : 0);
229
+ }
230
+ return { maxDrawdown: e, maxDrawdownPct: n };
418
231
  }
419
- function b(e, t) {
420
- for (let n = e.length - 1; n > 0; n--) {
421
- let r = Math.floor(t() * (n + 1)), i = e[n];
422
- e[n] = e[r], e[r] = i;
423
- }
232
+ function b(i) {
233
+ if (i.length === 0)
234
+ return {
235
+ winRate: 0,
236
+ profitFactor: 0,
237
+ expectancy: 0,
238
+ averageWin: 0,
239
+ averageLoss: 0,
240
+ trades: 0
241
+ };
242
+ let t = 0, e = 0, n = 0, s = 0;
243
+ for (const h of i)
244
+ h.pnl > 0 ? (t++, n += h.pnl) : h.pnl < 0 && (e++, s += -h.pnl);
245
+ const o = t / i.length, r = t > 0 ? n / t : 0, a = e > 0 ? s / e : 0, c = s > 0 ? n / s : n > 0 ? 1 / 0 : 0, u = o * r - (1 - o) * a;
246
+ return {
247
+ winRate: o,
248
+ profitFactor: c,
249
+ expectancy: u,
250
+ averageWin: r,
251
+ averageLoss: a,
252
+ trades: i.length
253
+ };
424
254
  }
425
- function x(e, t) {
426
- if (e.length === 0) return 0;
427
- if (e.length === 1) return e[0];
428
- let n = t / 100 * (e.length - 1), r = Math.floor(n), i = Math.ceil(n);
429
- return r === i ? e[r] : e[r] * (i - n) + e[i] * (n - r);
255
+ function _(i) {
256
+ if (i.length < 2) return 252;
257
+ const t = [];
258
+ for (let s = 1; s < i.length && s < 50; s++)
259
+ t.push(i[s].time - i[s - 1].time);
260
+ const e = z(t);
261
+ if (e <= 0) return 252;
262
+ const n = E / e;
263
+ return n > 2e5 ? 365 * 24 * 60 : n > 5e4 ? 365 * 24 * 4 : n > 5e3 ? 365 * 24 : n > 200 ? 252 : n > 40 ? 52 : 12;
430
264
  }
431
- function S(e) {
432
- let t = e >>> 0;
433
- return () => {
434
- t = t + 1831565813 >>> 0;
435
- let e = t;
436
- return e = Math.imul(e ^ e >>> 15, e | 1), e ^= e + Math.imul(e ^ e >>> 7, e | 61), ((e ^ e >>> 14) >>> 0) / 4294967296;
437
- };
265
+ class x {
266
+ commission;
267
+ slippage;
268
+ allowShort;
269
+ portfolio;
270
+ pendingOrders = [];
271
+ orderSeq = 0;
272
+ constructor(t) {
273
+ this.commission = t.commission ?? A, this.slippage = t.slippage ?? L, this.allowShort = t.allowShort ?? !0, this.portfolio = new I({ initialCash: t.initialCash });
274
+ }
275
+ run(t, e) {
276
+ if (t.length < 2)
277
+ throw new Error("Backtester requires at least 2 bars");
278
+ for (let a = 0; a < t.length; a++) {
279
+ const c = t[a];
280
+ if (this.fillPendingOrders(c), this.portfolio.mark(c.time, c.close), a < t.length - 1) {
281
+ const u = this.makeContext(c, a, t.slice(0, a + 1));
282
+ e(u);
283
+ }
284
+ }
285
+ for (const a of this.pendingOrders)
286
+ a.status === "pending" && (a.status = "cancelled");
287
+ const n = this.portfolio.getEquityCurve(), s = this.portfolio.getInitialCash(), o = this.portfolio.getTrades(), r = n.length > 0 ? n[n.length - 1].equity : s;
288
+ return {
289
+ fills: this.portfolio.getFills(),
290
+ trades: o,
291
+ equityCurve: n,
292
+ initialCash: s,
293
+ finalEquity: r,
294
+ metrics: T(s, n, o)
295
+ };
296
+ }
297
+ fillPendingOrders(t) {
298
+ for (const e of this.pendingOrders) {
299
+ if (e.status !== "pending") continue;
300
+ const n = this.resolveFillPrice(e, t);
301
+ if (n === null) {
302
+ (e.timeInForce === "day" || e.timeInForce === "ioc") && (e.status = "cancelled");
303
+ continue;
304
+ }
305
+ const s = this.slippage.apply(n, e.side, t), o = this.commission.calculate(e.quantity, s), r = {
306
+ orderId: e.id,
307
+ time: t.time,
308
+ price: s,
309
+ quantity: e.quantity,
310
+ side: e.side,
311
+ commission: o,
312
+ slippage: Math.abs(s - n),
313
+ tag: e.tag
314
+ };
315
+ this.portfolio.applyFill(r), e.status = "filled";
316
+ }
317
+ this.pendingOrders = this.pendingOrders.filter(
318
+ (e) => e.status === "pending"
319
+ );
320
+ }
321
+ resolveFillPrice(t, e) {
322
+ switch (t.type) {
323
+ case "market":
324
+ return e.open;
325
+ case "limit":
326
+ return t.price === void 0 ? null : t.side === "long" && e.low <= t.price ? Math.min(t.price, e.open) : t.side === "short" && e.high >= t.price ? Math.max(t.price, e.open) : null;
327
+ case "stop":
328
+ return t.price === void 0 ? null : t.side === "long" && e.high >= t.price ? Math.max(t.price, e.open) : t.side === "short" && e.low <= t.price ? Math.min(t.price, e.open) : null;
329
+ }
330
+ }
331
+ makeContext(t, e, n) {
332
+ const s = this.portfolio;
333
+ return {
334
+ bar: t,
335
+ index: e,
336
+ history: n,
337
+ position: s.getPosition(),
338
+ cash: s.getCash(),
339
+ equity: s.equity(t.close),
340
+ placeOrder: (o) => this.placeOrder(o, t.time),
341
+ close: (o) => this.closePosition(t.time, o),
342
+ cancel: (o) => this.cancelOrder(o)
343
+ };
344
+ }
345
+ placeOrder(t, e) {
346
+ if (t.quantity <= 0)
347
+ throw new Error("order quantity must be > 0");
348
+ if (!this.allowShort && t.side === "short") {
349
+ const s = this.portfolio.getPosition();
350
+ if (!(s !== null && s.side === "long" && t.quantity <= s.quantity))
351
+ throw new Error("shorting is disabled");
352
+ }
353
+ const n = t.id ?? `o-${++this.orderSeq}`;
354
+ return this.pendingOrders.push({
355
+ id: n,
356
+ side: t.side,
357
+ type: t.type,
358
+ quantity: t.quantity,
359
+ price: t.price,
360
+ tag: t.tag,
361
+ timeInForce: t.timeInForce ?? "gtc",
362
+ status: "pending",
363
+ placedAt: e
364
+ }), n;
365
+ }
366
+ closePosition(t, e) {
367
+ const n = this.portfolio.getPosition();
368
+ if (!n) return null;
369
+ const s = n.side === "long" ? "short" : "long";
370
+ return this.placeOrder(
371
+ { side: s, type: "market", quantity: n.quantity, tag: e },
372
+ t
373
+ );
374
+ }
375
+ cancelOrder(t) {
376
+ const e = this.pendingOrders.find((n) => n.id === t);
377
+ return !e || e.status !== "pending" ? !1 : (e.status = "cancelled", !0);
378
+ }
438
379
  }
439
- //#endregion
440
- //#region src/strategies/smaCross.ts
441
- function C(e = {}) {
442
- let t = e.fastPeriod ?? 10, n = e.slowPeriod ?? 30, r = e.size ?? 1, i = e.tag ?? "sma-cross";
443
- if (t >= n) throw Error("smaCrossStrategy: fastPeriod must be less than slowPeriod");
444
- return (e) => {
445
- if (e.index < n) return;
446
- let a = e.history, o = w(a, e.index, t), s = w(a, e.index, n), c = w(a, e.index - 1, t), l = w(a, e.index - 1, n);
447
- c <= l && o > s ? (e.position?.side === "short" && e.close(i), (!e.position || e.position.side === "short") && e.placeOrder({
448
- side: "long",
449
- type: "market",
450
- quantity: r,
451
- tag: i
452
- })) : c >= l && o < s && e.position?.side === "long" && e.close(i);
453
- };
380
+ function J(i, t, e = {}) {
381
+ const n = e.simulations ?? 1e3, s = (e.percentiles ?? [5, 25, 50, 75, 95]).slice().sort((l, p) => l - p), o = H(e.seed ?? Date.now() ^ 23100);
382
+ if (t.length === 0 || n <= 0)
383
+ return {
384
+ simulations: n,
385
+ initialCash: i,
386
+ equityBands: [{ step: 0, p5: i, p25: i, p50: i, p75: i, p95: i }],
387
+ finalEquityPercentiles: Object.fromEntries(s.map((l) => [`p${l}`, i])),
388
+ probabilityProfitable: 0,
389
+ worstMaxDrawdownPct: 0
390
+ };
391
+ const r = t.map((l) => l.pnl), a = t.length + 1, c = r.slice(), u = new Float64Array(n), h = new Float64Array(n * a);
392
+ let d = 0, m = 0;
393
+ for (let l = 0; l < n; l++) {
394
+ j(c, o);
395
+ let p = i, y = i, g = 0;
396
+ const v = l * a;
397
+ h[v] = p;
398
+ for (let P = 0; P < c.length; P++) {
399
+ p += c[P], h[v + P + 1] = p, p > y && (y = p);
400
+ const S = y > 0 ? (y - p) / y : 0;
401
+ S > g && (g = S);
402
+ }
403
+ u[l] = p, p > i && m++, g > d && (d = g);
404
+ }
405
+ const q = [], f = new Float64Array(n);
406
+ for (let l = 0; l < a; l++) {
407
+ for (let p = 0; p < n; p++)
408
+ f[p] = h[p * a + l];
409
+ f.sort(), q.push({
410
+ step: l,
411
+ p5: w(f, 5),
412
+ p25: w(f, 25),
413
+ p50: w(f, 50),
414
+ p75: w(f, 75),
415
+ p95: w(f, 95)
416
+ });
417
+ }
418
+ const O = new Float64Array(u);
419
+ O.sort();
420
+ const M = {};
421
+ for (const l of s)
422
+ M[`p${l}`] = w(O, l);
423
+ return {
424
+ simulations: n,
425
+ initialCash: i,
426
+ equityBands: q,
427
+ finalEquityPercentiles: M,
428
+ probabilityProfitable: m / n,
429
+ worstMaxDrawdownPct: d * 100
430
+ };
454
431
  }
455
- function w(e, t, n) {
456
- let r = 0;
457
- for (let i = t - n + 1; i <= t; i++) r += e[i].close;
458
- return r / n;
432
+ function j(i, t) {
433
+ for (let e = i.length - 1; e > 0; e--) {
434
+ const n = Math.floor(t() * (e + 1)), s = i[e];
435
+ i[e] = i[n], i[n] = s;
436
+ }
459
437
  }
460
- //#endregion
461
- //#region src/strategies/rsiReversion.ts
462
- function T(e = {}) {
463
- let t = e.period ?? 14, n = e.oversold ?? 30, r = e.overbought ?? 70, i = e.size ?? 1, a = e.tag ?? "rsi-reversion";
464
- return (e) => {
465
- if (e.index < t + 1) return;
466
- let o = E(e.history, e.index, t), s = E(e.history, e.index - 1, t), c = s < n && o >= n, l = s < r && o >= r;
467
- c && !e.position ? e.placeOrder({
468
- side: "long",
469
- type: "market",
470
- quantity: i,
471
- tag: a
472
- }) : l && e.position?.side === "long" && e.close(a);
473
- };
438
+ function w(i, t) {
439
+ if (i.length === 0) return 0;
440
+ if (i.length === 1) return i[0];
441
+ const e = t / 100 * (i.length - 1), n = Math.floor(e), s = Math.ceil(e);
442
+ return n === s ? i[n] : i[n] * (s - e) + i[s] * (e - n);
474
443
  }
475
- function E(e, t, n) {
476
- let r = 0, i = 0;
477
- for (let a = t - n + 1; a <= t; a++) {
478
- let t = e[a].close - e[a - 1].close;
479
- t >= 0 ? r += t : i -= t;
480
- }
481
- return i === 0 ? 100 : 100 - 100 / (1 + r / n / (i / n));
444
+ function H(i) {
445
+ let t = i >>> 0;
446
+ return () => {
447
+ t = t + 1831565813 >>> 0;
448
+ let e = t;
449
+ return e = Math.imul(e ^ e >>> 15, e | 1), e ^= e + Math.imul(e ^ e >>> 7, e | 61), ((e ^ e >>> 14) >>> 0) / 4294967296;
450
+ };
482
451
  }
483
- //#endregion
484
- //#region src/strategies/donchianBreakout.ts
485
- function D(e = {}) {
486
- let t = e.entryPeriod ?? 20, n = e.exitPeriod ?? 10, r = e.size ?? 1, i = e.tag ?? "donchian-breakout";
487
- return (e) => {
488
- if (e.index < t) return;
489
- let a = e.bar.close, o = O(e.history, e.index - t, e.index - 1), s = k(e.history, e.index - t, e.index - 1), c = O(e.history, e.index - n, e.index - 1), l = k(e.history, e.index - n, e.index - 1);
490
- e.position ? (e.position.side === "long" && a < l || e.position.side === "short" && a > c) && e.close(i) : a > o ? e.placeOrder({
491
- side: "long",
492
- type: "market",
493
- quantity: r,
494
- tag: i
495
- }) : a < s && e.placeOrder({
496
- side: "short",
497
- type: "market",
498
- quantity: r,
499
- tag: i
500
- });
501
- };
452
+ function K(i = {}) {
453
+ const t = i.fastPeriod ?? 10, e = i.slowPeriod ?? 30, n = i.size ?? 1, s = i.tag ?? "sma-cross";
454
+ if (t >= e)
455
+ throw new Error("smaCrossStrategy: fastPeriod must be less than slowPeriod");
456
+ return (o) => {
457
+ if (o.index < e) return;
458
+ const r = o.history, a = F(r, o.index, t), c = F(r, o.index, e), u = F(r, o.index - 1, t), h = F(r, o.index - 1, e), d = u <= h && a > c, m = u >= h && a < c;
459
+ d ? (o.position?.side === "short" && o.close(s), (!o.position || o.position.side === "short") && o.placeOrder({ side: "long", type: "market", quantity: n, tag: s })) : m && o.position?.side === "long" && o.close(s);
460
+ };
502
461
  }
503
- function O(e, t, n) {
504
- let r = -Infinity;
505
- for (let i = Math.max(0, t); i <= n; i++) e[i].high > r && (r = e[i].high);
506
- return r;
462
+ function F(i, t, e) {
463
+ let n = 0;
464
+ for (let s = t - e + 1; s <= t; s++)
465
+ n += i[s].close;
466
+ return n / e;
507
467
  }
508
- function k(e, t, n) {
509
- let r = Infinity;
510
- for (let i = Math.max(0, t); i <= n; i++) e[i].low < r && (r = e[i].low);
511
- return r;
468
+ function X(i = {}) {
469
+ const t = i.period ?? 14, e = i.oversold ?? 30, n = i.overbought ?? 70, s = i.size ?? 1, o = i.tag ?? "rsi-reversion";
470
+ return (r) => {
471
+ if (r.index < t + 1) return;
472
+ const a = k(r.history, r.index, t), c = k(r.history, r.index - 1, t), u = c < e && a >= e, h = c < n && a >= n;
473
+ u && !r.position ? r.placeOrder({ side: "long", type: "market", quantity: s, tag: o }) : h && r.position?.side === "long" && r.close(o);
474
+ };
512
475
  }
513
- //#endregion
514
- //#region src/strategies/bollingerReversion.ts
515
- function A(e = {}) {
516
- let t = e.period ?? 20, n = e.stdDev ?? 2, r = e.size ?? 1, i = e.tag ?? "bollinger-reversion";
517
- return (e) => {
518
- if (e.index < t) return;
519
- let { mid: a, lower: o } = j(e.history, e.index, t, n), s = e.bar.close;
520
- !e.position && s <= o ? e.placeOrder({
521
- side: "long",
522
- type: "market",
523
- quantity: r,
524
- tag: i
525
- }) : e.position?.side === "long" && s >= a && e.close(i);
526
- };
476
+ function k(i, t, e) {
477
+ let n = 0, s = 0;
478
+ for (let r = t - e + 1; r <= t; r++) {
479
+ const a = i[r].close - i[r - 1].close;
480
+ a >= 0 ? n += a : s -= a;
481
+ }
482
+ return s === 0 ? 100 : 100 - 100 / (1 + n / e / (s / e));
527
483
  }
528
- function j(e, t, n, r) {
529
- let i = 0;
530
- for (let r = t - n + 1; r <= t; r++) i += e[r].close;
531
- let a = i / n, o = 0;
532
- for (let r = t - n + 1; r <= t; r++) {
533
- let t = e[r].close - a;
534
- o += t * t;
535
- }
536
- let s = Math.sqrt(o / n);
537
- return {
538
- mid: a,
539
- upper: a + r * s,
540
- lower: a - r * s
541
- };
484
+ function tt(i = {}) {
485
+ const t = i.entryPeriod ?? 20, e = i.exitPeriod ?? 10, n = i.size ?? 1, s = i.tag ?? "donchian-breakout";
486
+ return (o) => {
487
+ if (o.index < t) return;
488
+ const r = o.bar.close, a = D(o.history, o.index - t, o.index - 1), c = R(o.history, o.index - t, o.index - 1), u = D(o.history, o.index - e, o.index - 1), h = R(o.history, o.index - e, o.index - 1);
489
+ o.position ? (o.position.side === "long" && r < h || o.position.side === "short" && r > u) && o.close(s) : r > a ? o.placeOrder({ side: "long", type: "market", quantity: n, tag: s }) : r < c && o.placeOrder({ side: "short", type: "market", quantity: n, tag: s });
490
+ };
542
491
  }
543
- //#endregion
544
- export { v as Backtester, e as FixedCommission, i as NO_SLIPPAGE, n as PerShareCommission, t as PercentCommission, a as PercentSlippage, s as Portfolio, o as RangeBasedSlippage, r as ZERO_COMMISSION, A as bollingerReversionStrategy, l as computeRiskMetrics, D as donchianBreakoutStrategy, T as rsiReversionStrategy, y as runMonteCarlo, C as smaCrossStrategy };
545
-
546
- //# sourceMappingURL=index.js.map
492
+ function D(i, t, e) {
493
+ let n = -1 / 0;
494
+ for (let s = Math.max(0, t); s <= e; s++)
495
+ i[s].high > n && (n = i[s].high);
496
+ return n;
497
+ }
498
+ function R(i, t, e) {
499
+ let n = 1 / 0;
500
+ for (let s = Math.max(0, t); s <= e; s++)
501
+ i[s].low < n && (n = i[s].low);
502
+ return n;
503
+ }
504
+ function et(i = {}) {
505
+ const t = i.period ?? 20, e = i.stdDev ?? 2, n = i.size ?? 1, s = i.tag ?? "bollinger-reversion";
506
+ return (o) => {
507
+ if (o.index < t) return;
508
+ const { mid: r, lower: a } = W(o.history, o.index, t, e), c = o.bar.close;
509
+ !o.position && c <= a ? o.placeOrder({ side: "long", type: "market", quantity: n, tag: s }) : o.position?.side === "long" && c >= r && o.close(s);
510
+ };
511
+ }
512
+ function W(i, t, e, n) {
513
+ let s = 0;
514
+ for (let c = t - e + 1; c <= t; c++) s += i[c].close;
515
+ const o = s / e;
516
+ let r = 0;
517
+ for (let c = t - e + 1; c <= t; c++) {
518
+ const u = i[c].close - o;
519
+ r += u * u;
520
+ }
521
+ const a = Math.sqrt(r / e);
522
+ return { mid: o, upper: o + n * a, lower: o - n * a };
523
+ }
524
+ export {
525
+ x as Backtester,
526
+ $ as FixedCommission,
527
+ L as NO_SLIPPAGE,
528
+ Q as PerShareCommission,
529
+ G as PercentCommission,
530
+ U as PercentSlippage,
531
+ I as Portfolio,
532
+ Z as RangeBasedSlippage,
533
+ A as ZERO_COMMISSION,
534
+ et as bollingerReversionStrategy,
535
+ T as computeRiskMetrics,
536
+ tt as donchianBreakoutStrategy,
537
+ X as rsiReversionStrategy,
538
+ J as runMonteCarlo,
539
+ K as smaCrossStrategy
540
+ };
541
+ //# sourceMappingURL=index.js.map