openbroker 1.3.1 → 1.4.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.
Files changed (171) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/SKILL.md +7 -4
  3. package/dist/auto/audit.d.ts +57 -0
  4. package/dist/auto/audit.d.ts.map +1 -0
  5. package/dist/auto/audit.js +407 -0
  6. package/dist/auto/cli.d.ts +2 -0
  7. package/dist/auto/cli.d.ts.map +1 -0
  8. package/dist/auto/cli.js +423 -0
  9. package/dist/auto/events.d.ts +11 -0
  10. package/dist/auto/events.d.ts.map +1 -0
  11. package/dist/auto/events.js +36 -0
  12. package/dist/auto/examples/dca.d.ts +4 -0
  13. package/dist/auto/examples/dca.d.ts.map +1 -0
  14. package/dist/auto/examples/dca.js +60 -0
  15. package/dist/auto/examples/funding-arb.d.ts +4 -0
  16. package/dist/auto/examples/funding-arb.d.ts.map +1 -0
  17. package/dist/auto/examples/funding-arb.js +81 -0
  18. package/dist/auto/examples/grid.d.ts +4 -0
  19. package/dist/auto/examples/grid.d.ts.map +1 -0
  20. package/dist/auto/examples/grid.js +114 -0
  21. package/dist/auto/examples/mm-maker.d.ts +4 -0
  22. package/dist/auto/examples/mm-maker.d.ts.map +1 -0
  23. package/dist/auto/examples/mm-maker.js +131 -0
  24. package/dist/auto/examples/mm-spread.d.ts +4 -0
  25. package/dist/auto/examples/mm-spread.d.ts.map +1 -0
  26. package/dist/auto/examples/mm-spread.js +119 -0
  27. package/dist/auto/examples/price-alert.d.ts +4 -0
  28. package/dist/auto/examples/price-alert.d.ts.map +1 -0
  29. package/dist/auto/examples/price-alert.js +85 -0
  30. package/dist/auto/keep-awake.d.ts +11 -0
  31. package/dist/auto/keep-awake.d.ts.map +1 -0
  32. package/dist/auto/keep-awake.js +70 -0
  33. package/dist/auto/loader.d.ts +22 -0
  34. package/dist/auto/loader.d.ts.map +1 -0
  35. package/dist/auto/loader.js +127 -0
  36. package/dist/auto/prune.d.ts +40 -0
  37. package/dist/auto/prune.d.ts.map +1 -0
  38. package/dist/auto/prune.js +204 -0
  39. package/dist/auto/registry.d.ts +24 -0
  40. package/dist/auto/registry.d.ts.map +1 -0
  41. package/dist/auto/registry.js +93 -0
  42. package/dist/auto/report.d.ts +3 -0
  43. package/dist/auto/report.d.ts.map +1 -0
  44. package/dist/auto/report.js +385 -0
  45. package/dist/auto/runtime.d.ts +33 -0
  46. package/dist/auto/runtime.d.ts.map +1 -0
  47. package/dist/auto/runtime.js +844 -0
  48. package/dist/auto/types.d.ts +236 -0
  49. package/dist/auto/types.d.ts.map +1 -0
  50. package/dist/auto/types.js +3 -0
  51. package/dist/core/client.d.ts +684 -0
  52. package/dist/core/client.d.ts.map +1 -0
  53. package/dist/core/client.js +2040 -0
  54. package/dist/core/config.d.ts +22 -0
  55. package/dist/core/config.d.ts.map +1 -0
  56. package/dist/core/config.js +143 -0
  57. package/dist/core/types.d.ts +221 -0
  58. package/dist/core/types.d.ts.map +1 -0
  59. package/dist/core/types.js +2 -0
  60. package/dist/core/utils.d.ts +61 -0
  61. package/dist/core/utils.d.ts.map +1 -0
  62. package/dist/core/utils.js +142 -0
  63. package/dist/core/ws.d.ts +121 -0
  64. package/dist/core/ws.d.ts.map +1 -0
  65. package/dist/core/ws.js +222 -0
  66. package/dist/info/account.d.ts +3 -0
  67. package/dist/info/account.d.ts.map +1 -0
  68. package/dist/info/account.js +198 -0
  69. package/dist/info/all-markets.d.ts +3 -0
  70. package/dist/info/all-markets.d.ts.map +1 -0
  71. package/dist/info/all-markets.js +272 -0
  72. package/dist/info/candles.d.ts +3 -0
  73. package/dist/info/candles.d.ts.map +1 -0
  74. package/dist/info/candles.js +120 -0
  75. package/dist/info/fees.d.ts +3 -0
  76. package/dist/info/fees.d.ts.map +1 -0
  77. package/dist/info/fees.js +87 -0
  78. package/dist/info/fills.d.ts +3 -0
  79. package/dist/info/fills.d.ts.map +1 -0
  80. package/dist/info/fills.js +105 -0
  81. package/dist/info/funding-history.d.ts +3 -0
  82. package/dist/info/funding-history.d.ts.map +1 -0
  83. package/dist/info/funding-history.js +98 -0
  84. package/dist/info/funding-scan.d.ts +3 -0
  85. package/dist/info/funding-scan.d.ts.map +1 -0
  86. package/dist/info/funding-scan.js +178 -0
  87. package/dist/info/funding.d.ts +3 -0
  88. package/dist/info/funding.d.ts.map +1 -0
  89. package/dist/info/funding.js +158 -0
  90. package/dist/info/markets.d.ts +3 -0
  91. package/dist/info/markets.d.ts.map +1 -0
  92. package/dist/info/markets.js +178 -0
  93. package/dist/info/order-status.d.ts +3 -0
  94. package/dist/info/order-status.d.ts.map +1 -0
  95. package/dist/info/order-status.js +85 -0
  96. package/dist/info/orders.d.ts +3 -0
  97. package/dist/info/orders.d.ts.map +1 -0
  98. package/dist/info/orders.js +162 -0
  99. package/dist/info/outcomes.d.ts +3 -0
  100. package/dist/info/outcomes.d.ts.map +1 -0
  101. package/dist/info/outcomes.js +175 -0
  102. package/dist/info/positions.d.ts +3 -0
  103. package/dist/info/positions.d.ts.map +1 -0
  104. package/dist/info/positions.js +127 -0
  105. package/dist/info/rate-limit.d.ts +3 -0
  106. package/dist/info/rate-limit.d.ts.map +1 -0
  107. package/dist/info/rate-limit.js +58 -0
  108. package/dist/info/search-markets.d.ts +3 -0
  109. package/dist/info/search-markets.d.ts.map +1 -0
  110. package/dist/info/search-markets.js +296 -0
  111. package/dist/info/spot.d.ts +3 -0
  112. package/dist/info/spot.d.ts.map +1 -0
  113. package/dist/info/spot.js +192 -0
  114. package/dist/info/trades.d.ts +3 -0
  115. package/dist/info/trades.d.ts.map +1 -0
  116. package/dist/info/trades.js +97 -0
  117. package/dist/lib.d.ts +14 -0
  118. package/dist/lib.d.ts.map +1 -0
  119. package/dist/lib.js +17 -0
  120. package/dist/operations/bracket.d.ts +28 -0
  121. package/dist/operations/bracket.d.ts.map +1 -0
  122. package/dist/operations/bracket.js +266 -0
  123. package/dist/operations/cancel.d.ts +3 -0
  124. package/dist/operations/cancel.d.ts.map +1 -0
  125. package/dist/operations/cancel.js +107 -0
  126. package/dist/operations/chase.d.ts +25 -0
  127. package/dist/operations/chase.d.ts.map +1 -0
  128. package/dist/operations/chase.js +215 -0
  129. package/dist/operations/limit-order.d.ts +3 -0
  130. package/dist/operations/limit-order.d.ts.map +1 -0
  131. package/dist/operations/limit-order.js +144 -0
  132. package/dist/operations/market-order.d.ts +3 -0
  133. package/dist/operations/market-order.d.ts.map +1 -0
  134. package/dist/operations/market-order.js +153 -0
  135. package/dist/operations/outcome-order.d.ts +3 -0
  136. package/dist/operations/outcome-order.d.ts.map +1 -0
  137. package/dist/operations/outcome-order.js +171 -0
  138. package/dist/operations/scale.d.ts +3 -0
  139. package/dist/operations/scale.d.ts.map +1 -0
  140. package/dist/operations/scale.js +212 -0
  141. package/dist/operations/set-tpsl.d.ts +3 -0
  142. package/dist/operations/set-tpsl.d.ts.map +1 -0
  143. package/dist/operations/set-tpsl.js +277 -0
  144. package/dist/operations/spot-order.d.ts +3 -0
  145. package/dist/operations/spot-order.d.ts.map +1 -0
  146. package/dist/operations/spot-order.js +173 -0
  147. package/dist/operations/trigger-order.d.ts +3 -0
  148. package/dist/operations/trigger-order.d.ts.map +1 -0
  149. package/dist/operations/trigger-order.js +177 -0
  150. package/dist/operations/twap-cancel.d.ts +3 -0
  151. package/dist/operations/twap-cancel.d.ts.map +1 -0
  152. package/dist/operations/twap-cancel.js +57 -0
  153. package/dist/operations/twap-status.d.ts +3 -0
  154. package/dist/operations/twap-status.d.ts.map +1 -0
  155. package/dist/operations/twap-status.js +81 -0
  156. package/dist/operations/twap.d.ts +3 -0
  157. package/dist/operations/twap.d.ts.map +1 -0
  158. package/dist/operations/twap.js +124 -0
  159. package/dist/setup/approve-builder.d.ts +3 -0
  160. package/dist/setup/approve-builder.d.ts.map +1 -0
  161. package/dist/setup/approve-builder.js +155 -0
  162. package/dist/setup/env.d.ts +4 -0
  163. package/dist/setup/env.d.ts.map +1 -0
  164. package/dist/setup/env.js +8 -0
  165. package/dist/setup/onboard.d.ts +10 -0
  166. package/dist/setup/onboard.d.ts.map +1 -0
  167. package/dist/setup/onboard.js +462 -0
  168. package/package.json +10 -4
  169. package/scripts/core/client.ts +13 -3
  170. package/scripts/info/all-markets.ts +18 -2
  171. package/scripts/info/search-markets.ts +18 -2
