@sherwoodagent/cli 0.42.0 → 0.51.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/dist/{addresses-DCQMJJFI.js → addresses-DOWLGXGE.js} +4 -4
- package/dist/{agent-5YEJ6W6W.js → agent-ZPAZTJG3.js} +2923 -2023
- package/dist/agent-ZPAZTJG3.js.map +1 -0
- package/dist/autoresearch-IA7HGSNW.js +267 -0
- package/dist/autoresearch-IA7HGSNW.js.map +1 -0
- package/dist/{chat-UWJ6JBFE.js → chat-MRTPUZ3X.js} +8 -8
- package/dist/{chunk-FOOZC4AM.js → chunk-2LRC7I33.js} +2 -2
- package/dist/{chunk-4EHI4SKM.js → chunk-2W4KGGPU.js} +11 -1
- package/dist/chunk-2W4KGGPU.js.map +1 -0
- package/dist/chunk-7HJXZ4NM.js +189 -0
- package/dist/chunk-7HJXZ4NM.js.map +1 -0
- package/dist/{chunk-JNDEZFXW.js → chunk-E6LH2FJ6.js} +4 -4
- package/dist/{chunk-XAUMJZU2.js → chunk-FWC57PT7.js} +2 -2
- package/dist/{chunk-4OAZ7TP5.js → chunk-IS2CLLUT.js} +4 -4
- package/dist/{chunk-T2IA6CCW.js → chunk-KJ35EF7N.js} +4 -4
- package/dist/{chunk-GMVDHSCF.js → chunk-OHQOTSFG.js} +39 -3
- package/dist/chunk-OHQOTSFG.js.map +1 -0
- package/dist/chunk-OMW47WOO.js +700 -0
- package/dist/chunk-OMW47WOO.js.map +1 -0
- package/dist/{chunk-5Q3VT6NQ.js → chunk-R7X33D2S.js} +5 -5
- package/dist/{chunk-6U3A6ZJ2.js → chunk-WMZEOR5W.js} +6 -127
- package/dist/chunk-WMZEOR5W.js.map +1 -0
- package/dist/{chunk-SWKHSKDH.js → chunk-XDBRKW7I.js} +3 -3
- package/dist/chunk-YTYXBX7B.js +495 -0
- package/dist/chunk-YTYXBX7B.js.map +1 -0
- package/dist/{client-DRVUILER.js → client-UHHCB6EZ.js} +4 -4
- package/dist/{config-YRERDZSL.js → config-REASKDIK.js} +6 -2
- package/dist/{eas-KBU2BAOD.js → eas-PEEWGSAO.js} +6 -6
- package/dist/{governor-KH7RNH2E.js → governor-MG5VVSU4.js} +6 -6
- package/dist/index.js +1424 -270
- package/dist/index.js.map +1 -1
- package/dist/{network-3HB46JMQ.js → network-3ZU7UBDU.js} +3 -3
- package/dist/{price-TTQWI672.js → price-WCCXWPDM.js} +5 -5
- package/dist/{research-J3HYU3N6.js → research-QBKISW2M.js} +9 -9
- package/dist/research-YJWSQS2A.js +14 -0
- package/dist/serve-6MYBOAX6.js +222 -0
- package/dist/serve-6MYBOAX6.js.map +1 -0
- package/dist/{session-NUPHC6ZN.js → session-FPCPCVQX.js} +10 -10
- package/dist/summary-formatter-JIGWN4QS.js +180 -0
- package/dist/summary-formatter-JIGWN4QS.js.map +1 -0
- package/dist/{trade-VGECC4QZ.js → trade-NO6CNWCA.js} +19 -17
- package/dist/{trade-VGECC4QZ.js.map → trade-NO6CNWCA.js.map} +1 -1
- package/dist/{xmtp-JO7W6RFK.js → xmtp-ACB2W573.js} +6 -6
- package/package.json +5 -2
- package/dist/agent-5YEJ6W6W.js.map +0 -1
- package/dist/chunk-4EHI4SKM.js.map +0 -1
- package/dist/chunk-6U3A6ZJ2.js.map +0 -1
- package/dist/chunk-GMVDHSCF.js.map +0 -1
- package/dist/research-NR4VC5LN.js +0 -14
- /package/dist/{addresses-DCQMJJFI.js.map → addresses-DOWLGXGE.js.map} +0 -0
- /package/dist/{chat-UWJ6JBFE.js.map → chat-MRTPUZ3X.js.map} +0 -0
- /package/dist/{chunk-FOOZC4AM.js.map → chunk-2LRC7I33.js.map} +0 -0
- /package/dist/{chunk-JNDEZFXW.js.map → chunk-E6LH2FJ6.js.map} +0 -0
- /package/dist/{chunk-XAUMJZU2.js.map → chunk-FWC57PT7.js.map} +0 -0
- /package/dist/{chunk-4OAZ7TP5.js.map → chunk-IS2CLLUT.js.map} +0 -0
- /package/dist/{chunk-T2IA6CCW.js.map → chunk-KJ35EF7N.js.map} +0 -0
- /package/dist/{chunk-5Q3VT6NQ.js.map → chunk-R7X33D2S.js.map} +0 -0
- /package/dist/{chunk-SWKHSKDH.js.map → chunk-XDBRKW7I.js.map} +0 -0
- /package/dist/{client-DRVUILER.js.map → client-UHHCB6EZ.js.map} +0 -0
- /package/dist/{config-YRERDZSL.js.map → config-REASKDIK.js.map} +0 -0
- /package/dist/{eas-KBU2BAOD.js.map → eas-PEEWGSAO.js.map} +0 -0
- /package/dist/{governor-KH7RNH2E.js.map → governor-MG5VVSU4.js.map} +0 -0
- /package/dist/{network-3HB46JMQ.js.map → network-3ZU7UBDU.js.map} +0 -0
- /package/dist/{price-TTQWI672.js.map → price-WCCXWPDM.js.map} +0 -0
- /package/dist/{research-J3HYU3N6.js.map → research-QBKISW2M.js.map} +0 -0
- /package/dist/{research-NR4VC5LN.js.map → research-YJWSQS2A.js.map} +0 -0
- /package/dist/{session-NUPHC6ZN.js.map → session-FPCPCVQX.js.map} +0 -0
- /package/dist/{xmtp-JO7W6RFK.js.map → xmtp-ACB2W573.js.map} +0 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
// src/agent/technical.ts
|
|
2
|
+
function calculateSMA(values, period) {
|
|
3
|
+
if (values.length === 0) return [];
|
|
4
|
+
const result = [];
|
|
5
|
+
for (let i = 0; i < values.length; i++) {
|
|
6
|
+
if (i < period - 1) {
|
|
7
|
+
result.push(NaN);
|
|
8
|
+
} else {
|
|
9
|
+
let sum = 0;
|
|
10
|
+
for (let j = i - period + 1; j <= i; j++) {
|
|
11
|
+
sum += values[j];
|
|
12
|
+
}
|
|
13
|
+
result.push(sum / period);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
function calculateEMA(values, period) {
|
|
19
|
+
if (values.length === 0) return [];
|
|
20
|
+
const result = [];
|
|
21
|
+
const k = 2 / (period + 1);
|
|
22
|
+
for (let i = 0; i < values.length; i++) {
|
|
23
|
+
if (i < period - 1) {
|
|
24
|
+
result.push(NaN);
|
|
25
|
+
} else if (i === period - 1) {
|
|
26
|
+
let sum = 0;
|
|
27
|
+
let count = 0;
|
|
28
|
+
for (let j = 0; j < period; j++) {
|
|
29
|
+
if (!isNaN(values[j])) {
|
|
30
|
+
sum += values[j];
|
|
31
|
+
count++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (count > 0) {
|
|
35
|
+
result.push(sum / count);
|
|
36
|
+
} else {
|
|
37
|
+
result.push(NaN);
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
const prev = result[i - 1];
|
|
41
|
+
const current = values[i];
|
|
42
|
+
if (!isNaN(current) && !isNaN(prev)) {
|
|
43
|
+
result.push(current * k + prev * (1 - k));
|
|
44
|
+
} else {
|
|
45
|
+
result.push(NaN);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function calculateRSI(candles, period = 14) {
|
|
52
|
+
if (candles.length < 2) return [];
|
|
53
|
+
const closes = candles.map((c) => c.close);
|
|
54
|
+
const result = [];
|
|
55
|
+
const changes = [];
|
|
56
|
+
for (let i = 0; i < closes.length; i++) {
|
|
57
|
+
if (i === 0) {
|
|
58
|
+
changes.push(0);
|
|
59
|
+
} else {
|
|
60
|
+
changes.push(closes[i] - closes[i - 1]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let avgGain = 0;
|
|
64
|
+
let avgLoss = 0;
|
|
65
|
+
for (let i = 0; i < closes.length; i++) {
|
|
66
|
+
if (i < period) {
|
|
67
|
+
result.push(NaN);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (i === period) {
|
|
71
|
+
let sumGain = 0;
|
|
72
|
+
let sumLoss = 0;
|
|
73
|
+
for (let j = 1; j <= period; j++) {
|
|
74
|
+
const change = changes[j];
|
|
75
|
+
sumGain += Math.max(change, 0);
|
|
76
|
+
sumLoss += Math.max(-change, 0);
|
|
77
|
+
}
|
|
78
|
+
avgGain = sumGain / period;
|
|
79
|
+
avgLoss = sumLoss / period;
|
|
80
|
+
} else {
|
|
81
|
+
const change = changes[i];
|
|
82
|
+
const alpha = 1 / period;
|
|
83
|
+
avgGain = (1 - alpha) * avgGain + alpha * Math.max(change, 0);
|
|
84
|
+
avgLoss = (1 - alpha) * avgLoss + alpha * Math.max(-change, 0);
|
|
85
|
+
}
|
|
86
|
+
if (avgLoss === 0) {
|
|
87
|
+
result.push(100);
|
|
88
|
+
} else {
|
|
89
|
+
const rs = avgGain / avgLoss;
|
|
90
|
+
result.push(100 - 100 / (1 + rs));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
function calculateMACD(candles, fast = 12, slow = 26, signalPeriod = 9) {
|
|
96
|
+
const closes = candles.map((c) => c.close);
|
|
97
|
+
const emaFast = calculateEMA(closes, fast);
|
|
98
|
+
const emaSlow = calculateEMA(closes, slow);
|
|
99
|
+
const macdLine = [];
|
|
100
|
+
for (let i = 0; i < closes.length; i++) {
|
|
101
|
+
if (isNaN(emaFast[i]) || isNaN(emaSlow[i])) {
|
|
102
|
+
macdLine.push(NaN);
|
|
103
|
+
} else {
|
|
104
|
+
macdLine.push(emaFast[i] - emaSlow[i]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const signalLine = calculateEMA(macdLine, signalPeriod);
|
|
108
|
+
const histogram = [];
|
|
109
|
+
for (let i = 0; i < macdLine.length; i++) {
|
|
110
|
+
if (isNaN(macdLine[i]) || isNaN(signalLine[i])) {
|
|
111
|
+
histogram.push(NaN);
|
|
112
|
+
} else {
|
|
113
|
+
histogram.push(macdLine[i] - signalLine[i]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { macd: macdLine, signal: signalLine, histogram };
|
|
117
|
+
}
|
|
118
|
+
function calculateBollingerBands(candles, period = 20, stdDevMult = 2) {
|
|
119
|
+
if (candles.length === 0) {
|
|
120
|
+
return { upper: [], middle: [], lower: [], width: [] };
|
|
121
|
+
}
|
|
122
|
+
const closes = candles.map((c) => c.close);
|
|
123
|
+
const middle = calculateSMA(closes, period);
|
|
124
|
+
const upper = [];
|
|
125
|
+
const lower = [];
|
|
126
|
+
const width = [];
|
|
127
|
+
for (let i = 0; i < closes.length; i++) {
|
|
128
|
+
if (isNaN(middle[i])) {
|
|
129
|
+
upper.push(NaN);
|
|
130
|
+
lower.push(NaN);
|
|
131
|
+
width.push(NaN);
|
|
132
|
+
} else {
|
|
133
|
+
let sumSq = 0;
|
|
134
|
+
for (let j = i - period + 1; j <= i; j++) {
|
|
135
|
+
sumSq += (closes[j] - middle[i]) ** 2;
|
|
136
|
+
}
|
|
137
|
+
const sd = Math.sqrt(sumSq / period);
|
|
138
|
+
upper.push(middle[i] + stdDevMult * sd);
|
|
139
|
+
lower.push(middle[i] - stdDevMult * sd);
|
|
140
|
+
width.push(middle[i] !== 0 ? stdDevMult * 2 * sd / middle[i] : 0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { upper, middle, lower, width };
|
|
144
|
+
}
|
|
145
|
+
function calculateATR(candles, period = 14) {
|
|
146
|
+
if (candles.length === 0) return [];
|
|
147
|
+
const trueRanges = [];
|
|
148
|
+
for (let i = 0; i < candles.length; i++) {
|
|
149
|
+
const c = candles[i];
|
|
150
|
+
if (i === 0) {
|
|
151
|
+
trueRanges.push(c.high - c.low);
|
|
152
|
+
} else {
|
|
153
|
+
const prev = candles[i - 1];
|
|
154
|
+
const tr = Math.max(c.high - c.low, Math.abs(c.high - prev.close), Math.abs(c.low - prev.close));
|
|
155
|
+
trueRanges.push(tr);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const result = [];
|
|
159
|
+
for (let i = 0; i < trueRanges.length; i++) {
|
|
160
|
+
if (i < period - 1) {
|
|
161
|
+
result.push(NaN);
|
|
162
|
+
} else if (i === period - 1) {
|
|
163
|
+
let sum = 0;
|
|
164
|
+
for (let j = 0; j < period; j++) sum += trueRanges[j];
|
|
165
|
+
result.push(sum / period);
|
|
166
|
+
} else {
|
|
167
|
+
result.push((result[i - 1] * (period - 1) + trueRanges[i]) / period);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
function calculateVWAP(candles) {
|
|
173
|
+
const result = [];
|
|
174
|
+
let cumulativeTPV = 0;
|
|
175
|
+
let cumulativeVolume = 0;
|
|
176
|
+
for (const c of candles) {
|
|
177
|
+
const typicalPrice = (c.high + c.low + c.close) / 3;
|
|
178
|
+
cumulativeTPV += typicalPrice * c.volume;
|
|
179
|
+
cumulativeVolume += c.volume;
|
|
180
|
+
result.push(cumulativeVolume > 0 ? cumulativeTPV / cumulativeVolume : typicalPrice);
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
function detectBBSqueeze(candles) {
|
|
185
|
+
const bb = calculateBollingerBands(candles, 20, 2);
|
|
186
|
+
const validWidths = bb.width.filter((w) => !isNaN(w));
|
|
187
|
+
if (validWidths.length < 20) return false;
|
|
188
|
+
const current = validWidths[validWidths.length - 1];
|
|
189
|
+
const recent20 = validWidths.slice(-20);
|
|
190
|
+
const minWidth = Math.min(...recent20);
|
|
191
|
+
return current <= minWidth * 1.01;
|
|
192
|
+
}
|
|
193
|
+
function getLatestSignals(candles) {
|
|
194
|
+
if (candles.length < 2) {
|
|
195
|
+
throw new Error("Need at least 2 candles for technical analysis");
|
|
196
|
+
}
|
|
197
|
+
const last = (arr) => {
|
|
198
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
199
|
+
if (!isNaN(arr[i])) return arr[i];
|
|
200
|
+
}
|
|
201
|
+
return NaN;
|
|
202
|
+
};
|
|
203
|
+
const rsiArr = calculateRSI(candles, 8);
|
|
204
|
+
const macdResult = calculateMACD(candles, 12, 26, 9);
|
|
205
|
+
const bbResult = calculateBollingerBands(candles, 20, 2);
|
|
206
|
+
const closes = candles.map((c) => c.close);
|
|
207
|
+
const ema8 = calculateEMA(closes, 8);
|
|
208
|
+
const ema21 = calculateEMA(closes, 21);
|
|
209
|
+
const ema50 = calculateEMA(closes, 50);
|
|
210
|
+
const ema200 = calculateEMA(closes, 200);
|
|
211
|
+
const atrArr = calculateATR(candles, 14);
|
|
212
|
+
const vwapArr = calculateVWAP(candles);
|
|
213
|
+
const volumes = candles.map((c) => c.volume);
|
|
214
|
+
const currentVol = volumes[volumes.length - 1] ?? 0;
|
|
215
|
+
const recent20Vol = volumes.slice(-20);
|
|
216
|
+
const avg20Vol = recent20Vol.length > 0 ? recent20Vol.reduce((a, b) => a + b, 0) / recent20Vol.length : 0;
|
|
217
|
+
return {
|
|
218
|
+
rsi: last(rsiArr),
|
|
219
|
+
macd: {
|
|
220
|
+
value: last(macdResult.macd),
|
|
221
|
+
signal: last(macdResult.signal),
|
|
222
|
+
histogram: last(macdResult.histogram)
|
|
223
|
+
},
|
|
224
|
+
bb: {
|
|
225
|
+
upper: last(bbResult.upper),
|
|
226
|
+
middle: last(bbResult.middle),
|
|
227
|
+
lower: last(bbResult.lower),
|
|
228
|
+
width: last(bbResult.width),
|
|
229
|
+
squeeze: detectBBSqueeze(candles)
|
|
230
|
+
},
|
|
231
|
+
ema: {
|
|
232
|
+
ema8: last(ema8),
|
|
233
|
+
ema21: last(ema21),
|
|
234
|
+
ema50: last(ema50),
|
|
235
|
+
ema200: last(ema200)
|
|
236
|
+
},
|
|
237
|
+
atr: last(atrArr),
|
|
238
|
+
vwap: last(vwapArr),
|
|
239
|
+
volume: {
|
|
240
|
+
current: currentVol,
|
|
241
|
+
avg20: avg20Vol,
|
|
242
|
+
ratio: avg20Vol > 0 ? currentVol / avg20Vol : 0
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/providers/data/hyperliquid.ts
|
|
248
|
+
import { mkdir, readFile, writeFile, rename } from "fs/promises";
|
|
249
|
+
import { join } from "path";
|
|
250
|
+
import { homedir } from "os";
|
|
251
|
+
var HYPERLIQUID_BASE = "https://api.hyperliquid.xyz/info";
|
|
252
|
+
function safeNumber(x) {
|
|
253
|
+
if (x === void 0 || x === null) return null;
|
|
254
|
+
const n = typeof x === "number" ? x : parseFloat(x);
|
|
255
|
+
return Number.isFinite(n) ? n : null;
|
|
256
|
+
}
|
|
257
|
+
var OI_STALE_MS = 30 * 60 * 1e3;
|
|
258
|
+
var TOKEN_TO_COIN = {
|
|
259
|
+
bitcoin: "BTC",
|
|
260
|
+
ethereum: "ETH",
|
|
261
|
+
solana: "SOL",
|
|
262
|
+
arbitrum: "ARB",
|
|
263
|
+
chainlink: "LINK",
|
|
264
|
+
aave: "AAVE",
|
|
265
|
+
uniswap: "UNI",
|
|
266
|
+
dogecoin: "DOGE",
|
|
267
|
+
avalanche: "AVAX",
|
|
268
|
+
"avalanche-2": "AVAX",
|
|
269
|
+
near: "NEAR",
|
|
270
|
+
sui: "SUI",
|
|
271
|
+
aptos: "APT",
|
|
272
|
+
injective: "INJ",
|
|
273
|
+
pendle: "PENDLE",
|
|
274
|
+
pepe: "PEPE",
|
|
275
|
+
polygon: "MATIC",
|
|
276
|
+
optimism: "OP",
|
|
277
|
+
litecoin: "LTC",
|
|
278
|
+
cosmos: "ATOM",
|
|
279
|
+
filecoin: "FIL",
|
|
280
|
+
maker: "MKR",
|
|
281
|
+
cardano: "ADA",
|
|
282
|
+
polkadot: "DOT",
|
|
283
|
+
render: "RENDER",
|
|
284
|
+
jupiter: "JUP",
|
|
285
|
+
// Extended for full agent watchlist — all HL perps
|
|
286
|
+
hyperliquid: "HYPE",
|
|
287
|
+
ethena: "ENA",
|
|
288
|
+
zcash: "ZEC",
|
|
289
|
+
ripple: "XRP",
|
|
290
|
+
bittensor: "TAO",
|
|
291
|
+
fartcoin: "FARTCOIN",
|
|
292
|
+
binancecoin: "BNB",
|
|
293
|
+
blur: "BLUR",
|
|
294
|
+
"worldcoin-wld": "WLD",
|
|
295
|
+
"pudgy-penguins": "PENGU",
|
|
296
|
+
"fetch-ai": "FET"
|
|
297
|
+
};
|
|
298
|
+
var HyperliquidProvider = class {
|
|
299
|
+
cacheDir;
|
|
300
|
+
cacheTTL = 2 * 60 * 1e3;
|
|
301
|
+
// 2 minutes
|
|
302
|
+
oiCache = /* @__PURE__ */ new Map();
|
|
303
|
+
// persisted; see loadOiCache
|
|
304
|
+
oiCacheFile;
|
|
305
|
+
oiCacheLoaded = false;
|
|
306
|
+
oiCacheLoadPromise = null;
|
|
307
|
+
oiCacheSeq = 0;
|
|
308
|
+
// per-process monotonically-increasing tmp suffix
|
|
309
|
+
constructor() {
|
|
310
|
+
this.cacheDir = join(homedir(), ".sherwood", "agent", "cache");
|
|
311
|
+
this.oiCacheFile = join(this.cacheDir, "hl-oi.json");
|
|
312
|
+
this.oiCacheLoadPromise = this.loadOiCache();
|
|
313
|
+
}
|
|
314
|
+
/** Load the OI cache from disk. Called once at construction. Never throws. */
|
|
315
|
+
async loadOiCache() {
|
|
316
|
+
try {
|
|
317
|
+
const raw = await readFile(this.oiCacheFile, "utf-8");
|
|
318
|
+
const parsed = JSON.parse(raw);
|
|
319
|
+
if (parsed && typeof parsed === "object") {
|
|
320
|
+
for (const [coin, entry] of Object.entries(parsed)) {
|
|
321
|
+
if (entry && Number.isFinite(entry.openInterest) && Number.isFinite(entry.timestamp)) {
|
|
322
|
+
this.oiCache.set(coin, entry);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
} finally {
|
|
328
|
+
this.oiCacheLoaded = true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async ensureOiCacheLoaded() {
|
|
332
|
+
if (this.oiCacheLoaded) return;
|
|
333
|
+
if (this.oiCacheLoadPromise) await this.oiCacheLoadPromise;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Persist the OI cache to disk using an atomic tmp-rename write.
|
|
337
|
+
* Fire-and-forget: callers do NOT await this. Errors are logged as warnings
|
|
338
|
+
* so a slow or full disk never blocks the signal hot path.
|
|
339
|
+
*/
|
|
340
|
+
persistOiCache() {
|
|
341
|
+
const snapshot = {};
|
|
342
|
+
for (const [coin, entry] of this.oiCache.entries()) {
|
|
343
|
+
snapshot[coin] = entry;
|
|
344
|
+
}
|
|
345
|
+
const tmp = `${this.oiCacheFile}.tmp.${process.pid}.${++this.oiCacheSeq}`;
|
|
346
|
+
void (async () => {
|
|
347
|
+
try {
|
|
348
|
+
await mkdir(this.cacheDir, { recursive: true });
|
|
349
|
+
await writeFile(tmp, JSON.stringify(snapshot), "utf-8");
|
|
350
|
+
await rename(tmp, this.oiCacheFile);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
console.warn(`[hyperliquid] oi-cache persist failed: ${err.message}`);
|
|
353
|
+
}
|
|
354
|
+
})();
|
|
355
|
+
}
|
|
356
|
+
/** Get comprehensive Hyperliquid data for a token. Returns null if token not supported or API failure. */
|
|
357
|
+
async getHyperliquidData(tokenId) {
|
|
358
|
+
const coin = TOKEN_TO_COIN[tokenId];
|
|
359
|
+
if (!coin) return null;
|
|
360
|
+
const cached = await this.readCache(tokenId);
|
|
361
|
+
if (cached) return cached;
|
|
362
|
+
await this.ensureOiCacheLoaded();
|
|
363
|
+
try {
|
|
364
|
+
const [metaResult, bookResult, tradesResult] = await Promise.allSettled([
|
|
365
|
+
this.fetchMetaAndAssetCtxs(),
|
|
366
|
+
this.fetchL2Book(coin),
|
|
367
|
+
this.fetchRecentTrades(coin)
|
|
368
|
+
]);
|
|
369
|
+
if (metaResult.status !== "fulfilled" || !metaResult.value) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
const [meta, assetCtxs] = metaResult.value;
|
|
373
|
+
const assetIdx = meta.universe.findIndex((asset) => asset.name === coin);
|
|
374
|
+
if (assetIdx === -1 || !assetCtxs[assetIdx]) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const assetData = assetCtxs[assetIdx];
|
|
378
|
+
const fundingRate = safeNumber(assetData.funding);
|
|
379
|
+
const openInterest = safeNumber(assetData.openInterest);
|
|
380
|
+
const volume24h = safeNumber(assetData.dayNtlVlm);
|
|
381
|
+
const markPrice = safeNumber(assetData.markPx);
|
|
382
|
+
const oraclePrice = safeNumber(assetData.oraclePx);
|
|
383
|
+
const prevDayPrice = safeNumber(assetData.prevDayPx);
|
|
384
|
+
if (fundingRate === null || openInterest === null || volume24h === null || markPrice === null || oraclePrice === null || prevDayPrice === null) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const prior = this.oiCache.get(coin);
|
|
388
|
+
const now = Date.now();
|
|
389
|
+
const priorIsFresh = prior !== void 0 && now - prior.timestamp < OI_STALE_MS;
|
|
390
|
+
const prevOI = priorIsFresh ? prior.openInterest : openInterest;
|
|
391
|
+
const oiChangePct = prevOI > 0 ? (openInterest - prevOI) / prevOI * 100 : 0;
|
|
392
|
+
this.oiCache.set(coin, { openInterest, timestamp: now });
|
|
393
|
+
this.persistOiCache();
|
|
394
|
+
let orderBookImbalance = 0;
|
|
395
|
+
if (bookResult.status === "fulfilled" && bookResult.value) {
|
|
396
|
+
orderBookImbalance = this.calculateOrderBookImbalance(bookResult.value);
|
|
397
|
+
}
|
|
398
|
+
let largeTradesBias = 0;
|
|
399
|
+
if (tradesResult.status === "fulfilled" && tradesResult.value) {
|
|
400
|
+
largeTradesBias = this.calculateLargeTradesBias(tradesResult.value);
|
|
401
|
+
}
|
|
402
|
+
const data = {
|
|
403
|
+
fundingRate,
|
|
404
|
+
openInterest,
|
|
405
|
+
oiChangePct,
|
|
406
|
+
volume24h,
|
|
407
|
+
markPrice,
|
|
408
|
+
oraclePrice,
|
|
409
|
+
prevDayPrice,
|
|
410
|
+
orderBookImbalance,
|
|
411
|
+
largeTradesBias
|
|
412
|
+
};
|
|
413
|
+
await this.writeCache(tokenId, data);
|
|
414
|
+
return data;
|
|
415
|
+
} catch (err) {
|
|
416
|
+
console.error(`Hyperliquid API error for ${tokenId}: ${err.message}`);
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/** Fetch metadata and asset contexts (funding, OI, volume, prices). */
|
|
421
|
+
async fetchMetaAndAssetCtxs() {
|
|
422
|
+
const response = await fetch(HYPERLIQUID_BASE, {
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers: { "Content-Type": "application/json" },
|
|
425
|
+
body: JSON.stringify({ type: "metaAndAssetCtxs" })
|
|
426
|
+
});
|
|
427
|
+
if (!response.ok) return null;
|
|
428
|
+
const data = await response.json();
|
|
429
|
+
return data;
|
|
430
|
+
}
|
|
431
|
+
/** Fetch L2 order book for a coin. */
|
|
432
|
+
async fetchL2Book(coin) {
|
|
433
|
+
const response = await fetch(HYPERLIQUID_BASE, {
|
|
434
|
+
method: "POST",
|
|
435
|
+
headers: { "Content-Type": "application/json" },
|
|
436
|
+
body: JSON.stringify({ type: "l2Book", coin })
|
|
437
|
+
});
|
|
438
|
+
if (!response.ok) return null;
|
|
439
|
+
return await response.json();
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Fetch OHLCV candles from Hyperliquid. Free, no rate limit, real-time.
|
|
443
|
+
* Replaces CoinGecko OHLC as the primary candle source so the technical
|
|
444
|
+
* signal stack doesn't go blind when CG's free tier 429s.
|
|
445
|
+
*
|
|
446
|
+
* @param tokenId CoinGecko token ID (resolved via TOKEN_TO_COIN)
|
|
447
|
+
* @param interval HL interval string: '1h', '4h', '1d'
|
|
448
|
+
* @param lookbackMs How far back to fetch (default 30 days)
|
|
449
|
+
* @returns Array of Candle objects, or null if the token isn't mapped
|
|
450
|
+
*/
|
|
451
|
+
async getCandles(tokenId, interval = "4h", lookbackMs = 30 * 24 * 60 * 60 * 1e3) {
|
|
452
|
+
const coin = TOKEN_TO_COIN[tokenId];
|
|
453
|
+
if (!coin) return null;
|
|
454
|
+
try {
|
|
455
|
+
const now = Date.now();
|
|
456
|
+
const response = await fetch(HYPERLIQUID_BASE, {
|
|
457
|
+
method: "POST",
|
|
458
|
+
headers: { "Content-Type": "application/json" },
|
|
459
|
+
body: JSON.stringify({
|
|
460
|
+
type: "candleSnapshot",
|
|
461
|
+
req: { coin, interval, startTime: now - lookbackMs, endTime: now }
|
|
462
|
+
})
|
|
463
|
+
});
|
|
464
|
+
if (!response.ok) return null;
|
|
465
|
+
const raw = await response.json();
|
|
466
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
467
|
+
return raw.map((c) => {
|
|
468
|
+
const o = safeNumber(c.o);
|
|
469
|
+
const h = safeNumber(c.h);
|
|
470
|
+
const l = safeNumber(c.l);
|
|
471
|
+
const close = safeNumber(c.c);
|
|
472
|
+
const v = safeNumber(c.v);
|
|
473
|
+
if (o === null || h === null || l === null || close === null) return null;
|
|
474
|
+
return { timestamp: c.t, open: o, high: h, low: l, close, volume: v ?? 0 };
|
|
475
|
+
}).filter((c) => c !== null);
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/** Fetch recent trades for a coin. */
|
|
481
|
+
async fetchRecentTrades(coin) {
|
|
482
|
+
const response = await fetch(HYPERLIQUID_BASE, {
|
|
483
|
+
method: "POST",
|
|
484
|
+
headers: { "Content-Type": "application/json" },
|
|
485
|
+
body: JSON.stringify({ type: "recentTrades", coin })
|
|
486
|
+
});
|
|
487
|
+
if (!response.ok) return null;
|
|
488
|
+
return await response.json();
|
|
489
|
+
}
|
|
490
|
+
/** Calculate order book imbalance from L2 book. */
|
|
491
|
+
calculateOrderBookImbalance(book) {
|
|
492
|
+
const [bids, asks] = book.levels;
|
|
493
|
+
const sumDepth = (levels) => levels.slice(0, 10).reduce((sum, level) => {
|
|
494
|
+
const sz = safeNumber(level.sz);
|
|
495
|
+
return sz === null ? sum : sum + sz;
|
|
496
|
+
}, 0);
|
|
497
|
+
const bidDepth = sumDepth(bids);
|
|
498
|
+
const askDepth = sumDepth(asks);
|
|
499
|
+
const totalDepth = bidDepth + askDepth;
|
|
500
|
+
if (totalDepth === 0) return 0;
|
|
501
|
+
return (bidDepth - askDepth) / totalDepth;
|
|
502
|
+
}
|
|
503
|
+
/** Calculate large trades bias from recent trades (>$50K in last hour). */
|
|
504
|
+
calculateLargeTradesBias(trades) {
|
|
505
|
+
const oneHourAgo = Date.now() - 60 * 60 * 1e3;
|
|
506
|
+
const recentTrades = trades.filter((t) => t.time > oneHourAgo);
|
|
507
|
+
let buyVolume = 0;
|
|
508
|
+
let sellVolume = 0;
|
|
509
|
+
for (const trade of recentTrades) {
|
|
510
|
+
const size = safeNumber(trade.sz);
|
|
511
|
+
const price = safeNumber(trade.px);
|
|
512
|
+
if (size === null || price === null) continue;
|
|
513
|
+
const notional = size * price;
|
|
514
|
+
if (notional > 5e4) {
|
|
515
|
+
if (trade.side === "B") {
|
|
516
|
+
buyVolume += notional;
|
|
517
|
+
} else if (trade.side === "A") {
|
|
518
|
+
sellVolume += notional;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const totalVolume = buyVolume + sellVolume;
|
|
523
|
+
if (totalVolume === 0) return 0;
|
|
524
|
+
return (buyVolume - sellVolume) / totalVolume;
|
|
525
|
+
}
|
|
526
|
+
/** Read cached Hyperliquid data. */
|
|
527
|
+
async readCache(tokenId) {
|
|
528
|
+
try {
|
|
529
|
+
const cacheFile = join(this.cacheDir, `hl-${tokenId}.json`);
|
|
530
|
+
const raw = await readFile(cacheFile, "utf-8");
|
|
531
|
+
const cached = JSON.parse(raw);
|
|
532
|
+
if (Date.now() - cached.ts < this.cacheTTL) {
|
|
533
|
+
return cached.data;
|
|
534
|
+
}
|
|
535
|
+
} catch {
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
/** Write Hyperliquid data to cache. */
|
|
540
|
+
async writeCache(tokenId, data) {
|
|
541
|
+
try {
|
|
542
|
+
await mkdir(this.cacheDir, { recursive: true });
|
|
543
|
+
const cacheFile = join(this.cacheDir, `hl-${tokenId}.json`);
|
|
544
|
+
await writeFile(cacheFile, JSON.stringify({ ts: Date.now(), data }), "utf-8");
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// src/lib/hyperliquid-executor.ts
|
|
551
|
+
import { execFile } from "child_process";
|
|
552
|
+
import { resolve } from "path";
|
|
553
|
+
import { homedir as homedir2 } from "os";
|
|
554
|
+
var HL_SCRIPT = resolve(
|
|
555
|
+
homedir2(),
|
|
556
|
+
".hermes/skills/openclaw-imports/hyperliquid/scripts/hyperliquid.mjs"
|
|
557
|
+
);
|
|
558
|
+
var TOKEN_TO_HL_COIN = {
|
|
559
|
+
bitcoin: "BTC",
|
|
560
|
+
ethereum: "ETH",
|
|
561
|
+
solana: "SOL",
|
|
562
|
+
arbitrum: "ARB",
|
|
563
|
+
dogecoin: "DOGE",
|
|
564
|
+
chainlink: "LINK",
|
|
565
|
+
aave: "AAVE",
|
|
566
|
+
uniswap: "UNI",
|
|
567
|
+
ripple: "XRP",
|
|
568
|
+
polkadot: "DOT",
|
|
569
|
+
avalanche: "AVAX",
|
|
570
|
+
hyperliquid: "HYPE",
|
|
571
|
+
zcash: "ZEC",
|
|
572
|
+
bittensor: "TAO",
|
|
573
|
+
"worldcoin-wld": "WLD",
|
|
574
|
+
fartcoin: "FARTCOIN",
|
|
575
|
+
"fetch-ai": "FET",
|
|
576
|
+
pepe: "PEPE",
|
|
577
|
+
pendle: "PENDLE",
|
|
578
|
+
sui: "SUI",
|
|
579
|
+
near: "NEAR",
|
|
580
|
+
aptos: "APT"
|
|
581
|
+
};
|
|
582
|
+
function resolveHLCoin(tokenId) {
|
|
583
|
+
return TOKEN_TO_HL_COIN[tokenId];
|
|
584
|
+
}
|
|
585
|
+
function runHLScript(command, args = []) {
|
|
586
|
+
return new Promise((resolve2, reject) => {
|
|
587
|
+
const env = { ...process.env };
|
|
588
|
+
if (process.env.HYPERLIQUID_PRIVATE_KEY) {
|
|
589
|
+
env.HYPERLIQUID_PRIVATE_KEY = process.env.HYPERLIQUID_PRIVATE_KEY;
|
|
590
|
+
}
|
|
591
|
+
if (process.env.HYPERLIQUID_ADDRESS) {
|
|
592
|
+
env.HYPERLIQUID_ADDRESS = process.env.HYPERLIQUID_ADDRESS;
|
|
593
|
+
}
|
|
594
|
+
execFile(
|
|
595
|
+
"node",
|
|
596
|
+
[HL_SCRIPT, command, ...args],
|
|
597
|
+
{ env, timeout: 3e4 },
|
|
598
|
+
(error, stdout, stderr) => {
|
|
599
|
+
if (error) {
|
|
600
|
+
const msg = stderr?.trim() || error.message;
|
|
601
|
+
reject(new Error(`HL script failed: ${msg}`));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
resolve2(stdout.trim());
|
|
605
|
+
}
|
|
606
|
+
);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function parseOrderResponse(raw) {
|
|
610
|
+
try {
|
|
611
|
+
const data = JSON.parse(raw);
|
|
612
|
+
const statuses = data?.response?.data?.statuses;
|
|
613
|
+
if (!statuses || statuses.length === 0) {
|
|
614
|
+
if (data?.response?.type === "error") {
|
|
615
|
+
return { success: false, error: data.response.data || "Unknown HL error" };
|
|
616
|
+
}
|
|
617
|
+
return { success: false, error: `Unexpected response: ${raw.slice(0, 200)}` };
|
|
618
|
+
}
|
|
619
|
+
const status = statuses[0];
|
|
620
|
+
if (status.filled) {
|
|
621
|
+
return {
|
|
622
|
+
success: true,
|
|
623
|
+
orderId: String(status.filled.oid),
|
|
624
|
+
executedPrice: parseFloat(status.filled.avgPx),
|
|
625
|
+
executedSize: parseFloat(status.filled.totalSz)
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
if (status.resting) {
|
|
629
|
+
return {
|
|
630
|
+
success: true,
|
|
631
|
+
orderId: String(status.resting.oid)
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
if (status.error) {
|
|
635
|
+
return { success: false, error: status.error };
|
|
636
|
+
}
|
|
637
|
+
return { success: false, error: `Unrecognized status: ${JSON.stringify(status)}` };
|
|
638
|
+
} catch {
|
|
639
|
+
return { success: false, error: `Failed to parse HL response: ${raw.slice(0, 200)}` };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function hlMarketBuy(coin, sizeInToken) {
|
|
643
|
+
const raw = await runHLScript("market-buy", [coin, String(sizeInToken)]);
|
|
644
|
+
return parseOrderResponse(raw);
|
|
645
|
+
}
|
|
646
|
+
async function hlMarketSell(coin, sizeInToken) {
|
|
647
|
+
const raw = await runHLScript("market-sell", [coin, String(sizeInToken)]);
|
|
648
|
+
return parseOrderResponse(raw);
|
|
649
|
+
}
|
|
650
|
+
async function hlPlaceLimitOrder(coin, isBuy, sizeInToken, limitPrice) {
|
|
651
|
+
const cmd = isBuy ? "limit-buy" : "limit-sell";
|
|
652
|
+
const raw = await runHLScript(cmd, [coin, String(sizeInToken), String(limitPrice)]);
|
|
653
|
+
return parseOrderResponse(raw);
|
|
654
|
+
}
|
|
655
|
+
async function hlCancelAllOrders(coin) {
|
|
656
|
+
const args = coin ? [coin] : [];
|
|
657
|
+
return runHLScript("cancel-all", args);
|
|
658
|
+
}
|
|
659
|
+
function validateHLEnv() {
|
|
660
|
+
if (!process.env.HYPERLIQUID_PRIVATE_KEY) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
"HYPERLIQUID_PRIVATE_KEY env var is required for hyperliquid-perp mode. Export it before running with --mode hyperliquid-perp."
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
var META_TTL_MS = 60 * 60 * 1e3;
|
|
667
|
+
var _metaCache = null;
|
|
668
|
+
var _metaCachedAt = 0;
|
|
669
|
+
async function hlGetMeta() {
|
|
670
|
+
if (_metaCache && Date.now() - _metaCachedAt < META_TTL_MS) return _metaCache;
|
|
671
|
+
const raw = await runHLScript("meta");
|
|
672
|
+
const universe = JSON.parse(raw);
|
|
673
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
674
|
+
for (const u of universe) {
|
|
675
|
+
const pxDecimals = 6 - u.szDecimals;
|
|
676
|
+
fresh.set(u.name, { name: u.name, szDecimals: u.szDecimals, pxDecimals });
|
|
677
|
+
}
|
|
678
|
+
_metaCache = fresh;
|
|
679
|
+
_metaCachedAt = Date.now();
|
|
680
|
+
return _metaCache;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export {
|
|
684
|
+
safeNumber,
|
|
685
|
+
HyperliquidProvider,
|
|
686
|
+
calculateEMA,
|
|
687
|
+
calculateRSI,
|
|
688
|
+
calculateMACD,
|
|
689
|
+
calculateBollingerBands,
|
|
690
|
+
calculateATR,
|
|
691
|
+
getLatestSignals,
|
|
692
|
+
resolveHLCoin,
|
|
693
|
+
hlMarketBuy,
|
|
694
|
+
hlMarketSell,
|
|
695
|
+
hlPlaceLimitOrder,
|
|
696
|
+
hlCancelAllOrders,
|
|
697
|
+
validateHLEnv,
|
|
698
|
+
hlGetMeta
|
|
699
|
+
};
|
|
700
|
+
//# sourceMappingURL=chunk-OMW47WOO.js.map
|