@pear-protocol/hyperliquid-sdk 0.1.28 → 0.1.30
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/index.d.ts +1 -0
- package/dist/index.js +171 -27
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -972,6 +972,20 @@ const useUserSelection$1 = create((set, get) => ({
|
|
|
972
972
|
resetToDefaults: () => set((prev) => ({ ...prev, ...DEFAULT_STATE })),
|
|
973
973
|
}));
|
|
974
974
|
|
|
975
|
+
const CANDLE_STALE_WARNING_MS = 90000;
|
|
976
|
+
const getCandleSubscriptionKey = (coin, interval) => `${coin}:${interval}`;
|
|
977
|
+
const debugHyperliquidWs = (message, payload) => {
|
|
978
|
+
if (typeof window === "undefined")
|
|
979
|
+
return;
|
|
980
|
+
if (window.localStorage.getItem("pear:debug:hl-ws") !== "1")
|
|
981
|
+
return;
|
|
982
|
+
if (payload === undefined) {
|
|
983
|
+
console.debug(`[HyperLiquid WS] ${message}`);
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
console.debug(`[HyperLiquid WS] ${message}`, payload);
|
|
987
|
+
}
|
|
988
|
+
};
|
|
975
989
|
const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }) => {
|
|
976
990
|
const { setAllMids, setActiveAssetData, upsertActiveAssetData, setCandleData, deleteCandleSymbol, deleteActiveAssetData, addCandleData, setFinalAtOICaps, setAggregatedClearingHouseState, setRawClearinghouseStates, setAssetContextsByDex, } = useHyperliquidData();
|
|
977
991
|
const { setSpotState, setUserAbstractionMode } = useUserData();
|
|
@@ -986,7 +1000,10 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
986
1000
|
const [subscribedCandleTokens, setSubscribedCandleTokens] = useState([]);
|
|
987
1001
|
const [clearinghouseStateReceived, setClearinghouseStateReceived] = useState(false);
|
|
988
1002
|
const prevCandleIntervalRef = useRef(null);
|
|
1003
|
+
const currentCandleIntervalRef = useRef(candleInterval);
|
|
989
1004
|
const prevActiveAssetAddressRef = useRef(null);
|
|
1005
|
+
const lastCandleMessageAtRef = useRef(null);
|
|
1006
|
+
const lastCandleBySubscriptionRef = useRef(new Map());
|
|
990
1007
|
const pingIntervalRef = useRef(null);
|
|
991
1008
|
const wsRef = useRef(null);
|
|
992
1009
|
const reconnectAttemptsRef = useRef(0);
|
|
@@ -998,8 +1015,11 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
998
1015
|
useEffect(() => {
|
|
999
1016
|
onUserFillsRef.current = onUserFills;
|
|
1000
1017
|
}, [onUserFills]);
|
|
1018
|
+
useEffect(() => {
|
|
1019
|
+
currentCandleIntervalRef.current = candleInterval;
|
|
1020
|
+
}, [candleInterval]);
|
|
1001
1021
|
const handleMessage = useCallback((event) => {
|
|
1002
|
-
var _a;
|
|
1022
|
+
var _a, _b;
|
|
1003
1023
|
try {
|
|
1004
1024
|
const message = JSON.parse(event.data);
|
|
1005
1025
|
// Handle subscription responses
|
|
@@ -1097,7 +1117,27 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1097
1117
|
{
|
|
1098
1118
|
const candleDataItem = response.data;
|
|
1099
1119
|
const symbol = candleDataItem.s || "";
|
|
1120
|
+
const messageInterval = (_b = candleDataItem.i) !== null && _b !== void 0 ? _b : currentCandleIntervalRef.current;
|
|
1121
|
+
if (messageInterval !== currentCandleIntervalRef.current) {
|
|
1122
|
+
debugHyperliquidWs("ignoring stale candle interval", {
|
|
1123
|
+
symbol,
|
|
1124
|
+
messageInterval,
|
|
1125
|
+
currentInterval: currentCandleIntervalRef.current,
|
|
1126
|
+
t: candleDataItem.t,
|
|
1127
|
+
T: candleDataItem.T,
|
|
1128
|
+
});
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1100
1131
|
addCandleData(symbol, candleDataItem);
|
|
1132
|
+
const now = Date.now();
|
|
1133
|
+
lastCandleMessageAtRef.current = now;
|
|
1134
|
+
lastCandleBySubscriptionRef.current.set(getCandleSubscriptionKey(symbol, messageInterval), now);
|
|
1135
|
+
debugHyperliquidWs("candle received", {
|
|
1136
|
+
symbol,
|
|
1137
|
+
interval: messageInterval,
|
|
1138
|
+
t: candleDataItem.t,
|
|
1139
|
+
T: candleDataItem.T,
|
|
1140
|
+
});
|
|
1101
1141
|
}
|
|
1102
1142
|
break;
|
|
1103
1143
|
case "spotState":
|
|
@@ -1166,6 +1206,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1166
1206
|
setSubscribedAddress(null);
|
|
1167
1207
|
setSubscribedTokens([]);
|
|
1168
1208
|
setSubscribedCandleTokens([]);
|
|
1209
|
+
lastCandleBySubscriptionRef.current.clear();
|
|
1169
1210
|
setClearinghouseStateReceived(false);
|
|
1170
1211
|
if (!manualCloseRef.current) {
|
|
1171
1212
|
reconnectAttemptsRef.current += 1;
|
|
@@ -1197,6 +1238,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1197
1238
|
setSubscribedAddress(null);
|
|
1198
1239
|
setSubscribedTokens([]);
|
|
1199
1240
|
setSubscribedCandleTokens([]);
|
|
1241
|
+
lastCandleBySubscriptionRef.current.clear();
|
|
1200
1242
|
setClearinghouseStateReceived(false);
|
|
1201
1243
|
// Close existing socket if in a bad state
|
|
1202
1244
|
if (wsRef.current &&
|
|
@@ -1486,9 +1528,13 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1486
1528
|
if (!isConnected)
|
|
1487
1529
|
return;
|
|
1488
1530
|
const effectiveTokens = selectedTokenSymbols;
|
|
1489
|
-
// Unsubscribe from previous candle subscriptions if interval changed
|
|
1490
1531
|
const prevInterval = prevCandleIntervalRef.current;
|
|
1491
|
-
|
|
1532
|
+
const intervalChanged = prevInterval !== null && prevInterval !== candleInterval;
|
|
1533
|
+
const activeSubscribedCandleTokens = intervalChanged
|
|
1534
|
+
? []
|
|
1535
|
+
: subscribedCandleTokens;
|
|
1536
|
+
// Unsubscribe from previous candle subscriptions if interval changed
|
|
1537
|
+
if (intervalChanged && prevInterval !== null) {
|
|
1492
1538
|
subscribedCandleTokens.forEach((token) => {
|
|
1493
1539
|
const unsubscribeMessage = {
|
|
1494
1540
|
method: "unsubscribe",
|
|
@@ -1499,12 +1545,17 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1499
1545
|
},
|
|
1500
1546
|
};
|
|
1501
1547
|
sendJsonMessage(unsubscribeMessage);
|
|
1548
|
+
lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, prevInterval));
|
|
1549
|
+
debugHyperliquidWs("candle unsubscribe", {
|
|
1550
|
+
token,
|
|
1551
|
+
interval: prevInterval,
|
|
1552
|
+
reason: "interval-change",
|
|
1553
|
+
});
|
|
1502
1554
|
});
|
|
1503
1555
|
setCandleData(new Map());
|
|
1504
|
-
setSubscribedCandleTokens([]);
|
|
1505
1556
|
}
|
|
1506
|
-
const tokensToSubscribe = effectiveTokens.filter((token) => token && !
|
|
1507
|
-
const tokensToUnsubscribe =
|
|
1557
|
+
const tokensToSubscribe = effectiveTokens.filter((token) => token && !activeSubscribedCandleTokens.includes(token));
|
|
1558
|
+
const tokensToUnsubscribe = activeSubscribedCandleTokens.filter((token) => !effectiveTokens.includes(token));
|
|
1508
1559
|
// Unsubscribe from tokens no longer in the list
|
|
1509
1560
|
tokensToUnsubscribe.forEach((token) => {
|
|
1510
1561
|
const unsubscribeMessage = {
|
|
@@ -1516,6 +1567,12 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1516
1567
|
},
|
|
1517
1568
|
};
|
|
1518
1569
|
sendJsonMessage(unsubscribeMessage);
|
|
1570
|
+
lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, candleInterval));
|
|
1571
|
+
debugHyperliquidWs("candle unsubscribe", {
|
|
1572
|
+
token,
|
|
1573
|
+
interval: candleInterval,
|
|
1574
|
+
reason: "token-removed",
|
|
1575
|
+
});
|
|
1519
1576
|
});
|
|
1520
1577
|
// Subscribe to new tokens
|
|
1521
1578
|
tokensToSubscribe.forEach((token) => {
|
|
@@ -1528,6 +1585,11 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1528
1585
|
},
|
|
1529
1586
|
};
|
|
1530
1587
|
sendJsonMessage(subscribeMessage);
|
|
1588
|
+
lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, candleInterval));
|
|
1589
|
+
debugHyperliquidWs("candle subscribe", {
|
|
1590
|
+
token,
|
|
1591
|
+
interval: candleInterval,
|
|
1592
|
+
});
|
|
1531
1593
|
});
|
|
1532
1594
|
// Update subscribed state
|
|
1533
1595
|
if (tokensToSubscribe.length > 0 ||
|
|
@@ -1544,7 +1606,48 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1544
1606
|
subscribedCandleTokens,
|
|
1545
1607
|
sendJsonMessage,
|
|
1546
1608
|
setCandleData,
|
|
1609
|
+
deleteCandleSymbol,
|
|
1547
1610
|
]);
|
|
1611
|
+
useEffect(() => {
|
|
1612
|
+
if (!isConnected)
|
|
1613
|
+
return;
|
|
1614
|
+
const checkCandleSubscriptionHealth = () => {
|
|
1615
|
+
const effectiveTokens = selectedTokenSymbols.filter((token) => token);
|
|
1616
|
+
if (effectiveTokens.length === 0)
|
|
1617
|
+
return;
|
|
1618
|
+
const now = Date.now();
|
|
1619
|
+
const missingSubscriptionState = effectiveTokens.filter((token) => !subscribedCandleTokens.includes(token));
|
|
1620
|
+
const missingCandles = effectiveTokens.filter((token) => {
|
|
1621
|
+
const key = getCandleSubscriptionKey(token, candleInterval);
|
|
1622
|
+
return !lastCandleBySubscriptionRef.current.has(key);
|
|
1623
|
+
});
|
|
1624
|
+
const staleCandles = effectiveTokens.filter((token) => {
|
|
1625
|
+
const key = getCandleSubscriptionKey(token, candleInterval);
|
|
1626
|
+
const lastSeenAt = lastCandleBySubscriptionRef.current.get(key);
|
|
1627
|
+
return (lastSeenAt !== undefined &&
|
|
1628
|
+
now - lastSeenAt > CANDLE_STALE_WARNING_MS);
|
|
1629
|
+
});
|
|
1630
|
+
if (missingSubscriptionState.length === 0 &&
|
|
1631
|
+
missingCandles.length === 0 &&
|
|
1632
|
+
staleCandles.length === 0) {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
debugHyperliquidWs("candle subscription health", {
|
|
1636
|
+
interval: candleInterval,
|
|
1637
|
+
selectedTokens: effectiveTokens,
|
|
1638
|
+
subscribedTokens: subscribedCandleTokens,
|
|
1639
|
+
missingSubscriptionState,
|
|
1640
|
+
missingCandles,
|
|
1641
|
+
staleCandles,
|
|
1642
|
+
lastAnyCandleAgeMs: lastCandleMessageAtRef.current === null
|
|
1643
|
+
? null
|
|
1644
|
+
: now - lastCandleMessageAtRef.current,
|
|
1645
|
+
});
|
|
1646
|
+
};
|
|
1647
|
+
checkCandleSubscriptionHealth();
|
|
1648
|
+
const healthInterval = setInterval(checkCandleSubscriptionHealth, 30000);
|
|
1649
|
+
return () => clearInterval(healthInterval);
|
|
1650
|
+
}, [isConnected, selectedTokenSymbols, candleInterval, subscribedCandleTokens]);
|
|
1548
1651
|
return {
|
|
1549
1652
|
isConnected,
|
|
1550
1653
|
lastError,
|
|
@@ -6633,6 +6736,22 @@ const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
|
|
|
6633
6736
|
return basketCandles;
|
|
6634
6737
|
};
|
|
6635
6738
|
|
|
6739
|
+
const CANDLE_INTERVAL_MS = {
|
|
6740
|
+
'1m': 60 * 1000,
|
|
6741
|
+
'3m': 3 * 60 * 1000,
|
|
6742
|
+
'5m': 5 * 60 * 1000,
|
|
6743
|
+
'15m': 15 * 60 * 1000,
|
|
6744
|
+
'30m': 30 * 60 * 1000,
|
|
6745
|
+
'1h': 60 * 60 * 1000,
|
|
6746
|
+
'2h': 2 * 60 * 60 * 1000,
|
|
6747
|
+
'4h': 4 * 60 * 60 * 1000,
|
|
6748
|
+
'8h': 8 * 60 * 60 * 1000,
|
|
6749
|
+
'12h': 12 * 60 * 60 * 1000,
|
|
6750
|
+
'1d': 24 * 60 * 60 * 1000,
|
|
6751
|
+
'3d': 3 * 24 * 60 * 60 * 1000,
|
|
6752
|
+
'1w': 7 * 24 * 60 * 60 * 1000,
|
|
6753
|
+
'1M': 30 * 24 * 60 * 60 * 1000,
|
|
6754
|
+
};
|
|
6636
6755
|
/**
|
|
6637
6756
|
* Composes historical price fetching with basket candle computation.
|
|
6638
6757
|
* - Listens to `longTokens` and `shortTokens` from user selection.
|
|
@@ -6642,6 +6761,7 @@ const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
|
|
|
6642
6761
|
const useBasketCandles = () => {
|
|
6643
6762
|
const longTokens = useUserSelection$1((state) => state.longTokens);
|
|
6644
6763
|
const shortTokens = useUserSelection$1((state) => state.shortTokens);
|
|
6764
|
+
const candleInterval = useUserSelection$1((state) => state.candleInterval);
|
|
6645
6765
|
const candleData = useHyperliquidData((s) => s.candleData);
|
|
6646
6766
|
const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData, getEffectiveDataBoundary } = useHistoricalPriceData();
|
|
6647
6767
|
const fetchBasketCandles = useCallback(async (startTime, endTime, interval) => {
|
|
@@ -6747,40 +6867,56 @@ const useBasketCandles = () => {
|
|
|
6747
6867
|
const removeRealtimeListener = useCallback((id) => {
|
|
6748
6868
|
listenersRef.current.delete(id);
|
|
6749
6869
|
}, []);
|
|
6750
|
-
// Helper: compute weighted bar from latest snapshot
|
|
6870
|
+
// Helper: compute weighted bar from latest snapshot, carrying forward lagging
|
|
6871
|
+
// symbols so one sparse feed does not suppress every realtime chart update.
|
|
6751
6872
|
const computeRealtimeBar = useCallback(() => {
|
|
6752
6873
|
if (!candleData)
|
|
6753
6874
|
return null;
|
|
6754
6875
|
const allTokens = [...longTokens, ...shortTokens];
|
|
6755
6876
|
if (allTokens.length === 0)
|
|
6756
6877
|
return null;
|
|
6757
|
-
// Collect candles, ensure presence and alignment
|
|
6758
6878
|
const symbolSet = new Set(allTokens.map((t) => t.symbol));
|
|
6759
6879
|
const snapshot = {};
|
|
6880
|
+
let t = null;
|
|
6881
|
+
let T = null;
|
|
6760
6882
|
for (const symbol of symbolSet) {
|
|
6761
6883
|
const c = candleData.get(symbol);
|
|
6762
6884
|
if (!c)
|
|
6763
6885
|
return null; // missing latest candle for symbol
|
|
6764
6886
|
snapshot[symbol] = c;
|
|
6765
|
-
|
|
6766
|
-
// Verify same interval window (t and T match across symbols)
|
|
6767
|
-
let t = null;
|
|
6768
|
-
let T = null;
|
|
6769
|
-
for (const symbol of symbolSet) {
|
|
6770
|
-
const c = snapshot[symbol];
|
|
6771
|
-
if (t === null) {
|
|
6887
|
+
if (t === null || c.t > t) {
|
|
6772
6888
|
t = c.t;
|
|
6773
6889
|
T = c.T;
|
|
6774
6890
|
}
|
|
6775
|
-
else if (c.t !== t || c.T !== T) {
|
|
6776
|
-
return null; // not aligned yet
|
|
6777
|
-
}
|
|
6778
6891
|
}
|
|
6892
|
+
if (t === null || T === null)
|
|
6893
|
+
return null;
|
|
6894
|
+
const intervalMs = CANDLE_INTERVAL_MS[candleInterval];
|
|
6895
|
+
const maxCarryForwardMs = intervalMs * 2;
|
|
6896
|
+
const getCandleForTargetWindow = (symbol) => {
|
|
6897
|
+
const c = snapshot[symbol];
|
|
6898
|
+
if (c.t === t)
|
|
6899
|
+
return c;
|
|
6900
|
+
if (c.t > t || t - c.t > maxCarryForwardMs)
|
|
6901
|
+
return null;
|
|
6902
|
+
const price = c.c;
|
|
6903
|
+
if (!(price > 0))
|
|
6904
|
+
return null;
|
|
6905
|
+
return {
|
|
6906
|
+
s: c.s,
|
|
6907
|
+
t,
|
|
6908
|
+
T,
|
|
6909
|
+
o: price,
|
|
6910
|
+
h: price,
|
|
6911
|
+
l: price,
|
|
6912
|
+
c: price,
|
|
6913
|
+
};
|
|
6914
|
+
};
|
|
6779
6915
|
// Compute weighted OHLC similar to computeBasketCandles
|
|
6780
6916
|
let longOpen = 1, longHigh = 1, longLow = 1, longClose = 1;
|
|
6781
6917
|
let shortOpen = 1, shortHigh = 1, shortLow = 1, shortClose = 1;
|
|
6782
6918
|
for (const token of longTokens) {
|
|
6783
|
-
const c =
|
|
6919
|
+
const c = getCandleForTargetWindow(token.symbol);
|
|
6784
6920
|
if (!c)
|
|
6785
6921
|
return null;
|
|
6786
6922
|
const w = token.weight / 100;
|
|
@@ -6793,7 +6929,7 @@ const useBasketCandles = () => {
|
|
|
6793
6929
|
longClose *= Math.pow(cl, w);
|
|
6794
6930
|
}
|
|
6795
6931
|
for (const token of shortTokens) {
|
|
6796
|
-
const c =
|
|
6932
|
+
const c = getCandleForTargetWindow(token.symbol);
|
|
6797
6933
|
if (!c)
|
|
6798
6934
|
return null;
|
|
6799
6935
|
const w = -(token.weight / 100);
|
|
@@ -6805,8 +6941,6 @@ const useBasketCandles = () => {
|
|
|
6805
6941
|
shortLow *= Math.pow(l, w);
|
|
6806
6942
|
shortClose *= Math.pow(cl, w);
|
|
6807
6943
|
}
|
|
6808
|
-
if (t === null || T === null)
|
|
6809
|
-
return null;
|
|
6810
6944
|
const weighted = {
|
|
6811
6945
|
t,
|
|
6812
6946
|
T,
|
|
@@ -6816,8 +6950,8 @@ const useBasketCandles = () => {
|
|
|
6816
6950
|
c: longClose * shortClose,
|
|
6817
6951
|
};
|
|
6818
6952
|
return weighted;
|
|
6819
|
-
}, [candleData, longTokens, shortTokens]);
|
|
6820
|
-
// Emit realtime bars when
|
|
6953
|
+
}, [candleData, candleInterval, longTokens, shortTokens]);
|
|
6954
|
+
// Emit realtime bars when the latest snapshot can produce a valid bar.
|
|
6821
6955
|
useEffect(() => {
|
|
6822
6956
|
if (listenersRef.current.size === 0)
|
|
6823
6957
|
return;
|
|
@@ -7157,7 +7291,9 @@ const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, t
|
|
|
7157
7291
|
};
|
|
7158
7292
|
};
|
|
7159
7293
|
const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) => {
|
|
7160
|
-
|
|
7294
|
+
const ifHip4Position = (position) => position.longAssets.some((a) => a.coin.startsWith("#")) ||
|
|
7295
|
+
position.shortAssets.some((a) => a.coin.startsWith("#"));
|
|
7296
|
+
return rawPositions.filter((p) => !ifHip4Position(p)).map((position) => {
|
|
7161
7297
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
7162
7298
|
let mappedPosition = {
|
|
7163
7299
|
positionId: position.positionId,
|
|
@@ -7300,7 +7436,12 @@ function usePosition() {
|
|
|
7300
7436
|
Object.keys(tokenMetadata).length === 0)
|
|
7301
7437
|
return null;
|
|
7302
7438
|
return buildPositionValue(userOpenPositions, aggregatedClearingHouseState, getAssetByName);
|
|
7303
|
-
}, [
|
|
7439
|
+
}, [
|
|
7440
|
+
userOpenPositions,
|
|
7441
|
+
aggregatedClearingHouseState,
|
|
7442
|
+
tokenMetadata,
|
|
7443
|
+
getAssetByName,
|
|
7444
|
+
]);
|
|
7304
7445
|
const planRebalance = (positionId, targetWeights) => {
|
|
7305
7446
|
var _a;
|
|
7306
7447
|
if (!openPositions) {
|
|
@@ -7313,7 +7454,10 @@ function usePosition() {
|
|
|
7313
7454
|
const assets = [];
|
|
7314
7455
|
const allAssets = [
|
|
7315
7456
|
...position.longAssets.map((a) => ({ asset: a, side: 'long' })),
|
|
7316
|
-
...position.shortAssets.map((a) => ({
|
|
7457
|
+
...position.shortAssets.map((a) => ({
|
|
7458
|
+
asset: a,
|
|
7459
|
+
side: 'short',
|
|
7460
|
+
})),
|
|
7317
7461
|
];
|
|
7318
7462
|
const totalValue = allAssets.reduce((sum, { asset }) => sum + asset.positionValue, 0);
|
|
7319
7463
|
for (const { asset, side } of allAssets) {
|
package/dist/types.d.ts
CHANGED