tradelab 1.0.1 → 1.2.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/CHANGELOG.md +112 -0
- package/README.md +188 -328
- package/bin/tradelab-mcp.js +7 -0
- package/bin/tradelab.js +29 -0
- package/dist/cjs/data.cjs +149 -26
- package/dist/cjs/index.cjs +1917 -1005
- package/dist/cjs/live.cjs +536 -25
- package/dist/cjs/ta.cjs +339 -0
- package/docs/README.md +32 -66
- package/docs/api-reference.md +283 -112
- package/docs/backtest-engine.md +210 -252
- package/docs/data-reporting-cli.md +114 -156
- package/docs/examples.md +6 -6
- package/docs/live-trading.md +263 -92
- package/docs/mcp.md +285 -0
- package/docs/research.md +157 -0
- package/examples/liveDashboard.js +33 -0
- package/examples/llmSignal.js +33 -0
- package/examples/mcpLiveTrading.js +77 -0
- package/examples/optimize.js +25 -0
- package/package.json +26 -4
- package/src/engine/asyncSignal.js +28 -0
- package/src/engine/backtest.js +13 -1
- package/src/engine/backtestAsync.js +27 -0
- package/src/engine/backtestTicks.js +13 -2
- package/src/engine/barSystemRunner.js +96 -41
- package/src/engine/execution.js +39 -0
- package/src/engine/grid.js +15 -0
- package/src/engine/llmSignal.js +84 -0
- package/src/engine/optimize.js +110 -0
- package/src/engine/optimizeWorker.js +67 -0
- package/src/engine/portfolio.js +4 -1
- package/src/engine/walkForward.js +1 -0
- package/src/index.js +9 -0
- package/src/live/dashboard/server.js +179 -0
- package/src/live/engine/liveEngine.js +2 -2
- package/src/live/engine/paperEngine.js +5 -0
- package/src/live/index.js +3 -0
- package/src/live/session.js +402 -0
- package/src/mcp/liveTools.js +179 -0
- package/src/mcp/schemas.js +167 -0
- package/src/mcp/server.js +35 -0
- package/src/mcp/tools.js +265 -0
- package/src/metrics/annualize.js +32 -0
- package/src/metrics/benchmark.js +55 -0
- package/src/metrics/buildMetrics.js +34 -13
- package/src/metrics/finite.js +17 -0
- package/src/research/combinations.js +18 -0
- package/src/research/cpcv.js +47 -0
- package/src/research/deflatedSharpe.js +35 -0
- package/src/research/index.js +6 -0
- package/src/research/monteCarlo.js +88 -0
- package/src/research/pbo.js +69 -0
- package/src/research/stats.js +78 -0
- package/src/strategies/builtins.js +96 -0
- package/src/strategies/index.js +30 -0
- package/src/ta/channels.js +67 -0
- package/src/ta/index.js +16 -0
- package/src/ta/oscillators.js +70 -0
- package/src/ta/trend.js +78 -0
- package/src/utils/random.js +33 -0
- package/templates/dashboard.html +661 -0
- package/types/index.d.ts +179 -0
- package/types/live.d.ts +114 -0
- package/types/mcp.d.ts +17 -0
- package/types/ta.d.ts +45 -0
package/bin/tradelab.js
CHANGED
|
@@ -116,6 +116,22 @@ function createBrokerAdapter(args, overrides = {}) {
|
|
|
116
116
|
throw new Error(`Unsupported broker "${brokerName}"`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
async function maybeStartDashboard(args, source) {
|
|
120
|
+
if (!toBoolean(args.dashboard, false)) return null;
|
|
121
|
+
const { createDashboardServer } = await import("../src/live/dashboard/server.js");
|
|
122
|
+
const dashboard = createDashboardServer({
|
|
123
|
+
source,
|
|
124
|
+
port: toNumber(args.dashboardPort, 4317),
|
|
125
|
+
});
|
|
126
|
+
const url = await dashboard.start();
|
|
127
|
+
console.log(`dashboard: ${url}`);
|
|
128
|
+
return dashboard;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function closeDashboard(dashboard) {
|
|
132
|
+
if (dashboard) await dashboard.close();
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
function brokerConfigFromArgs(args, overrides = {}) {
|
|
120
136
|
return {
|
|
121
137
|
apiKey: overrides.apiKey ?? args.apiKey,
|
|
@@ -514,6 +530,7 @@ async function commandLive(args, overrides = {}) {
|
|
|
514
530
|
maxDailyLossPct: toNumber(fileConfig.maxDailyLossPct ?? args.maxDailyLossPct, 0),
|
|
515
531
|
equity: toNumber(fileConfig.equity ?? args.equity, 10_000),
|
|
516
532
|
});
|
|
533
|
+
const dashboard = await maybeStartDashboard(args, orchestrator);
|
|
517
534
|
await broker.connect(brokerConfig);
|
|
518
535
|
await orchestrator.start();
|
|
519
536
|
|
|
@@ -526,6 +543,15 @@ async function commandLive(args, overrides = {}) {
|
|
|
526
543
|
|
|
527
544
|
if (!watch) {
|
|
528
545
|
await orchestrator.stop();
|
|
546
|
+
await closeDashboard(dashboard);
|
|
547
|
+
} else if (dashboard) {
|
|
548
|
+
const shutdown = async () => {
|
|
549
|
+
await orchestrator.stop();
|
|
550
|
+
await closeDashboard(dashboard);
|
|
551
|
+
process.exit(0);
|
|
552
|
+
};
|
|
553
|
+
process.once("SIGINT", shutdown);
|
|
554
|
+
process.once("SIGTERM", shutdown);
|
|
529
555
|
}
|
|
530
556
|
return;
|
|
531
557
|
}
|
|
@@ -551,6 +577,7 @@ async function commandLive(args, overrides = {}) {
|
|
|
551
577
|
storage,
|
|
552
578
|
brokerConfig,
|
|
553
579
|
});
|
|
580
|
+
const dashboard = await maybeStartDashboard(args, engine);
|
|
554
581
|
|
|
555
582
|
await engine.start();
|
|
556
583
|
|
|
@@ -563,11 +590,13 @@ async function commandLive(args, overrides = {}) {
|
|
|
563
590
|
|
|
564
591
|
if (!watch) {
|
|
565
592
|
await engine.stop();
|
|
593
|
+
await closeDashboard(dashboard);
|
|
566
594
|
return;
|
|
567
595
|
}
|
|
568
596
|
|
|
569
597
|
const shutdown = async () => {
|
|
570
598
|
await engine.stop();
|
|
599
|
+
await closeDashboard(dashboard);
|
|
571
600
|
process.exit(0);
|
|
572
601
|
};
|
|
573
602
|
process.once("SIGINT", shutdown);
|
package/dist/cjs/data.cjs
CHANGED
|
@@ -119,22 +119,93 @@ function calculatePositionSize({
|
|
|
119
119
|
return quantity >= minQty ? quantity : 0;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// src/metrics/finite.js
|
|
123
|
+
var BIG_NUMBER = 1e9;
|
|
124
|
+
function clampFinite(value, fallback = 0) {
|
|
125
|
+
if (value === Infinity) return BIG_NUMBER;
|
|
126
|
+
if (value === -Infinity) return -BIG_NUMBER;
|
|
127
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
128
|
+
return fallback;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/metrics/annualize.js
|
|
132
|
+
var TRADING_DAYS = 252;
|
|
133
|
+
var RTH_HOURS = 6.5;
|
|
134
|
+
var MS_PER_YEAR = 365 * 24 * 60 * 60 * 1e3;
|
|
135
|
+
var INTERVAL_PERIODS = {
|
|
136
|
+
"1m": TRADING_DAYS * RTH_HOURS * 60,
|
|
137
|
+
"2m": TRADING_DAYS * RTH_HOURS * 30,
|
|
138
|
+
"5m": TRADING_DAYS * RTH_HOURS * 12,
|
|
139
|
+
"15m": TRADING_DAYS * RTH_HOURS * 4,
|
|
140
|
+
"30m": TRADING_DAYS * RTH_HOURS * 2,
|
|
141
|
+
"1h": TRADING_DAYS * RTH_HOURS,
|
|
142
|
+
"60m": TRADING_DAYS * RTH_HOURS,
|
|
143
|
+
"1d": TRADING_DAYS,
|
|
144
|
+
"1wk": 52,
|
|
145
|
+
"1mo": 12
|
|
146
|
+
};
|
|
147
|
+
function periodsPerYear(interval, estBarMs) {
|
|
148
|
+
if (interval && INTERVAL_PERIODS[interval]) return INTERVAL_PERIODS[interval];
|
|
149
|
+
if (Number.isFinite(estBarMs) && estBarMs > 0) {
|
|
150
|
+
return Math.round(MS_PER_YEAR / estBarMs);
|
|
151
|
+
}
|
|
152
|
+
return TRADING_DAYS;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/metrics/benchmark.js
|
|
156
|
+
function mean(xs) {
|
|
157
|
+
return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
|
|
158
|
+
}
|
|
159
|
+
function benchmarkStats(strategyReturns, benchmarkReturns) {
|
|
160
|
+
const nullStats = {
|
|
161
|
+
alpha: null,
|
|
162
|
+
beta: null,
|
|
163
|
+
correlation: null,
|
|
164
|
+
informationRatio: null,
|
|
165
|
+
trackingError: null
|
|
166
|
+
};
|
|
167
|
+
if (!Array.isArray(strategyReturns) || !Array.isArray(benchmarkReturns) || strategyReturns.length === 0 || strategyReturns.length !== benchmarkReturns.length) {
|
|
168
|
+
return nullStats;
|
|
169
|
+
}
|
|
170
|
+
const meanStrat = mean(strategyReturns);
|
|
171
|
+
const meanBench = mean(benchmarkReturns);
|
|
172
|
+
let covar = 0;
|
|
173
|
+
let varBench = 0;
|
|
174
|
+
let varStrat = 0;
|
|
175
|
+
for (let i = 0; i < strategyReturns.length; i += 1) {
|
|
176
|
+
const ds = strategyReturns[i] - meanStrat;
|
|
177
|
+
const db = benchmarkReturns[i] - meanBench;
|
|
178
|
+
covar += ds * db;
|
|
179
|
+
varBench += db * db;
|
|
180
|
+
varStrat += ds * ds;
|
|
181
|
+
}
|
|
182
|
+
const beta = varBench === 0 ? 0 : covar / varBench;
|
|
183
|
+
const alpha = meanStrat - beta * meanBench;
|
|
184
|
+
const denom = Math.sqrt(varStrat * varBench);
|
|
185
|
+
const correlation = denom === 0 ? 0 : covar / denom;
|
|
186
|
+
const active = strategyReturns.map((r, i) => r - benchmarkReturns[i]);
|
|
187
|
+
const meanActive = mean(active);
|
|
188
|
+
const trackingError = Math.sqrt(mean(active.map((a) => (a - meanActive) ** 2)));
|
|
189
|
+
const informationRatio = trackingError === 0 ? 0 : meanActive / trackingError;
|
|
190
|
+
return { alpha, beta, correlation, informationRatio, trackingError };
|
|
191
|
+
}
|
|
192
|
+
|
|
122
193
|
// src/metrics/buildMetrics.js
|
|
123
194
|
function sum(values) {
|
|
124
195
|
return values.reduce((total, value) => total + value, 0);
|
|
125
196
|
}
|
|
126
|
-
function
|
|
197
|
+
function mean2(values) {
|
|
127
198
|
return values.length ? sum(values) / values.length : 0;
|
|
128
199
|
}
|
|
129
200
|
function stddev(values) {
|
|
130
201
|
if (values.length <= 1) return 0;
|
|
131
|
-
const avg =
|
|
132
|
-
return Math.sqrt(
|
|
202
|
+
const avg = mean2(values);
|
|
203
|
+
return Math.sqrt(mean2(values.map((value) => (value - avg) ** 2)));
|
|
133
204
|
}
|
|
134
205
|
function sortino(values) {
|
|
135
206
|
const losses = values.filter((value) => value < 0);
|
|
136
207
|
const downsideDeviation = stddev(losses.length ? losses : [0]);
|
|
137
|
-
const avg =
|
|
208
|
+
const avg = mean2(values);
|
|
138
209
|
return downsideDeviation === 0 ? avg > 0 ? Infinity : 0 : avg / downsideDeviation;
|
|
139
210
|
}
|
|
140
211
|
function dayKeyUTC(timeMs) {
|
|
@@ -220,14 +291,22 @@ function percentile(values, percentileRank) {
|
|
|
220
291
|
const index = Math.floor((sorted.length - 1) * percentileRank);
|
|
221
292
|
return sorted[index];
|
|
222
293
|
}
|
|
223
|
-
var PROFIT_FACTOR_CAP = 1e6;
|
|
224
294
|
function finiteProfitFactor(grossProfit, grossLoss) {
|
|
225
295
|
if (grossLoss === 0) {
|
|
226
|
-
return grossProfit > 0 ?
|
|
296
|
+
return grossProfit > 0 ? BIG_NUMBER : 0;
|
|
227
297
|
}
|
|
228
298
|
return grossProfit / grossLoss;
|
|
229
299
|
}
|
|
230
|
-
function buildMetrics({
|
|
300
|
+
function buildMetrics({
|
|
301
|
+
closed,
|
|
302
|
+
equityStart,
|
|
303
|
+
equityFinal,
|
|
304
|
+
candles,
|
|
305
|
+
estBarMs,
|
|
306
|
+
eqSeries,
|
|
307
|
+
interval,
|
|
308
|
+
benchmarkReturns
|
|
309
|
+
}) {
|
|
231
310
|
const legs = [...closed].sort((left, right) => left.exit.time - right.exit.time);
|
|
232
311
|
const completedTrades = [];
|
|
233
312
|
const tradeRs = [];
|
|
@@ -297,11 +376,11 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
|
|
|
297
376
|
if (pnl > 0) shortTradeWins += 1;
|
|
298
377
|
}
|
|
299
378
|
}
|
|
300
|
-
const avgR =
|
|
379
|
+
const avgR = mean2(tradeRs);
|
|
301
380
|
const { maxWin, maxLoss } = streaks(labels);
|
|
302
|
-
const expectancy =
|
|
381
|
+
const expectancy = mean2(tradePnls);
|
|
303
382
|
const tradeReturnStd = stddev(tradeReturns);
|
|
304
|
-
const sharpePerTrade = tradeReturnStd === 0 ? tradeReturns.length ? Infinity : 0 :
|
|
383
|
+
const sharpePerTrade = tradeReturnStd === 0 ? tradeReturns.length ? Infinity : 0 : mean2(tradeReturns) / tradeReturnStd;
|
|
305
384
|
const sortinoPerTrade = sortino(tradeReturns);
|
|
306
385
|
const profitFactorPositions = finiteProfitFactor(grossProfitPositions, grossLossPositions);
|
|
307
386
|
const profitFactorLegs = finiteProfitFactor(grossProfitLegs, grossLossLegs);
|
|
@@ -309,11 +388,11 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
|
|
|
309
388
|
const calmar = maxDrawdown === 0 ? returnPct > 0 ? Infinity : 0 : returnPct / maxDrawdown;
|
|
310
389
|
const totalBars = Math.max(1, candles.length);
|
|
311
390
|
const exposurePct = openBars / totalBars;
|
|
312
|
-
const avgHoldMin =
|
|
391
|
+
const avgHoldMin = mean2(holdDurationsMinutes);
|
|
313
392
|
const equitySeries = eqSeries && eqSeries.length ? eqSeries : buildEquitySeriesFromLegs({ legs, equityStart });
|
|
314
393
|
const dailyReturnsSeries = dailyReturns(equitySeries);
|
|
315
394
|
const dailyStd = stddev(dailyReturnsSeries);
|
|
316
|
-
const sharpeDaily = dailyStd === 0 ? dailyReturnsSeries.length ? Infinity : 0 :
|
|
395
|
+
const sharpeDaily = dailyStd === 0 ? dailyReturnsSeries.length ? Infinity : 0 : mean2(dailyReturnsSeries) / dailyStd;
|
|
317
396
|
const sortinoDaily = sortino(dailyReturnsSeries);
|
|
318
397
|
const dailyWinRate = dailyReturnsSeries.length ? dailyReturnsSeries.filter((value) => value > 0).length / dailyReturnsSeries.length : 0;
|
|
319
398
|
const rDistribution = {
|
|
@@ -335,28 +414,36 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
|
|
|
335
414
|
trades: longTradesCount,
|
|
336
415
|
winRate: longTradesCount ? longTradeWins / longTradesCount : 0,
|
|
337
416
|
avgPnL: longTradesCount ? longPnLSum / longTradesCount : 0,
|
|
338
|
-
avgR:
|
|
417
|
+
avgR: mean2(longRs)
|
|
339
418
|
},
|
|
340
419
|
short: {
|
|
341
420
|
trades: shortTradesCount,
|
|
342
421
|
winRate: shortTradesCount ? shortTradeWins / shortTradesCount : 0,
|
|
343
422
|
avgPnL: shortTradesCount ? shortPnLSum / shortTradesCount : 0,
|
|
344
|
-
avgR:
|
|
423
|
+
avgR: mean2(shortRs)
|
|
345
424
|
}
|
|
346
425
|
};
|
|
426
|
+
const periods = periodsPerYear(interval, estBarMs);
|
|
427
|
+
const sqrtPeriods = Math.sqrt(periods);
|
|
428
|
+
const sharpeAnnualized = clampFinite(clampFinite(sharpeDaily) * sqrtPeriods);
|
|
429
|
+
const sortinoAnnualized = clampFinite(clampFinite(sortinoDaily) * sqrtPeriods);
|
|
430
|
+
const benchmark = benchmarkStats(dailyReturnsSeries, benchmarkReturns ?? []);
|
|
347
431
|
return {
|
|
348
432
|
trades: completedTrades.length,
|
|
349
433
|
winRate: completedTrades.length ? winningTradeCount / completedTrades.length : 0,
|
|
350
|
-
profitFactor: profitFactorPositions,
|
|
434
|
+
profitFactor: clampFinite(profitFactorPositions),
|
|
351
435
|
expectancy,
|
|
352
436
|
totalR,
|
|
353
437
|
avgR,
|
|
354
|
-
sharpe: sharpeDaily,
|
|
355
|
-
|
|
356
|
-
|
|
438
|
+
sharpe: clampFinite(sharpeDaily),
|
|
439
|
+
sharpeAnnualized,
|
|
440
|
+
sortinoAnnualized,
|
|
441
|
+
sharpePerTrade: clampFinite(sharpePerTrade),
|
|
442
|
+
sortinoPerTrade: clampFinite(sortinoPerTrade),
|
|
443
|
+
annualizationPeriods: periods,
|
|
357
444
|
maxDrawdown,
|
|
358
445
|
maxDrawdownPct: maxDrawdown,
|
|
359
|
-
calmar,
|
|
446
|
+
calmar: clampFinite(calmar),
|
|
360
447
|
maxConsecWins: maxWin,
|
|
361
448
|
maxConsecLosses: maxLoss,
|
|
362
449
|
avgHold: avgHoldMin,
|
|
@@ -366,12 +453,13 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
|
|
|
366
453
|
returnPct,
|
|
367
454
|
finalEquity: equityFinal,
|
|
368
455
|
startEquity: equityStart,
|
|
369
|
-
profitFactor_pos: profitFactorPositions,
|
|
370
|
-
profitFactor_leg: profitFactorLegs,
|
|
456
|
+
profitFactor_pos: clampFinite(profitFactorPositions),
|
|
457
|
+
profitFactor_leg: clampFinite(profitFactorLegs),
|
|
371
458
|
winRate_pos: completedTrades.length ? winningTradeCount / completedTrades.length : 0,
|
|
372
459
|
winRate_leg: legs.length ? winningLegCount / legs.length : 0,
|
|
373
|
-
sharpeDaily,
|
|
374
|
-
sortinoDaily,
|
|
460
|
+
sharpeDaily: clampFinite(sharpeDaily),
|
|
461
|
+
sortinoDaily: clampFinite(sortinoDaily),
|
|
462
|
+
benchmark,
|
|
375
463
|
sideBreakdown,
|
|
376
464
|
long: sideBreakdown.long,
|
|
377
465
|
short: sideBreakdown.short,
|
|
@@ -380,7 +468,7 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
|
|
|
380
468
|
daily: {
|
|
381
469
|
count: dailyReturnsSeries.length,
|
|
382
470
|
winRate: dailyWinRate,
|
|
383
|
-
avgReturn:
|
|
471
|
+
avgReturn: mean2(dailyReturnsSeries)
|
|
384
472
|
}
|
|
385
473
|
};
|
|
386
474
|
}
|
|
@@ -800,6 +888,30 @@ function dayKeyET(timeMs) {
|
|
|
800
888
|
const pseudoEtTime = anchor.getTime() + hoursET * 60 * 60 * 1e3 + minutesETDay * 60 * 1e3;
|
|
801
889
|
return dayKeyUTC2(pseudoEtTime);
|
|
802
890
|
}
|
|
891
|
+
var MS_PER_YEAR2 = 365 * 24 * 60 * 60 * 1e3;
|
|
892
|
+
function fundingEvents(fromMs, toMs, intervalMs, anchorMs = 0) {
|
|
893
|
+
if (!(intervalMs > 0) || toMs <= fromMs) return 0;
|
|
894
|
+
const firstK = Math.floor((fromMs - anchorMs) / intervalMs) + 1;
|
|
895
|
+
const lastK = Math.floor((toMs - anchorMs) / intervalMs);
|
|
896
|
+
return Math.max(0, lastK - firstK + 1);
|
|
897
|
+
}
|
|
898
|
+
function financingCost({ side, notional, fromMs, toMs, costs }) {
|
|
899
|
+
const model = costs || {};
|
|
900
|
+
const absNotional = Math.abs(notional);
|
|
901
|
+
let cost = 0;
|
|
902
|
+
if (model.carry) {
|
|
903
|
+
const annualBps = side === "long" ? model.carry.longAnnualBps ?? 0 : model.carry.shortAnnualBps ?? 0;
|
|
904
|
+
const years = Math.max(0, toMs - fromMs) / MS_PER_YEAR2;
|
|
905
|
+
cost += absNotional * (annualBps / 1e4) * years;
|
|
906
|
+
}
|
|
907
|
+
const funding = model.funding;
|
|
908
|
+
if (funding && funding.intervalMs > 0 && Number.isFinite(funding.rateBps)) {
|
|
909
|
+
const count = fundingEvents(fromMs, toMs, funding.intervalMs, funding.anchorMs ?? 0);
|
|
910
|
+
const perEvent = absNotional * (funding.rateBps / 1e4);
|
|
911
|
+
cost += (side === "long" ? 1 : -1) * perEvent * count;
|
|
912
|
+
}
|
|
913
|
+
return cost;
|
|
914
|
+
}
|
|
803
915
|
|
|
804
916
|
// src/engine/backtest.js
|
|
805
917
|
function asNumber(value) {
|
|
@@ -931,6 +1043,7 @@ function mergeOptions(options) {
|
|
|
931
1043
|
maxSlipROnFill: options.maxSlipROnFill ?? 0.4,
|
|
932
1044
|
collectEqSeries: options.collectEqSeries ?? true,
|
|
933
1045
|
collectReplay: options.collectReplay ?? true,
|
|
1046
|
+
benchmarkReturns: Array.isArray(options.benchmarkReturns) ? options.benchmarkReturns : null,
|
|
934
1047
|
strict: options.strict ?? false
|
|
935
1048
|
};
|
|
936
1049
|
}
|
|
@@ -1053,7 +1166,14 @@ function backtest(rawOptions) {
|
|
|
1053
1166
|
const entryFill = openPos.entryFill;
|
|
1054
1167
|
const grossPnl = (exitPx - entryFill) * direction * qty;
|
|
1055
1168
|
const entryFeePortion = (openPos.entryFeeTotal || 0) * (qty / openPos.initSize);
|
|
1056
|
-
const
|
|
1169
|
+
const financing = financingCost({
|
|
1170
|
+
side: openPos.side,
|
|
1171
|
+
notional: entryFill * qty,
|
|
1172
|
+
fromMs: openPos.openTime,
|
|
1173
|
+
toMs: time,
|
|
1174
|
+
costs
|
|
1175
|
+
});
|
|
1176
|
+
const pnl = grossPnl - entryFeePortion - exitFeeTotal - financing;
|
|
1057
1177
|
currentEquity += pnl;
|
|
1058
1178
|
dayPnl += pnl;
|
|
1059
1179
|
if (wantEqSeries) {
|
|
@@ -1081,6 +1201,7 @@ function backtest(rawOptions) {
|
|
|
1081
1201
|
time,
|
|
1082
1202
|
reason,
|
|
1083
1203
|
pnl,
|
|
1204
|
+
financing,
|
|
1084
1205
|
exitATR: openPos._lastATR ?? void 0
|
|
1085
1206
|
},
|
|
1086
1207
|
mfeR: openPos._mfeR ?? 0,
|
|
@@ -1483,7 +1604,9 @@ function backtest(rawOptions) {
|
|
|
1483
1604
|
equityFinal: currentEquity,
|
|
1484
1605
|
candles,
|
|
1485
1606
|
estBarMs: estimatedBarMs,
|
|
1486
|
-
eqSeries
|
|
1607
|
+
eqSeries,
|
|
1608
|
+
interval: options.interval,
|
|
1609
|
+
benchmarkReturns: options.benchmarkReturns
|
|
1487
1610
|
});
|
|
1488
1611
|
const positions = closed.filter((trade) => trade.exit.reason !== "SCALE");
|
|
1489
1612
|
const lastPrice = asNumber(candles[candles.length - 1]?.close);
|