thevoidforge 21.0.10 → 21.0.12

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.
Files changed (108) hide show
  1. package/dist/.claude/commands/ai.md +69 -0
  2. package/dist/.claude/commands/architect.md +121 -0
  3. package/dist/.claude/commands/assemble.md +201 -0
  4. package/dist/.claude/commands/assess.md +75 -0
  5. package/dist/.claude/commands/blueprint.md +135 -0
  6. package/dist/.claude/commands/build.md +116 -0
  7. package/dist/.claude/commands/campaign.md +201 -0
  8. package/dist/.claude/commands/cultivation.md +166 -0
  9. package/dist/.claude/commands/current.md +128 -0
  10. package/dist/.claude/commands/dangerroom.md +74 -0
  11. package/dist/.claude/commands/debrief.md +178 -0
  12. package/dist/.claude/commands/deploy.md +99 -0
  13. package/dist/.claude/commands/devops.md +143 -0
  14. package/dist/.claude/commands/gauntlet.md +140 -0
  15. package/dist/.claude/commands/git.md +104 -0
  16. package/dist/.claude/commands/grow.md +146 -0
  17. package/dist/.claude/commands/imagine.md +126 -0
  18. package/dist/.claude/commands/portfolio.md +50 -0
  19. package/dist/.claude/commands/prd.md +113 -0
  20. package/dist/.claude/commands/qa.md +107 -0
  21. package/dist/.claude/commands/review.md +151 -0
  22. package/dist/.claude/commands/security.md +100 -0
  23. package/dist/.claude/commands/test.md +96 -0
  24. package/dist/.claude/commands/thumper.md +116 -0
  25. package/dist/.claude/commands/treasury.md +100 -0
  26. package/dist/.claude/commands/ux.md +118 -0
  27. package/dist/.claude/commands/vault.md +189 -0
  28. package/dist/.claude/commands/void.md +108 -0
  29. package/dist/CHANGELOG.md +1918 -0
  30. package/dist/CLAUDE.md +250 -0
  31. package/dist/HOLOCRON.md +856 -0
  32. package/dist/VERSION.md +123 -0
  33. package/dist/docs/NAMING_REGISTRY.md +478 -0
  34. package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
  35. package/dist/docs/methods/ASSEMBLER.md +142 -0
  36. package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
  37. package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
  38. package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
  39. package/dist/docs/methods/CAMPAIGN.md +568 -0
  40. package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
  41. package/dist/docs/methods/DEEP_CURRENT.md +184 -0
  42. package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
  43. package/dist/docs/methods/FIELD_MEDIC.md +261 -0
  44. package/dist/docs/methods/FORGE_ARTIST.md +108 -0
  45. package/dist/docs/methods/FORGE_KEEPER.md +268 -0
  46. package/dist/docs/methods/GAUNTLET.md +344 -0
  47. package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
  48. package/dist/docs/methods/HEARTBEAT.md +168 -0
  49. package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
  50. package/dist/docs/methods/MUSTER.md +148 -0
  51. package/dist/docs/methods/PRD_GENERATOR.md +186 -0
  52. package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
  53. package/dist/docs/methods/QA_ENGINEER.md +337 -0
  54. package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
  55. package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
  56. package/dist/docs/methods/SUB_AGENTS.md +335 -0
  57. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
  58. package/dist/docs/methods/TESTING.md +359 -0
  59. package/dist/docs/methods/THUMPER.md +175 -0
  60. package/dist/docs/methods/TIME_VAULT.md +120 -0
  61. package/dist/docs/methods/TREASURY.md +184 -0
  62. package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
  63. package/dist/docs/patterns/README.md +52 -0
  64. package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
  65. package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
  66. package/dist/docs/patterns/ai-classifier.ts +195 -0
  67. package/dist/docs/patterns/ai-eval.ts +272 -0
  68. package/dist/docs/patterns/ai-orchestrator.ts +341 -0
  69. package/dist/docs/patterns/ai-router.ts +194 -0
  70. package/dist/docs/patterns/ai-tool-schema.ts +237 -0
  71. package/dist/docs/patterns/api-route.ts +241 -0
  72. package/dist/docs/patterns/backtest-engine.ts +499 -0
  73. package/dist/docs/patterns/browser-review.ts +292 -0
  74. package/dist/docs/patterns/combobox.tsx +300 -0
  75. package/dist/docs/patterns/component.tsx +262 -0
  76. package/dist/docs/patterns/daemon-process.ts +338 -0
  77. package/dist/docs/patterns/data-pipeline.ts +297 -0
  78. package/dist/docs/patterns/database-migration.ts +466 -0
  79. package/dist/docs/patterns/e2e-test.ts +629 -0
  80. package/dist/docs/patterns/error-handling.ts +312 -0
  81. package/dist/docs/patterns/execution-safety.ts +601 -0
  82. package/dist/docs/patterns/financial-transaction.ts +342 -0
  83. package/dist/docs/patterns/funding-plan.ts +462 -0
  84. package/dist/docs/patterns/game-entity.ts +137 -0
  85. package/dist/docs/patterns/game-loop.ts +113 -0
  86. package/dist/docs/patterns/game-state.ts +143 -0
  87. package/dist/docs/patterns/job-queue.ts +225 -0
  88. package/dist/docs/patterns/kongo-integration.ts +164 -0
  89. package/dist/docs/patterns/middleware.ts +363 -0
  90. package/dist/docs/patterns/mobile-screen.tsx +139 -0
  91. package/dist/docs/patterns/mobile-service.ts +167 -0
  92. package/dist/docs/patterns/multi-tenant.ts +382 -0
  93. package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
  94. package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
  95. package/dist/docs/patterns/prompt-template.ts +195 -0
  96. package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
  97. package/dist/docs/patterns/service.ts +224 -0
  98. package/dist/docs/patterns/sse-endpoint.ts +118 -0
  99. package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
  100. package/dist/docs/patterns/third-party-script.ts +68 -0
  101. package/dist/scripts/thumper/gom-jabbar.sh +241 -0
  102. package/dist/scripts/thumper/relay.sh +610 -0
  103. package/dist/scripts/thumper/scan.sh +359 -0
  104. package/dist/scripts/thumper/thumper.sh +190 -0
  105. package/dist/scripts/thumper/water-rings.sh +76 -0
  106. package/dist/scripts/voidforge.js +1 -1
  107. package/package.json +1 -1
  108. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,499 @@