@@ -0,0 +1,423 @@
1
+ // CLI entry point for `openbroker auto` commands
2
+ import { spawnSync } from 'child_process';
3
+ import { fileURLToPath } from 'url';
4
+ import path from 'path';
5
+ import { parseArgs } from '../core/utils.js';
6
+ import { resolveScriptPath, resolveExamplePath, listAutomations, listExamples, loadExampleConfigs, ensureAutomationsDir } from './loader.js';
7
+ import { startAutomation, getRunningAutomations, getRegisteredAutomations } from './runtime.js';
8
+ import { unregisterAutomation, cleanRegistry } from './registry.js';
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ function printUsage() {
12
+ console.log(`
13
+ OpenBroker Automations — event-driven trading scripts
14
+
15
+ Usage:
16
+ openbroker auto run <script> [options] Run an automation script
17
+ openbroker auto run --example <name> Run a bundled example automation
18
+ openbroker auto report <id> Read the local audit report for an automation
19
+ openbroker auto examples List bundled example automations
20
+ openbroker auto stop <id> Unregister an automation (won't restart)
21
+ openbroker auto list List available automations
22
+ openbroker auto status Show running automations
23
+ openbroker auto clean Remove stale entries from registry + reconcile audit DB
24
+ openbroker auto prune [options] Delete stale runs from the audit DB
25
+
26
+ Options (for run):
27
+ --example <name> Run a bundled example (dca, grid, funding-arb, mm-spread, mm-maker)
28
+ --set key=value Set config values (repeatable, auto-parses numbers/booleans)
29
+ --dry Intercept write methods (no real trades)
30
+ --verbose Show debug output
31
+ --id <name> Custom automation ID (default: filename)
32
+ --poll <ms> Poll interval in milliseconds (default: 10000)
33
+ --no-ws Disable WebSocket; fall back to REST-only polling
34
+ --allow-sleep Do not request OS idle-sleep inhibition for this run
35
+
36
+ Options (for prune):
37
+ --older-than <d> Only prune runs started before this duration ago (e.g. 7d, 24h)
38
+ --status <list> CSV of statuses to consider (default: stopped,error,stale)
39
+ --keep-last <N> Keep the N most-recent runs per automation_id
40
+ --all Prune everything except runs that are still alive
41
+ --vacuum VACUUM the DB after deletion to reclaim disk space
42
+ --dry Preview what would be deleted without writing
43
+
44
+ Scripts are loaded from:
45
+ 1. Absolute or relative path
46
+ 2. ~/.openbroker/automations/<name>.ts
47
+ 3. Bundled examples (via --example)
48
+
49
+ Writing an automation:
50
+ export default function(api) {
51
+ api.on('price_change', async ({ coin, changePct }) => {
52
+ api.log.info(\`\${coin} moved \${changePct.toFixed(2)}%\`);
53
+ });
54
+ }
55
+
56
+ Events: tick, price_change, funding_update, position_opened,
57
+ position_closed, position_changed, pnl_threshold, margin_warning
58
+
59
+ Examples:
60
+ openbroker auto run --example dca --set coin=BTC --set amount=50 --dry
61
+ openbroker auto run --example grid --set coin=ETH --set lower=3000 --set upper=4000
62
+ openbroker auto run my-strategy --dry
63
+ openbroker auto report hype-mm-v2-live-r4
64
+ openbroker auto examples
65
+ `);
66
+ }
67
+ /** Parse --set key=value flags from raw args, return parsed config object */
68
+ function parseSetFlags(rawArgs) {
69
+ const config = {};
70
+ for (let i = 0; i < rawArgs.length; i++) {
71
+ if (rawArgs[i] === '--set' && i + 1 < rawArgs.length) {
72
+ const pair = rawArgs[i + 1];
73
+ const eqIdx = pair.indexOf('=');
74
+ if (eqIdx === -1) {
75
+ console.error(`Error: --set requires key=value format, got: ${pair}`);
76
+ process.exit(1);
77
+ }
78
+ const key = pair.slice(0, eqIdx);
79
+ const raw = pair.slice(eqIdx + 1);
80
+ const isHexLike = /^0x[0-9a-fA-F]+$/.test(raw);
81
+ const isDecimalLike = /^-?(?:\d+|\d+\.\d+|\.\d+)$/.test(raw);
82
+ // Auto-parse numbers and booleans
83
+ if (raw === 'true')
84
+ config[key] = true;
85
+ else if (raw === 'false')
86
+ config[key] = false;
87
+ else if (!isHexLike && isDecimalLike)
88
+ config[key] = Number(raw);
89
+ else
90
+ config[key] = raw;
91
+ i++; // skip the value
92
+ }
93
+ }
94
+ return config;
95
+ }
96
+ /** Strip --set key=value pairs from raw args so parseArgs doesn't see them */
97
+ function stripSetFlags(rawArgs) {
98
+ const result = [];
99
+ for (let i = 0; i < rawArgs.length; i++) {
100
+ if (rawArgs[i] === '--set' && i + 1 < rawArgs.length) {
101
+ i++; // skip --set and its value
102
+ }
103
+ else {
104
+ result.push(rawArgs[i]);
105
+ }
106
+ }
107
+ return result;
108
+ }
109
+ async function runCommand(args, positional, initialState) {
110
+ const exampleName = args.example ? String(args.example) : undefined;
111
+ const scriptName = positional[0];
112
+ if (!scriptName && !exampleName) {
113
+ console.error('Error: script name or path required (or use --example <name>)');
114
+ console.log('Usage: openbroker auto run <script> [--dry] [--verbose]');
115
+ console.log(' openbroker auto run --example <name> [--set key=value] [--dry]');
116
+ process.exit(1);
117
+ }
118
+ const scriptPath = exampleName ? resolveExamplePath(exampleName) : resolveScriptPath(scriptName);
119
+ const dryRun = args.dry === true;
120
+ const verbose = args.verbose === true;
121
+ const useWebSocket = args['no-ws'] !== true;
122
+ const keepAwake = args['allow-sleep'] === true ? false : undefined;
123
+ const pollIntervalMs = args.poll ? parseInt(String(args.poll), 10) : undefined;
124
+ const id = args.id ? String(args.id) : undefined;
125
+ if (args.testnet === true) {
126
+ process.env.HYPERLIQUID_NETWORK = 'testnet';
127
+ }
128
+ else if (args.mainnet === true) {
129
+ process.env.HYPERLIQUID_NETWORK = 'mainnet';
130
+ }
131
+ if (pollIntervalMs !== undefined && (isNaN(pollIntervalMs) || pollIntervalMs < 1000)) {
132
+ console.error('Error: --poll must be at least 1000ms');
133
+ process.exit(1);
134
+ }
135
+ const envHooksToken = process.env.OPENCLAW_HOOKS_TOKEN;
136
+ const envGatewayPortStr = process.env.OPENCLAW_GATEWAY_PORT;
137
+ const envGatewayPort = envGatewayPortStr ? parseInt(envGatewayPortStr, 10) : undefined;
138
+ const automation = await startAutomation({
139
+ scriptPath,
140
+ id,
141
+ dryRun,
142
+ verbose,
143
+ pollIntervalMs,
144
+ useWebSocket,
145
+ keepAwake,
146
+ initialState: Object.keys(initialState).length > 0 ? initialState : undefined,
147
+ hooksToken: envHooksToken,
148
+ gatewayPort: envGatewayPort && !isNaN(envGatewayPort) ? envGatewayPort : undefined,
149
+ });
150
+ // Graceful shutdown on SIGINT/SIGTERM
151
+ const shutdown = async () => {
152
+ console.log('\nShutting down...');
153
+ await automation.stop();
154
+ process.exit(0);
155
+ };
156
+ process.on('SIGINT', shutdown);
157
+ process.on('SIGTERM', shutdown);
158
+ // Keep process alive
159
+ await new Promise(() => { });
160
+ }
161
+ async function examplesCommand() {
162
+ const configs = await loadExampleConfigs();
163
+ const names = Object.keys(configs);
164
+ if (names.length === 0) {
165
+ console.log('No bundled examples found.');
166
+ return;
167
+ }
168
+ console.log('Bundled example automations:\n');
169
+ for (const name of names) {
170
+ const cfg = configs[name];
171
+ console.log(` ${name}`);
172
+ console.log(` ${cfg.description}\n`);
173
+ console.log(` Config (--set key=value):`);
174
+ for (const [key, field] of Object.entries(cfg.fields)) {
175
+ const def = JSON.stringify(field.default);
176
+ console.log(` ${key.padEnd(14)} ${field.type.padEnd(8)} ${field.description} (default: ${def})`);
177
+ }
178
+ console.log('');
179
+ }
180
+ console.log(`Run with: openbroker auto run --example <name> [--set key=value] [--dry]`);
181
+ console.log('Copy to ~/.openbroker/automations/ to customize.');
182
+ }
183
+ function listCommand() {
184
+ ensureAutomationsDir();
185
+ const automations = listAutomations();
186
+ const examples = listExamples();
187
+ if (automations.length === 0 && examples.length === 0) {
188
+ console.log('No automations found in ~/.openbroker/automations/');
189
+ console.log('\nCreate a .ts file there with:');
190
+ console.log(' export default function(api) { ... }');
191
+ console.log('\nOr run a bundled example: openbroker auto examples');
192
+ return;
193
+ }
194
+ if (automations.length > 0) {
195
+ console.log('User automations (~/.openbroker/automations/):\n');
196
+ for (const a of automations) {
197
+ console.log(` ${a.name.padEnd(30)} ${a.path}`);
198
+ }
199
+ console.log(`\nRun with: openbroker auto run <name>`);
200
+ }
201
+ if (examples.length > 0) {
202
+ if (automations.length > 0)
203
+ console.log('');
204
+ console.log(`${examples.length} bundled examples available — run: openbroker auto examples`);
205
+ }
206
+ }
207
+ function statusCommand() {
208
+ // Show in-process automations (if any running in this process)
209
+ const inProcess = getRunningAutomations();
210
+ // Show all registered automations from file-based registry (cross-process)
211
+ const registered = getRegisteredAutomations();
212
+ if (inProcess.length === 0 && registered.length === 0) {
213
+ console.log('No automations running');
214
+ return;
215
+ }
216
+ // Show in-process automations with live stats
217
+ if (inProcess.length > 0) {
218
+ console.log('Running in this process:\n');
219
+ for (const a of inProcess) {
220
+ const uptime = Math.round((Date.now() - a.startedAt.getTime()) / 1000);
221
+ console.log(` ${a.id}`);
222
+ console.log(` Script: ${a.scriptPath}`);
223
+ console.log(` Uptime: ${uptime}s`);
224
+ console.log(` Polls: ${a.pollCount}`);
225
+ console.log(` Events: ${a.eventsEmitted}`);
226
+ console.log(` Dry run: ${a.dryRun}`);
227
+ console.log('');
228
+ }
229
+ }
230
+ // Show all registered automations (may include ones from other processes)
231
+ const external = registered.filter(r => !inProcess.some(ip => ip.id === r.id));
232
+ if (external.length > 0) {
233
+ if (inProcess.length > 0)
234
+ console.log('Other processes:\n');
235
+ else
236
+ console.log('Registered automations:\n');
237
+ for (const a of external) {
238
+ const uptime = a.status === 'running'
239
+ ? `${Math.round((Date.now() - new Date(a.startedAt).getTime()) / 1000)}s`
240
+ : '-';
241
+ console.log(` ${a.id}`);
242
+ console.log(` Script: ${a.scriptPath}`);
243
+ console.log(` Status: ${a.status}${a.error ? ` (${a.error})` : ''}`);
244
+ console.log(` PID: ${a.pid}`);
245
+ console.log(` Uptime: ${uptime}`);
246
+ console.log(` Dry run: ${a.dryRun}`);
247
+ console.log('');
248
+ }
249
+ }
250
+ }
251
+ function stopCommand(positional) {
252
+ const id = positional[0];
253
+ if (!id) {
254
+ console.error('Error: automation ID required');
255
+ console.log('Usage: openbroker auto stop <id>');
256
+ process.exit(1);
257
+ }
258
+ // Check if running in this process
259
+ const inProcess = getRunningAutomations();
260
+ const running = inProcess.find(a => a.id === id);
261
+ if (running) {
262
+ running.stop().then(() => {
263
+ console.log(`Stopped and unregistered: ${id}`);
264
+ });
265
+ return;
266
+ }
267
+ // Otherwise just remove from file registry (prevents restart)
268
+ unregisterAutomation(id);
269
+ console.log(`Unregistered: ${id} (will not restart on next gateway start)`);
270
+ }
271
+ async function cleanCommand() {
272
+ cleanRegistry();
273
+ console.log('Cleaned stale entries from registry');
274
+ // Also reconcile the audit DB so dead processes whose rows still say
275
+ // 'running' get marked 'stopped'. Without this, the dashboard keeps showing
276
+ // 'stale' badges for automations the operator already cleaned out of the
277
+ // registry.
278
+ try {
279
+ const { prune } = await import('./prune.js');
280
+ const result = prune({ reconcileOnly: true });
281
+ if (result.reconciled > 0) {
282
+ console.log(`Reconciled ${result.reconciled} orphan run row${result.reconciled === 1 ? '' : 's'} in audit DB (status: running → stopped)`);
283
+ }
284
+ else {
285
+ console.log('Audit DB already consistent');
286
+ }
287
+ }
288
+ catch (err) {
289
+ console.warn(`Could not reconcile audit DB: ${err instanceof Error ? err.message : String(err)}`);
290
+ }
291
+ }
292
+ async function pruneCommand(args) {
293
+ const { fmtBytes, parseDuration, prune } = await import('./prune.js');
294
+ const olderThanRaw = args['older-than'];
295
+ const olderThanMs = typeof olderThanRaw === 'string' ? parseDuration(olderThanRaw) : undefined;
296
+ const statusesRaw = typeof args.status === 'string' ? args.status : undefined;
297
+ const statuses = statusesRaw
298
+ ? new Set(statusesRaw.split(',').map((s) => s.trim()).filter(Boolean))
299
+ : undefined;
300
+ const keepLastRaw = args['keep-last'];
301
+ const keepLast = typeof keepLastRaw === 'string' ? Number(keepLastRaw)
302
+ : typeof keepLastRaw === 'number' ? keepLastRaw
303
+ : undefined;
304
+ if (keepLast !== undefined && (!Number.isFinite(keepLast) || keepLast < 0)) {
305
+ console.error('Error: --keep-last must be a non-negative integer');
306
+ process.exit(1);
307
+ }
308
+ const opts = {
309
+ olderThanMs,
310
+ statuses,
311
+ keepLastPerAutomation: keepLast,
312
+ all: args.all === true,
313
+ vacuum: args.vacuum === true,
314
+ dryRun: args.dry === true,
315
+ };
316
+ const result = prune(opts);
317
+ if (result.reconciled > 0) {
318
+ const verb = result.dryRun ? 'Would reconcile' : 'Reconciled';
319
+ console.log(`${verb} ${result.reconciled} orphan run row${result.reconciled === 1 ? '' : 's'} (running → stopped)`);
320
+ }
321
+ const verb = result.dryRun ? 'Would delete' : 'Deleted';
322
+ if (result.candidateRunIds.length === 0) {
323
+ console.log('No runs matched pruning filters.');
324
+ return;
325
+ }
326
+ console.log(`${verb} ${result.candidateRunIds.length} automation run${result.candidateRunIds.length === 1 ? '' : 's'}:`);
327
+ for (const id of result.candidateRunIds.slice(0, 25)) {
328
+ console.log(` · ${id}`);
329
+ }
330
+ if (result.candidateRunIds.length > 25) {
331
+ console.log(` … and ${result.candidateRunIds.length - 25} more`);
332
+ }
333
+ if (!result.dryRun) {
334
+ const totalChild = Object.values(result.deletedRows).reduce((a, b) => a + b, 0);
335
+ console.log(`Removed ${totalChild.toLocaleString()} child rows across ${Object.keys(result.deletedRows).length} tables`);
336
+ if (result.freedBytes > 0) {
337
+ console.log(`Reclaimed ~${fmtBytes(result.freedBytes)}${opts.vacuum ? ' (post-VACUUM)' : ''}`);
338
+ }
339
+ else if (opts.vacuum) {
340
+ console.log('No disk reclaimed (VACUUM completed)');
341
+ }
342
+ else {
343
+ console.log('Run again with --vacuum to reclaim disk space.');
344
+ }
345
+ }
346
+ }
347
+ function reportCommand(rawArgs) {
348
+ const scriptPath = path.join(__dirname, 'report.ts');
349
+ const result = spawnSync(process.execPath, ['--experimental-sqlite', '--no-warnings', '--import', 'tsx', scriptPath, ...rawArgs], {
350
+ stdio: 'inherit',
351
+ cwd: path.resolve(__dirname, '../..'),
352
+ env: { ...process.env },
353
+ });
354
+ if (result.error) {
355
+ throw result.error;
356
+ }
357
+ if (typeof result.status === 'number' && result.status !== 0) {
358
+ process.exit(result.status);
359
+ }
360
+ }
361
+ async function main() {
362
+ const rawArgs = process.argv.slice(2);
363
+ if (rawArgs.length === 0 || rawArgs[0] === '--help' || rawArgs[0] === '-h') {
364
+ printUsage();
365
+ process.exit(0);
366
+ }
367
+ const subcommand = rawArgs[0];
368
+ const restArgs = rawArgs.slice(1);
369
+ // Parse --set flags before stripping them
370
+ const initialState = parseSetFlags(restArgs);
371
+ const cleanedArgs = stripSetFlags(restArgs);
372
+ // Extract positional args (non-flag args)
373
+ const positional = [];
374
+ const flagArgs = [];
375
+ for (let i = 0; i < cleanedArgs.length; i++) {
376
+ if (cleanedArgs[i].startsWith('--')) {
377
+ flagArgs.push(cleanedArgs[i]);
378
+ // If next arg doesn't start with --, it's a flag value
379
+ if (i + 1 < cleanedArgs.length && !cleanedArgs[i + 1].startsWith('--')) {
380
+ flagArgs.push(cleanedArgs[i + 1]);
381
+ i++;
382
+ }
383
+ }
384
+ else {
385
+ positional.push(cleanedArgs[i]);
386
+ }
387
+ }
388
+ const args = parseArgs(flagArgs);
389
+ switch (subcommand) {
390
+ case 'run':
391
+ await runCommand(args, positional, initialState);
392
+ break;
393
+ case 'stop':
394
+ stopCommand(positional);
395
+ break;
396
+ case 'list':
397
+ listCommand();
398
+ break;
399
+ case 'examples':
400
+ await examplesCommand();
401
+ break;
402
+ case 'status':
403
+ statusCommand();
404
+ break;
405
+ case 'clean':
406
+ await cleanCommand();
407
+ break;
408
+ case 'prune':
409
+ await pruneCommand(args);
410
+ break;
411
+ case 'report':
412
+ reportCommand(restArgs);
413
+ break;
414
+ default:
415
+ console.error(`Unknown subcommand: ${subcommand}`);
416
+ console.log('Run "openbroker auto --help" for usage');
417
+ process.exit(1);
418
+ }
419
+ }
420
+ main().catch(err => {
421
+ console.error(err.message || err);
422
+ process.exit(1);
423
+ });
@@ -0,0 +1,11 @@
1
+ import type { AutomationEventType, AutomationEventPayloads, AutomationEventHandler } from './types.js';
2
+ export declare class AutomationEventBus {
3
+ private handlers;
4
+ on<E extends AutomationEventType>(event: E, handler: AutomationEventHandler<E>): void;
5
+ /** Emit an event — handlers run sequentially, errors are returned (not thrown) */
6
+ emit<E extends AutomationEventType>(event: E, payload: AutomationEventPayloads[E]): Promise<Error[]>;
7
+ /** Check if any handlers are registered for an event */
8
+ has(event: AutomationEventType): boolean;
9
+ removeAll(): void;
10
+ }
11
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../scripts/auto/events.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAEpB,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAiD;IAEjE,EAAE,CAAC,CAAC,SAAS,mBAAmB,EAC9B,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GACjC,IAAI;IASP,kFAAkF;IAC5E,IAAI,CAAC,CAAC,SAAS,mBAAmB,EACtC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,KAAK,EAAE,CAAC;IAenB,wDAAwD;IACxD,GAAG,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO;IAKxC,SAAS,IAAI,IAAI;CAGlB"}
@@ -0,0 +1,36 @@
1
+ // Lightweight typed event bus for the automation runtime
2
+ export class AutomationEventBus {
3
+ handlers = new Map();
4
+ on(event, handler) {
5
+ let set = this.handlers.get(event);
6
+ if (!set) {
7
+ set = new Set();
8
+ this.handlers.set(event, set);
9
+ }
10
+ set.add(handler);
11
+ }
12
+ /** Emit an event — handlers run sequentially, errors are returned (not thrown) */
13
+ async emit(event, payload) {
14
+ const set = this.handlers.get(event);
15
+ if (!set || set.size === 0)
16
+ return [];
17
+ const errors = [];
18
+ for (const handler of set) {
19
+ try {
20
+ await handler(payload);
21
+ }
22
+ catch (err) {
23
+ errors.push(err instanceof Error ? err : new Error(String(err)));
24
+ }
25
+ }
26
+ return errors;
27
+ }
28
+ /** Check if any handlers are registered for an event */
29
+ has(event) {
30
+ const set = this.handlers.get(event);
31
+ return set !== undefined && set.size > 0;
32
+ }
33
+ removeAll() {
34
+ this.handlers.clear();
35
+ }
36
+ }
@@ -0,0 +1,4 @@
1
+ import type { AutomationAPI, AutomationConfig } from '../types.js';
2
+ export declare const config: AutomationConfig;
3
+ export default function dca(api: AutomationAPI): void;
4
+ //# sourceMappingURL=dca.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dca.d.ts","sourceRoot":"","sources":["../../../scripts/auto/examples/dca.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEnE,eAAO,MAAM,MAAM,EAAE,gBAQpB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,GAAG,EAAE,aAAa,QAyD7C"}
@@ -0,0 +1,60 @@
1
+ // DCA (Dollar Cost Averaging) — Buy fixed USD amounts at regular intervals
2
+ export const config = {
3
+ description: 'Dollar cost averaging — buy fixed USD amounts at regular intervals',
4
+ fields: {
5
+ coin: { type: 'string', description: 'Asset to accumulate', default: 'HYPE' },
6
+ amount: { type: 'number', description: 'USD per purchase', default: 100 },
7
+ interval: { type: 'number', description: 'Milliseconds between buys (3600000 = 1h)', default: 3_600_000 },
8
+ count: { type: 'number', description: 'Total number of purchases', default: 24 },
9
+ },
10
+ };
11
+ export default function dca(api) {
12
+ const COIN = api.state.get('coin', 'HYPE');
13
+ const AMOUNT_USD = api.state.get('amount', 100);
14
+ const INTERVAL_MS = api.state.get('interval', 3_600_000);
15
+ const MAX_PURCHASES = api.state.get('count', 24);
16
+ let purchased = api.state.get('purchased', 0);
17
+ let totalSpent = api.state.get('totalSpent', 0);
18
+ let totalAcquired = api.state.get('totalAcquired', 0);
19
+ api.onStart(() => {
20
+ api.log.info(`DCA: ${MAX_PURCHASES} buys of $${AMOUNT_USD} ${COIN} every ${INTERVAL_MS / 60000}m`);
21
+ api.log.info(`Progress: ${purchased}/${MAX_PURCHASES} completed`);
22
+ });
23
+ api.every(INTERVAL_MS, async () => {
24
+ if (purchased >= MAX_PURCHASES) {
25
+ api.log.info(`DCA complete: ${purchased} purchases, $${totalSpent.toFixed(2)} spent, ${totalAcquired.toFixed(6)} ${COIN} acquired`);
26
+ return;
27
+ }
28
+ const mids = await api.client.getAllMids();
29
+ const price = parseFloat(mids[COIN]);
30
+ if (!price) {
31
+ api.log.warn(`No price for ${COIN}, skipping`);
32
+ return;
33
+ }
34
+ const size = AMOUNT_USD / price;
35
+ api.log.info(`[${purchased + 1}/${MAX_PURCHASES}] Buying ~$${AMOUNT_USD} of ${COIN} @ $${price.toFixed(2)}`);
36
+ const response = await api.client.marketOrder(COIN, true, size);
37
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
38
+ const status = response.response.data.statuses[0];
39
+ if (status?.filled) {
40
+ const filledSize = parseFloat(status.filled.totalSz);
41
+ const avgPx = parseFloat(status.filled.avgPx);
42
+ totalSpent += filledSize * avgPx;
43
+ totalAcquired += filledSize;
44
+ purchased++;
45
+ api.state.set('purchased', purchased);
46
+ api.state.set('totalSpent', totalSpent);
47
+ api.state.set('totalAcquired', totalAcquired);
48
+ const avgPrice = totalSpent / totalAcquired;
49
+ api.log.info(`Filled ${filledSize.toFixed(6)} @ $${avgPx.toFixed(2)} | Avg: $${avgPrice.toFixed(2)} | Total: ${totalAcquired.toFixed(6)} ${COIN}`);
50
+ }
51
+ else if (status?.error) {
52
+ api.log.error(`Order failed: ${status.error}`);
53
+ }
54
+ }
55
+ });
56
+ api.onStop(() => {
57
+ const avgPrice = totalAcquired > 0 ? totalSpent / totalAcquired : 0;
58
+ api.log.info(`DCA stopped: ${purchased}/${MAX_PURCHASES} | Spent: $${totalSpent.toFixed(2)} | Acquired: ${totalAcquired.toFixed(6)} ${COIN} | Avg: $${avgPrice.toFixed(2)}`);
59
+ });
60
+ }
@@ -0,0 +1,4 @@
1
+ import type { AutomationAPI, AutomationConfig } from '../types.js';
2
+ export declare const config: AutomationConfig;
3
+ export default function fundingArb(api: AutomationAPI): void;
4
+ //# sourceMappingURL=funding-arb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"funding-arb.d.ts","sourceRoot":"","sources":["../../../scripts/auto/examples/funding-arb.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEnE,eAAO,MAAM,MAAM,EAAE,gBASpB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,GAAG,EAAE,aAAa,QAkFpD"}
@@ -0,0 +1,81 @@
1
+ // Funding Arbitrage — Collect funding by positioning opposite to the crowd
2
+ export const config = {
3
+ description: 'Funding arbitrage — collect funding by positioning opposite to the crowd',
4
+ fields: {
5
+ coin: { type: 'string', description: 'Asset to trade', default: 'HYPE' },
6
+ sizeUsd: { type: 'number', description: 'Position size in USD notional', default: 5000 },
7
+ minFunding: { type: 'number', description: 'Min annualized % to enter', default: 20 },
8
+ maxFunding: { type: 'number', description: 'Max annualized % — avoid squeezes', default: 200 },
9
+ closeAt: { type: 'number', description: 'Close when funding drops below this %', default: 5 },
10
+ },
11
+ };
12
+ export default function fundingArb(api) {
13
+ const COIN = api.state.get('coin', 'HYPE');
14
+ const SIZE_USD = api.state.get('sizeUsd', 5000);
15
+ const MIN_FUNDING = api.state.get('minFunding', 20);
16
+ const MAX_FUNDING = api.state.get('maxFunding', 200);
17
+ const CLOSE_AT = api.state.get('closeAt', 5);
18
+ let inPosition = api.state.get('inPosition', false);
19
+ let positionSide = api.state.get('positionSide', '');
20
+ let entryPrice = api.state.get('entryPrice', 0);
21
+ let positionSize = api.state.get('positionSize', 0);
22
+ let totalFunding = api.state.get('totalFunding', 0);
23
+ api.onStart(() => {
24
+ api.log.info(`Funding arb: ${COIN} | $${SIZE_USD} | Enter >${MIN_FUNDING}% | Close <${CLOSE_AT}%`);
25
+ if (inPosition) {
26
+ api.log.info(`Resuming ${positionSide} position: ${positionSize.toFixed(6)} @ $${entryPrice.toFixed(2)}`);
27
+ }
28
+ });
29
+ api.on('funding_update', async ({ coin, annualized }) => {
30
+ if (coin !== COIN)
31
+ return;
32
+ const annualizedPct = annualized * 100;
33
+ const absAnnualized = Math.abs(annualizedPct);
34
+ if (inPosition) {
35
+ // Check if we should close
36
+ const shouldClose = (positionSide === 'short' && annualizedPct < CLOSE_AT) ||
37
+ (positionSide === 'long' && annualizedPct > -CLOSE_AT);
38
+ if (shouldClose) {
39
+ api.log.info(`Funding dropped to ${annualizedPct.toFixed(2)}% (below ${CLOSE_AT}%), closing ${positionSide}`);
40
+ const closeIsBuy = positionSide === 'short';
41
+ await api.client.marketOrder(coin, closeIsBuy, positionSize);
42
+ inPosition = false;
43
+ api.state.set('inPosition', false);
44
+ api.log.info(`Position closed. Funding collected: ~$${totalFunding.toFixed(2)}`);
45
+ }
46
+ else {
47
+ api.log.debug(`${coin} funding: ${annualizedPct.toFixed(2)}% — holding ${positionSide}`);
48
+ }
49
+ return;
50
+ }
51
+ // Not in position — check if we should enter
52
+ if (absAnnualized >= MIN_FUNDING && absAnnualized <= MAX_FUNDING) {
53
+ const shouldShort = annualizedPct > 0; // Positive = longs pay shorts
54
+ const side = shouldShort ? 'short' : 'long';
55
+ const mids = await api.client.getAllMids();
56
+ const price = parseFloat(mids[coin]);
57
+ const size = SIZE_USD / price;
58
+ api.log.info(`Funding at ${annualizedPct.toFixed(2)}% — opening ${side} ${size.toFixed(6)} ${coin}`);
59
+ const response = await api.client.marketOrder(coin, !shouldShort, size);
60
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
61
+ const status = response.response.data.statuses[0];
62
+ if (status?.filled) {
63
+ positionSize = parseFloat(status.filled.totalSz);
64
+ entryPrice = parseFloat(status.filled.avgPx);
65
+ positionSide = side;
66
+ inPosition = true;
67
+ api.state.set('inPosition', true);
68
+ api.state.set('positionSide', side);
69
+ api.state.set('entryPrice', entryPrice);
70
+ api.state.set('positionSize', positionSize);
71
+ api.log.info(`Entered ${side} ${positionSize.toFixed(6)} @ $${entryPrice.toFixed(2)}`);
72
+ }
73
+ }
74
+ }
75
+ });
76
+ api.onStop(() => {
77
+ if (inPosition) {
78
+ api.log.warn(`Stopping with open ${positionSide} position of ${positionSize.toFixed(6)} ${COIN} — close manually if desired`);
79
+ }
80
+ });
81
+ }
@@ -0,0 +1,4 @@
1
+ import type { AutomationAPI, AutomationConfig } from '../types.js';
2
+ export declare const config: AutomationConfig;
3
+ export default function grid(api: AutomationAPI): void;
4
+ //# sourceMappingURL=grid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../scripts/auto/examples/grid.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEnE,eAAO,MAAM,MAAM,EAAE,gBAUpB,CAAC;AASF,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,GAAG,EAAE,aAAa,QA+G9C"}