thevoidforge 21.0.11 → 21.0.13
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/.claude/commands/ai.md +69 -0
- package/dist/.claude/commands/architect.md +121 -0
- package/dist/.claude/commands/assemble.md +201 -0
- package/dist/.claude/commands/assess.md +75 -0
- package/dist/.claude/commands/blueprint.md +135 -0
- package/dist/.claude/commands/build.md +116 -0
- package/dist/.claude/commands/campaign.md +201 -0
- package/dist/.claude/commands/cultivation.md +166 -0
- package/dist/.claude/commands/current.md +128 -0
- package/dist/.claude/commands/dangerroom.md +74 -0
- package/dist/.claude/commands/debrief.md +178 -0
- package/dist/.claude/commands/deploy.md +99 -0
- package/dist/.claude/commands/devops.md +143 -0
- package/dist/.claude/commands/gauntlet.md +140 -0
- package/dist/.claude/commands/git.md +104 -0
- package/dist/.claude/commands/grow.md +146 -0
- package/dist/.claude/commands/imagine.md +126 -0
- package/dist/.claude/commands/portfolio.md +50 -0
- package/dist/.claude/commands/prd.md +113 -0
- package/dist/.claude/commands/qa.md +107 -0
- package/dist/.claude/commands/review.md +151 -0
- package/dist/.claude/commands/security.md +100 -0
- package/dist/.claude/commands/test.md +96 -0
- package/dist/.claude/commands/thumper.md +116 -0
- package/dist/.claude/commands/treasury.md +100 -0
- package/dist/.claude/commands/ux.md +118 -0
- package/dist/.claude/commands/vault.md +189 -0
- package/dist/.claude/commands/void.md +108 -0
- package/dist/CHANGELOG.md +1918 -0
- package/dist/CLAUDE.md +250 -0
- package/dist/HOLOCRON.md +856 -0
- package/dist/VERSION.md +123 -0
- package/dist/docs/NAMING_REGISTRY.md +478 -0
- package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
- package/dist/docs/methods/ASSEMBLER.md +142 -0
- package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
- package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
- package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
- package/dist/docs/methods/CAMPAIGN.md +568 -0
- package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
- package/dist/docs/methods/DEEP_CURRENT.md +184 -0
- package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
- package/dist/docs/methods/FIELD_MEDIC.md +261 -0
- package/dist/docs/methods/FORGE_ARTIST.md +108 -0
- package/dist/docs/methods/FORGE_KEEPER.md +268 -0
- package/dist/docs/methods/GAUNTLET.md +344 -0
- package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
- package/dist/docs/methods/HEARTBEAT.md +168 -0
- package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
- package/dist/docs/methods/MUSTER.md +148 -0
- package/dist/docs/methods/PRD_GENERATOR.md +186 -0
- package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
- package/dist/docs/methods/QA_ENGINEER.md +337 -0
- package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
- package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
- package/dist/docs/methods/SUB_AGENTS.md +335 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
- package/dist/docs/methods/TESTING.md +359 -0
- package/dist/docs/methods/THUMPER.md +175 -0
- package/dist/docs/methods/TIME_VAULT.md +120 -0
- package/dist/docs/methods/TREASURY.md +184 -0
- package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
- package/dist/docs/patterns/README.md +52 -0
- package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
- package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
- package/dist/docs/patterns/ai-classifier.ts +195 -0
- package/dist/docs/patterns/ai-eval.ts +272 -0
- package/dist/docs/patterns/ai-orchestrator.ts +341 -0
- package/dist/docs/patterns/ai-router.ts +194 -0
- package/dist/docs/patterns/ai-tool-schema.ts +237 -0
- package/dist/docs/patterns/api-route.ts +241 -0
- package/dist/docs/patterns/backtest-engine.ts +499 -0
- package/dist/docs/patterns/browser-review.ts +292 -0
- package/dist/docs/patterns/combobox.tsx +300 -0
- package/dist/docs/patterns/component.tsx +262 -0
- package/dist/docs/patterns/daemon-process.ts +338 -0
- package/dist/docs/patterns/data-pipeline.ts +297 -0
- package/dist/docs/patterns/database-migration.ts +466 -0
- package/dist/docs/patterns/e2e-test.ts +629 -0
- package/dist/docs/patterns/error-handling.ts +312 -0
- package/dist/docs/patterns/execution-safety.ts +601 -0
- package/dist/docs/patterns/financial-transaction.ts +342 -0
- package/dist/docs/patterns/funding-plan.ts +462 -0
- package/dist/docs/patterns/game-entity.ts +137 -0
- package/dist/docs/patterns/game-loop.ts +113 -0
- package/dist/docs/patterns/game-state.ts +143 -0
- package/dist/docs/patterns/job-queue.ts +225 -0
- package/dist/docs/patterns/kongo-integration.ts +164 -0
- package/dist/docs/patterns/middleware.ts +363 -0
- package/dist/docs/patterns/mobile-screen.tsx +139 -0
- package/dist/docs/patterns/mobile-service.ts +167 -0
- package/dist/docs/patterns/multi-tenant.ts +382 -0
- package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
- package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
- package/dist/docs/patterns/prompt-template.ts +195 -0
- package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
- package/dist/docs/patterns/service.ts +224 -0
- package/dist/docs/patterns/sse-endpoint.ts +118 -0
- package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
- package/dist/docs/patterns/third-party-script.ts +68 -0
- package/dist/scripts/thumper/gom-jabbar.sh +241 -0
- package/dist/scripts/thumper/relay.sh +610 -0
- package/dist/scripts/thumper/scan.sh +359 -0
- package/dist/scripts/thumper/thumper.sh +190 -0
- package/dist/scripts/thumper/water-rings.sh +76 -0
- package/dist/wizard/ui/index.html +1 -1
- package/package.json +1 -1
- 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)
|