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
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { once } from 'events';
|
|
4
|
+
import net, { type Socket } from 'net';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import { setTimeout as delay } from 'timers/promises';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { ensureConfigDir } from '../core/config.js';
|
|
10
|
+
|
|
11
|
+
export interface AutomationAuditSink {
|
|
12
|
+
readonly runId: string;
|
|
13
|
+
readonly dbPath: string;
|
|
14
|
+
recordLog(level: 'info' | 'warn' | 'error' | 'debug', message: string, timestamp?: number): void;
|
|
15
|
+
recordEvent(eventType: string, source: 'poll' | 'ws' | 'manual', payload: unknown, timestamp?: number): void;
|
|
16
|
+
recordAction(args: {
|
|
17
|
+
actionId?: string;
|
|
18
|
+
phase: 'request' | 'response' | 'error';
|
|
19
|
+
method: string;
|
|
20
|
+
payload?: unknown;
|
|
21
|
+
result?: unknown;
|
|
22
|
+
error?: unknown;
|
|
23
|
+
dryRun?: boolean;
|
|
24
|
+
timestamp?: number;
|
|
25
|
+
}): void;
|
|
26
|
+
recordSnapshot(snapshot: {
|
|
27
|
+
pollCount: number;
|
|
28
|
+
equity: number;
|
|
29
|
+
marginUsed: number;
|
|
30
|
+
marginUsedPct: number;
|
|
31
|
+
positions: unknown[];
|
|
32
|
+
timestamp?: number;
|
|
33
|
+
}): void;
|
|
34
|
+
recordOrderUpdate(payload: unknown, timestamp?: number): void;
|
|
35
|
+
recordFill(payload: unknown, timestamp?: number): void;
|
|
36
|
+
recordUserEvent(payload: unknown, timestamp?: number): void;
|
|
37
|
+
recordStateChange(op: 'set' | 'delete' | 'clear', key: string | null, value?: unknown, timestamp?: number): void;
|
|
38
|
+
recordPublish(message: string, options: unknown, delivered: boolean, timestamp?: number): void;
|
|
39
|
+
recordError(stage: string, error: unknown, timestamp?: number): void;
|
|
40
|
+
recordNote(kind: string, payload?: unknown, timestamp?: number): void;
|
|
41
|
+
recordMetric(name: string, value: number, tags?: Record<string, unknown>, timestamp?: number): void;
|
|
42
|
+
stop(args: {
|
|
43
|
+
status: 'stopped' | 'error';
|
|
44
|
+
stopReason: string;
|
|
45
|
+
pollCount: number;
|
|
46
|
+
eventsEmitted: number;
|
|
47
|
+
timestamp?: number;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type AuditMessageType =
|
|
52
|
+
| 'init'
|
|
53
|
+
| 'log'
|
|
54
|
+
| 'event'
|
|
55
|
+
| 'action'
|
|
56
|
+
| 'snapshot'
|
|
57
|
+
| 'order_update'
|
|
58
|
+
| 'fill'
|
|
59
|
+
| 'user_event'
|
|
60
|
+
| 'state_change'
|
|
61
|
+
| 'publish'
|
|
62
|
+
| 'error'
|
|
63
|
+
| 'note'
|
|
64
|
+
| 'metric'
|
|
65
|
+
| 'stop';
|
|
66
|
+
|
|
67
|
+
type AuditPayload = Record<string, unknown>;
|
|
68
|
+
|
|
69
|
+
type AuditMessage = {
|
|
70
|
+
messageId: string;
|
|
71
|
+
type: AuditMessageType;
|
|
72
|
+
payload: AuditPayload;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type AuditResponse = {
|
|
76
|
+
messageId: string;
|
|
77
|
+
ok: boolean;
|
|
78
|
+
error?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export interface AuditStartOptions {
|
|
82
|
+
automationId: string;
|
|
83
|
+
scriptPath: string;
|
|
84
|
+
dryRun: boolean;
|
|
85
|
+
verbose: boolean;
|
|
86
|
+
pollIntervalMs: number;
|
|
87
|
+
useWebSocket: boolean;
|
|
88
|
+
accountAddress: string;
|
|
89
|
+
walletAddress: string;
|
|
90
|
+
isApiWallet: boolean;
|
|
91
|
+
initialState?: Record<string, unknown>;
|
|
92
|
+
persistedState?: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const AUDIT_DB_PATH = process.env.OPENBROKER_AUDIT_DB_PATH
|
|
96
|
+
|| path.join(ensureConfigDir(), 'automation-audit.sqlite');
|
|
97
|
+
|
|
98
|
+
export const AUDIT_SOCKET_PATH = process.env.OPENBROKER_AUDIT_SOCKET_PATH
|
|
99
|
+
|| (process.platform === 'win32'
|
|
100
|
+
? '\\\\.\\pipe\\openbroker-automation-audit-v2'
|
|
101
|
+
: path.join(ensureConfigDir(), 'automation-audit.v2.sock'));
|
|
102
|
+
|
|
103
|
+
function internalWarn(automationId: string, message: string): void {
|
|
104
|
+
console.error(`[auto:${automationId}:audit] ${message}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function toSerializable<T = unknown>(value: T): T {
|
|
108
|
+
const seen = new WeakSet<object>();
|
|
109
|
+
const encoded = JSON.stringify(value, (_key, currentValue) => {
|
|
110
|
+
if (typeof currentValue === 'bigint') {
|
|
111
|
+
return currentValue.toString();
|
|
112
|
+
}
|
|
113
|
+
if (currentValue instanceof Error) {
|
|
114
|
+
return {
|
|
115
|
+
name: currentValue.name,
|
|
116
|
+
message: currentValue.message,
|
|
117
|
+
stack: currentValue.stack,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (currentValue instanceof Map) {
|
|
121
|
+
return Object.fromEntries(currentValue.entries());
|
|
122
|
+
}
|
|
123
|
+
if (currentValue instanceof Set) {
|
|
124
|
+
return [...currentValue.values()];
|
|
125
|
+
}
|
|
126
|
+
if (typeof currentValue === 'object' && currentValue !== null) {
|
|
127
|
+
if (seen.has(currentValue)) {
|
|
128
|
+
return '[Circular]';
|
|
129
|
+
}
|
|
130
|
+
seen.add(currentValue);
|
|
131
|
+
}
|
|
132
|
+
return currentValue;
|
|
133
|
+
});
|
|
134
|
+
if (encoded === undefined) {
|
|
135
|
+
return null as T;
|
|
136
|
+
}
|
|
137
|
+
return JSON.parse(encoded) as T;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class NoopAuditSink implements AutomationAuditSink {
|
|
141
|
+
readonly runId = randomUUID();
|
|
142
|
+
readonly dbPath = AUDIT_DB_PATH;
|
|
143
|
+
recordLog(): void {}
|
|
144
|
+
recordEvent(): void {}
|
|
145
|
+
recordAction(): void {}
|
|
146
|
+
recordSnapshot(): void {}
|
|
147
|
+
recordOrderUpdate(): void {}
|
|
148
|
+
recordFill(): void {}
|
|
149
|
+
recordUserEvent(): void {}
|
|
150
|
+
recordStateChange(): void {}
|
|
151
|
+
recordPublish(): void {}
|
|
152
|
+
recordError(): void {}
|
|
153
|
+
recordNote(): void {}
|
|
154
|
+
recordMetric(): void {}
|
|
155
|
+
async stop(): Promise<void> {}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class DaemonAuditSink implements AutomationAuditSink {
|
|
159
|
+
readonly runId = randomUUID();
|
|
160
|
+
readonly dbPath = AUDIT_DB_PATH;
|
|
161
|
+
|
|
162
|
+
private socketPath = AUDIT_SOCKET_PATH;
|
|
163
|
+
private socket: Socket | null = null;
|
|
164
|
+
private lineReader: readline.Interface | null = null;
|
|
165
|
+
private connectPromise: Promise<void> | null = null;
|
|
166
|
+
private flushPromise: Promise<void> | null = null;
|
|
167
|
+
private closed = false;
|
|
168
|
+
private daemonSpawnedAt = 0;
|
|
169
|
+
private pendingQueue: AuditMessage[] = [];
|
|
170
|
+
private inFlight = new Map<string, AuditMessage>();
|
|
171
|
+
private ackWaiters = new Map<string, { resolve: () => void; reject: (error: Error) => void }>();
|
|
172
|
+
|
|
173
|
+
constructor(private readonly automationId: string, options: AuditStartOptions) {
|
|
174
|
+
this.enqueue({
|
|
175
|
+
type: 'init',
|
|
176
|
+
payload: {
|
|
177
|
+
runId: this.runId,
|
|
178
|
+
automationId: options.automationId,
|
|
179
|
+
scriptPath: options.scriptPath,
|
|
180
|
+
dryRun: options.dryRun,
|
|
181
|
+
verbose: options.verbose,
|
|
182
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
183
|
+
useWebSocket: options.useWebSocket,
|
|
184
|
+
accountAddress: options.accountAddress,
|
|
185
|
+
walletAddress: options.walletAddress,
|
|
186
|
+
isApiWallet: options.isApiWallet,
|
|
187
|
+
initialState: toSerializable(options.initialState ?? {}),
|
|
188
|
+
persistedState: toSerializable(options.persistedState ?? {}),
|
|
189
|
+
pid: process.pid,
|
|
190
|
+
startedAt: Date.now(),
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private handleSocketClose(): void {
|
|
196
|
+
if (this.lineReader) {
|
|
197
|
+
this.lineReader.close();
|
|
198
|
+
this.lineReader = null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const inflight = [...this.inFlight.values()];
|
|
202
|
+
this.inFlight.clear();
|
|
203
|
+
if (inflight.length > 0) {
|
|
204
|
+
this.pendingQueue = inflight.concat(this.pendingQueue);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.socket = null;
|
|
208
|
+
if (!this.closed) {
|
|
209
|
+
void this.ensureConnected();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private handleResponse(line: string): void {
|
|
214
|
+
let response: AuditResponse;
|
|
215
|
+
try {
|
|
216
|
+
response = JSON.parse(line) as AuditResponse;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
internalWarn(this.automationId, `failed to parse audit daemon response: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.inFlight.delete(response.messageId);
|
|
223
|
+
const waiter = this.ackWaiters.get(response.messageId);
|
|
224
|
+
if (!waiter) return;
|
|
225
|
+
this.ackWaiters.delete(response.messageId);
|
|
226
|
+
if (response.ok) {
|
|
227
|
+
waiter.resolve();
|
|
228
|
+
} else {
|
|
229
|
+
waiter.reject(new Error(response.error || 'audit daemon returned an error'));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async openConnection(): Promise<void> {
|
|
234
|
+
const socket = net.createConnection(this.socketPath);
|
|
235
|
+
|
|
236
|
+
await new Promise<void>((resolve, reject) => {
|
|
237
|
+
let settled = false;
|
|
238
|
+
|
|
239
|
+
const onConnect = () => {
|
|
240
|
+
if (settled) return;
|
|
241
|
+
settled = true;
|
|
242
|
+
socket.off('error', onError);
|
|
243
|
+
resolve();
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const onError = (error: Error) => {
|
|
247
|
+
if (settled) return;
|
|
248
|
+
settled = true;
|
|
249
|
+
socket.off('connect', onConnect);
|
|
250
|
+
socket.destroy();
|
|
251
|
+
reject(error);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
socket.once('connect', onConnect);
|
|
255
|
+
socket.once('error', onError);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
socket.setEncoding('utf8');
|
|
259
|
+
socket.on('close', () => this.handleSocketClose());
|
|
260
|
+
socket.on('error', (error) => {
|
|
261
|
+
if (!this.closed) {
|
|
262
|
+
internalWarn(this.automationId, `audit socket error: ${error.message}`);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this.lineReader = readline.createInterface({
|
|
267
|
+
input: socket,
|
|
268
|
+
crlfDelay: Infinity,
|
|
269
|
+
});
|
|
270
|
+
this.lineReader.on('line', (line) => this.handleResponse(line));
|
|
271
|
+
|
|
272
|
+
this.socket = socket;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private async spawnDaemon(): Promise<void> {
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
if (now - this.daemonSpawnedAt < 1_000) return;
|
|
278
|
+
this.daemonSpawnedAt = now;
|
|
279
|
+
|
|
280
|
+
const daemonPath = fileURLToPath(new URL('./audit-daemon.js', import.meta.url));
|
|
281
|
+
const child = spawn(
|
|
282
|
+
process.execPath,
|
|
283
|
+
['--no-warnings', '--experimental-sqlite', daemonPath, this.dbPath, this.socketPath],
|
|
284
|
+
{
|
|
285
|
+
detached: true,
|
|
286
|
+
stdio: 'ignore',
|
|
287
|
+
env: { ...process.env },
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
child.unref();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async ensureConnected(): Promise<void> {
|
|
294
|
+
if (this.closed) return;
|
|
295
|
+
if (this.socket && !this.socket.destroyed) return;
|
|
296
|
+
if (this.connectPromise) return this.connectPromise;
|
|
297
|
+
|
|
298
|
+
this.connectPromise = (async () => {
|
|
299
|
+
try {
|
|
300
|
+
await this.openConnection();
|
|
301
|
+
} catch {
|
|
302
|
+
await this.spawnDaemon();
|
|
303
|
+
let lastError: Error | null = null;
|
|
304
|
+
|
|
305
|
+
for (let attempt = 0; attempt < 30 && !this.closed; attempt++) {
|
|
306
|
+
try {
|
|
307
|
+
await delay(100 + (attempt * 50));
|
|
308
|
+
await this.openConnection();
|
|
309
|
+
lastError = null;
|
|
310
|
+
break;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (lastError) {
|
|
317
|
+
throw lastError;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await this.flushQueue();
|
|
322
|
+
})().catch((error) => {
|
|
323
|
+
internalWarn(this.automationId, `audit daemon unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
324
|
+
}).finally(() => {
|
|
325
|
+
this.connectPromise = null;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return this.connectPromise;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private enqueue(message: { type: AuditMessageType; payload: AuditPayload }): AuditMessage {
|
|
332
|
+
const payload = message.type === 'init'
|
|
333
|
+
? message.payload
|
|
334
|
+
: { runId: this.runId, ...message.payload };
|
|
335
|
+
|
|
336
|
+
const wire: AuditMessage = {
|
|
337
|
+
messageId: randomUUID(),
|
|
338
|
+
type: message.type,
|
|
339
|
+
payload,
|
|
340
|
+
};
|
|
341
|
+
this.pendingQueue.push(wire);
|
|
342
|
+
void this.ensureConnected();
|
|
343
|
+
if (this.socket && !this.socket.destroyed) {
|
|
344
|
+
void this.flushQueue();
|
|
345
|
+
}
|
|
346
|
+
return wire;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private async flushQueue(): Promise<void> {
|
|
350
|
+
if (this.flushPromise) return this.flushPromise;
|
|
351
|
+
|
|
352
|
+
this.flushPromise = (async () => {
|
|
353
|
+
while (!this.closed && this.socket && !this.socket.destroyed && this.pendingQueue.length > 0) {
|
|
354
|
+
const message = this.pendingQueue.shift()!;
|
|
355
|
+
this.inFlight.set(message.messageId, message);
|
|
356
|
+
|
|
357
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
358
|
+
const writable = this.socket.write(line);
|
|
359
|
+
if (!writable && this.socket) {
|
|
360
|
+
await once(this.socket, 'drain');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
})().finally(() => {
|
|
364
|
+
this.flushPromise = null;
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return this.flushPromise;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private send(message: { type: AuditMessageType; payload: AuditPayload }, waitForAck = false): Promise<void> {
|
|
371
|
+
if (this.closed) return Promise.resolve();
|
|
372
|
+
|
|
373
|
+
const wire = this.enqueue(message);
|
|
374
|
+
if (!waitForAck) return Promise.resolve();
|
|
375
|
+
|
|
376
|
+
return new Promise<void>((resolve, reject) => {
|
|
377
|
+
this.ackWaiters.set(wire.messageId, { resolve, reject });
|
|
378
|
+
void this.flushQueue();
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
recordLog(level: 'info' | 'warn' | 'error' | 'debug', message: string, timestamp: number = Date.now()): void {
|
|
383
|
+
void this.send({ type: 'log', payload: { timestamp, level, message } });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
recordEvent(eventType: string, source: 'poll' | 'ws' | 'manual', payload: unknown, timestamp: number = Date.now()): void {
|
|
387
|
+
void this.send({ type: 'event', payload: { timestamp, eventType, source, payload: toSerializable(payload) } });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
recordAction(args: {
|
|
391
|
+
actionId?: string;
|
|
392
|
+
phase: 'request' | 'response' | 'error';
|
|
393
|
+
method: string;
|
|
394
|
+
payload?: unknown;
|
|
395
|
+
result?: unknown;
|
|
396
|
+
error?: unknown;
|
|
397
|
+
dryRun?: boolean;
|
|
398
|
+
timestamp?: number;
|
|
399
|
+
}): void {
|
|
400
|
+
void this.send({
|
|
401
|
+
type: 'action',
|
|
402
|
+
payload: {
|
|
403
|
+
timestamp: args.timestamp ?? Date.now(),
|
|
404
|
+
actionId: args.actionId ?? randomUUID(),
|
|
405
|
+
phase: args.phase,
|
|
406
|
+
method: args.method,
|
|
407
|
+
payload: toSerializable(args.payload),
|
|
408
|
+
result: toSerializable(args.result),
|
|
409
|
+
error: toSerializable(args.error),
|
|
410
|
+
dryRun: args.dryRun ?? false,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
recordSnapshot(snapshot: {
|
|
416
|
+
pollCount: number;
|
|
417
|
+
equity: number;
|
|
418
|
+
marginUsed: number;
|
|
419
|
+
marginUsedPct: number;
|
|
420
|
+
positions: unknown[];
|
|
421
|
+
timestamp?: number;
|
|
422
|
+
}): void {
|
|
423
|
+
void this.send({
|
|
424
|
+
type: 'snapshot',
|
|
425
|
+
payload: {
|
|
426
|
+
timestamp: snapshot.timestamp ?? Date.now(),
|
|
427
|
+
pollCount: snapshot.pollCount,
|
|
428
|
+
equity: snapshot.equity,
|
|
429
|
+
marginUsed: snapshot.marginUsed,
|
|
430
|
+
marginUsedPct: snapshot.marginUsedPct,
|
|
431
|
+
positions: toSerializable(snapshot.positions),
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
recordOrderUpdate(payload: unknown, timestamp: number = Date.now()): void {
|
|
437
|
+
void this.send({ type: 'order_update', payload: { timestamp, payload: toSerializable(payload) } });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
recordFill(payload: unknown, timestamp: number = Date.now()): void {
|
|
441
|
+
void this.send({ type: 'fill', payload: { timestamp, payload: toSerializable(payload) } });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
recordUserEvent(payload: unknown, timestamp: number = Date.now()): void {
|
|
445
|
+
void this.send({ type: 'user_event', payload: { timestamp, payload: toSerializable(payload) } });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
recordStateChange(op: 'set' | 'delete' | 'clear', key: string | null, value?: unknown, timestamp: number = Date.now()): void {
|
|
449
|
+
void this.send({
|
|
450
|
+
type: 'state_change',
|
|
451
|
+
payload: {
|
|
452
|
+
timestamp,
|
|
453
|
+
op,
|
|
454
|
+
key,
|
|
455
|
+
value: toSerializable(value),
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
recordPublish(message: string, options: unknown, delivered: boolean, timestamp: number = Date.now()): void {
|
|
461
|
+
void this.send({
|
|
462
|
+
type: 'publish',
|
|
463
|
+
payload: {
|
|
464
|
+
timestamp,
|
|
465
|
+
message,
|
|
466
|
+
options: toSerializable(options),
|
|
467
|
+
delivered,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
recordError(stage: string, error: unknown, timestamp: number = Date.now()): void {
|
|
473
|
+
void this.send({
|
|
474
|
+
type: 'error',
|
|
475
|
+
payload: {
|
|
476
|
+
timestamp,
|
|
477
|
+
stage,
|
|
478
|
+
error: toSerializable(error),
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
recordNote(kind: string, payload?: unknown, timestamp: number = Date.now()): void {
|
|
484
|
+
void this.send({
|
|
485
|
+
type: 'note',
|
|
486
|
+
payload: {
|
|
487
|
+
timestamp,
|
|
488
|
+
kind,
|
|
489
|
+
payload: toSerializable(payload),
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
recordMetric(name: string, value: number, tags?: Record<string, unknown>, timestamp: number = Date.now()): void {
|
|
495
|
+
void this.send({
|
|
496
|
+
type: 'metric',
|
|
497
|
+
payload: {
|
|
498
|
+
timestamp,
|
|
499
|
+
name,
|
|
500
|
+
value,
|
|
501
|
+
tags: toSerializable(tags ?? {}),
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async stop(args: {
|
|
507
|
+
status: 'stopped' | 'error';
|
|
508
|
+
stopReason: string;
|
|
509
|
+
pollCount: number;
|
|
510
|
+
eventsEmitted: number;
|
|
511
|
+
timestamp?: number;
|
|
512
|
+
}): Promise<void> {
|
|
513
|
+
if (this.closed) return;
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
await this.send({
|
|
517
|
+
type: 'stop',
|
|
518
|
+
payload: {
|
|
519
|
+
timestamp: args.timestamp ?? Date.now(),
|
|
520
|
+
status: args.status,
|
|
521
|
+
stopReason: args.stopReason,
|
|
522
|
+
pollCount: args.pollCount,
|
|
523
|
+
eventsEmitted: args.eventsEmitted,
|
|
524
|
+
},
|
|
525
|
+
}, true);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
internalWarn(this.automationId, `failed to flush stop audit message: ${error instanceof Error ? error.message : String(error)}`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.closed = true;
|
|
531
|
+
if (this.lineReader) {
|
|
532
|
+
this.lineReader.close();
|
|
533
|
+
this.lineReader = null;
|
|
534
|
+
}
|
|
535
|
+
if (this.socket && !this.socket.destroyed) {
|
|
536
|
+
this.socket.end();
|
|
537
|
+
}
|
|
538
|
+
this.socket = null;
|
|
539
|
+
this.pendingQueue = [];
|
|
540
|
+
this.inFlight.clear();
|
|
541
|
+
this.ackWaiters.clear();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function createAutomationAudit(options: AuditStartOptions): AutomationAuditSink {
|
|
546
|
+
try {
|
|
547
|
+
return new DaemonAuditSink(options.automationId, options);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
internalWarn(options.automationId, `audit disabled: ${error instanceof Error ? error.message : String(error)}`);
|
|
550
|
+
return new NoopAuditSink();
|
|
551
|
+
}
|
|
552
|
+
}
|
package/scripts/auto/cli.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
// CLI entry point for `openbroker auto` commands
|
|
2
2
|
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import path from 'path';
|
|
3
6
|
import { parseArgs } from '../core/utils.js';
|
|
4
7
|
import { resolveScriptPath, resolveExamplePath, listAutomations, listExamples, loadExampleConfigs, ensureAutomationsDir } from './loader.js';
|
|
5
8
|
import { startAutomation, getRunningAutomations, getRegisteredAutomations } from './runtime.js';
|
|
6
9
|
import { unregisterAutomation, cleanRegistry } from './registry.js';
|
|
7
10
|
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
8
14
|
function printUsage() {
|
|
9
15
|
console.log(`
|
|
10
16
|
OpenBroker Automations — event-driven trading scripts
|
|
@@ -12,6 +18,7 @@ OpenBroker Automations — event-driven trading scripts
|
|
|
12
18
|
Usage:
|
|
13
19
|
openbroker auto run <script> [options] Run an automation script
|
|
14
20
|
openbroker auto run --example <name> Run a bundled example automation
|
|
21
|
+
openbroker auto report <id> Read the local audit report for an automation
|
|
15
22
|
openbroker auto examples List bundled example automations
|
|
16
23
|
openbroker auto stop <id> Unregister an automation (won't restart)
|
|
17
24
|
openbroker auto list List available automations
|
|
@@ -45,6 +52,7 @@ Examples:
|
|
|
45
52
|
openbroker auto run --example dca --set coin=BTC --set amount=50 --dry
|
|
46
53
|
openbroker auto run --example grid --set coin=ETH --set lower=3000 --set upper=4000
|
|
47
54
|
openbroker auto run my-strategy --dry
|
|
55
|
+
openbroker auto report hype-mm-v2-live-r4
|
|
48
56
|
openbroker auto examples
|
|
49
57
|
`);
|
|
50
58
|
}
|
|
@@ -270,6 +278,27 @@ function cleanCommand() {
|
|
|
270
278
|
console.log('Cleaned stale entries from registry');
|
|
271
279
|
}
|
|
272
280
|
|
|
281
|
+
function reportCommand(rawArgs: string[]) {
|
|
282
|
+
const scriptPath = path.join(__dirname, 'report.ts');
|
|
283
|
+
const result = spawnSync(
|
|
284
|
+
process.execPath,
|
|
285
|
+
['--experimental-sqlite', '--no-warnings', '--import', 'tsx', scriptPath, ...rawArgs],
|
|
286
|
+
{
|
|
287
|
+
stdio: 'inherit',
|
|
288
|
+
cwd: path.resolve(__dirname, '../..'),
|
|
289
|
+
env: { ...process.env },
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (result.error) {
|
|
294
|
+
throw result.error;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
298
|
+
process.exit(result.status);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
273
302
|
async function main() {
|
|
274
303
|
const rawArgs = process.argv.slice(2);
|
|
275
304
|
|
|
@@ -322,6 +351,9 @@ async function main() {
|
|
|
322
351
|
case 'clean':
|
|
323
352
|
cleanCommand();
|
|
324
353
|
break;
|
|
354
|
+
case 'report':
|
|
355
|
+
reportCommand(restArgs);
|
|
356
|
+
break;
|
|
325
357
|
default:
|
|
326
358
|
console.error(`Unknown subcommand: ${subcommand}`);
|
|
327
359
|
console.log('Run "openbroker auto --help" for usage');
|