thevoidforge 21.0.11 → 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.
- 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/package.json +1 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Execution Safety (Trading / Financial Order Management)
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Exchange precision (tick size, lot size, min notional) fetched from API — NEVER hardcoded
|
|
6
|
+
* - Every order validated before submission: size limits, price bounds, rate limits
|
|
7
|
+
* - Paper/live toggle via interface — same code path, different backend
|
|
8
|
+
* - Reconciliation: compare local state vs exchange fills — detect drift immediately
|
|
9
|
+
* - Circuit breaker: auto-pause after consecutive losses or drawdown threshold
|
|
10
|
+
* - Audit trail: every order logged with timestamp, reason, fill, and result
|
|
11
|
+
* - Position limits enforced at portfolio level, not just per-order
|
|
12
|
+
*
|
|
13
|
+
* Agents: Stark (backend), Kenobi (security), L (monitoring)
|
|
14
|
+
*
|
|
15
|
+
* Framework adaptations:
|
|
16
|
+
* TypeScript: This file (generic execution backend interface)
|
|
17
|
+
* Python/CCXT: Crypto exchanges (see bottom)
|
|
18
|
+
* Python/Alpaca: US equities (see bottom)
|
|
19
|
+
* Any financial API: Implement ExecutionBackend interface
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ── Order Types ─────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
type OrderSide = 'buy' | 'sell';
|
|
25
|
+
type OrderType = 'market' | 'limit' | 'stop' | 'stop-limit';
|
|
26
|
+
type OrderStatus = 'pending' | 'submitted' | 'partial' | 'filled' | 'cancelled' | 'rejected';
|
|
27
|
+
|
|
28
|
+
interface OrderRequest {
|
|
29
|
+
symbol: string;
|
|
30
|
+
side: OrderSide;
|
|
31
|
+
type: OrderType;
|
|
32
|
+
/** Quantity in base units (e.g., shares, coins) */
|
|
33
|
+
size: number;
|
|
34
|
+
/** Required for limit and stop-limit orders */
|
|
35
|
+
price?: number;
|
|
36
|
+
/** Required for stop and stop-limit orders */
|
|
37
|
+
stopPrice?: number;
|
|
38
|
+
/** Strategy-provided reason for audit trail */
|
|
39
|
+
reason: string;
|
|
40
|
+
/** Client-generated ID for idempotent submission */
|
|
41
|
+
clientOrderId: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface OrderResult {
|
|
45
|
+
orderId: string;
|
|
46
|
+
clientOrderId: string;
|
|
47
|
+
status: OrderStatus;
|
|
48
|
+
filledSize: number;
|
|
49
|
+
avgFillPrice: number;
|
|
50
|
+
commission: number;
|
|
51
|
+
timestamp: string;
|
|
52
|
+
rawResponse: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Exchange Precision ──────────────────────────────────
|
|
56
|
+
// NEVER hardcode these values. Always fetch from the exchange API.
|
|
57
|
+
// Precision rules change — hardcoded values WILL cause order rejections.
|
|
58
|
+
|
|
59
|
+
interface ExchangePrecision {
|
|
60
|
+
symbol: string;
|
|
61
|
+
/** Minimum price increment (e.g., 0.01 for USD stocks) */
|
|
62
|
+
tickSize: number;
|
|
63
|
+
/** Minimum quantity increment (e.g., 0.001 for BTC) */
|
|
64
|
+
lotSize: number;
|
|
65
|
+
/** Minimum order value in quote currency (e.g., $10) */
|
|
66
|
+
minNotional: number;
|
|
67
|
+
/** Maximum order size */
|
|
68
|
+
maxSize: number;
|
|
69
|
+
/** Decimal places for price */
|
|
70
|
+
pricePrecision: number;
|
|
71
|
+
/** Decimal places for quantity */
|
|
72
|
+
quantityPrecision: number;
|
|
73
|
+
/** When this precision data was fetched — refetch if stale */
|
|
74
|
+
fetchedAt: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function roundToTickSize(price: number, tickSize: number): number {
|
|
78
|
+
return Math.round(price / tickSize) * tickSize;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function roundToLotSize(quantity: number, lotSize: number): number {
|
|
82
|
+
return Math.floor(quantity / lotSize) * lotSize;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function roundToPrecision(value: number, decimals: number): number {
|
|
86
|
+
const factor = 10 ** decimals;
|
|
87
|
+
return Math.round(value * factor) / factor;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Order Validation ────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
interface ValidationError {
|
|
93
|
+
field: string;
|
|
94
|
+
message: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateOrder(
|
|
98
|
+
order: OrderRequest,
|
|
99
|
+
precision: ExchangePrecision,
|
|
100
|
+
portfolio: { cash: number; maxOrderSize: number }
|
|
101
|
+
): ValidationError[] {
|
|
102
|
+
const errors: ValidationError[] = [];
|
|
103
|
+
|
|
104
|
+
// Size checks
|
|
105
|
+
if (order.size <= 0) {
|
|
106
|
+
errors.push({ field: 'size', message: 'Order size must be positive' });
|
|
107
|
+
}
|
|
108
|
+
if (order.size < precision.lotSize) {
|
|
109
|
+
errors.push({ field: 'size', message: `Size ${order.size} below lot size ${precision.lotSize}` });
|
|
110
|
+
}
|
|
111
|
+
if (order.size > precision.maxSize) {
|
|
112
|
+
errors.push({ field: 'size', message: `Size ${order.size} exceeds max ${precision.maxSize}` });
|
|
113
|
+
}
|
|
114
|
+
if (order.size > portfolio.maxOrderSize) {
|
|
115
|
+
errors.push({ field: 'size', message: `Size exceeds portfolio max order size ${portfolio.maxOrderSize}` });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Lot size alignment
|
|
119
|
+
const rounded = roundToLotSize(order.size, precision.lotSize);
|
|
120
|
+
if (rounded !== order.size) {
|
|
121
|
+
errors.push({ field: 'size', message: `Size ${order.size} not aligned to lot size ${precision.lotSize}` });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Price checks (for limit/stop orders)
|
|
125
|
+
if (order.type === 'limit' || order.type === 'stop-limit') {
|
|
126
|
+
if (order.price == null) {
|
|
127
|
+
errors.push({ field: 'price', message: 'Limit orders require a price' });
|
|
128
|
+
} else {
|
|
129
|
+
if (order.price <= 0) {
|
|
130
|
+
errors.push({ field: 'price', message: 'Price must be positive' });
|
|
131
|
+
}
|
|
132
|
+
const roundedPrice = roundToTickSize(order.price, precision.tickSize);
|
|
133
|
+
if (roundedPrice !== order.price) {
|
|
134
|
+
errors.push({ field: 'price', message: `Price not aligned to tick size ${precision.tickSize}` });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Stop price check
|
|
140
|
+
if ((order.type === 'stop' || order.type === 'stop-limit') && order.stopPrice == null) {
|
|
141
|
+
errors.push({ field: 'stopPrice', message: 'Stop orders require a stop price' });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Minimum notional check
|
|
145
|
+
const estimatedValue = order.size * (order.price ?? 0);
|
|
146
|
+
if (order.type !== 'market' && estimatedValue < precision.minNotional) {
|
|
147
|
+
errors.push({ field: 'notional', message: `Order value ${estimatedValue} below minimum ${precision.minNotional}` });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Cash sufficiency (buy orders only)
|
|
151
|
+
if (order.side === 'buy' && estimatedValue > portfolio.cash) {
|
|
152
|
+
errors.push({ field: 'cash', message: 'Insufficient cash for order' });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return errors;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Execution Backend Interface ─────────────────────────
|
|
159
|
+
// Paper and live trading implement the same interface.
|
|
160
|
+
// Switch between them via configuration — never via code branching.
|
|
161
|
+
|
|
162
|
+
interface ExecutionBackend {
|
|
163
|
+
name: string;
|
|
164
|
+
/** Fetch exchange precision rules — call on startup and periodically */
|
|
165
|
+
fetchPrecision(symbol: string): Promise<ExchangePrecision>;
|
|
166
|
+
/** Submit an order */
|
|
167
|
+
submitOrder(order: OrderRequest): Promise<OrderResult>;
|
|
168
|
+
/** Cancel an order */
|
|
169
|
+
cancelOrder(orderId: string): Promise<void>;
|
|
170
|
+
/** Get current open orders */
|
|
171
|
+
getOpenOrders(symbol?: string): Promise<OrderResult[]>;
|
|
172
|
+
/** Get fills for reconciliation */
|
|
173
|
+
getFills(since: string): Promise<OrderResult[]>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Paper Trading Backend ───────────────────────────────
|
|
177
|
+
|
|
178
|
+
class PaperBackend implements ExecutionBackend {
|
|
179
|
+
name = 'paper';
|
|
180
|
+
private orders: Map<string, OrderResult> = new Map();
|
|
181
|
+
private nextId = 1;
|
|
182
|
+
|
|
183
|
+
async fetchPrecision(symbol: string): Promise<ExchangePrecision> {
|
|
184
|
+
// Paper mode uses reasonable defaults — but real precision should
|
|
185
|
+
// still be fetched from the target exchange for realistic simulation
|
|
186
|
+
return {
|
|
187
|
+
symbol,
|
|
188
|
+
tickSize: 0.01,
|
|
189
|
+
lotSize: 1,
|
|
190
|
+
minNotional: 10,
|
|
191
|
+
maxSize: 100000,
|
|
192
|
+
pricePrecision: 2,
|
|
193
|
+
quantityPrecision: 0,
|
|
194
|
+
fetchedAt: new Date().toISOString(),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async submitOrder(order: OrderRequest): Promise<OrderResult> {
|
|
199
|
+
const result: OrderResult = {
|
|
200
|
+
orderId: `paper-${this.nextId++}`,
|
|
201
|
+
clientOrderId: order.clientOrderId,
|
|
202
|
+
status: 'filled',
|
|
203
|
+
filledSize: order.size,
|
|
204
|
+
avgFillPrice: order.price ?? 0,
|
|
205
|
+
commission: 0,
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
rawResponse: { simulated: true },
|
|
208
|
+
};
|
|
209
|
+
this.orders.set(result.orderId, result);
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async cancelOrder(orderId: string): Promise<void> {
|
|
214
|
+
const order = this.orders.get(orderId);
|
|
215
|
+
if (order) order.status = 'cancelled';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async getOpenOrders(): Promise<OrderResult[]> {
|
|
219
|
+
return [...this.orders.values()].filter(o => o.status === 'pending' || o.status === 'submitted');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async getFills(since: string): Promise<OrderResult[]> {
|
|
223
|
+
return [...this.orders.values()].filter(o => o.status === 'filled' && o.timestamp >= since);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Position Manager ────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
interface PositionLimits {
|
|
230
|
+
/** Maximum total exposure as fraction of equity (0.0 - 1.0) */
|
|
231
|
+
maxExposure: number;
|
|
232
|
+
/** Maximum exposure per symbol as fraction of equity */
|
|
233
|
+
maxPerSymbol: number;
|
|
234
|
+
/** Maximum number of concurrent positions */
|
|
235
|
+
maxPositions: number;
|
|
236
|
+
/** Stop-loss percentage — auto-close at this drawdown */
|
|
237
|
+
stopLossPct: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface ManagedPosition {
|
|
241
|
+
symbol: string;
|
|
242
|
+
side: OrderSide;
|
|
243
|
+
quantity: number;
|
|
244
|
+
entryPrice: number;
|
|
245
|
+
currentPrice: number;
|
|
246
|
+
stopLossPrice: number;
|
|
247
|
+
unrealizedPnl: number;
|
|
248
|
+
openedAt: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
class PositionManager {
|
|
252
|
+
private positions: Map<string, ManagedPosition> = new Map();
|
|
253
|
+
private limits: PositionLimits;
|
|
254
|
+
private equity: number;
|
|
255
|
+
|
|
256
|
+
constructor(limits: PositionLimits, initialEquity: number) {
|
|
257
|
+
this.limits = limits;
|
|
258
|
+
this.equity = initialEquity;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
canOpenPosition(symbol: string, size: number, price: number): ValidationError[] {
|
|
262
|
+
const errors: ValidationError[] = [];
|
|
263
|
+
const orderValue = size * price;
|
|
264
|
+
|
|
265
|
+
// Position count limit
|
|
266
|
+
if (!this.positions.has(symbol) && this.positions.size >= this.limits.maxPositions) {
|
|
267
|
+
errors.push({ field: 'positions', message: `At maximum position count (${this.limits.maxPositions})` });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Per-symbol exposure limit
|
|
271
|
+
const existingExposure = this.getSymbolExposure(symbol);
|
|
272
|
+
if ((existingExposure + orderValue) / this.equity > this.limits.maxPerSymbol) {
|
|
273
|
+
errors.push({ field: 'exposure', message: `Would exceed per-symbol limit (${this.limits.maxPerSymbol * 100}%)` });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Total portfolio exposure limit
|
|
277
|
+
const totalExposure = this.getTotalExposure();
|
|
278
|
+
if ((totalExposure + orderValue) / this.equity > this.limits.maxExposure) {
|
|
279
|
+
errors.push({ field: 'exposure', message: `Would exceed total exposure limit (${this.limits.maxExposure * 100}%)` });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return errors;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Check all positions for stop-loss triggers. Returns symbols to close. */
|
|
286
|
+
checkStopLosses(currentPrices: Map<string, number>): string[] {
|
|
287
|
+
const triggered: string[] = [];
|
|
288
|
+
for (const [symbol, pos] of this.positions) {
|
|
289
|
+
const currentPrice = currentPrices.get(symbol);
|
|
290
|
+
if (currentPrice == null) continue;
|
|
291
|
+
|
|
292
|
+
pos.currentPrice = currentPrice;
|
|
293
|
+
pos.unrealizedPnl = (currentPrice - pos.entryPrice) * pos.quantity * (pos.side === 'buy' ? 1 : -1);
|
|
294
|
+
|
|
295
|
+
if (pos.side === 'buy' && currentPrice <= pos.stopLossPrice) {
|
|
296
|
+
triggered.push(symbol);
|
|
297
|
+
} else if (pos.side === 'sell' && currentPrice >= pos.stopLossPrice) {
|
|
298
|
+
triggered.push(symbol);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return triggered;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private getSymbolExposure(symbol: string): number {
|
|
305
|
+
const pos = this.positions.get(symbol);
|
|
306
|
+
return pos ? pos.quantity * pos.currentPrice : 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private getTotalExposure(): number {
|
|
310
|
+
let total = 0;
|
|
311
|
+
for (const pos of this.positions.values()) {
|
|
312
|
+
total += pos.quantity * pos.currentPrice;
|
|
313
|
+
}
|
|
314
|
+
return total;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Circuit Breaker ─────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
interface CircuitBreakerConfig {
|
|
321
|
+
/** Pause after this many consecutive losses */
|
|
322
|
+
maxConsecutiveLosses: number;
|
|
323
|
+
/** Pause if drawdown exceeds this percentage (0.0 - 1.0) */
|
|
324
|
+
maxDrawdownPct: number;
|
|
325
|
+
/** Cooldown period before resuming (milliseconds) */
|
|
326
|
+
cooldownMs: number;
|
|
327
|
+
/** Callback when circuit breaker trips */
|
|
328
|
+
onTrip: (reason: string) => void;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
class CircuitBreaker {
|
|
332
|
+
private config: CircuitBreakerConfig;
|
|
333
|
+
private consecutiveLosses = 0;
|
|
334
|
+
private peakEquity: number;
|
|
335
|
+
private trippedAt: number | null = null;
|
|
336
|
+
|
|
337
|
+
constructor(config: CircuitBreakerConfig, initialEquity: number) {
|
|
338
|
+
this.config = config;
|
|
339
|
+
this.peakEquity = initialEquity;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Record a trade result. Returns true if trading should continue. */
|
|
343
|
+
recordTrade(pnl: number): boolean {
|
|
344
|
+
if (pnl < 0) {
|
|
345
|
+
this.consecutiveLosses++;
|
|
346
|
+
if (this.consecutiveLosses >= this.config.maxConsecutiveLosses) {
|
|
347
|
+
this.trip(`${this.consecutiveLosses} consecutive losses`);
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
this.consecutiveLosses = 0;
|
|
352
|
+
}
|
|
353
|
+
return !this.isTripped();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Update equity and check drawdown. Returns true if trading should continue. */
|
|
357
|
+
updateEquity(currentEquity: number): boolean {
|
|
358
|
+
if (currentEquity > this.peakEquity) {
|
|
359
|
+
this.peakEquity = currentEquity;
|
|
360
|
+
}
|
|
361
|
+
const drawdown = (this.peakEquity - currentEquity) / this.peakEquity;
|
|
362
|
+
if (drawdown >= this.config.maxDrawdownPct) {
|
|
363
|
+
this.trip(`Drawdown ${(drawdown * 100).toFixed(1)}% exceeds limit ${(this.config.maxDrawdownPct * 100).toFixed(1)}%`);
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
return !this.isTripped();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
isTripped(): boolean {
|
|
370
|
+
if (this.trippedAt == null) return false;
|
|
371
|
+
if (Date.now() - this.trippedAt >= this.config.cooldownMs) {
|
|
372
|
+
this.reset();
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private trip(reason: string): void {
|
|
379
|
+
this.trippedAt = Date.now();
|
|
380
|
+
this.config.onTrip(reason);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private reset(): void {
|
|
384
|
+
this.trippedAt = null;
|
|
385
|
+
this.consecutiveLosses = 0;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── Audit Trail ─────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
interface AuditEntry {
|
|
392
|
+
timestamp: string;
|
|
393
|
+
action: 'submit' | 'fill' | 'cancel' | 'reject' | 'stop-loss' | 'circuit-break';
|
|
394
|
+
order: OrderRequest | null;
|
|
395
|
+
result: OrderResult | null;
|
|
396
|
+
reason: string;
|
|
397
|
+
portfolioState: { cash: number; equity: number; positionCount: number };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
class AuditTrail {
|
|
401
|
+
private entries: AuditEntry[] = [];
|
|
402
|
+
private onLog?: (entry: AuditEntry) => void;
|
|
403
|
+
|
|
404
|
+
constructor(onLog?: (entry: AuditEntry) => void) {
|
|
405
|
+
this.onLog = onLog;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
record(entry: Omit<AuditEntry, 'timestamp'>): void {
|
|
409
|
+
const full: AuditEntry = { ...entry, timestamp: new Date().toISOString() };
|
|
410
|
+
this.entries.push(full);
|
|
411
|
+
this.onLog?.(full);
|
|
412
|
+
|
|
413
|
+
// Structured JSON logging — never log PII or API keys
|
|
414
|
+
console.log(JSON.stringify({
|
|
415
|
+
event: `order.${full.action}`,
|
|
416
|
+
symbol: full.order?.symbol,
|
|
417
|
+
side: full.order?.side,
|
|
418
|
+
size: full.order?.size,
|
|
419
|
+
reason: full.reason,
|
|
420
|
+
orderId: full.result?.orderId,
|
|
421
|
+
fillPrice: full.result?.avgFillPrice,
|
|
422
|
+
equity: full.portfolioState.equity,
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
getEntries(): readonly AuditEntry[] {
|
|
427
|
+
return this.entries;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Reconciliation ──────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
interface ReconciliationResult {
|
|
434
|
+
matched: number;
|
|
435
|
+
mismatched: number;
|
|
436
|
+
missingLocal: string[]; // Orders on exchange not tracked locally
|
|
437
|
+
missingExchange: string[]; // Local orders not found on exchange
|
|
438
|
+
priceDifferences: Array<{ orderId: string; localPrice: number; exchangePrice: number }>;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function reconcile(
|
|
442
|
+
localOrders: OrderResult[],
|
|
443
|
+
exchangeFills: OrderResult[]
|
|
444
|
+
): ReconciliationResult {
|
|
445
|
+
const exchangeMap = new Map(exchangeFills.map(f => [f.clientOrderId, f]));
|
|
446
|
+
const localMap = new Map(localOrders.map(o => [o.clientOrderId, o]));
|
|
447
|
+
|
|
448
|
+
let matched = 0;
|
|
449
|
+
let mismatched = 0;
|
|
450
|
+
const missingExchange: string[] = [];
|
|
451
|
+
const priceDifferences: Array<{ orderId: string; localPrice: number; exchangePrice: number }> = [];
|
|
452
|
+
|
|
453
|
+
for (const [clientId, local] of localMap) {
|
|
454
|
+
const exchange = exchangeMap.get(clientId);
|
|
455
|
+
if (!exchange) {
|
|
456
|
+
missingExchange.push(clientId);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (Math.abs(local.avgFillPrice - exchange.avgFillPrice) > 0.0001) {
|
|
460
|
+
priceDifferences.push({
|
|
461
|
+
orderId: clientId,
|
|
462
|
+
localPrice: local.avgFillPrice,
|
|
463
|
+
exchangePrice: exchange.avgFillPrice,
|
|
464
|
+
});
|
|
465
|
+
mismatched++;
|
|
466
|
+
} else {
|
|
467
|
+
matched++;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const missingLocal = exchangeFills
|
|
472
|
+
.filter(f => !localMap.has(f.clientOrderId))
|
|
473
|
+
.map(f => f.clientOrderId);
|
|
474
|
+
|
|
475
|
+
return { matched, mismatched, missingLocal, missingExchange, priceDifferences };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export type {
|
|
479
|
+
OrderRequest, OrderResult, OrderSide, OrderType, OrderStatus,
|
|
480
|
+
ExchangePrecision, ExecutionBackend, ValidationError,
|
|
481
|
+
PositionLimits, ManagedPosition, CircuitBreakerConfig,
|
|
482
|
+
AuditEntry, ReconciliationResult,
|
|
483
|
+
};
|
|
484
|
+
export {
|
|
485
|
+
roundToTickSize, roundToLotSize, roundToPrecision,
|
|
486
|
+
validateOrder, reconcile,
|
|
487
|
+
PaperBackend, PositionManager, CircuitBreaker, AuditTrail,
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// ── Framework Adaptations ───────────────────────────────
|
|
491
|
+
//
|
|
492
|
+
// === Python / CCXT (Crypto Exchanges) ===
|
|
493
|
+
//
|
|
494
|
+
// import ccxt
|
|
495
|
+
//
|
|
496
|
+
// exchange = ccxt.binance({"apiKey": "...", "secret": "..."})
|
|
497
|
+
// exchange.set_sandbox_mode(True) # Paper trading toggle
|
|
498
|
+
//
|
|
499
|
+
// # Fetch precision from exchange — NEVER hardcode
|
|
500
|
+
// markets = exchange.load_markets()
|
|
501
|
+
// info = markets["BTC/USDT"]
|
|
502
|
+
// tick_size = info["precision"]["price"]
|
|
503
|
+
// lot_size = info["precision"]["amount"]
|
|
504
|
+
// min_notional = info["limits"]["cost"]["min"]
|
|
505
|
+
//
|
|
506
|
+
// # Round to exchange precision
|
|
507
|
+
// price = exchange.price_to_precision("BTC/USDT", raw_price)
|
|
508
|
+
// amount = exchange.amount_to_precision("BTC/USDT", raw_amount)
|
|
509
|
+
//
|
|
510
|
+
// # Submit order
|
|
511
|
+
// order = exchange.create_limit_buy_order("BTC/USDT", amount, price)
|
|
512
|
+
//
|
|
513
|
+
// # Reconciliation
|
|
514
|
+
// fills = exchange.fetch_my_trades("BTC/USDT", since=timestamp)
|
|
515
|
+
// # Compare fills against local order log
|
|
516
|
+
//
|
|
517
|
+
// # CCXT handles 40+ exchanges with the same interface — same as ExecutionBackend pattern
|
|
518
|
+
//
|
|
519
|
+
// === Python / Alpaca (US Equities) ===
|
|
520
|
+
//
|
|
521
|
+
// from alpaca.trading.client import TradingClient
|
|
522
|
+
// from alpaca.trading.requests import MarketOrderRequest, LimitOrderRequest
|
|
523
|
+
// from alpaca.trading.enums import OrderSide, TimeInForce
|
|
524
|
+
//
|
|
525
|
+
// # Paper vs live: just change the base_url
|
|
526
|
+
// client = TradingClient(api_key, secret_key, paper=True)
|
|
527
|
+
//
|
|
528
|
+
// # Submit order
|
|
529
|
+
// order = client.submit_order(
|
|
530
|
+
// MarketOrderRequest(
|
|
531
|
+
// symbol="AAPL", qty=10, side=OrderSide.BUY,
|
|
532
|
+
// time_in_force=TimeInForce.DAY,
|
|
533
|
+
// client_order_id="my-unique-id" # Idempotency
|
|
534
|
+
// )
|
|
535
|
+
// )
|
|
536
|
+
//
|
|
537
|
+
// # Position management
|
|
538
|
+
// positions = client.get_all_positions()
|
|
539
|
+
// account = client.get_account()
|
|
540
|
+
// buying_power = float(account.buying_power)
|
|
541
|
+
//
|
|
542
|
+
// # Reconciliation: compare client.get_orders() vs local state
|
|
543
|
+
//
|
|
544
|
+
// === IBKR (Interactive Brokers) ===
|
|
545
|
+
//
|
|
546
|
+
// # Use ib_insync (Python) or official TWS API
|
|
547
|
+
// from ib_insync import IB, Stock, LimitOrder
|
|
548
|
+
//
|
|
549
|
+
// ib = IB()
|
|
550
|
+
// ib.connect("127.0.0.1", 7497, clientId=1) # 7497=paper, 7496=live
|
|
551
|
+
//
|
|
552
|
+
// contract = Stock("AAPL", "SMART", "USD")
|
|
553
|
+
// ib.qualifyContracts(contract)
|
|
554
|
+
//
|
|
555
|
+
// # Precision: contract details include min tick, lot size
|
|
556
|
+
// details = ib.reqContractDetails(contract)[0]
|
|
557
|
+
// min_tick = details.minTick
|
|
558
|
+
//
|
|
559
|
+
// order = LimitOrder("BUY", 10, round(price / min_tick) * min_tick)
|
|
560
|
+
// trade = ib.placeOrder(contract, order)
|
|
561
|
+
//
|
|
562
|
+
// # IBKR supports paper trading on port 7497 — same code, different port
|
|
563
|
+
|
|
564
|
+
// ── Anti-Patterns ──────────────────────────────────────
|
|
565
|
+
//
|
|
566
|
+
// === Never raw transfer() to smart contracts ===
|
|
567
|
+
//
|
|
568
|
+
// ERC20 `transfer(address, amount)` to a smart contract deposits funds with
|
|
569
|
+
// no guarantee of recovery. Smart contracts may lack withdrawal functions,
|
|
570
|
+
// absorb tokens into settlement pools, or have no rescue mechanism.
|
|
571
|
+
//
|
|
572
|
+
// Before any on-chain transfer:
|
|
573
|
+
// 1. Read the contract's ABI — verify the correct deposit/funding function
|
|
574
|
+
// 2. Check if the contract has a withdrawal or recovery function
|
|
575
|
+
// 3. Use the contract's own deposit method, not raw transfer()
|
|
576
|
+
// 4. For amounts >$100, simulate the transaction first (eth_call)
|
|
577
|
+
//
|
|
578
|
+
// This applies to any on-chain execution — Ethereum, L2s, Solana (via CPI).
|
|
579
|
+
//
|
|
580
|
+
// === Derive Don't Accumulate ===
|
|
581
|
+
//
|
|
582
|
+
// Never maintain a running balance by incrementing/decrementing on each event.
|
|
583
|
+
// Running totals drift due to: missed events, duplicate processing, rounding
|
|
584
|
+
// errors, and partial fills that update one side but not the other.
|
|
585
|
+
//
|
|
586
|
+
// Instead, derive the current state from the source of truth:
|
|
587
|
+
//
|
|
588
|
+
// // WRONG: running accumulator
|
|
589
|
+
// this.totalPnl += trade.pnl;
|
|
590
|
+
// this.positionSize += trade.filledSize;
|
|
591
|
+
//
|
|
592
|
+
// // RIGHT: derive from complete history
|
|
593
|
+
// const fills = await backend.getFills(since);
|
|
594
|
+
// const totalPnl = fills.reduce((sum, f) => sum + f.pnl, 0);
|
|
595
|
+
// const positionSize = fills.reduce(
|
|
596
|
+
// (sum, f) => sum + f.filledSize * (f.side === 'buy' ? 1 : -1), 0
|
|
597
|
+
// );
|
|
598
|
+
//
|
|
599
|
+
// For reconciliation, the derived value IS the value. If the derived value
|
|
600
|
+
// disagrees with an accumulator, the accumulator is wrong — always.
|
|
601
|
+
// (Field reports #271, #274, #275)
|