@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 CHANGED
@@ -888,6 +888,7 @@ type CandleInterval = '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' | '4h' |
888
888
  */
889
889
  interface CandleData {
890
890
  s?: string;
891
+ i?: CandleInterval;
891
892
  t: number;
892
893
  T: number;
893
894
  o: number;
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].map((t) => t.symbol), [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
- if (prevInterval && prevInterval !== candleInterval) {
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 && !subscribedCandleTokens.includes(token));
1507
- const tokensToUnsubscribe = subscribedCandleTokens.filter((token) => !effectiveTokens.includes(token));
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 = (_a = longMeta === null || longMeta === void 0 ? void 0 : longMeta.currentPrice) !== null && _a !== void 0 ? _a : 0;
1838
- const currentShort = (_b = shortMeta === null || shortMeta === void 0 ? void 0 : shortMeta.currentPrice) !== null && _b !== void 0 ? _b : 0;
1839
- const prevLong = (_c = longMeta === null || longMeta === void 0 ? void 0 : longMeta.prevDayPrice) !== null && _c !== void 0 ? _c : 0;
1840
- const prevShort = (_d = shortMeta === null || shortMeta === void 0 ? void 0 : shortMeta.prevDayPrice) !== null && _d !== void 0 ? _d : 0;
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
- if ((metadata === null || metadata === void 0 ? void 0 : metadata.currentPrice) && token.weight > 0) {
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(metadata.currentPrice, weightFactor);
1994
+ longProduct *= Math.pow(price, weightFactor);
1854
1995
  }
1855
1996
  });
1856
1997
  shortTokens.forEach((token) => {
1857
1998
  const metadata = shortTokensMetadata[token.symbol];
1858
- if ((metadata === null || metadata === void 0 ? void 0 : metadata.currentPrice) && token.weight > 0) {
1859
- const weightFactor = token.weight / 100;
1860
- shortProduct *= Math.pow(metadata.currentPrice, -weightFactor);
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
- if ((metadata === null || metadata === void 0 ? void 0 : metadata.prevDayPrice) && token.weight > 0) {
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(metadata.prevDayPrice, weightFactor);
2018
+ longProduct *= Math.pow(price, weightFactor);
1874
2019
  }
1875
2020
  });
1876
2021
  shortTokens.forEach((token) => {
1877
2022
  const metadata = shortTokensMetadata[token.symbol];
1878
- if ((metadata === null || metadata === void 0 ? void 0 : metadata.prevDayPrice) && token.weight > 0) {
1879
- const weightFactor = token.weight / 100;
1880
- shortProduct *= Math.pow(metadata.prevDayPrice, -weightFactor);
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 lookup = candleLookups[token.symbol];
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 lookup = candleLookups[token.symbol];
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 = token.weight / 100;
6501
- shortProduct *= Math.pow(price, -weightFactor);
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 = [...longTokens, ...shortTokens].map(t => t.symbol);
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 = (_a = candleLookups[token.symbol]) === null || _a === void 0 ? void 0 : _a.get(timestamp);
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 = (_b = candleLookups[token.symbol]) === null || _b === void 0 ? void 0 : _b.get(timestamp);
6768
+ const candle = getCandleOrStableQuote(candleLookups, token.symbol, timestamp);
6602
6769
  if (!candle) {
6603
6770
  missing = true;
6604
6771
  break;
6605
6772
  }
6606
- const weightFactor = -(token.weight / 100);
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 if all tokens aligned
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 = snapshot[token.symbol];
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 = snapshot[token.symbol];
7012
+ const c = getCandleForTargetWindow(token.symbol);
6797
7013
  if (!c)
6798
7014
  return null;
6799
- const w = -(token.weight / 100);
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 aligned snapshot updates
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
@@ -860,6 +860,7 @@ export type CandleInterval = '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' |
860
860
  */
861
861
  export interface CandleData {
862
862
  s?: string;
863
+ i?: CandleInterval;
863
864
  t: number;
864
865
  T: number;
865
866
  o: number;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/hyperliquid-sdk",
3
- "version": "0.1.28",
3
+ "version": "0.1.31",
4
4
  "description": "React SDK for Pear Protocol Hyperliquid API integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",