openbroker 1.0.79 → 1.0.82
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 +5 -1
- package/bin/cli.ts +22 -1
- package/bin/openbroker.js +4 -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 +41 -1
- package/scripts/auto/loader.ts +37 -3
- package/scripts/auto/report.ts +459 -0
- package/scripts/auto/runtime.ts +267 -81
- package/scripts/auto/types.ts +10 -0
- package/scripts/core/client.ts +603 -214
- package/scripts/core/config.ts +19 -0
- package/scripts/core/types.ts +1 -0
- package/scripts/core/utils.ts +4 -1
- package/scripts/info/account.ts +12 -8
- package/scripts/info/all-markets.ts +3 -0
- package/scripts/info/candles.ts +3 -0
- package/scripts/info/funding-history.ts +3 -0
- package/scripts/info/funding-scan.ts +4 -0
- package/scripts/info/funding.ts +7 -0
- package/scripts/info/markets.ts +7 -0
- package/scripts/info/orders.ts +5 -0
- package/scripts/info/search-markets.ts +8 -0
- package/scripts/info/trades.ts +3 -0
- package/scripts/setup/onboard.ts +95 -44
package/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Hyperliquid trading plugin with background position monitoring and
|
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Requires Node.js 22+, network access to api.hyperliquid.xyz
|
|
6
6
|
homepage: https://www.npmjs.com/package/openbroker
|
|
7
|
-
metadata: {"author": "monemetrics", "version": "1.0.
|
|
7
|
+
metadata: {"author": "monemetrics", "version": "1.0.82", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
|
|
8
8
|
allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_spot_buy ob_spot_sell ob_twap ob_twap_cancel ob_twap_status ob_bracket ob_chase ob_watcher_status ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -583,10 +583,14 @@ export default function(api) {
|
|
|
583
583
|
| `api.state.clear()` | Clear all state |
|
|
584
584
|
| `api.publish(message, options?)` | Send a message to the OpenClaw agent via webhook. Triggers an agent turn — the agent receives the message and can notify the user, take action, etc. Returns `true` if delivered. Options: `{ name?, wakeMode?, deliver?, channel? }` |
|
|
585
585
|
| `api.log.info/warn/error/debug(msg)` | Structured logger |
|
|
586
|
+
| `api.audit.record(kind, payload?)` | Add a custom audit note to the local SQLite trail for later reporting |
|
|
587
|
+
| `api.audit.metric(name, value, tags?)` | Add a numeric metric to the local SQLite trail |
|
|
586
588
|
| `api.utils` | `roundPrice`, `roundSize`, `sleep`, `normalizeCoin`, `formatUsd`, `annualizeFundingRate` |
|
|
587
589
|
| `api.id` | Automation ID (filename or `--id` flag) |
|
|
588
590
|
| `api.dryRun` | `true` if running with `--dry` (write methods are intercepted) |
|
|
589
591
|
|
|
592
|
+
Automations now write a local audit trail automatically to `~/.openbroker/automation-audit.sqlite`. The runtime records run config, logs, state changes, write actions, order updates, fills, user events, and per-poll account snapshots so you can generate performance reports later.
|
|
593
|
+
|
|
590
594
|
### Events
|
|
591
595
|
|
|
592
596
|
| Event | Payload | When |
|
package/bin/cli.ts
CHANGED
|
@@ -112,6 +112,8 @@ Automations:
|
|
|
112
112
|
auto status Show running automations
|
|
113
113
|
|
|
114
114
|
Options:
|
|
115
|
+
-c, --config <path> Use a specific .env config file
|
|
116
|
+
--testnet Use testnet
|
|
115
117
|
--help, -h Show help for a command
|
|
116
118
|
--dry Preview without executing
|
|
117
119
|
--verbose Show debug output
|
|
@@ -134,11 +136,30 @@ Documentation: https://github.com/aurracloud/open-broker
|
|
|
134
136
|
function runScript(scriptPath: string, args: string[]) {
|
|
135
137
|
const fullPath = path.join(scriptsDir, scriptPath);
|
|
136
138
|
|
|
139
|
+
// Handle global flags: set env vars and strip from args
|
|
140
|
+
const env = { ...process.env };
|
|
141
|
+
const testnetIdx = args.indexOf('--testnet');
|
|
142
|
+
if (testnetIdx !== -1) {
|
|
143
|
+
env.HYPERLIQUID_NETWORK = 'testnet';
|
|
144
|
+
args = [...args.slice(0, testnetIdx), ...args.slice(testnetIdx + 1)];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle -c / --config flag: load the specified .env file
|
|
148
|
+
// Resolve relative to the user's original working directory (not the package root)
|
|
149
|
+
let configIdx = args.indexOf('-c');
|
|
150
|
+
if (configIdx === -1) configIdx = args.indexOf('--config');
|
|
151
|
+
if (configIdx !== -1 && args[configIdx + 1]) {
|
|
152
|
+
const originalCwd = process.env.OPENBROKER_CWD || process.cwd();
|
|
153
|
+
const configPath = path.resolve(originalCwd, args[configIdx + 1]);
|
|
154
|
+
env.OPENBROKER_CONFIG = configPath;
|
|
155
|
+
args = [...args.slice(0, configIdx), ...args.slice(configIdx + 2)];
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
// Use tsx to run TypeScript directly
|
|
138
159
|
const child = spawn('npx', ['tsx', fullPath, ...args], {
|
|
139
160
|
stdio: 'inherit',
|
|
140
161
|
cwd: path.resolve(__dirname, '..'),
|
|
141
|
-
env
|
|
162
|
+
env,
|
|
142
163
|
});
|
|
143
164
|
|
|
144
165
|
child.on('error', (err) => {
|
package/bin/openbroker.js
CHANGED
|
@@ -56,6 +56,10 @@ child.on('error', (err) => {
|
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
// Let the child handle SIGINT (Ctrl+C) — don't exit the wrapper early
|
|
60
|
+
process.on('SIGINT', () => {});
|
|
61
|
+
process.on('SIGTERM', () => { child.kill('SIGTERM'); });
|
|
62
|
+
|
|
59
63
|
child.on('exit', (code) => {
|
|
60
64
|
process.exit(code ?? 0);
|
|
61
65
|
});
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, unlinkSync } from 'fs';
|
|
4
|
+
import net from 'net';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
8
|
+
|
|
9
|
+
const dbPath = process.argv[2];
|
|
10
|
+
const socketPath = process.argv[3];
|
|
11
|
+
|
|
12
|
+
if (!dbPath || !socketPath) {
|
|
13
|
+
console.error('usage: audit-daemon.js <db-path> <socket-path>');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
mkdirSync(path.dirname(dbPath), { recursive: true, mode: 0o700 });
|
|
18
|
+
if (process.platform !== 'win32') {
|
|
19
|
+
mkdirSync(path.dirname(socketPath), { recursive: true, mode: 0o700 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const db = new DatabaseSync(dbPath);
|
|
23
|
+
db.exec(`
|
|
24
|
+
PRAGMA journal_mode = WAL;
|
|
25
|
+
PRAGMA synchronous = NORMAL;
|
|
26
|
+
PRAGMA temp_store = MEMORY;
|
|
27
|
+
PRAGMA foreign_keys = ON;
|
|
28
|
+
PRAGMA busy_timeout = 5000;
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS automation_runs (
|
|
31
|
+
run_id TEXT PRIMARY KEY,
|
|
32
|
+
automation_id TEXT NOT NULL,
|
|
33
|
+
script_path TEXT NOT NULL,
|
|
34
|
+
account_address TEXT,
|
|
35
|
+
wallet_address TEXT,
|
|
36
|
+
is_api_wallet INTEGER NOT NULL,
|
|
37
|
+
dry_run INTEGER NOT NULL,
|
|
38
|
+
verbose INTEGER NOT NULL,
|
|
39
|
+
poll_interval_ms INTEGER,
|
|
40
|
+
use_websocket INTEGER NOT NULL,
|
|
41
|
+
pid INTEGER NOT NULL,
|
|
42
|
+
started_at INTEGER NOT NULL,
|
|
43
|
+
stopped_at INTEGER,
|
|
44
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
45
|
+
stop_reason TEXT,
|
|
46
|
+
initial_state_json TEXT,
|
|
47
|
+
persisted_state_json TEXT,
|
|
48
|
+
poll_count INTEGER,
|
|
49
|
+
events_emitted INTEGER
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_automation_runs_id_started
|
|
53
|
+
ON automation_runs (automation_id, started_at DESC);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS automation_logs (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
run_id TEXT NOT NULL,
|
|
58
|
+
timestamp INTEGER NOT NULL,
|
|
59
|
+
level TEXT NOT NULL,
|
|
60
|
+
message TEXT NOT NULL
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_automation_logs_run_time
|
|
64
|
+
ON automation_logs (run_id, timestamp DESC);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS automation_events (
|
|
67
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
+
run_id TEXT NOT NULL,
|
|
69
|
+
timestamp INTEGER NOT NULL,
|
|
70
|
+
event_type TEXT NOT NULL,
|
|
71
|
+
source TEXT NOT NULL,
|
|
72
|
+
payload_json TEXT
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_automation_events_run_time
|
|
76
|
+
ON automation_events (run_id, timestamp DESC);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS automation_actions (
|
|
79
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
|
+
run_id TEXT NOT NULL,
|
|
81
|
+
timestamp INTEGER NOT NULL,
|
|
82
|
+
action_id TEXT NOT NULL,
|
|
83
|
+
phase TEXT NOT NULL,
|
|
84
|
+
method TEXT NOT NULL,
|
|
85
|
+
dry_run INTEGER NOT NULL,
|
|
86
|
+
payload_json TEXT,
|
|
87
|
+
result_json TEXT,
|
|
88
|
+
error_json TEXT
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_automation_actions_run_time
|
|
92
|
+
ON automation_actions (run_id, timestamp DESC);
|
|
93
|
+
|
|
94
|
+
CREATE TABLE IF NOT EXISTS automation_snapshots (
|
|
95
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
|
+
run_id TEXT NOT NULL,
|
|
97
|
+
timestamp INTEGER NOT NULL,
|
|
98
|
+
poll_count INTEGER NOT NULL,
|
|
99
|
+
equity REAL NOT NULL,
|
|
100
|
+
margin_used REAL NOT NULL,
|
|
101
|
+
margin_used_pct REAL NOT NULL,
|
|
102
|
+
positions_json TEXT
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_automation_snapshots_run_time
|
|
106
|
+
ON automation_snapshots (run_id, timestamp DESC);
|
|
107
|
+
|
|
108
|
+
CREATE TABLE IF NOT EXISTS automation_order_updates (
|
|
109
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
110
|
+
run_id TEXT NOT NULL,
|
|
111
|
+
timestamp INTEGER NOT NULL,
|
|
112
|
+
oid INTEGER,
|
|
113
|
+
coin TEXT,
|
|
114
|
+
side TEXT,
|
|
115
|
+
size REAL,
|
|
116
|
+
price REAL,
|
|
117
|
+
orig_size REAL,
|
|
118
|
+
status TEXT,
|
|
119
|
+
status_timestamp INTEGER,
|
|
120
|
+
payload_json TEXT
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_automation_order_updates_run_time
|
|
124
|
+
ON automation_order_updates (run_id, timestamp DESC);
|
|
125
|
+
|
|
126
|
+
CREATE TABLE IF NOT EXISTS automation_fills (
|
|
127
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
128
|
+
run_id TEXT NOT NULL,
|
|
129
|
+
timestamp INTEGER NOT NULL,
|
|
130
|
+
coin TEXT,
|
|
131
|
+
side TEXT,
|
|
132
|
+
size REAL,
|
|
133
|
+
price REAL,
|
|
134
|
+
fee REAL,
|
|
135
|
+
closed_pnl REAL,
|
|
136
|
+
oid INTEGER,
|
|
137
|
+
crossed INTEGER,
|
|
138
|
+
payload_json TEXT
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_automation_fills_run_time
|
|
142
|
+
ON automation_fills (run_id, timestamp DESC);
|
|
143
|
+
|
|
144
|
+
CREATE TABLE IF NOT EXISTS automation_user_events (
|
|
145
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
146
|
+
run_id TEXT NOT NULL,
|
|
147
|
+
timestamp INTEGER NOT NULL,
|
|
148
|
+
event_kind TEXT NOT NULL,
|
|
149
|
+
payload_json TEXT
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_automation_user_events_run_time
|
|
153
|
+
ON automation_user_events (run_id, timestamp DESC);
|
|
154
|
+
|
|
155
|
+
CREATE TABLE IF NOT EXISTS automation_state_changes (
|
|
156
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
157
|
+
run_id TEXT NOT NULL,
|
|
158
|
+
timestamp INTEGER NOT NULL,
|
|
159
|
+
op TEXT NOT NULL,
|
|
160
|
+
key_name TEXT,
|
|
161
|
+
value_json TEXT
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_automation_state_changes_run_time
|
|
165
|
+
ON automation_state_changes (run_id, timestamp DESC);
|
|
166
|
+
|
|
167
|
+
CREATE TABLE IF NOT EXISTS automation_publishes (
|
|
168
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
169
|
+
run_id TEXT NOT NULL,
|
|
170
|
+
timestamp INTEGER NOT NULL,
|
|
171
|
+
delivered INTEGER NOT NULL,
|
|
172
|
+
message TEXT NOT NULL,
|
|
173
|
+
options_json TEXT
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
CREATE INDEX IF NOT EXISTS idx_automation_publishes_run_time
|
|
177
|
+
ON automation_publishes (run_id, timestamp DESC);
|
|
178
|
+
|
|
179
|
+
CREATE TABLE IF NOT EXISTS automation_errors (
|
|
180
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
181
|
+
run_id TEXT NOT NULL,
|
|
182
|
+
timestamp INTEGER NOT NULL,
|
|
183
|
+
stage TEXT NOT NULL,
|
|
184
|
+
error_json TEXT
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_automation_errors_run_time
|
|
188
|
+
ON automation_errors (run_id, timestamp DESC);
|
|
189
|
+
|
|
190
|
+
CREATE TABLE IF NOT EXISTS automation_notes (
|
|
191
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
192
|
+
run_id TEXT NOT NULL,
|
|
193
|
+
timestamp INTEGER NOT NULL,
|
|
194
|
+
kind TEXT NOT NULL,
|
|
195
|
+
payload_json TEXT
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_automation_notes_run_time
|
|
199
|
+
ON automation_notes (run_id, timestamp DESC);
|
|
200
|
+
|
|
201
|
+
CREATE TABLE IF NOT EXISTS automation_metrics (
|
|
202
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
203
|
+
run_id TEXT NOT NULL,
|
|
204
|
+
timestamp INTEGER NOT NULL,
|
|
205
|
+
name TEXT NOT NULL,
|
|
206
|
+
value REAL NOT NULL,
|
|
207
|
+
tags_json TEXT
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
CREATE INDEX IF NOT EXISTS idx_automation_metrics_run_time
|
|
211
|
+
ON automation_metrics (run_id, timestamp DESC);
|
|
212
|
+
`);
|
|
213
|
+
|
|
214
|
+
const statements = {
|
|
215
|
+
initRun: db.prepare(`
|
|
216
|
+
INSERT INTO automation_runs (
|
|
217
|
+
run_id, automation_id, script_path, account_address, wallet_address,
|
|
218
|
+
is_api_wallet, dry_run, verbose, poll_interval_ms, use_websocket,
|
|
219
|
+
pid, started_at, status, initial_state_json, persisted_state_json
|
|
220
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?)
|
|
221
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
222
|
+
automation_id = excluded.automation_id,
|
|
223
|
+
script_path = excluded.script_path,
|
|
224
|
+
account_address = excluded.account_address,
|
|
225
|
+
wallet_address = excluded.wallet_address,
|
|
226
|
+
is_api_wallet = excluded.is_api_wallet,
|
|
227
|
+
dry_run = excluded.dry_run,
|
|
228
|
+
verbose = excluded.verbose,
|
|
229
|
+
poll_interval_ms = excluded.poll_interval_ms,
|
|
230
|
+
use_websocket = excluded.use_websocket,
|
|
231
|
+
pid = excluded.pid,
|
|
232
|
+
started_at = excluded.started_at,
|
|
233
|
+
status = 'running',
|
|
234
|
+
initial_state_json = excluded.initial_state_json,
|
|
235
|
+
persisted_state_json = excluded.persisted_state_json
|
|
236
|
+
`),
|
|
237
|
+
finishRun: db.prepare(`
|
|
238
|
+
UPDATE automation_runs
|
|
239
|
+
SET stopped_at = ?, status = ?, stop_reason = ?, poll_count = ?, events_emitted = ?
|
|
240
|
+
WHERE run_id = ?
|
|
241
|
+
`),
|
|
242
|
+
log: db.prepare(`
|
|
243
|
+
INSERT INTO automation_logs (run_id, timestamp, level, message)
|
|
244
|
+
VALUES (?, ?, ?, ?)
|
|
245
|
+
`),
|
|
246
|
+
event: db.prepare(`
|
|
247
|
+
INSERT INTO automation_events (run_id, timestamp, event_type, source, payload_json)
|
|
248
|
+
VALUES (?, ?, ?, ?, ?)
|
|
249
|
+
`),
|
|
250
|
+
action: db.prepare(`
|
|
251
|
+
INSERT INTO automation_actions (
|
|
252
|
+
run_id, timestamp, action_id, phase, method, dry_run, payload_json, result_json, error_json
|
|
253
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
254
|
+
`),
|
|
255
|
+
snapshot: db.prepare(`
|
|
256
|
+
INSERT INTO automation_snapshots (
|
|
257
|
+
run_id, timestamp, poll_count, equity, margin_used, margin_used_pct, positions_json
|
|
258
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
259
|
+
`),
|
|
260
|
+
orderUpdate: db.prepare(`
|
|
261
|
+
INSERT INTO automation_order_updates (
|
|
262
|
+
run_id, timestamp, oid, coin, side, size, price, orig_size, status, status_timestamp, payload_json
|
|
263
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
264
|
+
`),
|
|
265
|
+
fill: db.prepare(`
|
|
266
|
+
INSERT INTO automation_fills (
|
|
267
|
+
run_id, timestamp, coin, side, size, price, fee, closed_pnl, oid, crossed, payload_json
|
|
268
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
269
|
+
`),
|
|
270
|
+
userEvent: db.prepare(`
|
|
271
|
+
INSERT INTO automation_user_events (run_id, timestamp, event_kind, payload_json)
|
|
272
|
+
VALUES (?, ?, ?, ?)
|
|
273
|
+
`),
|
|
274
|
+
stateChange: db.prepare(`
|
|
275
|
+
INSERT INTO automation_state_changes (run_id, timestamp, op, key_name, value_json)
|
|
276
|
+
VALUES (?, ?, ?, ?, ?)
|
|
277
|
+
`),
|
|
278
|
+
publish: db.prepare(`
|
|
279
|
+
INSERT INTO automation_publishes (run_id, timestamp, delivered, message, options_json)
|
|
280
|
+
VALUES (?, ?, ?, ?, ?)
|
|
281
|
+
`),
|
|
282
|
+
error: db.prepare(`
|
|
283
|
+
INSERT INTO automation_errors (run_id, timestamp, stage, error_json)
|
|
284
|
+
VALUES (?, ?, ?, ?)
|
|
285
|
+
`),
|
|
286
|
+
note: db.prepare(`
|
|
287
|
+
INSERT INTO automation_notes (run_id, timestamp, kind, payload_json)
|
|
288
|
+
VALUES (?, ?, ?, ?)
|
|
289
|
+
`),
|
|
290
|
+
metric: db.prepare(`
|
|
291
|
+
INSERT INTO automation_metrics (run_id, timestamp, name, value, tags_json)
|
|
292
|
+
VALUES (?, ?, ?, ?, ?)
|
|
293
|
+
`),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
function json(value) {
|
|
297
|
+
return value === undefined ? null : JSON.stringify(value);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function numberOrNull(value) {
|
|
301
|
+
if (value === undefined || value === null || value === '') return null;
|
|
302
|
+
const numeric = Number(value);
|
|
303
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function detectUserEventKind(payload) {
|
|
307
|
+
if (payload && typeof payload === 'object') {
|
|
308
|
+
if ('liquidation' in payload) return 'liquidation';
|
|
309
|
+
if ('funding' in payload) return 'funding';
|
|
310
|
+
if ('nonUserCancel' in payload) return 'non_user_cancel';
|
|
311
|
+
}
|
|
312
|
+
return 'unknown';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function send(socket, response) {
|
|
316
|
+
socket.write(`${JSON.stringify(response)}\n`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function handleMessage(message) {
|
|
320
|
+
const payload = message.payload;
|
|
321
|
+
|
|
322
|
+
switch (message.type) {
|
|
323
|
+
case 'init':
|
|
324
|
+
statements.initRun.run(
|
|
325
|
+
payload.runId,
|
|
326
|
+
payload.automationId,
|
|
327
|
+
payload.scriptPath,
|
|
328
|
+
payload.accountAddress ?? null,
|
|
329
|
+
payload.walletAddress ?? null,
|
|
330
|
+
payload.isApiWallet ? 1 : 0,
|
|
331
|
+
payload.dryRun ? 1 : 0,
|
|
332
|
+
payload.verbose ? 1 : 0,
|
|
333
|
+
payload.pollIntervalMs ?? null,
|
|
334
|
+
payload.useWebSocket ? 1 : 0,
|
|
335
|
+
payload.pid,
|
|
336
|
+
payload.startedAt,
|
|
337
|
+
json(payload.initialState ?? {}),
|
|
338
|
+
json(payload.persistedState ?? {}),
|
|
339
|
+
);
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
case 'log':
|
|
343
|
+
statements.log.run(payload.runId, payload.timestamp, payload.level, payload.message);
|
|
344
|
+
break;
|
|
345
|
+
|
|
346
|
+
case 'event':
|
|
347
|
+
statements.event.run(payload.runId, payload.timestamp, payload.eventType, payload.source, json(payload.payload));
|
|
348
|
+
break;
|
|
349
|
+
|
|
350
|
+
case 'action':
|
|
351
|
+
statements.action.run(
|
|
352
|
+
payload.runId,
|
|
353
|
+
payload.timestamp,
|
|
354
|
+
payload.actionId,
|
|
355
|
+
payload.phase,
|
|
356
|
+
payload.method,
|
|
357
|
+
payload.dryRun ? 1 : 0,
|
|
358
|
+
json(payload.payload),
|
|
359
|
+
json(payload.result),
|
|
360
|
+
json(payload.error),
|
|
361
|
+
);
|
|
362
|
+
break;
|
|
363
|
+
|
|
364
|
+
case 'snapshot':
|
|
365
|
+
statements.snapshot.run(
|
|
366
|
+
payload.runId,
|
|
367
|
+
payload.timestamp,
|
|
368
|
+
payload.pollCount,
|
|
369
|
+
payload.equity,
|
|
370
|
+
payload.marginUsed,
|
|
371
|
+
payload.marginUsedPct,
|
|
372
|
+
json(payload.positions ?? []),
|
|
373
|
+
);
|
|
374
|
+
break;
|
|
375
|
+
|
|
376
|
+
case 'order_update': {
|
|
377
|
+
const update = payload.payload ?? {};
|
|
378
|
+
statements.orderUpdate.run(
|
|
379
|
+
payload.runId,
|
|
380
|
+
payload.timestamp,
|
|
381
|
+
numberOrNull(update.oid),
|
|
382
|
+
update.coin ?? null,
|
|
383
|
+
update.side ?? null,
|
|
384
|
+
numberOrNull(update.size),
|
|
385
|
+
numberOrNull(update.price),
|
|
386
|
+
numberOrNull(update.origSize),
|
|
387
|
+
update.status ?? null,
|
|
388
|
+
numberOrNull(update.statusTimestamp),
|
|
389
|
+
json(update),
|
|
390
|
+
);
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
case 'fill': {
|
|
395
|
+
const fill = payload.payload ?? {};
|
|
396
|
+
statements.fill.run(
|
|
397
|
+
payload.runId,
|
|
398
|
+
payload.timestamp,
|
|
399
|
+
fill.coin ?? null,
|
|
400
|
+
fill.side ?? null,
|
|
401
|
+
numberOrNull(fill.size ?? fill.sz),
|
|
402
|
+
numberOrNull(fill.price ?? fill.px),
|
|
403
|
+
numberOrNull(fill.fee),
|
|
404
|
+
numberOrNull(fill.closedPnl),
|
|
405
|
+
numberOrNull(fill.oid),
|
|
406
|
+
fill.crossed ? 1 : 0,
|
|
407
|
+
json(fill),
|
|
408
|
+
);
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
case 'user_event':
|
|
413
|
+
statements.userEvent.run(payload.runId, payload.timestamp, detectUserEventKind(payload.payload), json(payload.payload));
|
|
414
|
+
break;
|
|
415
|
+
|
|
416
|
+
case 'state_change':
|
|
417
|
+
statements.stateChange.run(payload.runId, payload.timestamp, payload.op, payload.key, json(payload.value));
|
|
418
|
+
break;
|
|
419
|
+
|
|
420
|
+
case 'publish':
|
|
421
|
+
statements.publish.run(payload.runId, payload.timestamp, payload.delivered ? 1 : 0, payload.message, json(payload.options));
|
|
422
|
+
break;
|
|
423
|
+
|
|
424
|
+
case 'error':
|
|
425
|
+
statements.error.run(payload.runId, payload.timestamp, payload.stage, json(payload.error));
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case 'note':
|
|
429
|
+
statements.note.run(payload.runId, payload.timestamp, payload.kind, json(payload.payload));
|
|
430
|
+
break;
|
|
431
|
+
|
|
432
|
+
case 'metric':
|
|
433
|
+
statements.metric.run(payload.runId, payload.timestamp, payload.name, payload.value, json(payload.tags));
|
|
434
|
+
break;
|
|
435
|
+
|
|
436
|
+
case 'stop':
|
|
437
|
+
statements.finishRun.run(
|
|
438
|
+
payload.timestamp,
|
|
439
|
+
payload.status,
|
|
440
|
+
payload.stopReason,
|
|
441
|
+
payload.pollCount,
|
|
442
|
+
payload.eventsEmitted,
|
|
443
|
+
payload.runId,
|
|
444
|
+
);
|
|
445
|
+
break;
|
|
446
|
+
|
|
447
|
+
default:
|
|
448
|
+
throw new Error(`unknown audit message type: ${message.type}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function socketInUse(target) {
|
|
453
|
+
return await new Promise((resolve) => {
|
|
454
|
+
const socket = net.createConnection(target);
|
|
455
|
+
|
|
456
|
+
const finish = (value) => {
|
|
457
|
+
socket.removeAllListeners();
|
|
458
|
+
socket.destroy();
|
|
459
|
+
resolve(value);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
socket.once('connect', () => finish(true));
|
|
463
|
+
socket.once('error', () => finish(false));
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (process.platform !== 'win32' && existsSync(socketPath)) {
|
|
468
|
+
const active = await socketInUse(socketPath);
|
|
469
|
+
if (active) {
|
|
470
|
+
process.exit(0);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
unlinkSync(socketPath);
|
|
475
|
+
} catch {
|
|
476
|
+
// ignore stale socket cleanup failures
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const server = net.createServer((socket) => {
|
|
481
|
+
socket.setEncoding('utf8');
|
|
482
|
+
const rl = readline.createInterface({
|
|
483
|
+
input: socket,
|
|
484
|
+
crlfDelay: Infinity,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
rl.on('line', (line) => {
|
|
488
|
+
if (!line.trim()) return;
|
|
489
|
+
|
|
490
|
+
let message;
|
|
491
|
+
try {
|
|
492
|
+
message = JSON.parse(line);
|
|
493
|
+
handleMessage(message);
|
|
494
|
+
send(socket, { messageId: message.messageId, ok: true });
|
|
495
|
+
} catch (error) {
|
|
496
|
+
send(socket, {
|
|
497
|
+
messageId: message?.messageId ?? null,
|
|
498
|
+
ok: false,
|
|
499
|
+
error: error instanceof Error ? error.message : String(error),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
socket.on('close', () => {
|
|
505
|
+
rl.close();
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
function cleanupAndExit(code = 0) {
|
|
510
|
+
try {
|
|
511
|
+
server.close();
|
|
512
|
+
} catch {
|
|
513
|
+
// ignore close failures
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (process.platform !== 'win32' && existsSync(socketPath)) {
|
|
517
|
+
try {
|
|
518
|
+
unlinkSync(socketPath);
|
|
519
|
+
} catch {
|
|
520
|
+
// ignore cleanup failures
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
db.close();
|
|
526
|
+
} catch {
|
|
527
|
+
// ignore close failures
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
process.exit(code);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
server.on('error', async (error) => {
|
|
534
|
+
if (error.code === 'EADDRINUSE' && process.platform !== 'win32') {
|
|
535
|
+
const active = await socketInUse(socketPath);
|
|
536
|
+
if (active) {
|
|
537
|
+
process.exit(0);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
unlinkSync(socketPath);
|
|
543
|
+
server.listen(socketPath);
|
|
544
|
+
return;
|
|
545
|
+
} catch (retryError) {
|
|
546
|
+
console.error(retryError instanceof Error ? retryError.message : String(retryError));
|
|
547
|
+
cleanupAndExit(1);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.error(error.message);
|
|
553
|
+
cleanupAndExit(1);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
process.on('SIGINT', () => cleanupAndExit(0));
|
|
557
|
+
process.on('SIGTERM', () => cleanupAndExit(0));
|
|
558
|
+
|
|
559
|
+
server.listen(socketPath, () => {
|
|
560
|
+
if (process.platform !== 'win32') {
|
|
561
|
+
try {
|
|
562
|
+
chmodSync(socketPath, 0o600);
|
|
563
|
+
} catch {
|
|
564
|
+
// ignore chmod failures on some filesystems
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|