@pear-protocol/exchanges-sdk 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +78 -12
  2. package/dist/client/binance/rest.js +4 -3
  3. package/dist/client/bybit/rest.js +2 -1
  4. package/dist/client/hyperliquid/client.test.js +41 -0
  5. package/dist/client/hyperliquid/exchange-ws.js +3 -2
  6. package/dist/client/hyperliquid/rest.js +28 -5
  7. package/dist/client/hyperliquid/ws.js +16 -4
  8. package/dist/client/index.js +2 -2
  9. package/dist/client/lighter/constants.js +9 -1
  10. package/dist/client/lighter/constants.test.js +22 -0
  11. package/dist/client/lighter/exchange-ws.js +2 -2
  12. package/dist/client/lighter/rest.js +110 -37
  13. package/dist/client/lighter/rest.test.js +152 -0
  14. package/dist/client/lighter/ws.js +7 -2
  15. package/dist/client/okx/rest.js +20 -10
  16. package/dist/helper/factory.js +2 -2
  17. package/dist/helper/hyperliquid/helper.js +28 -13
  18. package/dist/helper/hyperliquid/helper.test.js +45 -0
  19. package/dist/helper/lighter/helper.js +77 -16
  20. package/dist/helper/lighter/helper.test.js +289 -0
  21. package/dist/helper/lighter/types.js +9 -8
  22. package/dist/index.d.ts +189 -38
  23. package/dist/sdk.js +22 -16
  24. package/dist/sdk.test.js +101 -0
  25. package/dist/status/hyperliquid/status.js +18 -5
  26. package/dist/status/hyperliquid/status.test.js +81 -0
  27. package/dist/status/lighter/status.js +18 -11
  28. package/dist/status/lighter/status.test.js +75 -0
  29. package/dist/syncer/base.js +0 -2
  30. package/dist/syncer/base.test.js +0 -1
  31. package/dist/syncer/hyperliquid/syncer.js +96 -43
  32. package/dist/syncer/hyperliquid/syncer.test.js +274 -0
  33. package/dist/syncer/lighter/funding.js +25 -0
  34. package/dist/syncer/lighter/funding.test.js +48 -0
  35. package/dist/syncer/lighter/syncer.js +30 -6
  36. package/dist/syncer/lighter/syncer.test.js +132 -6
  37. package/dist/tracker/base.js +23 -6
  38. package/dist/tracker/base.test.js +132 -0
  39. package/dist/tracker/binance/tracker.js +48 -18
  40. package/dist/tracker/binance/tracker.test.js +138 -0
  41. package/dist/tracker/bybit/tracker.js +78 -23
  42. package/dist/tracker/bybit/tracker.test.js +145 -0
  43. package/dist/tracker/hyperliquid/mapper.js +244 -44
  44. package/dist/tracker/hyperliquid/mapper.test.js +255 -0
  45. package/dist/tracker/hyperliquid/tracker.js +241 -74
  46. package/dist/tracker/hyperliquid/tracker.test.js +587 -0
  47. package/dist/tracker/index.js +15 -37
  48. package/dist/tracker/lighter/mapper.js +163 -33
  49. package/dist/tracker/lighter/mapper.test.js +199 -0
  50. package/dist/tracker/lighter/tracker.js +219 -45
  51. package/dist/tracker/lighter/tracker.test.js +515 -0
  52. package/dist/tracker/lighter/types.js +10 -2
  53. package/dist/tracker/okx/tracker.js +50 -16
  54. package/dist/tracker/okx/tracker.test.js +126 -0
  55. package/dist/tracker/types.js +2 -1
  56. package/dist/tracker/utils.test.js +8 -0
  57. package/package.json +6 -5
@@ -1,36 +1,53 @@
1
1
  import BigNumber from 'bignumber.js';
2
2
  import { positionKey } from '../utils';
3
3
  import { QUOTE_ASSET } from './const';
4
- import { LighterAccountType } from './types';
4
+ import { LighterAccountType, LighterAccountTier } from './types';
5
5
 