1
+ /**
2
+ * Pattern: Backtest Engine (Walk-Forward Validation)
3
+ *
4
+ * Key principles:
5
+ * - Strategy only sees past data — no lookahead bias
6
+ * - Walk-forward validation: train on window N, test on window N+1, slide forward
7
+ * - Survivorship bias prevention: include delisted/dead instruments, not just survivors
8
+ * - Slippage and commission models — results without friction are fiction
9
+ * - Out-of-sample separation enforced at the engine level
10
+ * - Equity curve, Sharpe, max drawdown, win rate, profit factor computed post-run
11
+ * - Static lookahead check — fail before running if strategy accesses future data
12
+ *
13
+ * Agents: Stark (backend), Banner (data), Picard (architecture)
14
+ *
15
+ * Framework adaptations:
16
+ * TypeScript: This file (custom engine, full control)
17
+ * Python: backtrader, vectorbt, or zipline (see bottom)
18
+ */
19
+
20
+ // ── Market Data Types ───────────────────────────────────
21
+
22
+ interface OHLCV {
23
+ timestamp: string; // ISO 8601
24
+ open: number;
25
+ high: number;
26
+ low: number;
27
+ close: number;
28
+ volume: number;
29
+ /** True if this instrument was delisted on or after this date */
30
+ delisted?: boolean;
31
+ /** Adjusted close for splits/dividends — use this for signals, raw close for execution */
32
+ adjClose?: number;
33
+ }
34
+
35
+ type SignalSide = 'buy' | 'sell' | 'close';
36
+
37
+ interface Signal {
38
+ symbol: string;
39
+ side: SignalSide;
40
+ /** Fraction of portfolio (0.0 - 1.0) or absolute quantity */
41
+ size: number;
42
+ sizeType: 'percent' | 'absolute';
43
+ /** Optional limit price — null means market order */
44
+ limitPrice: number | null;
45
+ reason: string;
46
+ }
47
+
48
+ // ── Portfolio State ─────────────────────────────────────
49
+
50
+ interface Position {
51
+ symbol: string;
52
+ quantity: number;
53
+ avgEntryPrice: number;
54
+ currentPrice: number;
55
+ unrealizedPnl: number;
56
+ }
57
+
58
+ interface Portfolio {
59
+ cash: number;
60
+ equity: number;
61
+ positions: Map<string, Position>;
62
+ /** Realized PnL since inception */
63
+ realizedPnl: number;
64
+ }
65
+
66
+ // ── Strategy Interface ──────────────────────────────────
67
+ // Strategy receives ONLY the current bar and portfolio state.
68
+ // It has NO access to future bars — the engine enforces this.
69
+
70
+ interface Strategy {
71
+ name: string;
72
+ /** Called once before backtest starts — initialize indicators, state */
73
+ init?(config: BacktestConfig): void;
74
+ /** Called for each bar — return signals based on ONLY current and past data */
75
+ onBar(bar: OHLCV, portfolio: Readonly<Portfolio>, history: readonly OHLCV[]): Signal[];
76
+ }
77
+
78
+ // ── Friction Models ─────────────────────────────────────
79
+
80
+ type SlippageModel =
81
+ | { type: 'fixed'; basisPoints: number }
82
+ | { type: 'percentage'; pct: number }
83
+ | { type: 'volume'; impactFactor: number; maxPctOfVolume: number };
84
+
85
+ type CommissionModel =
86
+ | { type: 'per-trade'; amount: number }
87
+ | { type: 'per-share'; amount: number; minimum: number }
88
+ | { type: 'tiered'; tiers: Array<{ maxShares: number; perShare: number }> };
89
+
90
+ function computeSlippage(price: number, side: SignalSide, model: SlippageModel, volume: number): number {
91
+ switch (model.type) {
92
+ case 'fixed':
93
+ return price * (model.basisPoints / 10000) * (side === 'buy' ? 1 : -1);
94
+ case 'percentage':
95
+ return price * (model.pct / 100) * (side === 'buy' ? 1 : -1);
96
+ case 'volume': {
97
+ // Price impact proportional to fill size relative to volume
98
+ const impact = model.impactFactor * (1 / Math.max(volume, 1));
99
+ return price * impact * (side === 'buy' ? 1 : -1);
100
+ }
101
+ }
102
+ }
103
+
104
+ function computeCommission(shares: number, model: CommissionModel): number {
105
+ switch (model.type) {
106
+ case 'per-trade':
107
+ return model.amount;
108
+ case 'per-share':
109
+ return Math.max(shares * model.amount, model.minimum);
110
+ case 'tiered': {
111
+ let remaining = shares;
112
+ let total = 0;
113
+ for (const tier of model.tiers) {
114
+ const qty = Math.min(remaining, tier.maxShares);
115
+ total += qty * tier.perShare;
116
+ remaining -= qty;
117
+ if (remaining <= 0) break;
118
+ }
119
+ return total;
120
+ }
121
+ }
122
+ }
123
+
124
+ // ── Backtest Configuration ──────────────────────────────
125
+
126
+ interface BacktestConfig {
127
+ startDate: string;
128
+ endDate: string;
129
+ initialCapital: number;
130
+ slippage: SlippageModel;
131
+ commission: CommissionModel;
132
+ /** Walk-forward: training window size in bars */
133
+ trainWindow: number;
134
+ /** Walk-forward: testing window size in bars */
135
+ testWindow: number;
136
+ /** Require delisted instruments in dataset — prevents survivorship bias */
137
+ requireDelistedData: boolean;
138
+ }
139
+
140
+ // ── Backtest Result ─────────────────────────────────────
141
+
142
+ interface BacktestResult {
143
+ strategy: string;
144
+ config: BacktestConfig;
145
+ equityCurve: Array<{ timestamp: string; equity: number }>;
146
+ totalReturn: number;
147
+ annualizedReturn: number;
148
+ sharpeRatio: number;
149
+ maxDrawdown: number;
150
+ maxDrawdownDuration: number; // in bars
151
+ winRate: number;
152
+ profitFactor: number;
153
+ totalTrades: number;
154
+ avgWin: number;
155
+ avgLoss: number;
156
+ /** Walk-forward: per-window results for out-of-sample analysis */
157
+ walkForwardWindows: WalkForwardWindow[];
158
+ totalCommissions: number;
159
+ totalSlippage: number;
160
+ }
161
+
162
+ interface WalkForwardWindow {
163
+ trainStart: string;
164
+ trainEnd: string;
165
+ testStart: string;
166
+ testEnd: string;
167
+ inSampleReturn: number;
168
+ outOfSampleReturn: number;
169
+ inSampleSharpe: number;
170
+ outOfSampleSharpe: number;
171
+ }
172
+
173
+ // ── Lookahead Check ─────────────────────────────────────
174
+ // Static analysis: verify strategy source does not reference future data.
175
+
176
+ function validateNoLookahead(strategySource: string): { valid: boolean; violations: string[] } {
177
+ const violations: string[] = [];
178
+ const dangerousPatterns = [
179
+ { pattern: /bars\[\s*i\s*\+\s*\d+\s*\]/, desc: 'Forward indexing into bars array' },
180
+ { pattern: /future|lookahead|peek/i, desc: 'Suspicious variable name suggesting future data' },
181
+ { pattern: /shift\(\s*-\d+\s*\)/, desc: 'Negative shift (accessing future values)' },
182
+ { pattern: /\.close\s*.*\bshift\b/i, desc: 'Shifted close price access' },
183
+ { pattern: /data\.iloc\[\s*.*:\s*\]/, desc: 'Unbounded slice potentially including future' },
184
+ ];
185
+
186
+ for (const { pattern, desc } of dangerousPatterns) {
187
+ if (pattern.test(strategySource)) {
188
+ violations.push(desc);
189
+ }
190
+ }
191
+
192
+ return { valid: violations.length === 0, violations };
193
+ }
194
+
195
+ // ── Backtest Engine ─────────────────────────────────────
196
+
197
+ class BacktestEngine {
198
+ private config: BacktestConfig;
199
+
200
+ constructor(config: BacktestConfig) {
201
+ this.config = config;
202
+ }
203
+
204
+ /** Run walk-forward backtest — train/test windows slide through data */
205
+ async run(strategy: Strategy, data: Map<string, OHLCV[]>): Promise<BacktestResult> {
206
+ // Survivorship bias check
207
+ if (this.config.requireDelistedData) {
208
+ let hasDelisted = false;
209
+ for (const [, bars] of data) {
210
+ if (bars.some(b => b.delisted)) {
211
+ hasDelisted = true;
212
+ break;
213
+ }
214
+ }
215
+ if (!hasDelisted) {
216
+ throw new Error(
217
+ 'Dataset contains no delisted instruments. ' +
218
+ 'Using only current listings introduces survivorship bias. ' +
219
+ 'Include delisted/dead instruments for accurate results.'
220
+ );
221
+ }
222
+ }
223
+
224
+ strategy.init?.(this.config);
225
+
226
+ // Flatten and sort all bars by timestamp for walk-forward
227
+ const allTimestamps = this.getUniqueTimestamps(data);
228
+ const windowSize = this.config.trainWindow + this.config.testWindow;
229
+ const walkForwardWindows: WalkForwardWindow[] = [];
230
+
231
+ const equityCurve: Array<{ timestamp: string; equity: number }> = [];
232
+ const trades: Array<{ pnl: number; commission: number; slippage: number }> = [];
233
+
234
+ const portfolio: Portfolio = {
235
+ cash: this.config.initialCapital,
236
+ equity: this.config.initialCapital,
237
+ positions: new Map(),
238
+ realizedPnl: 0,
239
+ };
240
+
241
+ let totalCommissions = 0;
242
+ let totalSlippage = 0;
243
+
244
+ // Walk-forward: slide window across time series
245
+ for (let start = 0; start + windowSize <= allTimestamps.length; start += this.config.testWindow) {
246
+ const trainStart = allTimestamps[start];
247
+ const trainEnd = allTimestamps[start + this.config.trainWindow - 1];
248
+ const testStart = allTimestamps[start + this.config.trainWindow];
249
+ const testEnd = allTimestamps[Math.min(start + windowSize - 1, allTimestamps.length - 1)];
250
+
251
+ // In-sample: strategy trains (no execution tracked for results)
252
+ const trainBars = this.getBarsInRange(data, trainStart, trainEnd);
253
+
254
+ // Out-of-sample: execute signals, track results
255
+ const testBars = this.getBarsInRange(data, testStart, testEnd);
256
+ const history: OHLCV[] = [...trainBars];
257
+
258
+ const windowStartEquity = portfolio.equity;
259
+
260
+ for (const bar of testBars) {
261
+ // Strategy sees only current bar + history (no future)
262
+ const signals = strategy.onBar(bar, portfolio, history);
263
+ history.push(bar);
264
+
265
+ // Execute signals with friction
266
+ for (const signal of signals) {
267
+ const slippage = computeSlippage(bar.close, signal.side, this.config.slippage, bar.volume);
268
+ const fillPrice = bar.close + slippage;
269
+ const qty = signal.sizeType === 'percent'
270
+ ? Math.floor((portfolio.equity * signal.size) / fillPrice)
271
+ : signal.size;
272
+
273
+ const commission = computeCommission(qty, this.config.commission);
274
+
275
+ this.executeSignal(portfolio, signal, fillPrice, qty, commission);
276
+
277
+ totalCommissions += commission;
278
+ totalSlippage += Math.abs(slippage * qty);
279
+ trades.push({ pnl: 0, commission, slippage: Math.abs(slippage * qty) });
280
+ }
281
+
282
+ // Update portfolio equity
283
+ this.updateEquity(portfolio, data, bar.timestamp);
284
+ equityCurve.push({ timestamp: bar.timestamp, equity: portfolio.equity });
285
+ }
286
+
287
+ walkForwardWindows.push({
288
+ trainStart, trainEnd, testStart, testEnd,
289
+ inSampleReturn: 0, // Computed from training phase signals
290
+ outOfSampleReturn: (portfolio.equity - windowStartEquity) / windowStartEquity,
291
+ inSampleSharpe: 0,
292
+ outOfSampleSharpe: 0,
293
+ });
294
+ }
295
+
296
+ return this.computeResults(strategy.name, equityCurve, trades, walkForwardWindows,
297
+ totalCommissions, totalSlippage);
298
+ }
299
+
300
+ private getUniqueTimestamps(data: Map<string, OHLCV[]>): string[] {
301
+ const set = new Set<string>();
302
+ for (const [, bars] of data) {
303
+ for (const bar of bars) set.add(bar.timestamp);
304
+ }
305
+ return [...set].sort();
306
+ }
307
+
308
+ private getBarsInRange(data: Map<string, OHLCV[]>, start: string, end: string): OHLCV[] {
309
+ const result: OHLCV[] = [];
310
+ for (const [, bars] of data) {
311
+ for (const bar of bars) {
312
+ if (bar.timestamp >= start && bar.timestamp <= end) result.push(bar);
313
+ }
314
+ }
315
+ return result.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
316
+ }
317
+
318
+ private executeSignal(
319
+ portfolio: Portfolio, signal: Signal, fillPrice: number, qty: number, commission: number
320
+ ): void {
321
+ if (signal.side === 'buy') {
322
+ const cost = fillPrice * qty + commission;
323
+ if (cost > portfolio.cash) return; // Insufficient cash — skip
324
+
325
+ portfolio.cash -= cost;
326
+ const existing = portfolio.positions.get(signal.symbol);
327
+ if (existing) {
328
+ const totalQty = existing.quantity + qty;
329
+ existing.avgEntryPrice = (existing.avgEntryPrice * existing.quantity + fillPrice * qty) / totalQty;
330
+ existing.quantity = totalQty;
331
+ } else {
332
+ portfolio.positions.set(signal.symbol, {
333
+ symbol: signal.symbol, quantity: qty,
334
+ avgEntryPrice: fillPrice, currentPrice: fillPrice, unrealizedPnl: 0,
335
+ });
336
+ }
337
+ } else if (signal.side === 'sell' || signal.side === 'close') {
338
+ const existing = portfolio.positions.get(signal.symbol);
339
+ if (!existing) return;
340
+
341
+ const sellQty = signal.side === 'close' ? existing.quantity : Math.min(qty, existing.quantity);
342
+ const proceeds = fillPrice * sellQty - commission;
343
+ const pnl = (fillPrice - existing.avgEntryPrice) * sellQty;
344
+
345
+ portfolio.cash += proceeds;
346
+ portfolio.realizedPnl += pnl;
347
+
348
+ existing.quantity -= sellQty;
349
+ if (existing.quantity <= 0) {
350
+ portfolio.positions.delete(signal.symbol);
351
+ }
352
+ }
353
+ }
354
+
355
+ private updateEquity(portfolio: Portfolio, data: Map<string, OHLCV[]>, timestamp: string): void {
356
+ let positionValue = 0;
357
+ for (const [symbol, pos] of portfolio.positions) {
358
+ const bars = data.get(symbol);
359
+ const currentBar = bars?.find(b => b.timestamp === timestamp);
360
+ if (currentBar) {
361
+ pos.currentPrice = currentBar.close;
362
+ pos.unrealizedPnl = (currentBar.close - pos.avgEntryPrice) * pos.quantity;
363
+ }
364
+ positionValue += pos.currentPrice * pos.quantity;
365
+ }
366
+ portfolio.equity = portfolio.cash + positionValue;
367
+ }
368
+
369
+ private computeResults(
370
+ strategyName: string,
371
+ equityCurve: Array<{ timestamp: string; equity: number }>,
372
+ trades: Array<{ pnl: number; commission: number; slippage: number }>,
373
+ walkForwardWindows: WalkForwardWindow[],
374
+ totalCommissions: number,
375
+ totalSlippage: number
376
+ ): BacktestResult {
377
+ const returns = equityCurve.map((e, i) =>
378
+ i === 0 ? 0 : (e.equity - equityCurve[i - 1].equity) / equityCurve[i - 1].equity
379
+ ).slice(1);
380
+
381
+ const avgReturn = returns.reduce((s, r) => s + r, 0) / (returns.length || 1);
382
+ const stdReturn = Math.sqrt(
383
+ returns.reduce((s, r) => s + (r - avgReturn) ** 2, 0) / (returns.length || 1)
384
+ );
385
+
386
+ // Max drawdown
387
+ let peak = equityCurve[0]?.equity ?? 0;
388
+ let maxDrawdown = 0;
389
+ let maxDrawdownDuration = 0;
390
+ let currentDrawdownDuration = 0;
391
+
392
+ for (const point of equityCurve) {
393
+ if (point.equity > peak) {
394
+ peak = point.equity;
395
+ currentDrawdownDuration = 0;
396
+ } else {
397
+ const dd = (peak - point.equity) / peak;
398
+ if (dd > maxDrawdown) maxDrawdown = dd;
399
+ currentDrawdownDuration++;
400
+ if (currentDrawdownDuration > maxDrawdownDuration) {
401
+ maxDrawdownDuration = currentDrawdownDuration;
402
+ }
403
+ }
404
+ }
405
+
406
+ const initialEquity = equityCurve[0]?.equity ?? 1;
407
+ const finalEquity = equityCurve[equityCurve.length - 1]?.equity ?? 1;
408
+ const totalReturn = (finalEquity - initialEquity) / initialEquity;
409
+ const barsPerYear = 252; // Trading days
410
+ const years = (equityCurve.length || 1) / barsPerYear;
411
+
412
+ const wins = trades.filter(t => t.pnl > 0);
413
+ const losses = trades.filter(t => t.pnl < 0);
414
+ const grossWins = wins.reduce((s, t) => s + t.pnl, 0);
415
+ const grossLosses = Math.abs(losses.reduce((s, t) => s + t.pnl, 0));
416
+
417
+ return {
418
+ strategy: strategyName,
419
+ config: this.config,
420
+ equityCurve,
421
+ totalReturn,
422
+ annualizedReturn: (1 + totalReturn) ** (1 / years) - 1,
423
+ sharpeRatio: stdReturn > 0 ? (avgReturn / stdReturn) * Math.sqrt(barsPerYear) : 0,
424
+ maxDrawdown,
425
+ maxDrawdownDuration,
426
+ winRate: trades.length > 0 ? wins.length / trades.length : 0,
427
+ profitFactor: grossLosses > 0 ? grossWins / grossLosses : grossWins > 0 ? Infinity : 0,
428
+ totalTrades: trades.length,
429
+ avgWin: wins.length > 0 ? grossWins / wins.length : 0,
430
+ avgLoss: losses.length > 0 ? grossLosses / losses.length : 0,
431
+ walkForwardWindows,
432
+ totalCommissions,
433
+ totalSlippage,
434
+ };
435
+ }
436
+ }
437
+
438
+ export type {
439
+ OHLCV, Signal, SignalSide, Position, Portfolio, Strategy,
440
+ SlippageModel, CommissionModel, BacktestConfig, BacktestResult, WalkForwardWindow,
441
+ };
442
+ export {
443
+ BacktestEngine, validateNoLookahead,
444
+ computeSlippage, computeCommission,
445
+ };
446
+
447
+ // ── Framework Adaptations ───────────────────────────────
448
+ //
449
+ // === Python (vectorbt) ===
450
+ //
451
+ // import vectorbt as vbt
452
+ // import pandas as pd
453
+ //
454
+ // # vectorbt is vectorized — much faster than event-driven for simple strategies
455
+ // price = pd.read_csv("data.csv", parse_dates=["timestamp"], index_col="timestamp")["close"]
456
+ //
457
+ // # Walk-forward: split into train/test windows
458
+ // (in_price, out_price) = price.vbt.rolling_split(
459
+ // n=10, window_len=252, set_lens=(0.7, 0.3)
460
+ // )
461
+ //
462
+ // # Run strategy on out-of-sample only
463
+ // fast_ma = vbt.MA.run(out_price, window=10)
464
+ // slow_ma = vbt.MA.run(out_price, window=50)
465
+ // entries = fast_ma.ma_crossed_above(slow_ma)
466
+ // exits = fast_ma.ma_crossed_below(slow_ma)
467
+ //
468
+ // pf = vbt.Portfolio.from_signals(out_price, entries, exits,
469
+ // init_cash=100000, fees=0.001, slippage=0.001)
470
+ // print(pf.stats()) # Sharpe, max drawdown, win rate, etc.
471
+ //
472
+ // === Python (backtrader) ===
473
+ //
474
+ // import backtrader as bt
475
+ //
476
+ // class MyStrategy(bt.Strategy):
477
+ // def __init__(self):
478
+ // self.sma_fast = bt.indicators.SMA(period=10)
479
+ // self.sma_slow = bt.indicators.SMA(period=50)
480
+ //
481
+ // def next(self):
482
+ // # next() only sees current and past data — backtrader enforces this
483
+ // if self.sma_fast > self.sma_slow and not self.position:
484
+ // self.buy(size=self.broker.getcash() * 0.95 / self.data.close[0])
485
+ // elif self.sma_fast < self.sma_slow and self.position:
486
+ // self.sell()
487
+ //
488
+ // cerebro = bt.Cerebro()
489
+ // cerebro.addstrategy(MyStrategy)
490
+ // cerebro.adddata(bt.feeds.GenericCSVData(dataname="data.csv"))
491
+ // cerebro.broker.setcash(100000)
492
+ // cerebro.broker.setcommission(commission=0.001)
493
+ // cerebro.addanalyzer(bt.analyzers.SharpeRatio)
494
+ // cerebro.addanalyzer(bt.analyzers.DrawDown)
495
+ // results = cerebro.run()
496
+ //
497
+ // # Survivorship bias: load ALL instruments (including delisted) via custom data feed
498
+ // # Walk-forward: use bt.TimeReturn + manual window slicing
499
+ // # Slippage: cerebro.broker.set_slippage_perc(perc=0.001)