@pear-protocol/hyperliquid-sdk 0.1.27 → 0.1.29

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
@@ -820,22 +820,13 @@ function validateClosePositionRequest(payload) {
820
820
  }
821
821
 
822
822
  const DEFAULT_STATE = {
823
- longTokens: [
824
- { symbol: "HYPE", weight: 25 },
825
- { symbol: "BTC", weight: 25 },
826
- ],
827
- shortTokens: [
828
- { symbol: "AVAX", weight: 10 },
829
- { symbol: "SEI", weight: 10 },
830
- { symbol: "ADA", weight: 10 },
831
- { symbol: "TRUMP", weight: 10 },
832
- { symbol: "SUI", weight: 10 },
833
- ],
823
+ longTokens: [],
824
+ shortTokens: [],
834
825
  openTokenSelector: false,
835
826
  selectorConfig: null,
836
827
  openConflictModal: false,
837
828
  conflicts: [],
838
- candleInterval: "1h",
829
+ candleInterval: '1h',
839
830
  isWeightBalanced: true,
840
831
  };
841
832
  const useUserSelection$1 = create((set, get) => ({
@@ -855,7 +846,11 @@ const useUserSelection$1 = create((set, get) => ({
855
846
  const longTotal = updated.reduce((sum, t) => sum + t.weight, 0);
856
847
  const shortTotal = prev.shortTokens.reduce((sum, t) => sum + t.weight, 0);
857
848
  const isWeightBalanced = longTotal + shortTotal === 100;
858
- return { ...prev, longTokens: updated, isWeightBalanced };
849
+ return {
850
+ ...prev,
851
+ longTokens: updated,
852
+ isWeightBalanced,
853
+ };
859
854
  }
860
855
  else {
861
856
  const updated = [...prev.shortTokens];
@@ -863,7 +858,11 @@ const useUserSelection$1 = create((set, get) => ({
863
858
  const longTotal = prev.longTokens.reduce((sum, t) => sum + t.weight, 0);
864
859
  const shortTotal = updated.reduce((sum, t) => sum + t.weight, 0);
865
860
  const isWeightBalanced = longTotal + shortTotal === 100;
866
- return { ...prev, shortTokens: updated, isWeightBalanced };
861
+ return {
862
+ ...prev,
863
+ shortTokens: updated,
864
+ isWeightBalanced,
865
+ };
867
866
  }
868
867
  });
869
868
  },
@@ -892,14 +891,22 @@ const useUserSelection$1 = create((set, get) => ({
892
891
  const longTotal = updated.reduce((sum, t) => sum + t.weight, 0);
893
892
  const shortTotal = prev.shortTokens.reduce((sum, t) => sum + t.weight, 0);
894
893
  const isWeightBalanced = longTotal + shortTotal === 100;
895
- return { ...prev, longTokens: updated, isWeightBalanced };
894
+ return {
895
+ ...prev,
896
+ longTokens: updated,
897
+ isWeightBalanced,
898
+ };
896
899
  }
897
900
  else {
898
901
  const updated = prev.shortTokens.filter((_, i) => i !== index);
899
902
  const longTotal = prev.longTokens.reduce((sum, t) => sum + t.weight, 0);
900
903
  const shortTotal = updated.reduce((sum, t) => sum + t.weight, 0);
901
904
  const isWeightBalanced = longTotal + shortTotal === 100;
902
- return { ...prev, shortTokens: updated, isWeightBalanced };
905
+ return {
906
+ ...prev,
907
+ shortTokens: updated,
908
+ isWeightBalanced,
909
+ };
903
910
  }
904
911
  });
905
912
  },
@@ -965,6 +972,20 @@ const useUserSelection$1 = create((set, get) => ({
965
972
  resetToDefaults: () => set((prev) => ({ ...prev, ...DEFAULT_STATE })),
966
973
  }));
967
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
+ };
968
989
  const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }) => {
969
990
  const { setAllMids, setActiveAssetData, upsertActiveAssetData, setCandleData, deleteCandleSymbol, deleteActiveAssetData, addCandleData, setFinalAtOICaps, setAggregatedClearingHouseState, setRawClearinghouseStates, setAssetContextsByDex, } = useHyperliquidData();
970
991
  const { setSpotState, setUserAbstractionMode } = useUserData();
@@ -979,7 +1000,10 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
979
1000
  const [subscribedCandleTokens, setSubscribedCandleTokens] = useState([]);
980
1001
  const [clearinghouseStateReceived, setClearinghouseStateReceived] = useState(false);
981
1002
  const prevCandleIntervalRef = useRef(null);
1003
+ const currentCandleIntervalRef = useRef(candleInterval);
982
1004
  const prevActiveAssetAddressRef = useRef(null);
1005
+ const lastCandleMessageAtRef = useRef(null);
1006
+ const lastCandleBySubscriptionRef = useRef(new Map());
983
1007
  const pingIntervalRef = useRef(null);
984
1008
  const wsRef = useRef(null);
985
1009
  const reconnectAttemptsRef = useRef(0);
@@ -991,8 +1015,11 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
991
1015
  useEffect(() => {
992
1016
  onUserFillsRef.current = onUserFills;
993
1017
  }, [onUserFills]);
1018
+ useEffect(() => {
1019
+ currentCandleIntervalRef.current = candleInterval;
1020
+ }, [candleInterval]);
994
1021
  const handleMessage = useCallback((event) => {
995
- var _a;
1022
+ var _a, _b;
996
1023
  try {
997
1024
  const message = JSON.parse(event.data);
998
1025
  // Handle subscription responses
@@ -1090,7 +1117,27 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1090
1117
  {
1091
1118
  const candleDataItem = response.data;
1092
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
+ }
1093
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
+ });
1094
1141
  }
1095
1142
  break;
1096
1143
  case "spotState":
@@ -1159,6 +1206,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1159
1206
  setSubscribedAddress(null);
1160
1207
  setSubscribedTokens([]);
1161
1208
  setSubscribedCandleTokens([]);
1209
+ lastCandleBySubscriptionRef.current.clear();
1162
1210
  setClearinghouseStateReceived(false);
1163
1211
  if (!manualCloseRef.current) {
1164
1212
  reconnectAttemptsRef.current += 1;
@@ -1190,6 +1238,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1190
1238
  setSubscribedAddress(null);
1191
1239
  setSubscribedTokens([]);
1192
1240
  setSubscribedCandleTokens([]);
1241
+ lastCandleBySubscriptionRef.current.clear();
1193
1242
  setClearinghouseStateReceived(false);
1194
1243
  // Close existing socket if in a bad state
1195
1244
  if (wsRef.current &&
@@ -1479,9 +1528,13 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1479
1528
  if (!isConnected)
1480
1529
  return;
1481
1530
  const effectiveTokens = selectedTokenSymbols;
1482
- // Unsubscribe from previous candle subscriptions if interval changed
1483
1531
  const prevInterval = prevCandleIntervalRef.current;
1484
- if (prevInterval && prevInterval !== candleInterval) {
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) {
1485
1538
  subscribedCandleTokens.forEach((token) => {
1486
1539
  const unsubscribeMessage = {
1487
1540
  method: "unsubscribe",
@@ -1492,12 +1545,17 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1492
1545
  },
1493
1546
  };
1494
1547
  sendJsonMessage(unsubscribeMessage);
1548
+ lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, prevInterval));
1549
+ debugHyperliquidWs("candle unsubscribe", {
1550
+ token,
1551
+ interval: prevInterval,
1552
+ reason: "interval-change",
1553
+ });
1495
1554
  });
1496
1555
  setCandleData(new Map());
1497
- setSubscribedCandleTokens([]);
1498
1556
  }
1499
- const tokensToSubscribe = effectiveTokens.filter((token) => token && !subscribedCandleTokens.includes(token));
1500
- const tokensToUnsubscribe = subscribedCandleTokens.filter((token) => !effectiveTokens.includes(token));
1557
+ const tokensToSubscribe = effectiveTokens.filter((token) => token && !activeSubscribedCandleTokens.includes(token));
1558
+ const tokensToUnsubscribe = activeSubscribedCandleTokens.filter((token) => !effectiveTokens.includes(token));
1501
1559
  // Unsubscribe from tokens no longer in the list
1502
1560
  tokensToUnsubscribe.forEach((token) => {
1503
1561
  const unsubscribeMessage = {
@@ -1509,6 +1567,12 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1509
1567
  },
1510
1568
  };
1511
1569
  sendJsonMessage(unsubscribeMessage);
1570
+ lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, candleInterval));
1571
+ debugHyperliquidWs("candle unsubscribe", {
1572
+ token,
1573
+ interval: candleInterval,
1574
+ reason: "token-removed",
1575
+ });
1512
1576
  });
