@pear-protocol/hyperliquid-sdk 0.1.28 → 0.1.31
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 +271 -55
- package/dist/types.d.ts +1 -0
- package/dist/utils/quote-assets.d.ts +6 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -972,6 +972,37 @@ const useUserSelection$1 = create((set, get) => ({
|
|
|
972
972
|
resetToDefaults: () => set((prev) => ({ ...prev, ...DEFAULT_STATE })),
|
|
973
973
|
}));
|
|
974
974
|
|
|
975
|
+
const STABLE_QUOTE_SYMBOLS = new Set([
|
|
976
|
+
'USDC',
|
|
977
|
+
'USDH',
|
|
978
|
+
'USDE',
|
|
979
|
+
'USDT',
|
|
980
|
+
'USDT0',
|
|
981
|
+
]);
|
|
982
|
+
const isStableQuoteSymbol = (symbol) => STABLE_QUOTE_SYMBOLS.has(symbol.toUpperCase());
|
|
983
|
+
const hasNonStableTokens = (tokens) => tokens.some((token) => !isStableQuoteSymbol(token.symbol));
|
|
984
|
+
const isStableOnlySide = (tokens) => tokens.length === 0 || tokens.every((token) => isStableQuoteSymbol(token.symbol));
|
|
985
|
+
const isSingleAssetSideAgainstStableQuote = (longTokens, shortTokens) => {
|
|
986
|
+
const longHasNonStable = hasNonStableTokens(longTokens);
|
|
987
|
+
const shortHasNonStable = hasNonStableTokens(shortTokens);
|
|
988
|
+
return ((longHasNonStable && isStableOnlySide(shortTokens)) ||
|
|
989
|
+
(shortHasNonStable && isStableOnlySide(longTokens)));
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const CANDLE_STALE_WARNING_MS = 90000;
|
|
993
|
+
const getCandleSubscriptionKey = (coin, interval) => `${coin}:${interval}`;
|
|
994
|
+
const debugHyperliquidWs = (message, payload) => {
|
|
995
|
+
if (typeof window === "undefined")
|
|
996
|
+
return;
|
|
997
|
+
if (window.localStorage.getItem("pear:debug:hl-ws") !== "1")
|
|
998
|
+
return;
|
|
999
|
+
if (payload === undefined) {
|
|
1000
|
+
console.debug(`[HyperLiquid WS] ${message}`);
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
console.debug(`[HyperLiquid WS] ${message}`, payload);
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
975
1006
|
const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }) => {
|
|
976
1007
|
const { setAllMids, setActiveAssetData, upsertActiveAssetData, setCandleData, deleteCandleSymbol, deleteActiveAssetData, addCandleData, setFinalAtOICaps, setAggregatedClearingHouseState, setRawClearinghouseStates, setAssetContextsByDex, } = useHyperliquidData();
|
|
977
1008
|
const { setSpotState, setUserAbstractionMode } = useUserData();
|
|
@@ -979,14 +1010,19 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
979
1010
|
const userSummary = useUserData((state) => state.accountSummary);
|
|
980
1011
|
const longTokens = useUserSelection$1((s) => s.longTokens);
|
|
981
1012
|
const shortTokens = useUserSelection$1((s) => s.shortTokens);
|
|
982
|
-
const selectedTokenSymbols = useMemo(() => [...longTokens, ...shortTokens]
|
|
1013
|
+
const selectedTokenSymbols = useMemo(() => [...longTokens, ...shortTokens]
|
|
1014
|
+
.map((t) => t.symbol)
|
|
1015
|
+
.filter((symbol) => !isStableQuoteSymbol(symbol)), [longTokens, shortTokens]);
|
|
983
1016
|
const [lastError, setLastError] = useState(null);
|
|
984
1017
|
const [subscribedAddress, setSubscribedAddress] = useState(null);
|
|
985
1018
|
const [subscribedTokens, setSubscribedTokens] = useState([]);
|
|
986
1019
|
const [subscribedCandleTokens, setSubscribedCandleTokens] = useState([]);
|
|
987
1020
|
const [clearinghouseStateReceived, setClearinghouseStateReceived] = useState(false);
|
|
988
1021
|
const prevCandleIntervalRef = useRef(null);
|
|
1022
|
+
const currentCandleIntervalRef = useRef(candleInterval);
|
|
989
1023
|
const prevActiveAssetAddressRef = useRef(null);
|
|
1024
|
+
const lastCandleMessageAtRef = useRef(null);
|
|
1025
|
+
const lastCandleBySubscriptionRef = useRef(new Map());
|
|
990
1026
|
const pingIntervalRef = useRef(null);
|
|
991
1027
|
const wsRef = useRef(null);
|
|
992
1028
|
const reconnectAttemptsRef = useRef(0);
|
|
@@ -998,8 +1034,11 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
998
1034
|
useEffect(() => {
|
|
999
1035
|
onUserFillsRef.current = onUserFills;
|
|
1000
1036
|
}, [onUserFills]);
|
|
1037
|
+
useEffect(() => {
|
|
1038
|
+
currentCandleIntervalRef.current = candleInterval;
|
|
1039
|
+
}, [candleInterval]);
|
|
1001
1040
|
const handleMessage = useCallback((event) => {
|
|
1002
|
-
var _a;
|
|
1041
|
+
var _a, _b;
|
|
1003
1042
|
try {
|
|
1004
1043
|
const message = JSON.parse(event.data);
|
|
1005
1044
|
// Handle subscription responses
|
|
@@ -1097,7 +1136,27 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1097
1136
|
{
|
|
1098
1137
|
const candleDataItem = response.data;
|
|
1099
1138
|
const symbol = candleDataItem.s || "";
|
|
1139
|
+
const messageInterval = (_b = candleDataItem.i) !== null && _b !== void 0 ? _b : currentCandleIntervalRef.current;
|
|
1140
|
+
if (messageInterval !== currentCandleIntervalRef.current) {
|
|
1141
|
+
debugHyperliquidWs("ignoring stale candle interval", {
|
|
1142
|
+
symbol,
|
|
1143
|
+
messageInterval,
|
|
1144
|
+
currentInterval: currentCandleIntervalRef.current,
|
|
1145
|
+
t: candleDataItem.t,
|
|
1146
|
+
T: candleDataItem.T,
|
|
1147
|
+
});
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1100
1150
|
addCandleData(symbol, candleDataItem);
|
|
1151
|
+
const now = Date.now();
|
|
1152
|
+
lastCandleMessageAtRef.current = now;
|
|
1153
|
+
lastCandleBySubscriptionRef.current.set(getCandleSubscriptionKey(symbol, messageInterval), now);
|
|
1154
|
+
debugHyperliquidWs("candle received", {
|
|
1155
|
+
symbol,
|
|
1156
|
+
interval: messageInterval,
|
|
1157
|
+
t: candleDataItem.t,
|
|
1158
|
+
T: candleDataItem.T,
|
|
1159
|
+
});
|
|
1101
1160
|
}
|
|
1102
1161
|
break;
|
|
1103
1162
|
case "spotState":
|
|
@@ -1166,6 +1225,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1166
1225
|
setSubscribedAddress(null);
|
|
1167
1226
|
setSubscribedTokens([]);
|
|
1168
1227
|
setSubscribedCandleTokens([]);
|
|
1228
|
+
lastCandleBySubscriptionRef.current.clear();
|
|
1169
1229
|
setClearinghouseStateReceived(false);
|
|
1170
1230
|
if (!manualCloseRef.current) {
|
|
1171
1231
|
reconnectAttemptsRef.current += 1;
|
|
@@ -1197,6 +1257,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1197
1257
|
setSubscribedAddress(null);
|
|
1198
1258
|
setSubscribedTokens([]);
|
|
1199
1259
|
setSubscribedCandleTokens([]);
|
|
1260
|
+
lastCandleBySubscriptionRef.current.clear();
|
|
1200
1261
|
setClearinghouseStateReceived(false);
|
|
1201
1262
|
// Close existing socket if in a bad state
|
|
1202
1263
|
if (wsRef.current &&
|
|
@@ -1486,9 +1547,13 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1486
1547
|
if (!isConnected)
|
|
1487
1548
|
return;
|
|
1488
1549
|
const effectiveTokens = selectedTokenSymbols;
|
|
1489
|
-
// Unsubscribe from previous candle subscriptions if interval changed
|
|
1490
1550
|
const prevInterval = prevCandleIntervalRef.current;
|
|
1491
|
-
|
|
1551
|
+
const intervalChanged = prevInterval !== null && prevInterval !== candleInterval;
|
|
1552
|
+
const activeSubscribedCandleTokens = intervalChanged
|
|
1553
|
+
? []
|
|
1554
|
+
: subscribedCandleTokens;
|
|
1555
|
+
// Unsubscribe from previous candle subscriptions if interval changed
|
|
1556
|
+
if (intervalChanged && prevInterval !== null) {
|
|
1492
1557
|
subscribedCandleTokens.forEach((token) => {
|
|
1493
1558
|
const unsubscribeMessage = {
|
|
1494
1559
|
method: "unsubscribe",
|
|
@@ -1499,12 +1564,17 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1499
1564
|
},
|
|
1500
1565
|
};
|
|
1501
1566
|
sendJsonMessage(unsubscribeMessage);
|
|
1567
|
+
lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, prevInterval));
|
|
1568
|
+
debugHyperliquidWs("candle unsubscribe", {
|
|
1569
|
+
token,
|
|
1570
|
+
interval: prevInterval,
|
|
1571
|
+
reason: "interval-change",
|
|
1572
|
+
});
|
|
1502
1573
|
});
|
|
1503
1574
|
setCandleData(new Map());
|
|
1504
|
-
setSubscribedCandleTokens([]);
|
|
1505
1575
|
}
|
|
1506
|
-
const tokensToSubscribe = effectiveTokens.filter((token) => token && !
|
|
1507
|
-
const tokensToUnsubscribe =
|
|
1576
|
+
const tokensToSubscribe = effectiveTokens.filter((token) => token && !activeSubscribedCandleTokens.includes(token));
|
|
1577
|
+
const tokensToUnsubscribe = activeSubscribedCandleTokens.filter((token) => !effectiveTokens.includes(token));
|
|
1508
1578
|
// Unsubscribe from tokens no longer in the list
|
|
1509
1579
|
tokensToUnsubscribe.forEach((token) => {
|
|
1510
1580
|
const unsubscribeMessage = {
|
|
@@ -1516,6 +1586,12 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1516
1586
|
},
|
|
1517
1587
|
};
|
|
1518
1588
|
sendJsonMessage(unsubscribeMessage);
|
|
1589
|
+
lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, candleInterval));
|
|
1590
|
+
debugHyperliquidWs("candle unsubscribe", {
|
|
1591
|
+
token,
|
|
1592
|
+
interval: candleInterval,
|
|
1593
|
+
reason: "token-removed",
|
|
1594
|
+
});
|
|
1519
1595
|
});
|
|
1520
1596
|
// Subscribe to new tokens
|
|
1521
1597
|
tokensToSubscribe.forEach((token) => {
|
|
@@ -1528,6 +1604,11 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1528
1604
|
},
|
|
1529
1605
|
};
|
|
1530
1606
|
sendJsonMessage(subscribeMessage);
|
|
1607
|
+
lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, candleInterval));
|
|
1608
|
+
debugHyperliquidWs("candle subscribe", {
|
|
1609
|
+
token,
|
|
1610
|
+
interval: candleInterval,
|
|
1611
|
+
});
|
|
1531
1612
|
});
|
|
1532
1613
|
// Update subscribed state
|
|
1533
1614
|
if (tokensToSubscribe.length > 0 ||
|
|
@@ -1544,7 +1625,48 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
|
|
|
1544
1625
|
subscribedCandleTokens,
|
|
1545
1626
|
sendJsonMessage,
|
|
1546
1627
|
setCandleData,
|
|
1628
|
+
deleteCandleSymbol,
|
|
1547
1629
|
]);
|
|
1630
|
+
useEffect(() => {
|
|
1631
|
+
if (!isConnected)
|
|
1632
|
+
return;
|
|
1633
|
+
const checkCandleSubscriptionHealth = () => {
|
|
1634
|
+
const effectiveTokens = selectedTokenSymbols.filter((token) => token);
|
|
1635
|
+
if (effectiveTokens.length === 0)
|
|
1636
|
+
return;
|
|
1637
|
+
const now = Date.now();
|
|
1638
|
+
const missingSubscriptionState = effectiveTokens.filter((token) => !subscribedCandleTokens.includes(token));
|
|
1639
|
+
const missingCandles = effectiveTokens.filter((token) => {
|
|
1640
|
+
const key = getCandleSubscriptionKey(token, candleInterval);
|
|
1641
|
+
return !lastCandleBySubscriptionRef.current.has(key);
|
|
1642
|
+
});
|
|
1643
|
+
const staleCandles = effectiveTokens.filter((token) => {
|
|
1644
|
+
const key = getCandleSubscriptionKey(token, candleInterval);
|
|
1645
|
+
const lastSeenAt = lastCandleBySubscriptionRef.current.get(key);
|
|
1646
|
+
return (lastSeenAt !== undefined &&
|
|
1647
|
+
now - lastSeenAt > CANDLE_STALE_WARNING_MS);
|
|
1648
|
+
});
|
|
1649
|
+
if (missingSubscriptionState.length === 0 &&
|
|
1650
|
+
missingCandles.length === 0 &&
|
|
1651
|
+
staleCandles.length === 0) {
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
debugHyperliquidWs("candle subscription health", {
|
|
1655
|
+
interval: candleInterval,
|
|
1656
|
+
selectedTokens: effectiveTokens,
|
|
1657
|
+
subscribedTokens: subscribedCandleTokens,
|
|
1658
|
+
missingSubscriptionState,
|
|
1659
|
+
missingCandles,
|
|
1660
|
+
staleCandles,
|
|
1661
|
+
lastAnyCandleAgeMs: lastCandleMessageAtRef.current === null
|
|
1662
|
+
? null
|
|
1663
|
+
: now - lastCandleMessageAtRef.current,
|
|
1664
|
+
});
|
|
1665
|
+
};
|
|
1666
|
+
checkCandleSubscriptionHealth();
|
|
1667
|
+
const healthInterval = setInterval(checkCandleSubscriptionHealth, 30000);
|
|
1668
|
+
return () => clearInterval(healthInterval);
|
|
1669
|
+
}, [isConnected, selectedTokenSymbols, candleInterval, subscribedCandleTokens]);
|
|
1548
1670
|
return {
|
|
1549
1671
|
isConnected,
|
|
1550
1672
|
lastError,
|
|
@@ -1801,7 +1923,7 @@ const useTokenSelectionMetadataStore = create((set) => ({
|
|
|
1801
1923
|
...longTokensMetadata,
|
|
1802
1924
|
...shortTokensMetadata,
|
|
1803
1925
|
};
|
|
1804
|
-
return allTokens.some((token) => !allMetadata[token.symbol]);
|
|
1926
|
+
return allTokens.some((token) => !isStableQuoteSymbol(token.symbol) && !allMetadata[token.symbol]);
|
|
1805
1927
|
})();
|
|
1806
1928
|
// Open interest and volume (from market data for matching asset basket)
|
|
1807
1929
|
const { openInterest, volume } = (() => {
|
|
@@ -1824,9 +1946,15 @@ const useTokenSelectionMetadataStore = create((set) => ({
|
|
|
1824
1946
|
? { openInterest: match.openInterest, volume: match.volume }
|
|
1825
1947
|
: empty;
|
|
1826
1948
|
})();
|
|
1949
|
+
const getPrice = (metadata, symbol, field) => {
|
|
1950
|
+
var _a;
|
|
1951
|
+
if (isStableQuoteSymbol(symbol))
|
|
1952
|
+
return 1;
|
|
1953
|
+
return (_a = metadata === null || metadata === void 0 ? void 0 : metadata[field]) !== null && _a !== void 0 ? _a : 0;
|
|
1954
|
+
};
|
|
1955
|
+
const shouldUseAbsoluteQuoteRatio = isSingleAssetSideAgainstStableQuote(longTokens, shortTokens);
|
|
1827
1956
|
// Price ratio (only when exactly one long and one short)
|
|
1828
1957
|
const { priceRatio, priceRatio24h } = (() => {
|
|
1829
|
-
var _a, _b, _c, _d;
|
|
1830
1958
|
if (longTokens.length !== 1 || shortTokens.length !== 1) {
|
|
1831
1959
|
return { priceRatio: 1, priceRatio24h: 1 };
|
|
1832
1960
|
}
|
|
@@ -1834,10 +1962,22 @@ const useTokenSelectionMetadataStore = create((set) => ({
|
|
|
1834
1962
|
const shortSymbol = shortTokens[0].symbol;
|
|
1835
1963
|
const longMeta = longTokensMetadata[longSymbol];
|
|
1836
1964
|
const shortMeta = shortTokensMetadata[shortSymbol];
|
|
1837
|
-
const currentLong = (
|
|
1838
|
-
const currentShort = (
|
|
1839
|
-
const prevLong = (
|
|
1840
|
-
const prevShort = (
|
|
1965
|
+
const currentLong = getPrice(longMeta, longSymbol, "currentPrice");
|
|
1966
|
+
const currentShort = getPrice(shortMeta, shortSymbol, "currentPrice");
|
|
1967
|
+
const prevLong = getPrice(longMeta, longSymbol, "prevDayPrice");
|
|
1968
|
+
const prevShort = getPrice(shortMeta, shortSymbol, "prevDayPrice");
|
|
1969
|
+
if (isStableQuoteSymbol(longSymbol) && !isStableQuoteSymbol(shortSymbol)) {
|
|
1970
|
+
return {
|
|
1971
|
+
priceRatio: currentShort || 1,
|
|
1972
|
+
priceRatio24h: prevShort || 1,
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
if (isStableQuoteSymbol(shortSymbol) && !isStableQuoteSymbol(longSymbol)) {
|
|
1976
|
+
return {
|
|
1977
|
+
priceRatio: currentLong || 1,
|
|
1978
|
+
priceRatio24h: prevLong || 1,
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1841
1981
|
const ratio = currentShort !== 0 ? currentLong / currentShort : 1;
|
|
1842
1982
|
const ratio24h = prevShort !== 0 ? prevLong / prevShort : 1;
|
|
1843
1983
|
return { priceRatio: ratio, priceRatio24h: ratio24h };
|
|
@@ -1848,16 +1988,20 @@ const useTokenSelectionMetadataStore = create((set) => ({
|
|
|
1848
1988
|
let shortProduct = 1;
|
|
1849
1989
|
longTokens.forEach((token) => {
|
|
1850
1990
|
const metadata = longTokensMetadata[token.symbol];
|
|
1851
|
-
|
|
1991
|
+
const price = getPrice(metadata, token.symbol, "currentPrice");
|
|
1992
|
+
if (price && token.weight > 0) {
|
|
1852
1993
|
const weightFactor = token.weight / 100;
|
|
1853
|
-
longProduct *= Math.pow(
|
|
1994
|
+
longProduct *= Math.pow(price, weightFactor);
|
|
1854
1995
|
}
|
|
1855
1996
|
});
|
|
1856
1997
|
shortTokens.forEach((token) => {
|
|
1857
1998
|
const metadata = shortTokensMetadata[token.symbol];
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1999
|
+
const price = getPrice(metadata, token.symbol, "currentPrice");
|
|
2000
|
+
if (price && token.weight > 0) {
|
|
2001
|
+
const weightFactor = shouldUseAbsoluteQuoteRatio
|
|
2002
|
+
? token.weight / 100
|
|
2003
|
+
: -(token.weight / 100);
|
|
2004
|
+
shortProduct *= Math.pow(price, weightFactor);
|
|
1861
2005
|
}
|
|
1862
2006
|
});
|
|
1863
2007
|
return longProduct * shortProduct;
|
|
@@ -1868,16 +2012,20 @@ const useTokenSelectionMetadataStore = create((set) => ({
|
|
|
1868
2012
|
let shortProduct = 1;
|
|
1869
2013
|
longTokens.forEach((token) => {
|
|
1870
2014
|
const metadata = longTokensMetadata[token.symbol];
|
|
1871
|
-
|
|
2015
|
+
const price = getPrice(metadata, token.symbol, "prevDayPrice");
|
|
2016
|
+
if (price && token.weight > 0) {
|
|
1872
2017
|
const weightFactor = token.weight / 100;
|
|
1873
|
-
longProduct *= Math.pow(
|
|
2018
|
+
longProduct *= Math.pow(price, weightFactor);
|
|
1874
2019
|
}
|
|
1875
2020
|
});
|
|
1876
2021
|
shortTokens.forEach((token) => {
|
|
1877
2022
|
const metadata = shortTokensMetadata[token.symbol];
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2023
|
+
const price = getPrice(metadata, token.symbol, "prevDayPrice");
|
|
2024
|
+
if (price && token.weight > 0) {
|
|
2025
|
+
const weightFactor = shouldUseAbsoluteQuoteRatio
|
|
2026
|
+
? token.weight / 100
|
|
2027
|
+
: -(token.weight / 100);
|
|
2028
|
+
shortProduct *= Math.pow(price, weightFactor);
|
|
1881
2029
|
}
|
|
1882
2030
|
});
|
|
1883
2031
|
return longProduct * shortProduct;
|
|
@@ -6337,7 +6485,7 @@ const useHistoricalPriceData = () => {
|
|
|
6337
6485
|
const prevIntervalRef = useRef(null);
|
|
6338
6486
|
// Get all tokens from long and short selections
|
|
6339
6487
|
const getAllTokens = useCallback(() => {
|
|
6340
|
-
return [...longTokens, ...shortTokens];
|
|
6488
|
+
return [...longTokens, ...shortTokens].filter((token) => !isStableQuoteSymbol(token.symbol));
|
|
6341
6489
|
}, [longTokens, shortTokens]);
|
|
6342
6490
|
// Track token and interval changes and manage cache accordingly
|
|
6343
6491
|
useEffect(() => {
|
|
@@ -6475,10 +6623,10 @@ const createCandleLookups = (tokenCandles) => {
|
|
|
6475
6623
|
const calculateWeightedRatio = (longTokens, shortTokens, candleLookups, timestamp, priceType) => {
|
|
6476
6624
|
let longProduct = 1;
|
|
6477
6625
|
let shortProduct = 1;
|
|
6626
|
+
const shouldUseAbsoluteQuoteRatio = isSingleAssetSideAgainstStableQuote(longTokens, shortTokens);
|
|
6478
6627
|
// Long side: PRICE^(WEIGHT/100)
|
|
6479
6628
|
for (const token of longTokens) {
|
|
6480
|
-
const
|
|
6481
|
-
const candle = lookup === null || lookup === void 0 ? void 0 : lookup.get(timestamp);
|
|
6629
|
+
const candle = getCandleOrStableQuote(candleLookups, token.symbol, timestamp);
|
|
6482
6630
|
if (candle) {
|
|
6483
6631
|
const price = candle[priceType];
|
|
6484
6632
|
if (price > 0) {
|
|
@@ -6492,13 +6640,14 @@ const calculateWeightedRatio = (longTokens, shortTokens, candleLookups, timestam
|
|
|
6492
6640
|
}
|
|
6493
6641
|
// Short side: PRICE^-(WEIGHT/100)
|
|
6494
6642
|
for (const token of shortTokens) {
|
|
6495
|
-
const
|
|
6496
|
-
const candle = lookup === null || lookup === void 0 ? void 0 : lookup.get(timestamp);
|
|
6643
|
+
const candle = getCandleOrStableQuote(candleLookups, token.symbol, timestamp);
|
|
6497
6644
|
if (candle) {
|
|
6498
6645
|
const price = candle[priceType];
|
|
6499
6646
|
if (price > 0) {
|
|
6500
|
-
const weightFactor =
|
|
6501
|
-
|
|
6647
|
+
const weightFactor = shouldUseAbsoluteQuoteRatio
|
|
6648
|
+
? token.weight / 100
|
|
6649
|
+
: -(token.weight / 100);
|
|
6650
|
+
shortProduct *= Math.pow(price, weightFactor);
|
|
6502
6651
|
}
|
|
6503
6652
|
}
|
|
6504
6653
|
else {
|
|
@@ -6551,19 +6700,37 @@ const getCompleteTimestamps = (candleLookups, requiredSymbols) => {
|
|
|
6551
6700
|
result.sort((a, b) => a - b);
|
|
6552
6701
|
return result;
|
|
6553
6702
|
};
|
|
6703
|
+
const getRequiredSymbols = (longTokens, shortTokens) => [...longTokens, ...shortTokens]
|
|
6704
|
+
.map((token) => token.symbol)
|
|
6705
|
+
.filter((symbol) => !isStableQuoteSymbol(symbol));
|
|
6706
|
+
const getCandleOrStableQuote = (candleLookups, symbol, timestamp) => {
|
|
6707
|
+
var _a, _b;
|
|
6708
|
+
if (isStableQuoteSymbol(symbol)) {
|
|
6709
|
+
return {
|
|
6710
|
+
s: symbol,
|
|
6711
|
+
t: timestamp,
|
|
6712
|
+
T: timestamp,
|
|
6713
|
+
o: 1,
|
|
6714
|
+
h: 1,
|
|
6715
|
+
l: 1,
|
|
6716
|
+
c: 1,
|
|
6717
|
+
};
|
|
6718
|
+
}
|
|
6719
|
+
return (_b = (_a = candleLookups[symbol]) === null || _a === void 0 ? void 0 : _a.get(timestamp)) !== null && _b !== void 0 ? _b : null;
|
|
6720
|
+
};
|
|
6554
6721
|
/**
|
|
6555
6722
|
* Compute basket candles from individual token candles using weighted ratios
|
|
6556
6723
|
* Optimized version that creates lookup maps once and reuses them
|
|
6557
6724
|
*/
|
|
6558
6725
|
const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
|
|
6559
|
-
var _a, _b;
|
|
6560
6726
|
if (longTokens.length === 0 && shortTokens.length === 0) {
|
|
6561
6727
|
return [];
|
|
6562
6728
|
}
|
|
6563
6729
|
// Create efficient lookup maps once
|
|
6564
6730
|
const candleLookups = createCandleLookups(tokenCandles);
|
|
6565
|
-
const allSymbols =
|
|
6731
|
+
const allSymbols = getRequiredSymbols(longTokens, shortTokens);
|
|
6566
6732
|
const completeTimestamps = getCompleteTimestamps(candleLookups, allSymbols);
|
|
6733
|
+
const shouldUseAbsoluteQuoteRatio = isSingleAssetSideAgainstStableQuote(longTokens, shortTokens);
|
|
6567
6734
|
const basketCandles = [];
|
|
6568
6735
|
for (const timestamp of completeTimestamps) {
|
|
6569
6736
|
// Compute all OHLC products in a single pass per side
|
|
@@ -6573,7 +6740,7 @@ const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
|
|
|
6573
6740
|
let missing = false;
|
|
6574
6741
|
// Accumulate volume/trades and compute long side contributions
|
|
6575
6742
|
for (const token of longTokens) {
|
|
6576
|
-
const candle = (
|
|
6743
|
+
const candle = getCandleOrStableQuote(candleLookups, token.symbol, timestamp);
|
|
6577
6744
|
if (!candle) {
|
|
6578
6745
|
missing = true;
|
|
6579
6746
|
break;
|
|
@@ -6598,12 +6765,14 @@ const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
|
|
|
6598
6765
|
continue;
|
|
6599
6766
|
// Short side contributions (negative exponent)
|
|
6600
6767
|
for (const token of shortTokens) {
|
|
6601
|
-
const candle = (
|
|
6768
|
+
const candle = getCandleOrStableQuote(candleLookups, token.symbol, timestamp);
|
|
6602
6769
|
if (!candle) {
|
|
6603
6770
|
missing = true;
|
|
6604
6771
|
break;
|
|
6605
6772
|
}
|
|
6606
|
-
const weightFactor =
|
|
6773
|
+
const weightFactor = shouldUseAbsoluteQuoteRatio
|
|
6774
|
+
? token.weight / 100
|
|
6775
|
+
: -(token.weight / 100);
|
|
6607
6776
|
const o = candle.o;
|
|
6608
6777
|
const h = candle.h;
|
|
6609
6778
|
const l = candle.l;
|
|
@@ -6633,6 +6802,22 @@ const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
|
|
|
6633
6802
|
return basketCandles;
|
|
6634
6803
|
};
|
|
6635
6804
|
|
|
6805
|
+
const CANDLE_INTERVAL_MS = {
|
|
6806
|
+
'1m': 60 * 1000,
|
|
6807
|
+
'3m': 3 * 60 * 1000,
|
|
6808
|
+
'5m': 5 * 60 * 1000,
|
|
6809
|
+
'15m': 15 * 60 * 1000,
|
|
6810
|
+
'30m': 30 * 60 * 1000,
|
|
6811
|
+
'1h': 60 * 60 * 1000,
|
|
6812
|
+
'2h': 2 * 60 * 60 * 1000,
|
|
6813
|
+
'4h': 4 * 60 * 60 * 1000,
|
|
6814
|
+
'8h': 8 * 60 * 60 * 1000,
|
|
6815
|
+
'12h': 12 * 60 * 60 * 1000,
|
|
6816
|
+
'1d': 24 * 60 * 60 * 1000,
|
|
6817
|
+
'3d': 3 * 24 * 60 * 60 * 1000,
|
|
6818
|
+
'1w': 7 * 24 * 60 * 60 * 1000,
|
|
6819
|
+
'1M': 30 * 24 * 60 * 60 * 1000,
|
|
6820
|
+
};
|
|
6636
6821
|
/**
|
|
6637
6822
|
* Composes historical price fetching with basket candle computation.
|
|
6638
6823
|
* - Listens to `longTokens` and `shortTokens` from user selection.
|
|
@@ -6642,6 +6827,7 @@ const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
|
|
|
6642
6827
|
const useBasketCandles = () => {
|
|
6643
6828
|
const longTokens = useUserSelection$1((state) => state.longTokens);
|
|
6644
6829
|
const shortTokens = useUserSelection$1((state) => state.shortTokens);
|
|
6830
|
+
const candleInterval = useUserSelection$1((state) => state.candleInterval);
|
|
6645
6831
|
const candleData = useHyperliquidData((s) => s.candleData);
|
|
6646
6832
|
const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData, getEffectiveDataBoundary } = useHistoricalPriceData();
|
|
6647
6833
|
const fetchBasketCandles = useCallback(async (startTime, endTime, interval) => {
|
|
@@ -6660,7 +6846,7 @@ const useBasketCandles = () => {
|
|
|
6660
6846
|
const fetchOverallPerformanceCandles = useCallback(async (startTime, endTime, interval) => {
|
|
6661
6847
|
await fetchHistoricalPriceData(startTime, endTime, interval);
|
|
6662
6848
|
const allCandles = await getAllHistoricalPriceData();
|
|
6663
|
-
const allTokens = [...longTokens, ...shortTokens];
|
|
6849
|
+
const allTokens = [...longTokens, ...shortTokens].filter((token) => !isStableQuoteSymbol(token.symbol));
|
|
6664
6850
|
if (allTokens.length === 0)
|
|
6665
6851
|
return [];
|
|
6666
6852
|
const symbolsData = {};
|
|
@@ -6747,40 +6933,70 @@ const useBasketCandles = () => {
|
|
|
6747
6933
|
const removeRealtimeListener = useCallback((id) => {
|
|
6748
6934
|
listenersRef.current.delete(id);
|
|
6749
6935
|
}, []);
|
|
6750
|
-
// Helper: compute weighted bar from latest snapshot
|
|
6936
|
+
// Helper: compute weighted bar from latest snapshot, carrying forward lagging
|
|
6937
|
+
// symbols so one sparse feed does not suppress every realtime chart update.
|
|
6751
6938
|
const computeRealtimeBar = useCallback(() => {
|
|
6752
6939
|
if (!candleData)
|
|
6753
6940
|
return null;
|
|
6754
6941
|
const allTokens = [...longTokens, ...shortTokens];
|
|
6755
6942
|
if (allTokens.length === 0)
|
|
6756
6943
|
return null;
|
|
6757
|
-
// Collect candles, ensure presence and alignment
|
|
6758
6944
|
const symbolSet = new Set(allTokens.map((t) => t.symbol));
|
|
6759
6945
|
const snapshot = {};
|
|
6946
|
+
let t = null;
|
|
6947
|
+
let T = null;
|
|
6760
6948
|
for (const symbol of symbolSet) {
|
|
6949
|
+
if (isStableQuoteSymbol(symbol))
|
|
6950
|
+
continue;
|
|
6761
6951
|
const c = candleData.get(symbol);
|
|
6762
6952
|
if (!c)
|
|
6763
6953
|
return null; // missing latest candle for symbol
|
|
6764
6954
|
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) {
|
|
6955
|
+
if (t === null || c.t > t) {
|
|
6772
6956
|
t = c.t;
|
|
6773
6957
|
T = c.T;
|
|
6774
6958
|
}
|
|
6775
|
-
else if (c.t !== t || c.T !== T) {
|
|
6776
|
-
return null; // not aligned yet
|
|
6777
|
-
}
|
|
6778
6959
|
}
|
|
6960
|
+
if (t === null || T === null)
|
|
6961
|
+
return null;
|
|
6962
|
+
const intervalMs = CANDLE_INTERVAL_MS[candleInterval];
|
|
6963
|
+
const maxCarryForwardMs = intervalMs * 2;
|
|
6964
|
+
const getCandleForTargetWindow = (symbol) => {
|
|
6965
|
+
if (isStableQuoteSymbol(symbol)) {
|
|
6966
|
+
return {
|
|
6967
|
+
s: symbol,
|
|
6968
|
+
t,
|
|
6969
|
+
T,
|
|
6970
|
+
o: 1,
|
|
6971
|
+
h: 1,
|
|
6972
|
+
l: 1,
|
|
6973
|
+
c: 1,
|
|
6974
|
+
};
|
|
6975
|
+
}
|
|
6976
|
+
const c = snapshot[symbol];
|
|
6977
|
+
if (c.t === t)
|
|
6978
|
+
return c;
|
|
6979
|
+
if (c.t > t || t - c.t > maxCarryForwardMs)
|
|
6980
|
+
return null;
|
|
6981
|
+
const price = c.c;
|
|
6982
|
+
if (!(price > 0))
|
|
6983
|
+
return null;
|
|
6984
|
+
return {
|
|
6985
|
+
s: c.s,
|
|
6986
|
+
t,
|
|
6987
|
+
T,
|
|
6988
|
+
o: price,
|
|
6989
|
+
h: price,
|
|
6990
|
+
l: price,
|
|
6991
|
+
c: price,
|
|
6992
|
+
};
|
|
6993
|
+
};
|
|
6779
6994
|
// Compute weighted OHLC similar to computeBasketCandles
|
|
6780
6995
|
let longOpen = 1, longHigh = 1, longLow = 1, longClose = 1;
|
|
6781
6996
|
let shortOpen = 1, shortHigh = 1, shortLow = 1, shortClose = 1;
|
|
6997
|
+
const shouldUseAbsoluteQuoteRatio = isSingleAssetSideAgainstStableQuote(longTokens, shortTokens);
|
|
6782
6998
|
for (const token of longTokens) {
|
|
6783
|
-
const c =
|
|
6999
|
+
const c = getCandleForTargetWindow(token.symbol);
|
|
6784
7000
|
if (!c)
|
|
6785
7001
|
return null;
|
|
6786
7002
|
const w = token.weight / 100;
|
|
@@ -6793,10 +7009,12 @@ const useBasketCandles = () => {
|
|
|
6793
7009
|
longClose *= Math.pow(cl, w);
|
|
6794
7010
|
}
|
|
6795
7011
|
for (const token of shortTokens) {
|
|
6796
|
-
const c =
|
|
7012
|
+
const c = getCandleForTargetWindow(token.symbol);
|
|
6797
7013
|
if (!c)
|
|
6798
7014
|
return null;
|
|
6799
|
-
const w =
|
|
7015
|
+
const w = shouldUseAbsoluteQuoteRatio
|
|
7016
|
+
? token.weight / 100
|
|
7017
|
+
: -(token.weight / 100);
|
|
6800
7018
|
const o = c.o, h = c.h, l = c.l, cl = c.c;
|
|
6801
7019
|
if (!(o > 0 && h > 0 && l > 0 && cl > 0))
|
|
6802
7020
|
return null;
|
|
@@ -6805,8 +7023,6 @@ const useBasketCandles = () => {
|
|
|
6805
7023
|
shortLow *= Math.pow(l, w);
|
|
6806
7024
|
shortClose *= Math.pow(cl, w);
|
|
6807
7025
|
}
|
|
6808
|
-
if (t === null || T === null)
|
|
6809
|
-
return null;
|
|
6810
7026
|
const weighted = {
|
|
6811
7027
|
t,
|
|
6812
7028
|
T,
|
|
@@ -6816,8 +7032,8 @@ const useBasketCandles = () => {
|
|
|
6816
7032
|
c: longClose * shortClose,
|
|
6817
7033
|
};
|
|
6818
7034
|
return weighted;
|
|
6819
|
-
}, [candleData, longTokens, shortTokens]);
|
|
6820
|
-
// Emit realtime bars when
|
|
7035
|
+
}, [candleData, candleInterval, longTokens, shortTokens]);
|
|
7036
|
+
// Emit realtime bars when the latest snapshot can produce a valid bar.
|
|
6821
7037
|
useEffect(() => {
|
|
6822
7038
|
if (listenersRef.current.size === 0)
|
|
6823
7039
|
return;
|
package/dist/types.d.ts
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TokenSelection } from '../types';
|
|
2
|
+
export declare const STABLE_QUOTE_SYMBOLS: Set<string>;
|
|
3
|
+
export declare const isStableQuoteSymbol: (symbol: string) => boolean;
|
|
4
|
+
export declare const hasNonStableTokens: (tokens: TokenSelection[]) => boolean;
|
|
5
|
+
export declare const isStableOnlySide: (tokens: TokenSelection[]) => boolean;
|
|
6
|
+
export declare const isSingleAssetSideAgainstStableQuote: (longTokens: TokenSelection[], shortTokens: TokenSelection[]) => boolean;
|