openbroker 1.2.0 → 1.3.0
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/CHANGELOG.md +14 -0
- package/bin/openbroker.js +4 -0
- package/package.json +2 -1
- package/scripts/auto/cli.ts +89 -3
- package/scripts/auto/prune.ts +252 -0
- package/scripts/auto/runtime.ts +26 -11
- package/scripts/core/client.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Open Broker will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.0] - 2026-05-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`openbroker auto prune`**: New subcommand for trimming the local audit DB
|
|
9
|
+
- `--older-than <duration>` accepts `7d`, `24h`, `30m`, etc.
|
|
10
|
+
- `--status <list>` filters by run status (default: `stopped,error,stale`)
|
|
11
|
+
- `--keep-last <N>` retains the N most-recent runs per `automation_id`
|
|
12
|
+
- `--all` deletes everything except runs whose process is still alive
|
|
13
|
+
- `--vacuum` reclaims disk after deletion
|
|
14
|
+
- `--dry` previews matches without writing — also reconcile is dry-safe
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **`openbroker auto clean`**: Now also reconciles the audit DB. Runs whose pid is dead but whose row still says `status='running'` get marked `stopped` with `stop_reason='reconciled (process exited)'`, fixing dashboards that previously kept rendering them as live/stale long after `auto stop`.
|
|
18
|
+
|
|
5
19
|
## [1.0.59] - 2026-03-11
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/bin/openbroker.js
CHANGED
|
@@ -25,6 +25,9 @@ const child = spawn(
|
|
|
25
25
|
OPENBROKER_CWD: process.cwd(),
|
|
26
26
|
// Suppress Node.js experimental warnings
|
|
27
27
|
NODE_NO_WARNINGS: '1',
|
|
28
|
+
// node:sqlite is stable in Node 24+, experimental-with-flag in 22/23.
|
|
29
|
+
// The flag is accepted (as a no-op) on 24+, so it's safe to set unconditionally.
|
|
30
|
+
NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --experimental-sqlite`.trim(),
|
|
28
31
|
},
|
|
29
32
|
}
|
|
30
33
|
);
|
|
@@ -42,6 +45,7 @@ child.on('error', (err) => {
|
|
|
42
45
|
...process.env,
|
|
43
46
|
OPENBROKER_CWD: process.cwd(),
|
|
44
47
|
NODE_NO_WARNINGS: '1',
|
|
48
|
+
NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --experimental-sqlite`.trim(),
|
|
45
49
|
},
|
|
46
50
|
}
|
|
47
51
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openbroker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@nktkas/hyperliquid": "^0.30.3",
|
|
51
51
|
"dotenv": "^17.2.3",
|
|
52
|
+
"openbroker-monitoring": "file:../openbroker-monitoring",
|
|
52
53
|
"tsx": "^4.19.0",
|
|
53
54
|
"viem": "^2.21.0"
|
|
54
55
|
},
|
package/scripts/auto/cli.ts
CHANGED
|
@@ -23,7 +23,8 @@ Usage:
|
|
|
23
23
|
openbroker auto stop <id> Unregister an automation (won't restart)
|
|
24
24
|
openbroker auto list List available automations
|
|
25
25
|
openbroker auto status Show running automations
|
|
26
|
-
openbroker auto clean Remove stale entries from registry
|
|
26
|
+
openbroker auto clean Remove stale entries from registry + reconcile audit DB
|
|
27
|
+
openbroker auto prune [options] Delete stale runs from the audit DB
|
|
27
28
|
|
|
28
29
|
Options (for run):
|
|
29
30
|
--example <name> Run a bundled example (dca, grid, funding-arb, mm-spread, mm-maker)
|
|
@@ -34,6 +35,14 @@ Options (for run):
|
|
|
34
35
|
--poll <ms> Poll interval in milliseconds (default: 10000)
|
|
35
36
|
--no-ws Disable WebSocket; fall back to REST-only polling
|
|
36
37
|
|
|
38
|
+
Options (for prune):
|
|
39
|
+
--older-than <d> Only prune runs started before this duration ago (e.g. 7d, 24h)
|
|
40
|
+
--status <list> CSV of statuses to consider (default: stopped,error,stale)
|
|
41
|
+
--keep-last <N> Keep the N most-recent runs per automation_id
|
|
42
|
+
--all Prune everything except runs that are still alive
|
|
43
|
+
--vacuum VACUUM the DB after deletion to reclaim disk space
|
|
44
|
+
--dry Preview what would be deleted without writing
|
|
45
|
+
|
|
37
46
|
Scripts are loaded from:
|
|
38
47
|
1. Absolute or relative path
|
|
39
48
|
2. ~/.openbroker/automations/<name>.ts
|
|
@@ -288,9 +297,83 @@ function stopCommand(positional: string[]) {
|
|
|
288
297
|
console.log(`Unregistered: ${id} (will not restart on next gateway start)`);
|
|
289
298
|
}
|
|
290
299
|
|
|
291
|
-
function cleanCommand() {
|
|
300
|
+
async function cleanCommand() {
|
|
292
301
|
cleanRegistry();
|
|
293
302
|
console.log('Cleaned stale entries from registry');
|
|
303
|
+
|
|
304
|
+
// Also reconcile the audit DB so dead processes whose rows still say
|
|
305
|
+
// 'running' get marked 'stopped'. Without this, the dashboard keeps showing
|
|
306
|
+
// 'stale' badges for automations the operator already cleaned out of the
|
|
307
|
+
// registry.
|
|
308
|
+
try {
|
|
309
|
+
const { prune } = await import('./prune.js');
|
|
310
|
+
const result = prune({ reconcileOnly: true });
|
|
311
|
+
if (result.reconciled > 0) {
|
|
312
|
+
console.log(`Reconciled ${result.reconciled} orphan run row${result.reconciled === 1 ? '' : 's'} in audit DB (status: running → stopped)`);
|
|
313
|
+
} else {
|
|
314
|
+
console.log('Audit DB already consistent');
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.warn(`Could not reconcile audit DB: ${err instanceof Error ? err.message : String(err)}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function pruneCommand(args: Record<string, string | boolean>) {
|
|
322
|
+
const { fmtBytes, parseDuration, prune } = await import('./prune.js');
|
|
323
|
+
const olderThanRaw = args['older-than'];
|
|
324
|
+
const olderThanMs = typeof olderThanRaw === 'string' ? parseDuration(olderThanRaw) : undefined;
|
|
325
|
+
const statusesRaw = typeof args.status === 'string' ? args.status : undefined;
|
|
326
|
+
const statuses = statusesRaw
|
|
327
|
+
? new Set(statusesRaw.split(',').map((s) => s.trim()).filter(Boolean))
|
|
328
|
+
: undefined;
|
|
329
|
+
const keepLastRaw = args['keep-last'];
|
|
330
|
+
const keepLast = typeof keepLastRaw === 'string' ? Number(keepLastRaw)
|
|
331
|
+
: typeof keepLastRaw === 'number' ? keepLastRaw
|
|
332
|
+
: undefined;
|
|
333
|
+
if (keepLast !== undefined && (!Number.isFinite(keepLast) || keepLast < 0)) {
|
|
334
|
+
console.error('Error: --keep-last must be a non-negative integer');
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const opts = {
|
|
339
|
+
olderThanMs,
|
|
340
|
+
statuses,
|
|
341
|
+
keepLastPerAutomation: keepLast,
|
|
342
|
+
all: args.all === true,
|
|
343
|
+
vacuum: args.vacuum === true,
|
|
344
|
+
dryRun: args.dry === true,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const result = prune(opts);
|
|
348
|
+
|
|
349
|
+
if (result.reconciled > 0) {
|
|
350
|
+
const verb = result.dryRun ? 'Would reconcile' : 'Reconciled';
|
|
351
|
+
console.log(`${verb} ${result.reconciled} orphan run row${result.reconciled === 1 ? '' : 's'} (running → stopped)`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const verb = result.dryRun ? 'Would delete' : 'Deleted';
|
|
355
|
+
if (result.candidateRunIds.length === 0) {
|
|
356
|
+
console.log('No runs matched pruning filters.');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
console.log(`${verb} ${result.candidateRunIds.length} automation run${result.candidateRunIds.length === 1 ? '' : 's'}:`);
|
|
360
|
+
for (const id of result.candidateRunIds.slice(0, 25)) {
|
|
361
|
+
console.log(` · ${id}`);
|
|
362
|
+
}
|
|
363
|
+
if (result.candidateRunIds.length > 25) {
|
|
364
|
+
console.log(` … and ${result.candidateRunIds.length - 25} more`);
|
|
365
|
+
}
|
|
366
|
+
if (!result.dryRun) {
|
|
367
|
+
const totalChild = Object.values(result.deletedRows).reduce((a, b) => a + b, 0);
|
|
368
|
+
console.log(`Removed ${totalChild.toLocaleString()} child rows across ${Object.keys(result.deletedRows).length} tables`);
|
|
369
|
+
if (result.freedBytes > 0) {
|
|
370
|
+
console.log(`Reclaimed ~${fmtBytes(result.freedBytes)}${opts.vacuum ? ' (post-VACUUM)' : ''}`);
|
|
371
|
+
} else if (opts.vacuum) {
|
|
372
|
+
console.log('No disk reclaimed (VACUUM completed)');
|
|
373
|
+
} else {
|
|
374
|
+
console.log('Run again with --vacuum to reclaim disk space.');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
294
377
|
}
|
|
295
378
|
|
|
296
379
|
function reportCommand(rawArgs: string[]) {
|
|
@@ -364,7 +447,10 @@ async function main() {
|
|
|
364
447
|
statusCommand();
|
|
365
448
|
break;
|
|
366
449
|
case 'clean':
|
|
367
|
-
cleanCommand();
|
|
450
|
+
await cleanCommand();
|
|
451
|
+
break;
|
|
452
|
+
case 'prune':
|
|
453
|
+
await pruneCommand(args);
|
|
368
454
|
break;
|
|
369
455
|
case 'report':
|
|
370
456
|
reportCommand(restArgs);
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Audit DB pruning — delete stale automation runs and their child rows.
|
|
2
|
+
//
|
|
3
|
+
// The audit DB is the same SQLite file the daemon writes to and the dashboard
|
|
4
|
+
// reads from. WAL mode lets us open it from another process for delete writes
|
|
5
|
+
// without blocking the daemon. We always protect runs whose status is 'running'
|
|
6
|
+
// AND whose pid is alive.
|
|
7
|
+
//
|
|
8
|
+
// Used by: `openbroker auto prune` and as a sub-step of `openbroker auto clean`.
|
|
9
|
+
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
14
|
+
import { ensureConfigDir } from '../core/config.js';
|
|
15
|
+
|
|
16
|
+
export const AUDIT_DB_PATH = process.env.OPENBROKER_AUDIT_DB_PATH
|
|
17
|
+
|| path.join(ensureConfigDir(), 'automation-audit.sqlite');
|
|
18
|
+
|
|
19
|
+
export interface PruneFilters {
|
|
20
|
+
/** Delete runs whose started_at < (now - olderThanMs). Falsy = no age filter. */
|
|
21
|
+
olderThanMs?: number;
|
|
22
|
+
/** Delete runs whose status is in this set. Default: stopped, error, stale. */
|
|
23
|
+
statuses?: Set<string>;
|
|
24
|
+
/** For each automation_id, keep the N most recent runs regardless of other filters. */
|
|
25
|
+
keepLastPerAutomation?: number;
|
|
26
|
+
/** Delete every run that is not currently alive (overrides status/age). */
|
|
27
|
+
all?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PruneOptions extends PruneFilters {
|
|
31
|
+
dbPath?: string;
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
vacuum?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* When true, skip the deletion phase and only update status of orphaned
|
|
36
|
+
* 'running' rows whose pid is dead — used by `auto clean` to reconcile state
|
|
37
|
+
* without losing history.
|
|
38
|
+
*/
|
|
39
|
+
reconcileOnly?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PruneResult {
|
|
43
|
+
reconciled: number;
|
|
44
|
+
candidateRunIds: string[];
|
|
45
|
+
deletedRows: Record<string, number>;
|
|
46
|
+
freedBytes: number;
|
|
47
|
+
dryRun: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const CHILD_TABLES = [
|
|
51
|
+
'automation_logs',
|
|
52
|
+
'automation_events',
|
|
53
|
+
'automation_actions',
|
|
54
|
+
'automation_snapshots',
|
|
55
|
+
'automation_order_updates',
|
|
56
|
+
'automation_fills',
|
|
57
|
+
'automation_user_events',
|
|
58
|
+
'automation_state_changes',
|
|
59
|
+
'automation_publishes',
|
|
60
|
+
'automation_errors',
|
|
61
|
+
'automation_notes',
|
|
62
|
+
'automation_metrics',
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
const DEFAULT_STATUSES = new Set(['stopped', 'error', 'stale']);
|
|
66
|
+
|
|
67
|
+
function isProcessAlive(pid: number | null | undefined): boolean {
|
|
68
|
+
if (typeof pid !== 'number' || !Number.isFinite(pid) || pid <= 0) return false;
|
|
69
|
+
try {
|
|
70
|
+
process.kill(pid, 0);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Parse human-friendly durations like `7d`, `24h`, `30m`, `45s`. */
|
|
78
|
+
export function parseDuration(input: string): number {
|
|
79
|
+
const m = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)?\s*$/.exec(input);
|
|
80
|
+
if (!m) throw new Error(`invalid duration: ${input}`);
|
|
81
|
+
const n = Number(m[1]);
|
|
82
|
+
const unit = m[2] ?? 'ms';
|
|
83
|
+
const mult: Record<string, number> = {
|
|
84
|
+
ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 7 * 86_400_000,
|
|
85
|
+
};
|
|
86
|
+
return n * mult[unit];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Reconcile orphan-running rows in the DB (process dead → mark stopped). */
|
|
90
|
+
export function reconcileStaleRuns(db: DatabaseSync, opts: { dryRun?: boolean; now?: number } = {}): number {
|
|
91
|
+
const now = opts.now ?? Date.now();
|
|
92
|
+
const runningRows = db.prepare(`
|
|
93
|
+
SELECT run_id, pid FROM automation_runs WHERE status = 'running'
|
|
94
|
+
`).all() as { run_id: string; pid: number | null }[];
|
|
95
|
+
|
|
96
|
+
const orphans = runningRows.filter((r) => !isProcessAlive(r.pid));
|
|
97
|
+
if (orphans.length === 0) return 0;
|
|
98
|
+
if (opts.dryRun) return orphans.length;
|
|
99
|
+
|
|
100
|
+
const update = db.prepare(`
|
|
101
|
+
UPDATE automation_runs
|
|
102
|
+
SET status = 'stopped',
|
|
103
|
+
stop_reason = COALESCE(stop_reason, 'reconciled (process exited)'),
|
|
104
|
+
stopped_at = COALESCE(stopped_at, ?)
|
|
105
|
+
WHERE run_id = ?
|
|
106
|
+
`);
|
|
107
|
+
let n = 0;
|
|
108
|
+
for (const o of orphans) {
|
|
109
|
+
update.run(now, o.run_id);
|
|
110
|
+
n++;
|
|
111
|
+
}
|
|
112
|
+
return n;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function prune(opts: PruneOptions = {}): PruneResult {
|
|
116
|
+
const dbPath = opts.dbPath ?? AUDIT_DB_PATH;
|
|
117
|
+
if (!existsSync(dbPath)) {
|
|
118
|
+
return {
|
|
119
|
+
reconciled: 0,
|
|
120
|
+
candidateRunIds: [],
|
|
121
|
+
deletedRows: {},
|
|
122
|
+
freedBytes: 0,
|
|
123
|
+
dryRun: !!opts.dryRun,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const db = new DatabaseSync(dbPath);
|
|
128
|
+
db.exec('PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;');
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const reconciled = reconcileStaleRuns(db, { dryRun: opts.dryRun });
|
|
132
|
+
if (opts.reconcileOnly) {
|
|
133
|
+
return {
|
|
134
|
+
reconciled,
|
|
135
|
+
candidateRunIds: [],
|
|
136
|
+
deletedRows: {},
|
|
137
|
+
freedBytes: 0,
|
|
138
|
+
dryRun: !!opts.dryRun,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const allRuns = db.prepare(`
|
|
143
|
+
SELECT run_id, automation_id, status, pid, started_at
|
|
144
|
+
FROM automation_runs
|
|
145
|
+
ORDER BY started_at DESC
|
|
146
|
+
`).all() as {
|
|
147
|
+
run_id: string;
|
|
148
|
+
automation_id: string;
|
|
149
|
+
status: string;
|
|
150
|
+
pid: number | null;
|
|
151
|
+
started_at: number;
|
|
152
|
+
}[];
|
|
153
|
+
|
|
154
|
+
const statuses = opts.statuses ?? DEFAULT_STATUSES;
|
|
155
|
+
const cutoff = opts.olderThanMs && opts.olderThanMs > 0 ? Date.now() - opts.olderThanMs : null;
|
|
156
|
+
|
|
157
|
+
// group runs per automation_id (already sorted DESC by started_at)
|
|
158
|
+
const byAuto = new Map<string, typeof allRuns>();
|
|
159
|
+
for (const r of allRuns) {
|
|
160
|
+
const arr = byAuto.get(r.automation_id) ?? [];
|
|
161
|
+
arr.push(r);
|
|
162
|
+
byAuto.set(r.automation_id, arr);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const candidates: typeof allRuns = [];
|
|
166
|
+
|
|
167
|
+
for (const [, runs] of byAuto) {
|
|
168
|
+
const protectedIdx = new Set<number>();
|
|
169
|
+
if (opts.keepLastPerAutomation && opts.keepLastPerAutomation > 0) {
|
|
170
|
+
for (let i = 0; i < Math.min(opts.keepLastPerAutomation, runs.length); i++) {
|
|
171
|
+
protectedIdx.add(i);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
runs.forEach((r, i) => {
|
|
175
|
+
if (protectedIdx.has(i)) return;
|
|
176
|
+
// never delete a truly-running automation
|
|
177
|
+
if (r.status === 'running' && isProcessAlive(r.pid)) return;
|
|
178
|
+
if (opts.all) {
|
|
179
|
+
candidates.push(r);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!statuses.has(r.status)) {
|
|
183
|
+
// 'running' rows that aren't actually alive were just reconciled to
|
|
184
|
+
// 'stopped' above, so the status check catches them.
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (cutoff !== null && r.started_at >= cutoff) return;
|
|
188
|
+
candidates.push(r);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const candidateRunIds = candidates.map((r) => r.run_id);
|
|
193
|
+
const deletedRows: Record<string, number> = {};
|
|
194
|
+
let freedBytes = 0;
|
|
195
|
+
|
|
196
|
+
if (!opts.dryRun && candidateRunIds.length > 0) {
|
|
197
|
+
const sizeBefore = db.prepare('PRAGMA page_count').get() as { page_count: number };
|
|
198
|
+
const pageSize = db.prepare('PRAGMA page_size').get() as { page_size: number };
|
|
199
|
+
|
|
200
|
+
db.exec('BEGIN');
|
|
201
|
+
try {
|
|
202
|
+
for (const table of CHILD_TABLES) {
|
|
203
|
+
let n = 0;
|
|
204
|
+
const stmt = db.prepare(`DELETE FROM ${table} WHERE run_id = ?`);
|
|
205
|
+
for (const id of candidateRunIds) {
|
|
206
|
+
const info = stmt.run(id) as { changes?: number };
|
|
207
|
+
n += Number(info.changes ?? 0);
|
|
208
|
+
}
|
|
209
|
+
deletedRows[table] = n;
|
|
210
|
+
}
|
|
211
|
+
const runStmt = db.prepare('DELETE FROM automation_runs WHERE run_id = ?');
|
|
212
|
+
let runChanges = 0;
|
|
213
|
+
for (const id of candidateRunIds) {
|
|
214
|
+
const info = runStmt.run(id) as { changes: number };
|
|
215
|
+
runChanges += Number(info.changes ?? 0);
|
|
216
|
+
}
|
|
217
|
+
deletedRows.automation_runs = runChanges;
|
|
218
|
+
db.exec('COMMIT');
|
|
219
|
+
} catch (err) {
|
|
220
|
+
db.exec('ROLLBACK');
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (opts.vacuum) {
|
|
225
|
+
// VACUUM cannot run inside a transaction
|
|
226
|
+
db.exec('VACUUM');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const sizeAfter = db.prepare('PRAGMA page_count').get() as { page_count: number };
|
|
230
|
+
freedBytes = (Number(sizeBefore.page_count) - Number(sizeAfter.page_count)) * Number(pageSize.page_size);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
reconciled,
|
|
235
|
+
candidateRunIds,
|
|
236
|
+
deletedRows,
|
|
237
|
+
freedBytes: Math.max(0, freedBytes),
|
|
238
|
+
dryRun: !!opts.dryRun,
|
|
239
|
+
};
|
|
240
|
+
} finally {
|
|
241
|
+
db.close();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function fmtBytes(n: number): string {
|
|
246
|
+
if (!Number.isFinite(n) || n <= 0) return '0 B';
|
|
247
|
+
const u = ['B', 'KB', 'MB', 'GB'];
|
|
248
|
+
let i = 0;
|
|
249
|
+
let v = n;
|
|
250
|
+
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
|
|
251
|
+
return `${v.toFixed(v >= 100 || i === 0 ? 0 : 1)} ${u[i]}`;
|
|
252
|
+
}
|
package/scripts/auto/runtime.ts
CHANGED
|
@@ -288,6 +288,12 @@ function createAuditedClient(
|
|
|
288
288
|
async function buildSnapshot(
|
|
289
289
|
client: HyperliquidClient,
|
|
290
290
|
): Promise<AutomationSnapshot> {
|
|
291
|
+
// `metaAndAssetCtxs` contains both mostly-static market metadata and live
|
|
292
|
+
// funding/premium values. The client caches it for market lookups, so clear
|
|
293
|
+
// it before each automation poll snapshot or funding_update events freeze at
|
|
294
|
+
// the first fetched value.
|
|
295
|
+
client.invalidateMetaCache();
|
|
296
|
+
|
|
291
297
|
const [state, mids, metaCtxs] = await Promise.all([
|
|
292
298
|
client.getUserStateAll(),
|
|
293
299
|
client.getAllMids(),
|
|
@@ -321,20 +327,29 @@ async function buildSnapshot(
|
|
|
321
327
|
|
|
322
328
|
// Build funding rates from asset contexts
|
|
323
329
|
const fundingRates = new Map<string, { rate: number; premium: number }>();
|
|
330
|
+
const addFundingRates = (
|
|
331
|
+
universe: Array<{ name?: string }> | undefined,
|
|
332
|
+
assetCtxs: Array<{ funding?: string | number | null; premium?: string | number | null }> | undefined,
|
|
333
|
+
) => {
|
|
334
|
+
if (!universe || !assetCtxs) return;
|
|
335
|
+
for (let i = 0; i < universe.length; i++) {
|
|
336
|
+
const meta = universe[i];
|
|
337
|
+
const ctx = assetCtxs[i];
|
|
338
|
+
if (ctx && meta?.name) {
|
|
339
|
+
fundingRates.set(meta.name, {
|
|
340
|
+
rate: parseFloat(String(ctx.funding || '0')),
|
|
341
|
+
premium: parseFloat(String(ctx.premium || '0')),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
324
347
|
if (metaCtxs && Array.isArray(metaCtxs)) {
|
|
325
348
|
for (const group of metaCtxs) {
|
|
326
|
-
|
|
327
|
-
for (let i = 0; i < group.universe.length; i++) {
|
|
328
|
-
const meta = group.universe[i];
|
|
329
|
-
const ctx = group.assetCtxs[i];
|
|
330
|
-
if (ctx && meta) {
|
|
331
|
-
fundingRates.set(meta.name, {
|
|
332
|
-
rate: parseFloat(ctx.funding || '0'),
|
|
333
|
-
premium: parseFloat(ctx.premium || '0'),
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
}
|
|
349
|
+
addFundingRates(group.universe, group.assetCtxs);
|
|
337
350
|
}
|
|
351
|
+
} else if (metaCtxs) {
|
|
352
|
+
addFundingRates(metaCtxs.meta?.universe, metaCtxs.assetCtxs);
|
|
338
353
|
}
|
|
339
354
|
|
|
340
355
|
return {
|
package/scripts/core/client.ts
CHANGED
|
@@ -1067,7 +1067,11 @@ export class HyperliquidClient {
|
|
|
1067
1067
|
|
|
1068
1068
|
const response = await fetch(baseUrl + '/info', {
|
|
1069
1069
|
method: 'POST',
|
|
1070
|
-
headers: {
|
|
1070
|
+
headers: {
|
|
1071
|
+
'Content-Type': 'application/json',
|
|
1072
|
+
'Cache-Control': 'no-cache, no-store, max-age=0',
|
|
1073
|
+
Pragma: 'no-cache',
|
|
1074
|
+
},
|
|
1071
1075
|
body: JSON.stringify({ type: 'predictedFundings' }),
|
|
1072
1076
|
});
|
|
1073
1077
|
const data = await response.json() as Array<[
|