@tradecanvas/analytics 1.4.0 → 1.7.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,541 +1,546 @@
1
- class $ {
2
- constructor(t) {
3
- this.perTrade = t;
4
- }
5
- calculate() {
6
- return this.perTrade;
7
- }
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
+ };
8
160
  }
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
- }
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
+ };
17
174
  }
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
- }
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;
26
186
  }
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
- }
41
- }
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
- }
51
- }
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
- }
158
- }
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
- };
175
- }
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
- };
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;
189
192
  }
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;
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));
201
198
  }
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;
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);
207
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));
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
+ };
213
219
  }
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);
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
+ };
222
240
  }
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 };
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;
231
249
  }
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
- };
254
- }
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;
264
- }
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
- }
379
- }
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
- };
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
+ };
431
418
  }
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
- }
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
+ }
437
424
  }
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);
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);
443
430
  }
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
- };
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
+ };
451
438
  }
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
- };
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
+ };
461
454
  }
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;
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;
467
459
  }
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
- };
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
+ };
475
474
  }
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));
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));
483
482
  }
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
- };
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
+ };
491
502
  }
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;
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;
497
507
  }
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;
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;
503
512
  }
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
- };
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
+ };
511
527
  }
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 };
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
+ };
523
542
  }
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
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