tradelab 1.0.0 → 1.1.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 +66 -0
- package/README.md +75 -12
- 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 +1893 -1003
- package/dist/cjs/live.cjs +134 -25
- package/dist/cjs/ta.cjs +339 -0
- package/docs/api-reference.md +46 -0
- package/docs/backtest-engine.md +112 -0
- package/docs/live-trading.md +51 -0
- package/docs/mcp.md +64 -0
- package/docs/research.md +103 -0
- package/docs/superpowers/plans/2026-00-overview.md +101 -0
- package/docs/superpowers/plans/2026-01-metrics-correctness.md +873 -0
- package/docs/superpowers/plans/2026-02-indicator-library.md +677 -0
- package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +882 -0
- package/docs/superpowers/plans/2026-04-async-signals-seeding.md +981 -0
- package/docs/superpowers/plans/2026-05-mcp-server.md +758 -0
- package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +508 -0
- package/docs/superpowers/plans/2026-07-funding-carry-costs.md +535 -0
- package/docs/superpowers/plans/2026-08-live-dashboard.md +547 -0
- package/docs/superpowers/plans/HANDOFF.md +88 -0
- package/examples/liveDashboard.js +33 -0
- package/examples/llmSignal.js +33 -0
- package/examples/optimize.js +25 -0
- package/package.json +16 -2
- 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 +86 -0
- package/src/engine/optimizeWorker.js +67 -0
- package/src/engine/walkForward.js +1 -0
- package/src/index.js +9 -0
- package/src/live/dashboard/server.js +120 -0
- package/src/live/engine/liveEngine.js +2 -2
- package/src/live/index.js +1 -0
- package/src/mcp/schemas.js +48 -0
- package/src/mcp/server.js +31 -0
- package/src/mcp/tools.js +142 -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 +174 -0
- package/types/index.d.ts +154 -0
- package/types/live.d.ts +15 -0
- package/types/ta.d.ts +45 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
# Funding, Borrow & Overnight Carry Costs Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Model the time-based costs the current engine ignores — overnight financing/borrow (annualized carry) and perpetual-futures funding — so leveraged longs and shorts don't show phantom edge.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Per-fill costs (slippage/spread/commission) already live in `applyFill`. Carry is _time_-based, not fill-based, so add a pure `financingCost({ side, notional, fromMs, toMs, costs })` to `execution.js` and deduct it inside each engine's leg-close accounting (`backtest.js`, `barSystemRunner.js`, `backtestTicks.js`). Each closed leg pays carry on its own quantity for its own hold duration. Config lives under `costs.carry` (annualized bps) and `costs.funding` (per-interval bps).
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node ESM, `node:test`. No new dependencies.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: `financingCost` + funding-event counting
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
|
|
17
|
+
- Modify: `src/engine/execution.js`
|
|
18
|
+
- Test: `test/engine/financing.test.js`
|
|
19
|
+
|
|
20
|
+
- [ ] **Step 1: Write the failing test**
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
// test/engine/financing.test.js
|
|
24
|
+
import test from "node:test";
|
|
25
|
+
import assert from "node:assert/strict";
|
|
26
|
+
import { financingCost, fundingEvents } from "../../src/engine/execution.js";
|
|
27
|
+
|
|
28
|
+
const YEAR = 365 * 24 * 60 * 60 * 1000;
|
|
29
|
+
|
|
30
|
+
test("carry: a long held one year at 5% annual on 10k notional costs 500", () => {
|
|
31
|
+
const cost = financingCost({
|
|
32
|
+
side: "long",
|
|
33
|
+
notional: 10_000,
|
|
34
|
+
fromMs: 0,
|
|
35
|
+
toMs: YEAR,
|
|
36
|
+
costs: { carry: { longAnnualBps: 500, shortAnnualBps: 800 } },
|
|
37
|
+
});
|
|
38
|
+
assert.ok(Math.abs(cost - 500) < 1e-6);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("carry: a short uses shortAnnualBps", () => {
|
|
42
|
+
const cost = financingCost({
|
|
43
|
+
side: "short",
|
|
44
|
+
notional: 10_000,
|
|
45
|
+
fromMs: 0,
|
|
46
|
+
toMs: YEAR / 2,
|
|
47
|
+
costs: { carry: { longAnnualBps: 500, shortAnnualBps: 800 } },
|
|
48
|
+
});
|
|
49
|
+
assert.ok(Math.abs(cost - 400) < 1e-6); // 8% * 0.5yr * 10k
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("fundingEvents counts boundaries strictly after from and up to to", () => {
|
|
53
|
+
const h8 = 8 * 60 * 60 * 1000;
|
|
54
|
+
// anchor 0, interval 8h: boundaries at 8h,16h,24h within (0, 24h] => 3
|
|
55
|
+
assert.equal(fundingEvents(0, 24 * 60 * 60 * 1000, h8, 0), 3);
|
|
56
|
+
assert.equal(fundingEvents(0, h8 - 1, h8, 0), 0);
|
|
57
|
+
assert.equal(fundingEvents(h8, 24 * 60 * 60 * 1000, h8, 0), 2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("funding: a long pays when rate is positive, a short receives", () => {
|
|
61
|
+
const h8 = 8 * 60 * 60 * 1000;
|
|
62
|
+
const base = { funding: { rateBps: 10, intervalMs: h8, anchorMs: 0 } };
|
|
63
|
+
const longCost = financingCost({
|
|
64
|
+
side: "long",
|
|
65
|
+
notional: 10_000,
|
|
66
|
+
fromMs: 0,
|
|
67
|
+
toMs: 24 * 60 * 60 * 1000,
|
|
68
|
+
costs: base,
|
|
69
|
+
});
|
|
70
|
+
const shortCost = financingCost({
|
|
71
|
+
side: "short",
|
|
72
|
+
notional: 10_000,
|
|
73
|
+
fromMs: 0,
|
|
74
|
+
toMs: 24 * 60 * 60 * 1000,
|
|
75
|
+
costs: base,
|
|
76
|
+
});
|
|
77
|
+
assert.ok(Math.abs(longCost - 30) < 1e-6); // 3 events * 10bps * 10k = 30
|
|
78
|
+
assert.ok(Math.abs(shortCost + 30) < 1e-6); // short receives funding
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("no carry/funding config => zero cost", () => {
|
|
82
|
+
assert.equal(
|
|
83
|
+
financingCost({ side: "long", notional: 10_000, fromMs: 0, toMs: YEAR, costs: {} }),
|
|
84
|
+
0
|
|
85
|
+
);
|
|
86
|
+
assert.equal(
|
|
87
|
+
financingCost({ side: "long", notional: 10_000, fromMs: 0, toMs: YEAR, costs: null }),
|
|
88
|
+
0
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- [ ] **Step 2: Run to verify it fails**
|
|
94
|
+
|
|
95
|
+
Run: `node --test test/engine/financing.test.js`
|
|
96
|
+
Expected: FAIL — `financingCost`/`fundingEvents` not exported.
|
|
97
|
+
|
|
98
|
+
- [ ] **Step 3: Add to src/engine/execution.js**
|
|
99
|
+
|
|
100
|
+
Append to `src/engine/execution.js`:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const MS_PER_YEAR = 365 * 24 * 60 * 60 * 1000;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Count funding boundaries in the half-open interval (fromMs, toMs], given a
|
|
107
|
+
* funding `intervalMs` cadence anchored at `anchorMs`.
|
|
108
|
+
*/
|
|
109
|
+
export function fundingEvents(fromMs, toMs, intervalMs, anchorMs = 0) {
|
|
110
|
+
if (!(intervalMs > 0) || toMs <= fromMs) return 0;
|
|
111
|
+
const firstK = Math.floor((fromMs - anchorMs) / intervalMs) + 1;
|
|
112
|
+
const lastK = Math.floor((toMs - anchorMs) / intervalMs);
|
|
113
|
+
return Math.max(0, lastK - firstK + 1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Time-based financing cost for holding `notional` from `fromMs` to `toMs`.
|
|
118
|
+
* Positive return = cost to the position (subtract from PnL).
|
|
119
|
+
*
|
|
120
|
+
* costs.carry = { longAnnualBps, shortAnnualBps } annualized borrow/margin
|
|
121
|
+
* costs.funding = { rateBps, intervalMs, anchorMs } perp funding per interval;
|
|
122
|
+
* longs pay when rateBps > 0, shorts receive (and vice versa).
|
|
123
|
+
*/
|
|
124
|
+
export function financingCost({ side, notional, fromMs, toMs, costs }) {
|
|
125
|
+
const model = costs || {};
|
|
126
|
+
const absNotional = Math.abs(notional);
|
|
127
|
+
let cost = 0;
|
|
128
|
+
|
|
129
|
+
if (model.carry) {
|
|
130
|
+
const annualBps =
|
|
131
|
+
side === "long" ? (model.carry.longAnnualBps ?? 0) : (model.carry.shortAnnualBps ?? 0);
|
|
132
|
+
const years = Math.max(0, toMs - fromMs) / MS_PER_YEAR;
|
|
133
|
+
cost += absNotional * (annualBps / 10000) * years;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const funding = model.funding;
|
|
137
|
+
if (funding && funding.intervalMs > 0 && Number.isFinite(funding.rateBps)) {
|
|
138
|
+
const count = fundingEvents(fromMs, toMs, funding.intervalMs, funding.anchorMs ?? 0);
|
|
139
|
+
const perEvent = absNotional * (funding.rateBps / 10000);
|
|
140
|
+
cost += (side === "long" ? 1 : -1) * perEvent * count;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return cost;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- [ ] **Step 4: Run to verify it passes**
|
|
148
|
+
|
|
149
|
+
Run: `node --test test/engine/financing.test.js`
|
|
150
|
+
Expected: PASS (5 tests).
|
|
151
|
+
|
|
152
|
+
- [ ] **Step 5: Commit**
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
git add src/engine/execution.js test/engine/financing.test.js
|
|
156
|
+
git commit -m "feat: add financingCost (carry + funding) helper
|
|
157
|
+
|
|
158
|
+
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### Task 2: Deduct financing in the candle engine
|
|
164
|
+
|
|
165
|
+
**Files:**
|
|
166
|
+
|
|
167
|
+
- Modify: `src/engine/backtest.js`
|
|
168
|
+
- Test: `test/financingIntegration.test.js`
|
|
169
|
+
|
|
170
|
+
- [ ] **Step 1: Write the failing test**
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
// test/financingIntegration.test.js
|
|
174
|
+
import test from "node:test";
|
|
175
|
+
import assert from "node:assert/strict";
|
|
176
|
+
import { backtest } from "../src/index.js";
|
|
177
|
+
|
|
178
|
+
// Daily candles, flat price so gross PnL is ~0; carry should make the long lose.
|
|
179
|
+
function flatCandles(n = 30) {
|
|
180
|
+
const start = Date.UTC(2025, 0, 2, 14, 30, 0);
|
|
181
|
+
return Array.from({ length: n }, (_, i) => ({
|
|
182
|
+
time: start + i * 86_400_000,
|
|
183
|
+
open: 100,
|
|
184
|
+
high: 100.5,
|
|
185
|
+
low: 99.5,
|
|
186
|
+
close: 100,
|
|
187
|
+
volume: 1000,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
test("overnight carry reduces a long's realized PnL vs no-carry", () => {
|
|
192
|
+
const candles = flatCandles();
|
|
193
|
+
const opts = {
|
|
194
|
+
candles,
|
|
195
|
+
interval: "1d",
|
|
196
|
+
warmupBars: 1,
|
|
197
|
+
flattenAtClose: false,
|
|
198
|
+
scaleOutAtR: 0,
|
|
199
|
+
signal({ index, bar, openPosition }) {
|
|
200
|
+
if (openPosition || index !== 1) return null;
|
|
201
|
+
return { side: "long", entry: bar.close, stop: bar.close - 2, rr: 50, _maxBarsInTrade: 20 };
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const noCarry = backtest(opts);
|
|
205
|
+
const withCarry = backtest({
|
|
206
|
+
...opts,
|
|
207
|
+
costs: { carry: { longAnnualBps: 1000, shortAnnualBps: 1000 } },
|
|
208
|
+
});
|
|
209
|
+
assert.ok(withCarry.metrics.totalPnL < noCarry.metrics.totalPnL);
|
|
210
|
+
// the closed leg records its financing
|
|
211
|
+
const leg = withCarry.positions[0];
|
|
212
|
+
assert.ok(leg.exit.financing > 0);
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- [ ] **Step 2: Run to verify it fails**
|
|
217
|
+
|
|
218
|
+
Run: `node --test test/financingIntegration.test.js`
|
|
219
|
+
Expected: FAIL — PnL unchanged (financing not deducted) and `leg.exit.financing`
|
|
220
|
+
is undefined.
|
|
221
|
+
|
|
222
|
+
- [ ] **Step 3: Import financingCost in backtest.js**
|
|
223
|
+
|
|
224
|
+
In `src/engine/backtest.js`, the existing import from `./execution.js` lists
|
|
225
|
+
`applyFill, clampStop, ...`. Add `financingCost` to that import list:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
import {
|
|
229
|
+
applyFill,
|
|
230
|
+
clampStop,
|
|
231
|
+
touchedLimit,
|
|
232
|
+
ocoExitCheck,
|
|
233
|
+
isEODBar,
|
|
234
|
+
roundStep,
|
|
235
|
+
estimateBarMs,
|
|
236
|
+
dayKeyUTC,
|
|
237
|
+
dayKeyET,
|
|
238
|
+
financingCost,
|
|
239
|
+
} from "./execution.js";
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- [ ] **Step 4: Deduct financing inside closeLeg**
|
|
243
|
+
|
|
244
|
+
In `backtest.js`'s `closeLeg`, find:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
const grossPnl = (exitPx - entryFill) * direction * qty;
|
|
248
|
+
const entryFeePortion = (openPos.entryFeeTotal || 0) * (qty / openPos.initSize);
|
|
249
|
+
const pnl = grossPnl - entryFeePortion - exitFeeTotal;
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Replace with:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
const grossPnl = (exitPx - entryFill) * direction * qty;
|
|
256
|
+
const entryFeePortion = (openPos.entryFeeTotal || 0) * (qty / openPos.initSize);
|
|
257
|
+
const financing = financingCost({
|
|
258
|
+
side: openPos.side,
|
|
259
|
+
notional: entryFill * qty,
|
|
260
|
+
fromMs: openPos.openTime,
|
|
261
|
+
toMs: time,
|
|
262
|
+
costs,
|
|
263
|
+
});
|
|
264
|
+
const pnl = grossPnl - entryFeePortion - exitFeeTotal - financing;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- [ ] **Step 5: Record financing on the leg**
|
|
268
|
+
|
|
269
|
+
In the same `closeLeg`, find the `record` object's `exit` block:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
exit: {
|
|
273
|
+
price: exitPx,
|
|
274
|
+
time,
|
|
275
|
+
reason,
|
|
276
|
+
pnl,
|
|
277
|
+
exitATR: openPos._lastATR ?? undefined,
|
|
278
|
+
},
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Replace with:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
exit: {
|
|
285
|
+
price: exitPx,
|
|
286
|
+
time,
|
|
287
|
+
reason,
|
|
288
|
+
pnl,
|
|
289
|
+
financing,
|
|
290
|
+
exitATR: openPos._lastATR ?? undefined,
|
|
291
|
+
},
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- [ ] **Step 6: Run test + suite**
|
|
295
|
+
|
|
296
|
+
Run: `node --test test/financingIntegration.test.js`
|
|
297
|
+
Expected: PASS.
|
|
298
|
+
|
|
299
|
+
Run: `node --test`
|
|
300
|
+
Expected: PASS — default `costs` (no carry/funding) yields `financing === 0`, so
|
|
301
|
+
all existing tests are unaffected.
|
|
302
|
+
|
|
303
|
+
- [ ] **Step 7: Commit**
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
git add src/engine/backtest.js test/financingIntegration.test.js
|
|
307
|
+
git commit -m "feat: deduct overnight/funding carry in candle backtest
|
|
308
|
+
|
|
309
|
+
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
### Task 3: Same deduction in portfolio runner + tick engine
|
|
315
|
+
|
|
316
|
+
**Files:**
|
|
317
|
+
|
|
318
|
+
- Modify: `src/engine/barSystemRunner.js`
|
|
319
|
+
- Modify: `src/engine/backtestTicks.js`
|
|
320
|
+
- Test: `test/financingIntegration.test.js` (append portfolio + tick cases)
|
|
321
|
+
|
|
322
|
+
- [ ] **Step 1: Append the failing tests**
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
// append to test/financingIntegration.test.js
|
|
326
|
+
import { backtestPortfolio, backtestTicks } from "../src/index.js";
|
|
327
|
+
|
|
328
|
+
test("portfolio runner deducts carry", () => {
|
|
329
|
+
const start = Date.UTC(2025, 0, 2, 14, 30, 0);
|
|
330
|
+
const candles = Array.from({ length: 30 }, (_, i) => ({
|
|
331
|
+
time: start + i * 86_400_000,
|
|
332
|
+
open: 100,
|
|
333
|
+
high: 100.5,
|
|
334
|
+
low: 99.5,
|
|
335
|
+
close: 100,
|
|
336
|
+
volume: 1000,
|
|
337
|
+
}));
|
|
338
|
+
const signal = ({ index, bar, openPosition }) =>
|
|
339
|
+
!openPosition && index === 1
|
|
340
|
+
? { side: "long", entry: bar.close, stop: bar.close - 2, rr: 50, _maxBarsInTrade: 20 }
|
|
341
|
+
: null;
|
|
342
|
+
const base = { equity: 10_000, systems: [{ symbol: "X", candles, signal }] };
|
|
343
|
+
const noCarry = backtestPortfolio({ ...base });
|
|
344
|
+
const withCarry = backtestPortfolio({
|
|
345
|
+
equity: 10_000,
|
|
346
|
+
costs: { carry: { longAnnualBps: 1000, shortAnnualBps: 1000 } },
|
|
347
|
+
systems: [
|
|
348
|
+
{
|
|
349
|
+
symbol: "X",
|
|
350
|
+
candles,
|
|
351
|
+
signal,
|
|
352
|
+
costs: { carry: { longAnnualBps: 1000, shortAnnualBps: 1000 } },
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
});
|
|
356
|
+
assert.ok(withCarry.metrics.totalPnL <= noCarry.metrics.totalPnL);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("tick engine deducts carry", () => {
|
|
360
|
+
const start = Date.UTC(2025, 0, 2, 14, 30, 0);
|
|
361
|
+
const ticks = Array.from({ length: 500 }, (_, i) => ({
|
|
362
|
+
time: start + i * 60_000,
|
|
363
|
+
bid: 100,
|
|
364
|
+
ask: 100.02,
|
|
365
|
+
}));
|
|
366
|
+
const signal = ({ index, bar, openPosition }) =>
|
|
367
|
+
!openPosition && index === 1
|
|
368
|
+
? { side: "long", entry: bar.close, stop: bar.close - 0.5, rr: 100 }
|
|
369
|
+
: null;
|
|
370
|
+
const withCarry = backtestTicks({
|
|
371
|
+
ticks,
|
|
372
|
+
signal,
|
|
373
|
+
costs: { carry: { longAnnualBps: 5000, shortAnnualBps: 5000 } },
|
|
374
|
+
});
|
|
375
|
+
assert.equal(typeof withCarry.metrics.totalPnL, "number");
|
|
376
|
+
if (withCarry.positions.length) assert.ok(withCarry.positions[0].exit.financing >= 0);
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Note: `backtestPortfolio` must thread `system.costs` into each runner's options.
|
|
381
|
+
Verify it already does (the runner reads `this.options.costs`). If portfolio does
|
|
382
|
+
not forward per-system `costs`, add that mapping where it constructs each
|
|
383
|
+
`BarSystemRunner`.
|
|
384
|
+
|
|
385
|
+
- [ ] **Step 2: Run to verify it fails**
|
|
386
|
+
|
|
387
|
+
Run: `node --test test/financingIntegration.test.js`
|
|
388
|
+
Expected: FAIL — `financing` not deducted in runner/tick engines.
|
|
389
|
+
|
|
390
|
+
- [ ] **Step 3: Edit barSystemRunner.js closeLeg**
|
|
391
|
+
|
|
392
|
+
Add `financingCost` to the `./execution.js` import list in
|
|
393
|
+
`src/engine/barSystemRunner.js`, then in its `closeLeg` find:
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
const grossPnl = (exitPx - entryFill) * direction * qty;
|
|
397
|
+
const entryFeePortion = (openPos.entryFeeTotal || 0) * (qty / openPos.initSize);
|
|
398
|
+
const pnl = grossPnl - entryFeePortion - exitFeeTotal;
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Replace with:
|
|
402
|
+
|
|
403
|
+
```js
|
|
404
|
+
const grossPnl = (exitPx - entryFill) * direction * qty;
|
|
405
|
+
const entryFeePortion = (openPos.entryFeeTotal || 0) * (qty / openPos.initSize);
|
|
406
|
+
const financing = financingCost({
|
|
407
|
+
side: openPos.side,
|
|
408
|
+
notional: entryFill * qty,
|
|
409
|
+
fromMs: openPos.openTime,
|
|
410
|
+
toMs: time,
|
|
411
|
+
costs: this.options.costs,
|
|
412
|
+
});
|
|
413
|
+
const pnl = grossPnl - entryFeePortion - exitFeeTotal - financing;
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
And add `financing,` to that method's `record.exit` object (same edit shape as
|
|
417
|
+
Task 2 Step 5).
|
|
418
|
+
|
|
419
|
+
- [ ] **Step 4: Edit backtestTicks.js closePosition**
|
|
420
|
+
|
|
421
|
+
Add `financingCost` to the `./execution.js` import in `src/engine/backtestTicks.js`,
|
|
422
|
+
then in `closePosition` find:
|
|
423
|
+
|
|
424
|
+
```js
|
|
425
|
+
const grossPnl = (price - open.entryFill) * direction * open.size;
|
|
426
|
+
const pnl = grossPnl - (open.entryFeeTotal || 0) - feeTotal;
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Replace with:
|
|
430
|
+
|
|
431
|
+
```js
|
|
432
|
+
const grossPnl = (price - open.entryFill) * direction * open.size;
|
|
433
|
+
const financing = financingCost({
|
|
434
|
+
side: open.side,
|
|
435
|
+
notional: open.entryFill * open.size,
|
|
436
|
+
fromMs: open.openTime,
|
|
437
|
+
toMs: tick.time,
|
|
438
|
+
costs,
|
|
439
|
+
});
|
|
440
|
+
const pnl = grossPnl - (open.entryFeeTotal || 0) - feeTotal - financing;
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Then in the `trade` object's `exit` block in the same function, add `financing,`:
|
|
444
|
+
|
|
445
|
+
```js
|
|
446
|
+
exit: {
|
|
447
|
+
price,
|
|
448
|
+
time: tick.time,
|
|
449
|
+
reason,
|
|
450
|
+
pnl,
|
|
451
|
+
financing,
|
|
452
|
+
},
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
- [ ] **Step 5: Run test + full suite**
|
|
456
|
+
|
|
457
|
+
Run: `node --test test/financingIntegration.test.js`
|
|
458
|
+
Expected: PASS.
|
|
459
|
+
|
|
460
|
+
Run: `node --test`
|
|
461
|
+
Expected: PASS.
|
|
462
|
+
|
|
463
|
+
- [ ] **Step 6: Lint + commit**
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
npm run lint
|
|
467
|
+
git add src/engine/barSystemRunner.js src/engine/backtestTicks.js test/financingIntegration.test.js
|
|
468
|
+
git commit -m "feat: deduct carry in portfolio and tick engines
|
|
469
|
+
|
|
470
|
+
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
### Task 4: Docs
|
|
476
|
+
|
|
477
|
+
**Files:**
|
|
478
|
+
|
|
479
|
+
- Modify: `docs/backtest-engine.md` and `README.md`
|
|
480
|
+
|
|
481
|
+
- [ ] **Step 1: Document the cost-model additions**
|
|
482
|
+
|
|
483
|
+
In the execution/cost-modeling section, add:
|
|
484
|
+
|
|
485
|
+
````markdown
|
|
486
|
+
### Financing & funding (time-based costs)
|
|
487
|
+
|
|
488
|
+
In addition to per-fill slippage/spread/commission, the cost model can charge
|
|
489
|
+
the cost of _holding_ a position:
|
|
490
|
+
|
|
491
|
+
```js
|
|
492
|
+
costs: {
|
|
493
|
+
carry: {
|
|
494
|
+
longAnnualBps: 600, // margin/borrow on a long, annualized bps
|
|
495
|
+
shortAnnualBps: 300, // borrow on a short, annualized bps
|
|
496
|
+
},
|
|
497
|
+
funding: {
|
|
498
|
+
rateBps: 1, // per-interval perp funding (bps of notional)
|
|
499
|
+
intervalMs: 8 * 60 * 60e3, // 8h funding cadence
|
|
500
|
+
anchorMs: 0, // funding timestamp alignment
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
- **Carry** accrues continuously: `notional * (annualBps/10000) * yearsHeld`.
|
|
506
|
+
- **Funding** charges at each interval boundary crossed while the position is
|
|
507
|
+
open. Longs pay when `rateBps > 0`; shorts receive (and vice versa).
|
|
508
|
+
- Each closed leg pays carry on its own quantity for its own hold time, and the
|
|
509
|
+
amount is reported on `position.exit.financing`.
|
|
510
|
+
````
|
|
511
|
+
|
|
512
|
+
- [ ] **Step 2: Update the README costs note**
|
|
513
|
+
|
|
514
|
+
Under "Execution and cost modeling", add a one-line mention of `costs.carry` and
|
|
515
|
+
`costs.funding` for perps/shorts, pointing to the docs section above.
|
|
516
|
+
|
|
517
|
+
- [ ] **Step 3: Format, test, commit**
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
npm run format:check && npm test
|
|
521
|
+
git add docs/backtest-engine.md README.md
|
|
522
|
+
git commit -m "docs: document carry and funding cost model
|
|
523
|
+
|
|
524
|
+
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Self-review checklist
|
|
530
|
+
|
|
531
|
+
- [ ] Default behavior unchanged: with no `costs.carry`/`costs.funding`, `financingCost` returns 0 and every existing test stays green. ✔ (Task 1 Step 1 last case)
|
|
532
|
+
- [ ] Carry charged per leg on the leg's own qty and hold window (correct under scale-outs). ✔ (Tasks 2, 3)
|
|
533
|
+
- [ ] Funding sign is correct: long pays positive rate, short receives. ✔ (Task 1 Step 1)
|
|
534
|
+
- [ ] `position.exit.financing` is surfaced in all three engines for transparency. ✔
|
|
535
|
+
- [ ] Portfolio forwards per-system `costs` to each runner (verified in Task 3 Step 1 note). ✔
|