openbroker 1.0.75 → 1.0.80
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/SKILL.md +6 -2
- package/bin/cli.ts +16 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/auto/audit-daemon.js +567 -0
- package/scripts/auto/audit.ts +552 -0
- package/scripts/auto/cli.ts +32 -0
- package/scripts/auto/report.ts +459 -0
- package/scripts/auto/runtime.ts +264 -79
- package/scripts/auto/types.ts +10 -0
- package/scripts/core/client.ts +245 -0
- package/scripts/core/ws.ts +25 -0
- package/scripts/info/funding-history.ts +5 -5
- package/scripts/info/search-markets.ts +30 -6
- package/scripts/info/spot.ts +23 -8
- package/scripts/operations/spot-order.ts +189 -0
- package/scripts/plugin/tools.ts +126 -6
package/scripts/auto/runtime.ts
CHANGED
|
@@ -13,12 +13,16 @@ import {
|
|
|
13
13
|
import { WebSocketManager } from '../core/ws.js';
|
|
14
14
|
import { AutomationEventBus } from './events.js';
|
|
15
15
|
import { loadAutomation } from './loader.js';
|
|
16
|
-
import { registerAutomation, unregisterAutomation,
|
|
16
|
+
import { registerAutomation, unregisterAutomation, getRegisteredAutomations as getRegisteredFromFile } from './registry.js';
|
|
17
|
+
import { createAutomationAudit, toSerializable, type AutomationAuditSink } from './audit.js';
|
|
17
18
|
import type {
|
|
18
19
|
AutomationAPI,
|
|
20
|
+
AutomationEventPayloads,
|
|
21
|
+
AutomationEventType,
|
|
19
22
|
AutomationLogger,
|
|
20
23
|
AutomationState,
|
|
21
24
|
AutomationSnapshot,
|
|
25
|
+
AutomationAudit,
|
|
22
26
|
PositionSnapshot,
|
|
23
27
|
PublishOptions,
|
|
24
28
|
ScheduledTask,
|
|
@@ -26,14 +30,27 @@ import type {
|
|
|
26
30
|
} from './types.js';
|
|
27
31
|
|
|
28
32
|
const STATE_DIR = path.join(os.homedir(), '.openbroker', 'state');
|
|
33
|
+
const AUDITED_WRITE_METHODS = new Set([
|
|
34
|
+
'order', 'marketOrder', 'limitOrder', 'triggerOrder',
|
|
35
|
+
'takeProfit', 'stopLoss', 'cancel', 'cancelAll',
|
|
36
|
+
'spotOrder', 'spotMarketOrder', 'spotLimitOrder', 'spotCancel',
|
|
37
|
+
'updateLeverage', 'approveBuilderFee', 'twapOrder', 'twapCancel',
|
|
38
|
+
]);
|
|
29
39
|
|
|
30
40
|
// ── State persistence ───────────────────────────────────────────────
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
interface StateController {
|
|
43
|
+
state: AutomationState;
|
|
44
|
+
snapshot(): Record<string, unknown>;
|
|
45
|
+
attachAudit(audit: AutomationAuditSink): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createState(id: string): StateController {
|
|
33
49
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
34
50
|
const stateFile = path.join(STATE_DIR, `${id}.json`);
|
|
35
51
|
|
|
36
52
|
let data: Record<string, unknown> = {};
|
|
53
|
+
let audit: AutomationAuditSink | null = null;
|
|
37
54
|
if (existsSync(stateFile)) {
|
|
38
55
|
try {
|
|
39
56
|
data = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
@@ -52,33 +69,59 @@ function createState(id: string): AutomationState {
|
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
state: {
|
|
73
|
+
get<T = unknown>(key: string, defaultValue?: T): T | undefined {
|
|
74
|
+
return (key in data ? data[key] : defaultValue) as T | undefined;
|
|
75
|
+
},
|
|
76
|
+
set<T = unknown>(key: string, value: T): void {
|
|
77
|
+
data[key] = value;
|
|
78
|
+
audit?.recordStateChange('set', key, value);
|
|
79
|
+
scheduleFlush();
|
|
80
|
+
},
|
|
81
|
+
delete(key: string): void {
|
|
82
|
+
const previous = key in data ? data[key] : undefined;
|
|
83
|
+
delete data[key];
|
|
84
|
+
audit?.recordStateChange('delete', key, previous);
|
|
85
|
+
scheduleFlush();
|
|
86
|
+
},
|
|
87
|
+
clear(): void {
|
|
88
|
+
data = {};
|
|
89
|
+
audit?.recordStateChange('clear', null);
|
|
90
|
+
scheduleFlush();
|
|
91
|
+
},
|
|
61
92
|
},
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
scheduleFlush();
|
|
93
|
+
snapshot(): Record<string, unknown> {
|
|
94
|
+
return toSerializable(data);
|
|
65
95
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
scheduleFlush();
|
|
96
|
+
attachAudit(nextAudit: AutomationAuditSink): void {
|
|
97
|
+
audit = nextAudit;
|
|
69
98
|
},
|
|
70
99
|
};
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
// ── Logger ──────────────────────────────────────────────────────────
|
|
74
103
|
|
|
75
|
-
function createLogger(id: string, verbose: boolean): AutomationLogger {
|
|
104
|
+
function createLogger(id: string, verbose: boolean, audit?: AutomationAuditSink): AutomationLogger {
|
|
76
105
|
const prefix = `[auto:${id}]`;
|
|
77
106
|
return {
|
|
78
|
-
info: (msg: string) =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
info: (msg: string) => {
|
|
108
|
+
audit?.recordLog('info', msg);
|
|
109
|
+
console.log(`${prefix} ${msg}`);
|
|
110
|
+
},
|
|
111
|
+
warn: (msg: string) => {
|
|
112
|
+
audit?.recordLog('warn', msg);
|
|
113
|
+
console.log(`${prefix} ⚠ ${msg}`);
|
|
114
|
+
},
|
|
115
|
+
error: (msg: string) => {
|
|
116
|
+
audit?.recordLog('error', msg);
|
|
117
|
+
console.error(`${prefix} ✗ ${msg}`);
|
|
118
|
+
},
|
|
119
|
+
debug: (msg: string) => {
|
|
120
|
+
if (verbose) {
|
|
121
|
+
audit?.recordLog('debug', msg);
|
|
122
|
+
console.log(`${prefix} … ${msg}`);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
82
125
|
};
|
|
83
126
|
}
|
|
84
127
|
|
|
@@ -88,6 +131,8 @@ const WRITE_METHODS = new Set([
|
|
|
88
131
|
'order', 'marketOrder', 'limitOrder', 'triggerOrder',
|
|
89
132
|
'takeProfit', 'stopLoss', 'cancel', 'cancelAll',
|
|
90
133
|
'updateLeverage', 'approveBuilderFee',
|
|
134
|
+
'spotOrder', 'spotMarketOrder', 'spotLimitOrder', 'spotCancel',
|
|
135
|
+
'twapOrder', 'twapCancel',
|
|
91
136
|
]);
|
|
92
137
|
|
|
93
138
|
function createDryClient(client: HyperliquidClient, log: AutomationLogger): HyperliquidClient {
|
|
@@ -105,6 +150,52 @@ function createDryClient(client: HyperliquidClient, log: AutomationLogger): Hype
|
|
|
105
150
|
});
|
|
106
151
|
}
|
|
107
152
|
|
|
153
|
+
function createAuditedClient(
|
|
154
|
+
client: HyperliquidClient,
|
|
155
|
+
audit: AutomationAuditSink,
|
|
156
|
+
dryRun: boolean,
|
|
157
|
+
): HyperliquidClient {
|
|
158
|
+
return new Proxy(client, {
|
|
159
|
+
get(target, prop, receiver) {
|
|
160
|
+
const value = Reflect.get(target, prop, receiver);
|
|
161
|
+
if (typeof prop === 'string' && AUDITED_WRITE_METHODS.has(prop) && typeof value === 'function') {
|
|
162
|
+
return async (...args: unknown[]) => {
|
|
163
|
+
const actionId = `${prop}:${Date.now()}:${Math.random().toString(16).slice(2)}`;
|
|
164
|
+
audit.recordAction({
|
|
165
|
+
actionId,
|
|
166
|
+
phase: 'request',
|
|
167
|
+
method: prop,
|
|
168
|
+
payload: { args },
|
|
169
|
+
dryRun,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const result = await value.apply(target, args);
|
|
174
|
+
audit.recordAction({
|
|
175
|
+
actionId,
|
|
176
|
+
phase: 'response',
|
|
177
|
+
method: prop,
|
|
178
|
+
result,
|
|
179
|
+
dryRun,
|
|
180
|
+
});
|
|
181
|
+
return result;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
audit.recordAction({
|
|
184
|
+
actionId,
|
|
185
|
+
phase: 'error',
|
|
186
|
+
method: prop,
|
|
187
|
+
error,
|
|
188
|
+
dryRun,
|
|
189
|
+
});
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
108
199
|
// ── Snapshot building ───────────────────────────────────────────────
|
|
109
200
|
|
|
110
201
|
async function buildSnapshot(
|
|
@@ -221,6 +312,22 @@ function createPublish(
|
|
|
221
312
|
};
|
|
222
313
|
}
|
|
223
314
|
|
|
315
|
+
function createAuditedPublish(
|
|
316
|
+
publish: (message: string, options?: PublishOptions) => Promise<boolean>,
|
|
317
|
+
audit: AutomationAuditSink,
|
|
318
|
+
): (message: string, options?: PublishOptions) => Promise<boolean> {
|
|
319
|
+
return async (message: string, options?: PublishOptions): Promise<boolean> => {
|
|
320
|
+
try {
|
|
321
|
+
const delivered = await publish(message, options);
|
|
322
|
+
audit.recordPublish(message, options, delivered);
|
|
323
|
+
return delivered;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
audit.recordError('publish', error);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
224
331
|
// ── Runtime ─────────────────────────────────────────────────────────
|
|
225
332
|
|
|
226
333
|
export interface RuntimeOptions {
|
|
@@ -279,14 +386,13 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
279
386
|
throw new Error(`Automation "${id}" is already running`);
|
|
280
387
|
}
|
|
281
388
|
|
|
282
|
-
const
|
|
283
|
-
const state = createState(id);
|
|
389
|
+
const stateController = createState(id);
|
|
284
390
|
|
|
285
391
|
// Pre-seed state from --set flags (doesn't overwrite already-persisted keys)
|
|
286
392
|
if (initialState) {
|
|
287
393
|
for (const [key, value] of Object.entries(initialState)) {
|
|
288
|
-
if (state.get(key) === undefined) {
|
|
289
|
-
state.set(key, value);
|
|
394
|
+
if (stateController.state.get(key) === undefined) {
|
|
395
|
+
stateController.state.set(key, value);
|
|
290
396
|
}
|
|
291
397
|
}
|
|
292
398
|
}
|
|
@@ -294,7 +400,24 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
294
400
|
const eventBus = new AutomationEventBus();
|
|
295
401
|
|
|
296
402
|
const rawClient = getClient();
|
|
297
|
-
const
|
|
403
|
+
const audit = createAutomationAudit({
|
|
404
|
+
automationId: id,
|
|
405
|
+
scriptPath,
|
|
406
|
+
dryRun,
|
|
407
|
+
verbose,
|
|
408
|
+
pollIntervalMs,
|
|
409
|
+
useWebSocket,
|
|
410
|
+
accountAddress: rawClient.address,
|
|
411
|
+
walletAddress: rawClient.walletAddress,
|
|
412
|
+
isApiWallet: rawClient.isApiWallet,
|
|
413
|
+
initialState,
|
|
414
|
+
persistedState: stateController.snapshot(),
|
|
415
|
+
});
|
|
416
|
+
stateController.attachAudit(audit);
|
|
417
|
+
|
|
418
|
+
const log = createLogger(id, verbose, audit);
|
|
419
|
+
const baseClient = dryRun ? createDryClient(rawClient, log) : rawClient;
|
|
420
|
+
const client = createAuditedClient(baseClient, audit, dryRun);
|
|
298
421
|
|
|
299
422
|
const startHooks: Array<() => void | Promise<void>> = [];
|
|
300
423
|
const stopHooks: Array<() => void | Promise<void>> = [];
|
|
@@ -302,7 +425,11 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
302
425
|
const scheduledTasks: ScheduledTask[] = [];
|
|
303
426
|
|
|
304
427
|
// Build the API object
|
|
305
|
-
const publish = createPublish(id, log, gatewayPort, hooksToken);
|
|
428
|
+
const publish = createAuditedPublish(createPublish(id, log, gatewayPort, hooksToken), audit);
|
|
429
|
+
const auditApi: AutomationAudit = {
|
|
430
|
+
record: (kind: string, payload?: unknown) => audit.recordNote(kind, payload),
|
|
431
|
+
metric: (name: string, value: number, tags?: Record<string, unknown>) => audit.recordMetric(name, value, tags),
|
|
432
|
+
};
|
|
306
433
|
const api: AutomationAPI = {
|
|
307
434
|
client,
|
|
308
435
|
utils: { roundPrice, roundSize, sleep, normalizeCoin, formatUsd, formatPercent, annualizeFundingRate },
|
|
@@ -312,22 +439,37 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
312
439
|
onStop: (handler) => stopHooks.push(handler),
|
|
313
440
|
onError: (handler) => errorHooks.push(handler),
|
|
314
441
|
publish,
|
|
315
|
-
state,
|
|
442
|
+
state: stateController.state,
|
|
316
443
|
log,
|
|
444
|
+
audit: auditApi,
|
|
317
445
|
id,
|
|
318
446
|
dryRun,
|
|
319
447
|
};
|
|
320
448
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
449
|
+
try {
|
|
450
|
+
// Load and execute the factory function (registers handlers)
|
|
451
|
+
log.info(`Loading automation: ${scriptPath}`);
|
|
452
|
+
const factory = await loadAutomation(scriptPath);
|
|
453
|
+
await factory(api);
|
|
325
454
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
455
|
+
// Call onStart hooks
|
|
456
|
+
for (const hook of startHooks) {
|
|
457
|
+
try { await hook(); } catch (err) {
|
|
458
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
459
|
+
audit.recordError('onStart', error);
|
|
460
|
+
log.error(`onStart hook error: ${error.message}`);
|
|
461
|
+
}
|
|
330
462
|
}
|
|
463
|
+
} catch (err) {
|
|
464
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
465
|
+
audit.recordError('startup', error);
|
|
466
|
+
await audit.stop({
|
|
467
|
+
status: 'error',
|
|
468
|
+
stopReason: 'startup_error',
|
|
469
|
+
pollCount: 0,
|
|
470
|
+
eventsEmitted: 0,
|
|
471
|
+
});
|
|
472
|
+
throw error;
|
|
331
473
|
}
|
|
332
474
|
|
|
333
475
|
// Polling state (declared early so WebSocket handlers can reference)
|
|
@@ -339,6 +481,7 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
339
481
|
|
|
340
482
|
async function handleErrors(errors: Error[]) {
|
|
341
483
|
for (const err of errors) {
|
|
484
|
+
audit.recordError('handler', err);
|
|
342
485
|
log.error(`Handler error: ${err.message}`);
|
|
343
486
|
for (const hook of errorHooks) {
|
|
344
487
|
try { await hook(err); } catch { /* swallow */ }
|
|
@@ -346,6 +489,23 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
346
489
|
}
|
|
347
490
|
}
|
|
348
491
|
|
|
492
|
+
function shouldPersistEvent(event: AutomationEventType): boolean {
|
|
493
|
+
return event !== 'tick' && event !== 'price_change';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function emitAutomationEvent<E extends AutomationEventType>(
|
|
497
|
+
event: E,
|
|
498
|
+
payload: AutomationEventPayloads[E],
|
|
499
|
+
source: 'poll' | 'ws' | 'manual',
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
if (shouldPersistEvent(event)) {
|
|
502
|
+
audit.recordEvent(event, source, payload);
|
|
503
|
+
}
|
|
504
|
+
const errors = await eventBus.emit(event, payload);
|
|
505
|
+
if (errors.length) await handleErrors(errors);
|
|
506
|
+
eventsEmitted++;
|
|
507
|
+
}
|
|
508
|
+
|
|
349
509
|
// ── WebSocket setup ─────────────────────────────────────────────
|
|
350
510
|
let ws: WebSocketManager | null = null;
|
|
351
511
|
let wsConnected = false;
|
|
@@ -368,17 +528,27 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
368
528
|
if (oldPrice !== undefined && oldPrice !== 0 && eventBus.has('price_change')) {
|
|
369
529
|
const changePct = ((newPrice - oldPrice) / oldPrice) * 100;
|
|
370
530
|
if (Math.abs(changePct) >= 0.01) {
|
|
371
|
-
|
|
372
|
-
.then(errors => { if (errors.length) handleErrors(errors); });
|
|
373
|
-
eventsEmitted++;
|
|
531
|
+
void emitAutomationEvent('price_change', { coin, oldPrice, newPrice, changePct }, 'ws');
|
|
374
532
|
}
|
|
375
533
|
}
|
|
376
534
|
}
|
|
377
535
|
});
|
|
378
536
|
|
|
379
537
|
ws.on('orderUpdate', (update) => {
|
|
538
|
+
audit.recordOrderUpdate({
|
|
539
|
+
coin: update.order.coin,
|
|
540
|
+
oid: update.order.oid,
|
|
541
|
+
side: update.order.side === 'B' ? 'buy' : 'sell',
|
|
542
|
+
size: parseFloat(update.order.sz),
|
|
543
|
+
price: parseFloat(update.order.limitPx),
|
|
544
|
+
origSize: parseFloat(update.order.origSz),
|
|
545
|
+
status: update.status,
|
|
546
|
+
statusTimestamp: update.statusTimestamp,
|
|
547
|
+
raw: update,
|
|
548
|
+
});
|
|
549
|
+
|
|
380
550
|
if (eventBus.has('order_update')) {
|
|
381
|
-
|
|
551
|
+
void emitAutomationEvent('order_update', {
|
|
382
552
|
coin: update.order.coin,
|
|
383
553
|
oid: update.order.oid,
|
|
384
554
|
side: update.order.side === 'B' ? 'buy' : 'sell',
|
|
@@ -387,45 +557,55 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
387
557
|
origSize: parseFloat(update.order.origSz),
|
|
388
558
|
status: update.status,
|
|
389
559
|
statusTimestamp: update.statusTimestamp,
|
|
390
|
-
}
|
|
391
|
-
eventsEmitted++;
|
|
560
|
+
}, 'ws');
|
|
392
561
|
}
|
|
393
562
|
|
|
394
563
|
// Also emit order_filled for backward compatibility
|
|
395
564
|
if (update.status === 'filled' && eventBus.has('order_filled')) {
|
|
396
|
-
|
|
565
|
+
void emitAutomationEvent('order_filled', {
|
|
397
566
|
coin: update.order.coin,
|
|
398
567
|
oid: update.order.oid,
|
|
399
568
|
side: update.order.side === 'B' ? 'buy' : 'sell',
|
|
400
569
|
size: parseFloat(update.order.sz),
|
|
401
570
|
price: parseFloat(update.order.limitPx),
|
|
402
|
-
}
|
|
403
|
-
eventsEmitted++;
|
|
571
|
+
}, 'ws');
|
|
404
572
|
}
|
|
405
573
|
});
|
|
406
574
|
|
|
407
575
|
ws.on('userFill', (fill) => {
|
|
408
576
|
// userFill events are already covered by order_update with status=filled
|
|
409
577
|
// But this provides the realized PnL and fee data that order_update doesn't have
|
|
578
|
+
audit.recordFill({
|
|
579
|
+
coin: fill.coin,
|
|
580
|
+
side: fill.side === 'B' ? 'buy' : 'sell',
|
|
581
|
+
size: fill.sz,
|
|
582
|
+
price: fill.px,
|
|
583
|
+
time: fill.time,
|
|
584
|
+
closedPnl: fill.closedPnl,
|
|
585
|
+
fee: fill.fee,
|
|
586
|
+
oid: fill.oid,
|
|
587
|
+
crossed: fill.crossed,
|
|
588
|
+
}, fill.time);
|
|
410
589
|
log.debug(`Fill: ${fill.side === 'B' ? 'BUY' : 'SELL'} ${fill.sz} ${fill.coin} @ ${fill.px} (PnL: ${fill.closedPnl})`);
|
|
411
590
|
});
|
|
412
591
|
|
|
413
592
|
ws.on('userEvent', (event) => {
|
|
593
|
+
audit.recordUserEvent(event);
|
|
414
594
|
// Handle liquidation events — only available through WebSocket
|
|
415
595
|
if ('liquidation' in event && eventBus.has('liquidation')) {
|
|
416
596
|
const liq = event.liquidation;
|
|
417
|
-
|
|
597
|
+
void emitAutomationEvent('liquidation', {
|
|
418
598
|
lid: liq.lid,
|
|
419
599
|
liquidator: liq.liquidator,
|
|
420
600
|
liquidatedUser: liq.liquidated_user,
|
|
421
601
|
liquidatedNtlPos: parseFloat(liq.liquidated_ntl_pos),
|
|
422
602
|
liquidatedAccountValue: parseFloat(liq.liquidated_account_value),
|
|
423
|
-
}
|
|
424
|
-
eventsEmitted++;
|
|
603
|
+
}, 'ws');
|
|
425
604
|
}
|
|
426
605
|
});
|
|
427
606
|
|
|
428
607
|
ws.on('error', ({ error }) => {
|
|
608
|
+
audit.recordError('websocket', error);
|
|
429
609
|
log.warn(`WebSocket error: ${error.message}`);
|
|
430
610
|
});
|
|
431
611
|
|
|
@@ -444,7 +624,9 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
444
624
|
await ws.subscribeAll(userAddress);
|
|
445
625
|
log.info('WebSocket subscriptions active (allMids, orderUpdates, userFills, userEvents)');
|
|
446
626
|
} catch (err) {
|
|
447
|
-
|
|
627
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
628
|
+
audit.recordError('websocket_setup', error);
|
|
629
|
+
log.warn(`WebSocket setup failed: ${error.message} — using REST polling only`);
|
|
448
630
|
ws = null;
|
|
449
631
|
wsConnected = false;
|
|
450
632
|
}
|
|
@@ -458,11 +640,17 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
458
640
|
const snapshot = await buildSnapshot(rawClient);
|
|
459
641
|
pollCount++;
|
|
460
642
|
const now = Date.now();
|
|
643
|
+
audit.recordSnapshot({
|
|
644
|
+
pollCount,
|
|
645
|
+
equity: snapshot.equity,
|
|
646
|
+
marginUsed: snapshot.marginUsed,
|
|
647
|
+
marginUsedPct: snapshot.marginUsedPct,
|
|
648
|
+
positions: [...snapshot.positions.values()],
|
|
649
|
+
timestamp: now,
|
|
650
|
+
});
|
|
461
651
|
|
|
462
652
|
// Always emit tick
|
|
463
|
-
|
|
464
|
-
if (tickErrors.length) await handleErrors(tickErrors);
|
|
465
|
-
eventsEmitted++;
|
|
653
|
+
await emitAutomationEvent('tick', { timestamp: now, pollCount }, 'poll');
|
|
466
654
|
|
|
467
655
|
if (previousSnapshot) {
|
|
468
656
|
// Price changes (skip when WebSocket is handling real-time prices)
|
|
@@ -472,9 +660,7 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
472
660
|
if (oldPrice === undefined || oldPrice === 0) continue;
|
|
473
661
|
const changePct = ((newPrice - oldPrice) / oldPrice) * 100;
|
|
474
662
|
if (Math.abs(changePct) >= 0.01) { // 0.01% minimum to fire (filters rounding noise)
|
|
475
|
-
|
|
476
|
-
if (errors.length) await handleErrors(errors);
|
|
477
|
-
eventsEmitted++;
|
|
663
|
+
await emitAutomationEvent('price_change', { coin, oldPrice, newPrice, changePct }, 'poll');
|
|
478
664
|
}
|
|
479
665
|
}
|
|
480
666
|
}
|
|
@@ -482,14 +668,12 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
482
668
|
// Funding updates
|
|
483
669
|
if (eventBus.has('funding_update')) {
|
|
484
670
|
for (const [coin, data] of snapshot.fundingRates) {
|
|
485
|
-
|
|
671
|
+
await emitAutomationEvent('funding_update', {
|
|
486
672
|
coin,
|
|
487
673
|
fundingRate: data.rate,
|
|
488
674
|
annualized: annualizeFundingRate(data.rate),
|
|
489
675
|
premium: data.premium,
|
|
490
|
-
});
|
|
491
|
-
if (errors.length) await handleErrors(errors);
|
|
492
|
-
eventsEmitted++;
|
|
676
|
+
}, 'poll');
|
|
493
677
|
}
|
|
494
678
|
}
|
|
495
679
|
|
|
@@ -497,14 +681,12 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
497
681
|
if (eventBus.has('position_opened')) {
|
|
498
682
|
for (const [coin, pos] of snapshot.positions) {
|
|
499
683
|
if (!previousSnapshot.positions.has(coin)) {
|
|
500
|
-
|
|
684
|
+
await emitAutomationEvent('position_opened', {
|
|
501
685
|
coin,
|
|
502
686
|
side: pos.size > 0 ? 'long' : 'short',
|
|
503
687
|
size: Math.abs(pos.size),
|
|
504
688
|
entryPrice: pos.entryPrice,
|
|
505
|
-
});
|
|
506
|
-
if (errors.length) await handleErrors(errors);
|
|
507
|
-
eventsEmitted++;
|
|
689
|
+
}, 'poll');
|
|
508
690
|
}
|
|
509
691
|
}
|
|
510
692
|
}
|
|
@@ -513,13 +695,11 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
513
695
|
if (eventBus.has('position_closed')) {
|
|
514
696
|
for (const [coin, prevPos] of previousSnapshot.positions) {
|
|
515
697
|
if (!snapshot.positions.has(coin)) {
|
|
516
|
-
|
|
698
|
+
await emitAutomationEvent('position_closed', {
|
|
517
699
|
coin,
|
|
518
700
|
previousSize: prevPos.size,
|
|
519
701
|
entryPrice: prevPos.entryPrice,
|
|
520
|
-
});
|
|
521
|
-
if (errors.length) await handleErrors(errors);
|
|
522
|
-
eventsEmitted++;
|
|
702
|
+
}, 'poll');
|
|
523
703
|
}
|
|
524
704
|
}
|
|
525
705
|
}
|
|
@@ -529,14 +709,12 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
529
709
|
for (const [coin, pos] of snapshot.positions) {
|
|
530
710
|
const prevPos = previousSnapshot.positions.get(coin);
|
|
531
711
|
if (prevPos && pos.size !== prevPos.size) {
|
|
532
|
-
|
|
712
|
+
await emitAutomationEvent('position_changed', {
|
|
533
713
|
coin,
|
|
534
714
|
oldSize: prevPos.size,
|
|
535
715
|
newSize: pos.size,
|
|
536
716
|
entryPrice: pos.entryPrice,
|
|
537
|
-
});
|
|
538
|
-
if (errors.length) await handleErrors(errors);
|
|
539
|
-
eventsEmitted++;
|
|
717
|
+
}, 'poll');
|
|
540
718
|
}
|
|
541
719
|
}
|
|
542
720
|
}
|
|
@@ -549,14 +727,12 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
549
727
|
const pnlChange = Math.abs(pos.unrealizedPnl - prevPos.unrealizedPnl);
|
|
550
728
|
const changePct = (pnlChange / pos.positionValue) * 100;
|
|
551
729
|
if (changePct >= 5) {
|
|
552
|
-
|
|
730
|
+
await emitAutomationEvent('pnl_threshold', {
|
|
553
731
|
coin,
|
|
554
732
|
unrealizedPnl: pos.unrealizedPnl,
|
|
555
733
|
changePct,
|
|
556
734
|
positionValue: pos.positionValue,
|
|
557
|
-
});
|
|
558
|
-
if (errors.length) await handleErrors(errors);
|
|
559
|
-
eventsEmitted++;
|
|
735
|
+
}, 'poll');
|
|
560
736
|
}
|
|
561
737
|
}
|
|
562
738
|
}
|
|
@@ -565,13 +741,11 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
565
741
|
if (eventBus.has('margin_warning') && snapshot.marginUsedPct >= 80) {
|
|
566
742
|
const prevPct = previousSnapshot.marginUsedPct;
|
|
567
743
|
if (prevPct < 80 || snapshot.marginUsedPct - prevPct >= 5) {
|
|
568
|
-
|
|
744
|
+
await emitAutomationEvent('margin_warning', {
|
|
569
745
|
marginUsedPct: snapshot.marginUsedPct,
|
|
570
746
|
equity: snapshot.equity,
|
|
571
747
|
marginUsed: snapshot.marginUsed,
|
|
572
|
-
});
|
|
573
|
-
if (errors.length) await handleErrors(errors);
|
|
574
|
-
eventsEmitted++;
|
|
748
|
+
}, 'poll');
|
|
575
749
|
}
|
|
576
750
|
}
|
|
577
751
|
|
|
@@ -586,6 +760,7 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
586
760
|
await task.handler();
|
|
587
761
|
} catch (err) {
|
|
588
762
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
763
|
+
audit.recordError('scheduled_task', error);
|
|
589
764
|
log.error(`Scheduled task error: ${error.message}`);
|
|
590
765
|
await handleErrors([error]);
|
|
591
766
|
}
|
|
@@ -595,7 +770,9 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
595
770
|
|
|
596
771
|
previousSnapshot = snapshot;
|
|
597
772
|
} catch (err) {
|
|
598
|
-
|
|
773
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
774
|
+
audit.recordError('poll', error);
|
|
775
|
+
log.error(`Poll error: ${error.message}`);
|
|
599
776
|
} finally {
|
|
600
777
|
isPolling = false;
|
|
601
778
|
}
|
|
@@ -624,7 +801,9 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
624
801
|
|
|
625
802
|
for (const hook of stopHooks) {
|
|
626
803
|
try { await hook(); } catch (err) {
|
|
627
|
-
|
|
804
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
805
|
+
audit.recordError('onStop', error);
|
|
806
|
+
log.error(`onStop hook error: ${error.message}`);
|
|
628
807
|
}
|
|
629
808
|
}
|
|
630
809
|
|
|
@@ -637,6 +816,12 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
637
816
|
unregisterAutomation(id);
|
|
638
817
|
}
|
|
639
818
|
log.info(`Stopped (${pollCount} polls, ${eventsEmitted} events)`);
|
|
819
|
+
await audit.stop({
|
|
820
|
+
status: 'stopped',
|
|
821
|
+
stopReason: opts?.persist === false ? 'shutdown_keep_registry' : 'manual_stop',
|
|
822
|
+
pollCount,
|
|
823
|
+
eventsEmitted,
|
|
824
|
+
});
|
|
640
825
|
}
|
|
641
826
|
|
|
642
827
|
const entry: RunningAutomation = {
|
package/scripts/auto/types.ts
CHANGED
|
@@ -87,6 +87,13 @@ export interface AutomationLogger {
|
|
|
87
87
|
debug(message: string): void;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
export interface AutomationAudit {
|
|
91
|
+
/** Record a custom audit note for later reporting. */
|
|
92
|
+
record(kind: string, payload?: unknown): void;
|
|
93
|
+
/** Record a numeric metric with optional dimensions/tags. */
|
|
94
|
+
metric(name: string, value: number, tags?: Record<string, unknown>): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
// ── Publish (webhook) ───────────────────────────────────────────────
|
|
91
98
|
|
|
92
99
|
export interface PublishOptions {
|
|
@@ -149,6 +156,9 @@ export interface AutomationAPI {
|
|
|
149
156
|
/** Structured logger */
|
|
150
157
|
log: AutomationLogger;
|
|
151
158
|
|
|
159
|
+
/** Local audit trail persisted to ~/.openbroker/automation-audit.sqlite */
|
|
160
|
+
audit: AutomationAudit;
|
|
161
|
+
|
|
152
162
|
/** Unique automation ID (derived from filename or --id flag) */
|
|
153
163
|
id: string;
|
|
154
164
|
|