-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhyperliquid_dca_bot.mts
More file actions
145 lines (127 loc) · 4.84 KB
/
Copy pathhyperliquid_dca_bot.mts
File metadata and controls
145 lines (127 loc) · 4.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import 'dotenv/config';
import WebSocket from 'ws';
import { loadDcaConfig, printDcaBanner } from './dca/config.js';
import { HlClient } from './shared/hl_client.js';
import { createLogger } from './shared/logger.js';
import { DcaStateStore } from './dca/state_store.js';
import { DcaTradeLog } from './dca/trade_log.js';
import { DcaJobManager } from './dca/job_manager.js';
const config = loadDcaConfig();
const logger = createLogger('DCA');
const client = new HlClient(config, logger);
const store = new DcaStateStore('dca_state.json');
const tradeLog = new DcaTradeLog(config.tradeLogDir);
const manager = new DcaJobManager(config, client, logger, store, tradeLog);
const WS_URL = config.testnet
? 'wss://api.hyperliquid-testnet.xyz/ws'
: 'wss://api.hyperliquid.xyz/ws';
let ws: WebSocket | null = null;
let pingInterval: ReturnType<typeof setInterval> | null = null;
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
let reconnectDelay = 1_000;
const MAX_RECONNECT_DELAY = 30_000;
function startPing(): void {
if (pingInterval !== null) clearInterval(pingInterval);
pingInterval = setInterval(() => {
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ method: 'ping' }));
}, 30_000);
}
function subscribe(sub: Record<string, unknown>): void {
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ method: 'subscribe', subscription: sub }));
}
function resubscribe(): void {
subscribe({ type: 'allMids' });
subscribe({ type: 'orderUpdates', user: config.mainWallet });
logger.info('WS subscriptions sent');
}
function onMessage(raw: Buffer): void {
let msg: unknown;
try { msg = JSON.parse(raw.toString()); } catch { return; }
if (typeof msg !== 'object' || msg === null) return;
const m = msg as Record<string, unknown>;
const channel = m['channel'];
const data = m['data'];
if (typeof channel !== 'string') return;
switch (channel) {
case 'pong':
case 'subscriptionResponse':
return;
case 'error':
logger.error('WS error from server:', data);
return;
case 'allMids': {
if (typeof data !== 'object' || data === null) return;
const mids = (data as Record<string, unknown>)['mids'];
if (typeof mids !== 'object' || mids === null) return;
for (const [coin, px] of Object.entries(mids as Record<string, string>)) {
client.updateMidPrice(coin, parseFloat(px));
}
return;
}
case 'orderUpdates': {
const updates: unknown[] = Array.isArray(data) ? data : [data];
for (const upd of updates) {
if (typeof upd !== 'object' || upd === null) continue;
const u = upd as Record<string, unknown>;
if (u['status'] !== 'filled') continue;
const orderField = u['order'];
const cloid: unknown = (typeof orderField === 'object' && orderField !== null)
? (orderField as Record<string, unknown>)['cloid']
: u['cloid'];
if (typeof cloid !== 'string' || !cloid) continue;
const filledSz = typeof u['filledSz'] === 'string' ? parseFloat(u['filledSz']) : 0;
const avgPx = typeof u['avgPx'] === 'string' ? parseFloat(u['avgPx']) : 0;
manager.notifyFill(cloid, filledSz, avgPx);
}
return;
}
}
}
function scheduleReconnect(): void {
if (reconnectTimeout !== null) clearTimeout(reconnectTimeout);
reconnectTimeout = setTimeout(() => {
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
connect();
}, reconnectDelay);
}
function connect(): void {
if (ws !== null) { try { ws.terminate(); } catch { /* ignore */ } ws = null; }
logger.info(`WS connecting → ${WS_URL}`);
ws = new WebSocket(WS_URL);
ws.on('open', () => {
reconnectDelay = 1_000;
resubscribe();
startPing();
logger.info('WS connected');
});
ws.on('message', (raw) => onMessage(raw as Buffer));
ws.on('close', (code) => {
logger.warn(`WS closed (${code}) — reconnecting in ${reconnectDelay}ms`);
if (pingInterval !== null) { clearInterval(pingInterval); pingInterval = null; }
scheduleReconnect();
});
ws.on('error', (e) => logger.error('WS error:', e.message));
}
async function main(): Promise<void> {
printDcaBanner(config);
await client.loadAssetMeta();
await client.load24hChanges();
connect();
await new Promise(r => setTimeout(r, 3000));
await manager.restoreState();
manager.startMonitor();
const shutdown = (): void => {
logger.info('Shutting down DCA bot...');
manager.stop();
if (pingInterval !== null) clearInterval(pingInterval);
if (reconnectTimeout !== null) clearTimeout(reconnectTimeout);
ws?.terminate();
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
main().catch((e: unknown) => {
logger.error('Fatal:', e instanceof Error ? e.message : String(e));
process.exit(1);
});