6
- function mapBalance(summary, assetsByAssetId) {
6
+ function buildAssetBalances(rawAssets) {
7
7
  const free = {};
8
8
  const used = {};
9
9
  const total = {};
10
10
  const assets = [];
11
- for (const a of assetsByAssetId.values()) {
12
- const balBN = new BigNumber(a.balance || "0");
13
- const lockedBN = new BigNumber(a.locked_balance || "0");
14
- const totalAmt = balBN.plus(lockedBN).toFixed();
15
- free[a.symbol] = a.balance;
16
- used[a.symbol] = a.locked_balance;
17
- total[a.symbol] = totalAmt;
11
+ for (const rawAsset of rawAssets) {
12
+ const balance = new BigNumber(rawAsset.balance || "0");
13
+ const lockedBalance = new BigNumber(rawAsset.locked_balance || "0");
14
+ const marginBalance = new BigNumber(rawAsset.margin_balance || "0");
15
+ const totalAmount = balance.plus(marginBalance).toFixed();
16
+ const freeAmount = BigNumber.max(balance.plus(marginBalance).minus(lockedBalance), 0).toFixed();
17
+ free[rawAsset.symbol] = freeAmount;
18
+ used[rawAsset.symbol] = rawAsset.locked_balance;
19
+ total[rawAsset.symbol] = totalAmount;
18
20
  assets.push({
19
- asset: a.symbol,
20
- free: a.balance,
21
- used: a.locked_balance,
22
- total: totalAmt,
23
- walletBalance: totalAmt,
24
- usdValue: a.symbol === QUOTE_ASSET ? totalAmt : "0"
21
+ asset: rawAsset.symbol,
22
+ free: freeAmount,
23
+ used: rawAsset.locked_balance,
24
+ total: totalAmount,
25
+ walletBalance: totalAmount,
26
+ usdValue: rawAsset.symbol === QUOTE_ASSET ? totalAmount : "0"
25
27
  });
26
28
  }
29
+ return { free, used, total, assets };
30
+ }
31
+ function mapBalance(summary, assetsByAssetId, rawPositions, marketsByMarketId) {
32
+ const { free, used, total, assets } = buildAssetBalances(assetsByAssetId.values());
27
33
  const portfolioValueBN = new BigNumber(summary.portfolioValue);
28
34
  const marginUsageFrac = new BigNumber(summary.marginUsagePct).div(100);
29
35
  const unrealizedPnl = portfolioValueBN.minus(summary.collateral).toFixed(6);
30
36
  const initialMarginUsed = marginUsageFrac.multipliedBy(portfolioValueBN).toFixed(6);
31
- const marginRatio = marginUsageFrac.toFixed(4);
37
+ const positionRisks = calculatePositionRisks(rawPositions, marketsByMarketId);
38
+ const maintenanceMargin = positionRisks.reduce(
39
+ (totalMaintenance, positionRisk) => totalMaintenance.plus(positionRisk.maintenance),
40
+ new BigNumber(0)
41
+ );
42
+ const isolatedEquity = positionRisks.reduce(
43
+ (totalEquity, positionRisk) => positionRisk.marginMode === "isolated" ? totalEquity.plus(positionRisk.equity) : totalEquity,
44
+ new BigNumber(0)
45
+ );
46
+ const crossEquity = new BigNumber(summary.crossPortfolioValue ?? portfolioValueBN.minus(isolatedEquity));
47
+ const marginRatio = maximumMarginRatio(positionRisks, crossEquity);
32
48
  return {
33
- accountType: LighterAccountType.BASIC,
49
+ accountType: summary.accountType,
50
+ accountTier: summary.accountTier,
34
51
  free,
35
52
  used,
36
53
  total,
@@ -39,32 +56,79 @@ function mapBalance(summary, assetsByAssetId) {
39
56
  walletBalance: summary.collateral,
40
57
  unrealizedPnl,
41
58
  initialMarginUsed,
42
- maintenanceMargin: "0",
59
+ maintenanceMargin: maintenanceMargin.toFixed(6),
43
60
  marginRatio,
44
61
  assets,
45
62
  timestamp: Date.now()
46
63
  };
47
64
  }
48
- function mapPositions(raw) {
65
+ function buildAccountBalance(account, marketsByMarketId, accountTier) {
66
+ const { free, used, total, assets } = buildAssetBalances(account.assets ?? []);
67
+ const positions = account.positions ?? [];
68
+ const allocatedMargin = sumPositionField(positions, "allocated_margin", (position) => position.margin_mode === 1);
69
+ const totalCollateral = new BigNumber(account.collateral).plus(allocatedMargin);
70
+ const isolatedInitialMargin = positions.reduce((totalInitialMargin, position) => {
71
+ if (position.margin_mode !== 1 || new BigNumber(position.position).isZero()) {
72
+ return totalInitialMargin;
73
+ }
74
+ return totalInitialMargin.plus(
75
+ new BigNumber(position.position_value).abs().times(position.initial_margin_fraction).div(100)
76
+ );
77
+ }, new BigNumber(0));
78
+ const initialMargin = new BigNumber(account.cross_initial_margin_requirement).plus(isolatedInitialMargin);
79
+ const positionRisks = calculatePositionRisks(positions, marketsByMarketId);
80
+ const isolatedMaintenanceMargin = positionRisks.reduce(
81
+ (totalMaintenance, positionRisk) => positionRisk.marginMode === "isolated" ? totalMaintenance.plus(positionRisk.maintenance) : totalMaintenance,
82
+ new BigNumber(0)
83
+ );
84
+ const maintenanceMarginBN = new BigNumber(account.cross_maintenance_margin_requirement).plus(
85
+ isolatedMaintenanceMargin
86
+ );
87
+ const marginRatio = maximumMarginRatio(
88
+ positionRisks.filter((positionRisk) => positionRisk.marginMode === "isolated"),
89
+ new BigNumber(account.cross_asset_value),
90
+ new BigNumber(account.cross_maintenance_margin_requirement)
91
+ );
92
+ const unrealizedPnl = sumPositionField(positions, "unrealized_pnl").toFixed(6);
93
+ return {
94
+ accountType: toLighterAccountType(account.account_trading_mode),
95
+ accountTier,
96
+ free,
97
+ used,
98
+ total,
99
+ availableToTrade: account.available_balance,
100
+ totalEquity: account.total_asset_value,
101
+ walletBalance: totalCollateral.toFixed(6),
102
+ unrealizedPnl,
103
+ initialMarginUsed: initialMargin.toFixed(6),
104
+ maintenanceMargin: maintenanceMarginBN.toFixed(6),
105
+ marginRatio,
106
+ assets,
107
+ timestamp: Date.now()
108
+ };
109
+ }
110
+ function mapPositions(raw, includeZeroPositions = false) {
49
111
  const positions = /* @__PURE__ */ new Map();
50
112
  const trackedAssets = /* @__PURE__ */ new Map();
51
- for (const p of Object.values(raw)) {
52
- const sizeBN = new BigNumber(p.position || "0");
53
- const side = p.sign === -1 ? "short" : "long";
54
- const leverage = leverageFromImf(p.initial_margin_fraction);
55
- const marginType = p.margin_mode === 1 ? "isolated" : "cross";
56
- const marketId = String(p.market_id);
57
- positions.set(positionKey(p.symbol, side), {
58
- symbol: p.symbol,
113
+ for (const rawPosition of Object.values(raw)) {
114
+ const unsignedSize = new BigNumber(rawPosition.position || "0");
115
+ const side = rawPosition.sign === -1 ? "short" : "long";
116
+ const size = rawPosition.sign === -1 ? unsignedSize.negated() : unsignedSize;
117
+ const leverage = leverageFromImf(rawPosition.initial_margin_fraction);
118
+ const marginType = rawPosition.margin_mode === 1 ? "isolated" : "cross";
119
+ const marketId = String(rawPosition.market_id);
120
+ trackedAssets.set(marketId, { coin: rawPosition.symbol, leverage, marginType });
121
+ if (!includeZeroPositions && unsignedSize.isZero()) continue;
122
+ positions.set(positionKey(rawPosition.symbol, side), {
123
+ symbol: rawPosition.symbol,
59
124
  side,
60
- size: sizeBN.isFinite() && !sizeBN.isZero() ? sizeBN.toFixed() : "0",
61
- entryPrice: p.avg_entry_price || "0",
62
- unrealizedPnl: p.unrealized_pnl || "0",
125
+ size: size.isFinite() && !size.isZero() ? size.toFixed() : "0",
126
+ entryPrice: rawPosition.avg_entry_price || "0",
127
+ unrealizedPnl: rawPosition.unrealized_pnl || "0",
63
128
  leverage,
64
129
  marginType,
65
- liquidationPrice: p.liquidation_price && p.liquidation_price !== "0" ? p.liquidation_price : null
130
+ liquidationPrice: rawPosition.liquidation_price && rawPosition.liquidation_price !== "0" ? rawPosition.liquidation_price : null
66
131
  });
67
- trackedAssets.set(marketId, { coin: p.symbol, leverage, marginType });
68
132
  }
69
133
  return { positions, trackedAssets };
70
134
  }
@@ -73,5 +137,71 @@ function leverageFromImf(imfPct) {
73
137
  if (!pct.isFinite() || pct.lte(0)) return "1";
74
138
  return new BigNumber(100).div(pct).integerValue(BigNumber.ROUND_FLOOR).toFixed();
75
139
  }
140
+ function mapMarketDefaultAssetInfo(market) {
141
+ return {
142
+ coin: market.symbol,
143
+ leverage: leverageFromImf(new BigNumber(market.default_initial_margin_fraction).div(100).toFixed()),
144
+ marginType: "cross"
145
+ };
146
+ }
147
+ function toLighterAccountType(accountTradingMode) {
148
+ return accountTradingMode === 1 ? LighterAccountType.UNIFIED : LighterAccountType.CLASSIC;
149
+ }
150
+ function toLighterAccountTier(userTier) {
151
+ switch (userTier?.trim().toLowerCase()) {
152
+ case "std":
153
+ case "standard":
154
+ return LighterAccountTier.STANDARD;
155
+ case "premium":
156
+ return LighterAccountTier.PREMIUM;
157
+ case "plus":
158
+ return LighterAccountTier.PLUS;
159
+ default:
160
+ return void 0;
161
+ }
162
+ }
163
+ function calculatePositionRisks(rawPositions, marketsByMarketId) {
164
+ const positionRisks = [];
165
+ for (const rawPosition of rawPositions) {
166
+ const market = marketsByMarketId.get(rawPosition.market_id);
167
+ if (!market || new BigNumber(rawPosition.position).isZero()) {
168
+ continue;
169
+ }
170
+ const marginMode = rawPosition.margin_mode === 1 ? "isolated" : "cross";
171
+ const maintenance = new BigNumber(rawPosition.position_value).abs().times(market.maintenance_margin_fraction).div(1e4);
172
+ const equity = marginMode === "isolated" ? new BigNumber(rawPosition.allocated_margin).plus(rawPosition.unrealized_pnl) : new BigNumber(0);
173
+ positionRisks.push({ marginMode, maintenance, equity });
174
+ }
175
+ return positionRisks;
176
+ }
177
+ function maximumMarginRatio(positionRisks, crossEquity, authoritativeCrossMaintenance) {
178
+ const calculatedCrossMaintenance = positionRisks.reduce(
179
+ (totalMaintenance, positionRisk) => positionRisk.marginMode === "cross" ? totalMaintenance.plus(positionRisk.maintenance) : totalMaintenance,
180
+ new BigNumber(0)
181
+ );
182
+ const crossMaintenance = authoritativeCrossMaintenance ?? calculatedCrossMaintenance;
183
+ let maximumRatio = maintenanceRatio(crossMaintenance, crossEquity);
184
+ for (const positionRisk of positionRisks) {
185
+ if (positionRisk.marginMode === "isolated") {
186
+ maximumRatio = BigNumber.max(maximumRatio, maintenanceRatio(positionRisk.maintenance, positionRisk.equity));
187
+ }
188
+ }
189
+ return maximumRatio.toFixed(4);
190
+ }
191
+ function maintenanceRatio(maintenance, equity) {
192
+ if (equity.gt(0)) {
193
+ return maintenance.div(equity);
194
+ }
195
+ return maintenance.gt(0) ? new BigNumber(1) : new BigNumber(0);
196
+ }
197
+ function sumPositionField(positions, field, include = () => true) {
198
+ let total = new BigNumber(0);
199
+ for (const position of positions) {
200
+ if (include(position)) {
201
+ total = total.plus(position[field] || "0");
202
+ }
203
+ }
204
+ return total;
205
+ }
76
206
 
77
- export { leverageFromImf, mapBalance, mapPositions };
207
+ export { buildAccountBalance, leverageFromImf, mapBalance, mapMarketDefaultAssetInfo, mapPositions, toLighterAccountTier, toLighterAccountType };
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildAccountBalance, mapBalance, toLighterAccountTier, mapPositions } from './mapper';
3
+ import { LighterAccountTier, LighterAccountType } from './types';
4
+
5
+ const markets = /* @__PURE__ */ new Map([
6
+ [
7
+ 1,
8
+ {
9
+ market_id: 1,
10
+ symbol: "BTC",
11
+ status: "active",
12
+ default_initial_margin_fraction: 1e3,
13
+ maintenance_margin_fraction: 1e3
14
+ }
15
+ ],
16
+ [
17
+ 2,
18
+ {
19
+ market_id: 2,
20
+ symbol: "ETH",
21
+ status: "active",
22
+ default_initial_margin_fraction: 2e3,
23
+ maintenance_margin_fraction: 500
24
+ }
25
+ ]
26
+ ]);
27
+ describe("Lighter account mapping", () => {
28
+ it("combines cross and isolated collateral, risk requirements, and unrealized PnL", () => {
29
+ const balance = buildAccountBalance(
30
+ {
31
+ account_type: 2,
32
+ account_trading_mode: 1,
33
+ collateral: "1000",
34
+ total_asset_value: "1145",
35
+ available_balance: "800",
36
+ cross_asset_value: "1020",
37
+ cross_initial_margin_requirement: "100",
38
+ cross_maintenance_margin_requirement: "50",
39
+ assets: [
40
+ {
41
+ symbol: "USDC",
42
+ balance: "10",
43
+ locked_balance: "2",
44
+ margin_balance: "1000"
45
+ }
46
+ ],
47
+ positions: [
48
+ {
49
+ market_id: 1,
50
+ symbol: "BTC",
51
+ initial_margin_fraction: "10",
52
+ sign: 1,
53
+ position: "1",
54
+ avg_entry_price: "1000",
55
+ position_value: "1000",
56
+ unrealized_pnl: "20",
57
+ liquidation_price: "500",
58
+ margin_mode: 0,
59
+ allocated_margin: "0"
60
+ },
61
+ {
62
+ market_id: 2,
63
+ symbol: "ETH",
64
+ initial_margin_fraction: "20",
65
+ sign: -1,
66
+ position: "2",
67
+ avg_entry_price: "250",
68
+ position_value: "500",
69
+ unrealized_pnl: "-25",
70
+ liquidation_price: "400",
71
+ margin_mode: 1,
72
+ allocated_margin: "150"
73
+ }
74
+ ]
75
+ },
76
+ markets,
77
+ LighterAccountTier.PLUS
78
+ );
79
+ expect(balance).toMatchObject({
80
+ accountType: "unified",
81
+ accountTier: "plus",
82
+ totalEquity: "1145",
83
+ walletBalance: "1150.000000",
84
+ unrealizedPnl: "-5.000000",
85
+ initialMarginUsed: "200.000000",
86
+ maintenanceMargin: "75.000000",
87
+ marginRatio: "0.2000",
88
+ free: { USDC: "1008" },
89
+ used: { USDC: "2" },
90
+ total: { USDC: "1010" }
91
+ });
92
+ });
93
+ it("uses the riskiest independently liquidatable bucket for live margin ratio", () => {
94
+ const balance = mapBalance(
95
+ {
96
+ collateral: "1050",
97
+ portfolioValue: "950",
98
+ crossPortfolioValue: "900",
99
+ availableToTrade: "800",
100
+ marginUsagePct: "0",
101
+ accountType: LighterAccountType.CLASSIC
102
+ },
103
+ /* @__PURE__ */ new Map(),
104
+ [
105
+ rawPosition({ position_value: "100", margin_mode: 0 }),
106
+ rawPosition({
107
+ market_id: 2,
108
+ symbol: "ETH",
109
+ position_value: "500",
110
+ unrealized_pnl: "-100",
111
+ margin_mode: 1,
112
+ allocated_margin: "150"
113
+ })
114
+ ],
115
+ markets
116
+ );
117
+ expect(balance.maintenanceMargin).toBe("35.000000");
118
+ expect(balance.marginRatio).toBe("0.5000");
119
+ });
120
+ it.each(["0", "-1"])("fails closed for live maintenance risk with nonpositive equity %s", (portfolioValue) => {
121
+ const balance = mapBalance(
122
+ {
123
+ collateral: "0",
124
+ portfolioValue,
125
+ availableToTrade: "0",
126
+ marginUsagePct: "0",
127
+ accountType: LighterAccountType.CLASSIC
128
+ },
129
+ /* @__PURE__ */ new Map(),
130
+ [rawPosition({ position_value: "1000" })],
131
+ markets
132
+ );
133
+ expect(balance.maintenanceMargin).toBe("100.000000");
134
+ expect(balance.marginRatio).toBe("1.0000");
135
+ });
136
+ it.each(["0", "-1"])("fails closed for REST maintenance risk with nonpositive equity %s", (totalAssetValue) => {
137
+ const balance = buildAccountBalance(
138
+ {
139
+ account_type: 0,
140
+ account_trading_mode: 0,
141
+ collateral: "0",
142
+ total_asset_value: totalAssetValue,
143
+ available_balance: "0",
144
+ cross_asset_value: totalAssetValue,
145
+ cross_initial_margin_requirement: "0",
146
+ cross_maintenance_margin_requirement: "10"
147
+ },
148
+ markets
149
+ );
150
+ expect(balance.maintenanceMargin).toBe("10.000000");
151
+ expect(balance.marginRatio).toBe("1.0000");
152
+ });
153
+ it("maps authenticated account-limit tier names and never infers a tier from account_type", () => {
154
+ expect(toLighterAccountTier("std")).toBe(LighterAccountTier.STANDARD);
155
+ expect(toLighterAccountTier("STANDARD")).toBe(LighterAccountTier.STANDARD);
156
+ expect(toLighterAccountTier("premium")).toBe(LighterAccountTier.PREMIUM);
157
+ expect(toLighterAccountTier("plus")).toBe(LighterAccountTier.PLUS);
158
+ expect(toLighterAccountTier("unknown")).toBeUndefined();
159
+ const balance = buildAccountBalance(
160
+ {
161
+ account_type: 2,
162
+ account_trading_mode: 0,
163
+ collateral: "0",
164
+ total_asset_value: "0",
165
+ available_balance: "0",
166
+ cross_asset_value: "0",
167
+ cross_initial_margin_requirement: "0",
168
+ cross_maintenance_margin_requirement: "0"
169
+ },
170
+ /* @__PURE__ */ new Map()
171
+ );
172
+ expect(balance.accountTier).toBeUndefined();
173
+ });
174
+ it("filters full-snapshot zero rows and preserves signed short exposure", () => {
175
+ const positions = mapPositions({
176
+ 1: rawPosition({ market_id: 1, symbol: "BTC", sign: -1, position: "2" }),
177
+ 2: rawPosition({ market_id: 2, symbol: "ETH", sign: 1, position: "0.0000" })
178
+ }).positions;
179
+ expect(Array.from(positions.values())).toEqual([
180
+ expect.objectContaining({ symbol: "BTC", side: "short", size: "-2" })
181
+ ]);
182
+ });
183
+ });
184
+ function rawPosition(overrides) {
185
+ return {
186
+ market_id: 1,
187
+ symbol: "BTC",
188
+ initial_margin_fraction: "10",
189
+ sign: 1,
190
+ position: "1",
191
+ avg_entry_price: "1000",
192
+ position_value: "1000",
193
+ unrealized_pnl: "0",
194
+ liquidation_price: "500",
195
+ margin_mode: 0,
196
+ allocated_margin: "0",
197
+ ...overrides
198
+ };
199
+ }