1513
1577
  // Subscribe to new tokens
1514
1578
  tokensToSubscribe.forEach((token) => {
@@ -1521,6 +1585,11 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1521
1585
  },
1522
1586
  };
1523
1587
  sendJsonMessage(subscribeMessage);
1588
+ lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, candleInterval));
1589
+ debugHyperliquidWs("candle subscribe", {
1590
+ token,
1591
+ interval: candleInterval,
1592
+ });
1524
1593
  });
1525
1594
  // Update subscribed state
1526
1595
  if (tokensToSubscribe.length > 0 ||
@@ -1537,7 +1606,48 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1537
1606
  subscribedCandleTokens,
1538
1607
  sendJsonMessage,
1539
1608
  setCandleData,
1609
+ deleteCandleSymbol,
1540
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]);
1541
1651
  return {
1542
1652
  isConnected,
1543
1653
  lastError,
@@ -6626,6 +6736,22 @@ const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
6626
6736
  return basketCandles;
6627
6737
  };
6628
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
+ };
6629
6755
  /**
6630
6756
  * Composes historical price fetching with basket candle computation.
6631
6757
  * - Listens to `longTokens` and `shortTokens` from user selection.
@@ -6635,6 +6761,7 @@ const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
6635
6761
  const useBasketCandles = () => {
6636
6762
  const longTokens = useUserSelection$1((state) => state.longTokens);
6637
6763
  const shortTokens = useUserSelection$1((state) => state.shortTokens);
6764
+ const candleInterval = useUserSelection$1((state) => state.candleInterval);
6638
6765
  const candleData = useHyperliquidData((s) => s.candleData);
6639
6766
  const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData, getEffectiveDataBoundary } = useHistoricalPriceData();
6640
6767
  const fetchBasketCandles = useCallback(async (startTime, endTime, interval) => {
@@ -6740,40 +6867,56 @@ const useBasketCandles = () => {
6740
6867
  const removeRealtimeListener = useCallback((id) => {
6741
6868
  listenersRef.current.delete(id);
6742
6869
  }, []);
6743
- // Helper: compute weighted bar from latest snapshot if all tokens aligned
6870
+ // Helper: compute weighted bar from latest snapshot, carrying forward lagging
6871
+ // symbols so one sparse feed does not suppress every realtime chart update.
6744
6872
  const computeRealtimeBar = useCallback(() => {
6745
6873
  if (!candleData)
6746
6874
  return null;
6747
6875
  const allTokens = [...longTokens, ...shortTokens];
6748
6876
  if (allTokens.length === 0)
6749
6877
  return null;
6750
- // Collect candles, ensure presence and alignment
6751
6878
  const symbolSet = new Set(allTokens.map((t) => t.symbol));
6752
6879
  const snapshot = {};
6880
+ let t = null;
6881
+ let T = null;
6753
6882
  for (const symbol of symbolSet) {
6754
6883
  const c = candleData.get(symbol);
6755
6884
  if (!c)
6756
6885
  return null; // missing latest candle for symbol
6757
6886
  snapshot[symbol] = c;
6758
- }
6759
- // Verify same interval window (t and T match across symbols)
6760
- let t = null;
6761
- let T = null;
6762
- for (const symbol of symbolSet) {
6763
- const c = snapshot[symbol];
6764
- if (t === null) {
6887
+ if (t === null || c.t > t) {
6765
6888
  t = c.t;
6766
6889
  T = c.T;
6767
6890
  }
6768
- else if (c.t !== t || c.T !== T) {
6769
- return null; // not aligned yet
6770
- }
6771
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
+ };
6772
6915
  // Compute weighted OHLC similar to computeBasketCandles
6773
6916
  let longOpen = 1, longHigh = 1, longLow = 1, longClose = 1;
6774
6917
  let shortOpen = 1, shortHigh = 1, shortLow = 1, shortClose = 1;
6775
6918
  for (const token of longTokens) {
6776
- const c = snapshot[token.symbol];
6919
+ const c = getCandleForTargetWindow(token.symbol);
6777
6920
  if (!c)
6778
6921
  return null;
6779
6922
  const w = token.weight / 100;
@@ -6786,7 +6929,7 @@ const useBasketCandles = () => {
6786
6929
  longClose *= Math.pow(cl, w);
6787
6930
  }
6788
6931
  for (const token of shortTokens) {
6789
- const c = snapshot[token.symbol];
6932
+ const c = getCandleForTargetWindow(token.symbol);
6790
6933
  if (!c)
6791
6934
  return null;
6792
6935
  const w = -(token.weight / 100);
@@ -6798,8 +6941,6 @@ const useBasketCandles = () => {
6798
6941
  shortLow *= Math.pow(l, w);
6799
6942
  shortClose *= Math.pow(cl, w);
6800
6943
  }
6801
- if (t === null || T === null)
6802
- return null;
6803
6944
  const weighted = {
6804
6945
  t,
6805
6946
  T,
@@ -6809,8 +6950,8 @@ const useBasketCandles = () => {
6809
6950
  c: longClose * shortClose,
6810
6951
  };
6811
6952
  return weighted;
6812
- }, [candleData, longTokens, shortTokens]);
6813
- // Emit realtime bars when aligned snapshot updates
6953
+ }, [candleData, candleInterval, longTokens, shortTokens]);
6954
+ // Emit realtime bars when the latest snapshot can produce a valid bar.
6814
6955
  useEffect(() => {
6815
6956
  if (listenersRef.current.size === 0)
6816
6957
  return;
@@ -1,4 +1,4 @@
1
- import type { CandleInterval, TokenConflict, TokenSelection, TokenSelectorConfig } from "../types";
1
+ import type { CandleInterval, TokenConflict, TokenSelection, TokenSelectorConfig } from '../types';
2
2
  interface UserSelectionState {
3
3
  longTokens: TokenSelection[];
4
4
  shortTokens: TokenSelection[];
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/hyperliquid-sdk",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "React SDK for Pear Protocol Hyperliquid API integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",