@pear-protocol/exchanges-sdk 0.2.0 → 0.4.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.
- package/README.md +78 -12
- package/dist/client/hyperliquid/client.test.js +41 -0
- package/dist/client/hyperliquid/exchange-ws.js +3 -2
- package/dist/client/hyperliquid/rest.js +26 -5
- package/dist/client/hyperliquid/ws.js +16 -4
- package/dist/client/index.js +2 -2
- package/dist/client/lighter/constants.js +9 -1
- package/dist/client/lighter/constants.test.js +22 -0
- package/dist/client/lighter/exchange-ws.js +2 -2
- package/dist/client/lighter/rest.js +101 -32
- package/dist/client/lighter/rest.test.js +152 -0
- package/dist/client/lighter/ws.js +7 -2
- package/dist/client/okx/rest.js +18 -9
- package/dist/helper/factory.js +2 -2
- package/dist/helper/hyperliquid/helper.js +28 -13
- package/dist/helper/hyperliquid/helper.test.js +45 -0
- package/dist/helper/lighter/helper.js +77 -16
- package/dist/helper/lighter/helper.test.js +289 -0
- package/dist/helper/lighter/types.js +9 -8
- package/dist/index.d.ts +239 -47
- package/dist/sdk.js +20 -16
- package/dist/sdk.test.js +101 -0
- package/dist/status/hyperliquid/status.js +18 -5
- package/dist/status/hyperliquid/status.test.js +81 -0
- package/dist/status/lighter/status.js +18 -11
- package/dist/status/lighter/status.test.js +75 -0
- package/dist/syncer/base.js +107 -25
- package/dist/syncer/base.test.js +131 -0
- package/dist/syncer/binance/syncer.js +48 -20
- package/dist/syncer/bybit/syncer.js +39 -15
- package/dist/syncer/hyperliquid/funding.js +24 -0
- package/dist/syncer/hyperliquid/funding.test.js +63 -0
- package/dist/syncer/hyperliquid/syncer.js +183 -24
- package/dist/syncer/hyperliquid/syncer.test.js +274 -0
- package/dist/syncer/lighter/funding.js +25 -0
- package/dist/syncer/lighter/funding.test.js +48 -0
- package/dist/syncer/lighter/syncer.js +93 -41
- package/dist/syncer/lighter/syncer.test.js +132 -6
- package/dist/syncer/okx/syncer.js +41 -17
- package/dist/tracker/base.js +23 -6
- package/dist/tracker/base.test.js +132 -0
- package/dist/tracker/binance/tracker.js +48 -18
- package/dist/tracker/binance/tracker.test.js +138 -0
- package/dist/tracker/bybit/tracker.js +78 -23
- package/dist/tracker/bybit/tracker.test.js +145 -0
- package/dist/tracker/hyperliquid/mapper.js +244 -44
- package/dist/tracker/hyperliquid/mapper.test.js +255 -0
- package/dist/tracker/hyperliquid/tracker.js +241 -74
- package/dist/tracker/hyperliquid/tracker.test.js +587 -0
- package/dist/tracker/index.js +15 -37
- package/dist/tracker/lighter/mapper.js +163 -33
- package/dist/tracker/lighter/mapper.test.js +199 -0
- package/dist/tracker/lighter/tracker.js +219 -45
- package/dist/tracker/lighter/tracker.test.js +515 -0
- package/dist/tracker/lighter/types.js +10 -2
- package/dist/tracker/okx/tracker.js +50 -16
- package/dist/tracker/okx/tracker.test.js +126 -0
- package/dist/tracker/types.js +2 -1
- package/dist/tracker/utils.test.js +8 -0
- package/package.json +6 -5
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
|
2
|
+
import { HyperliquidAccountSyncer } from './syncer';
|
|
3
|
+
|
|
4
|
+
const { fetchInfoMock } = vi.hoisted(() => ({ fetchInfoMock: vi.fn() }));
|
|
5
|
+
vi.mock("../../client/hyperliquid/rest", () => ({ fetchInfo: fetchInfoMock }));
|
|
6
|
+
const account = {
|
|
7
|
+
exchangeIdentifier: "0x1111111111111111111111111111111111111111"
|
|
8
|
+
};
|
|
9
|
+
const instrument = {
|
|
10
|
+
id: "HL-BTC",
|
|
11
|
+
base: "BTC"
|
|
12
|
+
};
|
|
13
|
+
function rawFill(time, tid = time) {
|
|
14
|
+
return {
|
|
15
|
+
coin: "BTC",
|
|
16
|
+
px: "100000",
|
|
17
|
+
sz: "0.1",
|
|
18
|
+
side: "B",
|
|
19
|
+
time,
|
|
20
|
+
startPosition: "0",
|
|
21
|
+
dir: "Open Long",
|
|
22
|
+
closedPnl: "0",
|
|
23
|
+
hash: "0xabc",
|
|
24
|
+
oid: 123,
|
|
25
|
+
crossed: true,
|
|
26
|
+
fee: "1",
|
|
27
|
+
builderFee: "0.2",
|
|
28
|
+
tid,
|
|
29
|
+
feeToken: "USDC",
|
|
30
|
+
twapId: null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createSyncer() {
|
|
34
|
+
return new HyperliquidAccountSyncer({
|
|
35
|
+
sdk: {},
|
|
36
|
+
connector: "hyperliquid",
|
|
37
|
+
account,
|
|
38
|
+
credentials: { signer_key: "sk" },
|
|
39
|
+
demo: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
describe("HyperliquidAccountSyncer fill mapping", () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
fetchInfoMock.mockReset().mockImplementation(async (body) => {
|
|
45
|
+
if (body.type === "userTwapSliceFillsByTime") {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
coin: "BTC",
|
|
51
|
+
px: "100000",
|
|
52
|
+
sz: "0.1",
|
|
53
|
+
side: "B",
|
|
54
|
+
time: 17e11,
|
|
55
|
+
startPosition: "0",
|
|
56
|
+
dir: "Open Long",
|
|
57
|
+
closedPnl: "0",
|
|
58
|
+
hash: "0xabc",
|
|
59
|
+
oid: 123,
|
|
60
|
+
crossed: true,
|
|
61
|
+
fee: "1",
|
|
62
|
+
builderFee: "0.2",
|
|
63
|
+
tid: 456,
|
|
64
|
+
feeToken: "USDC",
|
|
65
|
+
twapId: null
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
it("uses a globally unique identity, excludes builder fee from trade fee, and routes to testnet", async () => {
|
|
71
|
+
const syncer = createSyncer();
|
|
72
|
+
const fills = await syncer.listExternalFills({
|
|
73
|
+
since: /* @__PURE__ */ new Date(1699999e6),
|
|
74
|
+
instruments: [instrument],
|
|
75
|
+
openPositions: [],
|
|
76
|
+
provisionalFills: []
|
|
77
|
+
});
|
|
78
|
+
expect(fills).toEqual([
|
|
79
|
+
expect.objectContaining({
|
|
80
|
+
exchangeFillId: "1700000000000:BTC:456",
|
|
81
|
+
tradeFee: "0.8",
|
|
82
|
+
builderFee: "0.2"
|
|
83
|
+
})
|
|
84
|
+
]);
|
|
85
|
+
expect(fetchInfoMock).toHaveBeenCalledWith(expect.objectContaining({ type: "userFillsByTime" }), true);
|
|
86
|
+
expect(fetchInfoMock).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({ type: "userFillsByTime", aggregateByTime: false }),
|
|
88
|
+
true
|
|
89
|
+
);
|
|
90
|
+
expect(fetchInfoMock).toHaveBeenCalledWith(
|
|
91
|
+
expect.objectContaining({ type: "userTwapSliceFillsByTime", aggregateByTime: false }),
|
|
92
|
+
true
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
it("fails closed when a maker rebate cannot satisfy the backend external-fill fee contract", async () => {
|
|
96
|
+
fetchInfoMock.mockImplementation(async (body) => {
|
|
97
|
+
if (body.type === "userTwapSliceFillsByTime") return [];
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
coin: "BTC",
|
|
101
|
+
px: "100000",
|
|
102
|
+
sz: "0.1",
|
|
103
|
+
side: "B",
|
|
104
|
+
time: 17e11,
|
|
105
|
+
startPosition: "0",
|
|
106
|
+
dir: "Open Long",
|
|
107
|
+
closedPnl: "0",
|
|
108
|
+
hash: "0xabc",
|
|
109
|
+
oid: 123,
|
|
110
|
+
crossed: false,
|
|
111
|
+
fee: "-0.08",
|
|
112
|
+
builderFee: "0.02",
|
|
113
|
+
tid: 456,
|
|
114
|
+
feeToken: "USDC",
|
|
115
|
+
twapId: null
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
});
|
|
119
|
+
const syncer = createSyncer();
|
|
120
|
+
await expect(
|
|
121
|
+
syncer.listExternalFills({
|
|
122
|
+
since: /* @__PURE__ */ new Date(1699999e6),
|
|
123
|
+
instruments: [instrument],
|
|
124
|
+
openPositions: [],
|
|
125
|
+
provisionalFills: []
|
|
126
|
+
})
|
|
127
|
+
).rejects.toThrow(
|
|
128
|
+
"Hyperliquid fill 1700000000000:BTC:456 has a negative net trade fee (-0.1); the backend external-fill contract does not accept Hyperliquid rebates."
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
it("warns when inclusive fill pagination cannot move past the current timestamp", async () => {
|
|
132
|
+
const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => void 0);
|
|
133
|
+
const event = {
|
|
134
|
+
coin: "BTC",
|
|
135
|
+
px: "100000",
|
|
136
|
+
sz: "0.1",
|
|
137
|
+
side: "B",
|
|
138
|
+
time: 1e3,
|
|
139
|
+
startPosition: "0",
|
|
140
|
+
dir: "Open Long",
|
|
141
|
+
closedPnl: "0",
|
|
142
|
+
hash: "0xabc",
|
|
143
|
+
oid: 123,
|
|
144
|
+
crossed: true,
|
|
145
|
+
fee: "1",
|
|
146
|
+
builderFee: "0.2",
|
|
147
|
+
tid: 456,
|
|
148
|
+
feeToken: "USDC",
|
|
149
|
+
twapId: null
|
|
150
|
+
};
|
|
151
|
+
fetchInfoMock.mockImplementation(
|
|
152
|
+
async (body) => body.type === "userFillsByTime" ? Array.from({ length: 2e3 }, () => event) : []
|
|
153
|
+
);
|
|
154
|
+
const syncer = createSyncer();
|
|
155
|
+
await syncer.listExternalFills({
|
|
156
|
+
since: /* @__PURE__ */ new Date(1e3),
|
|
157
|
+
instruments: [instrument],
|
|
158
|
+
openPositions: [],
|
|
159
|
+
provisionalFills: []
|
|
160
|
+
});
|
|
161
|
+
expect(consoleWarn).toHaveBeenCalledWith(
|
|
162
|
+
"Hyperliquid fills pagination stalled at timestamp 1000; returned history may be incomplete."
|
|
163
|
+
);
|
|
164
|
+
expect(fetchInfoMock).toHaveBeenCalledTimes(2);
|
|
165
|
+
consoleWarn.mockRestore();
|
|
166
|
+
});
|
|
167
|
+
it("counts raw fills before rejecting history older than the 10,000-fill retention boundary", async () => {
|
|
168
|
+
let page = 0;
|
|
169
|
+
fetchInfoMock.mockImplementation(async (body) => {
|
|
170
|
+
if (body.type !== "userFillsByTime") return [];
|
|
171
|
+
const firstTime = 1e3 + page * 2e3;
|
|
172
|
+
page += 1;
|
|
173
|
+
return Array.from({ length: 2e3 }, (_, index) => rawFill(firstTime + index));
|
|
174
|
+
});
|
|
175
|
+
const syncer = createSyncer();
|
|
176
|
+
await expect(
|
|
177
|
+
syncer.listExternalFills({
|
|
178
|
+
since: /* @__PURE__ */ new Date(0),
|
|
179
|
+
instruments: [instrument],
|
|
180
|
+
openPositions: [],
|
|
181
|
+
provisionalFills: []
|
|
182
|
+
})
|
|
183
|
+
).rejects.toThrow(
|
|
184
|
+
"Hyperliquid fill history is incomplete: requested since 0 predates oldest reachable fill 1000 within the 10000-fill retention limit."
|
|
185
|
+
);
|
|
186
|
+
expect(fetchInfoMock).toHaveBeenCalledTimes(5);
|
|
187
|
+
expect(fetchInfoMock).toHaveBeenCalledWith(expect.objectContaining({ aggregateByTime: false }), true);
|
|
188
|
+
});
|
|
189
|
+
it("accepts a request exactly at the oldest reachable fill at the retention boundary", async () => {
|
|
190
|
+
let page = 0;
|
|
191
|
+
fetchInfoMock.mockImplementation(async (body) => {
|
|
192
|
+
if (body.type === "userTwapSliceFillsByTime") return [];
|
|
193
|
+
if (page >= 5) return [];
|
|
194
|
+
const firstTime = 1e3 + page * 2e3;
|
|
195
|
+
page += 1;
|
|
196
|
+
return Array.from({ length: 2e3 }, (_, index) => rawFill(firstTime + index));
|
|
197
|
+
});
|
|
198
|
+
const syncer = createSyncer();
|
|
199
|
+
const fills = await syncer.listExternalFills({
|
|
200
|
+
since: /* @__PURE__ */ new Date(1e3),
|
|
201
|
+
instruments: [instrument],
|
|
202
|
+
openPositions: [],
|
|
203
|
+
provisionalFills: []
|
|
204
|
+
});
|
|
205
|
+
expect(fills).toHaveLength(1e4);
|
|
206
|
+
expect(fetchInfoMock).toHaveBeenCalledTimes(7);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe("HyperliquidAccountSyncer position and funding snapshots", () => {
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
fetchInfoMock.mockReset();
|
|
212
|
+
});
|
|
213
|
+
it("de-duplicates DEX descriptors and derives mark price from current position value", async () => {
|
|
214
|
+
fetchInfoMock.mockImplementation(async (body) => {
|
|
215
|
+
if (body.type === "perpDexs") return [null, null, { name: "xyz" }];
|
|
216
|
+
if (body.type === "clearinghouseState" && body.dex === "") {
|
|
217
|
+
return {
|
|
218
|
+
assetPositions: [
|
|
219
|
+
{
|
|
220
|
+
position: {
|
|
221
|
+
coin: "BTC",
|
|
222
|
+
szi: "2",
|
|
223
|
+
entryPx: "150",
|
|
224
|
+
positionValue: "400"
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (body.type === "clearinghouseState") return { assetPositions: [] };
|
|
231
|
+
throw new Error(`Unexpected request: ${body.type}`);
|
|
232
|
+
});
|
|
233
|
+
const syncer = createSyncer();
|
|
234
|
+
const positions = await syncer.listExternalPositions({
|
|
235
|
+
instruments: [
|
|
236
|
+
{
|
|
237
|
+
...instrument,
|
|
238
|
+
precision: { quantity: { contractSize: 1 } }
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
});
|
|
242
|
+
expect(positions).toEqual([
|
|
243
|
+
expect.objectContaining({ symbol: "HL-BTC", size: "2", entryPrice: "150", markPrice: "200" })
|
|
244
|
+
]);
|
|
245
|
+
expect(fetchInfoMock).toHaveBeenCalledTimes(3);
|
|
246
|
+
});
|
|
247
|
+
it("de-duplicates an inclusive funding boundary and stops when its cursor cannot advance", async () => {
|
|
248
|
+
const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => void 0);
|
|
249
|
+
const event = {
|
|
250
|
+
time: 1e3,
|
|
251
|
+
hash: "0xabc",
|
|
252
|
+
delta: {
|
|
253
|
+
type: "funding",
|
|
254
|
+
coin: "BTC",
|
|
255
|
+
usdc: "-2",
|
|
256
|
+
szi: "4",
|
|
257
|
+
fundingRate: "0.0001",
|
|
258
|
+
nSamples: null
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
fetchInfoMock.mockResolvedValue(Array.from({ length: 500 }, () => event));
|
|
262
|
+
const syncer = createSyncer();
|
|
263
|
+
const payments = await syncer.listExternalFunding({
|
|
264
|
+
since: /* @__PURE__ */ new Date(1e3),
|
|
265
|
+
instruments: [instrument]
|
|
266
|
+
});
|
|
267
|
+
expect(payments).toHaveLength(1);
|
|
268
|
+
expect(fetchInfoMock).toHaveBeenCalledTimes(1);
|
|
269
|
+
expect(consoleWarn).toHaveBeenCalledWith(
|
|
270
|
+
"Hyperliquid funding pagination stalled at timestamp 1000; returned history may be incomplete."
|
|
271
|
+
);
|
|
272
|
+
consoleWarn.mockRestore();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
function toFundingPaymentEntries(events, instrumentIds) {
|
|
4
|
+
return events.reduce((entries, event) => {
|
|
5
|
+
const instrumentId = event.market_id.toString();
|
|
6
|
+
if (!instrumentIds.has(instrumentId)) {
|
|
7
|
+
console.warn(`Lighter market id ${event.market_id} not found in instrument id map.`);
|
|
8
|
+
return entries;
|
|
9
|
+
}
|
|
10
|
+
const unsignedSize = new BigNumber(event.position_size).abs();
|
|
11
|
+
const signedSize = event.position_side === "short" ? unsignedSize.negated() : unsignedSize;
|
|
12
|
+
const fundingPerUnit = signedSize.isZero() ? "0" : new BigNumber(event.change).div(signedSize).toFixed();
|
|
13
|
+
entries.push({
|
|
14
|
+
connector: "lighter",
|
|
15
|
+
instrumentId,
|
|
16
|
+
fundingRate: event.rate,
|
|
17
|
+
notional: event.change,
|
|
18
|
+
fundingPerUnit,
|
|
19
|
+
timestamp: event.timestamp
|
|
20
|
+
});
|
|
21
|
+
return entries;
|
|
22
|
+
}, []);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { toFundingPaymentEntries };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { toFundingPaymentEntries } from './funding';
|
|
3
|
+
|
|
4
|
+
describe("Lighter funding mapping", () => {
|
|
5
|
+
it("derives funding per unit against signed long and short exposure", () => {
|
|
6
|
+
const result = toFundingPaymentEntries(
|
|
7
|
+
[
|
|
8
|
+
fundingEvent({ funding_id: 1, position_side: "long", change: "-2" }),
|
|
9
|
+
fundingEvent({ funding_id: 2, position_side: "short", change: "2" })
|
|
10
|
+
],
|
|
11
|
+
/* @__PURE__ */ new Set(["1"])
|
|
12
|
+
);
|
|
13
|
+
expect(result).toEqual([
|
|
14
|
+
{
|
|
15
|
+
connector: "lighter",
|
|
16
|
+
instrumentId: "1",
|
|
17
|
+
fundingRate: "0.0001",
|
|
18
|
+
notional: "-2",
|
|
19
|
+
fundingPerUnit: "-0.5",
|
|
20
|
+
timestamp: 1e3
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
connector: "lighter",
|
|
24
|
+
instrumentId: "1",
|
|
25
|
+
fundingRate: "0.0001",
|
|
26
|
+
notional: "2",
|
|
27
|
+
fundingPerUnit: "-0.5",
|
|
28
|
+
timestamp: 1e3
|
|
29
|
+
}
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
it("skips funding for unknown instruments", () => {
|
|
33
|
+
expect(toFundingPaymentEntries([fundingEvent({})], /* @__PURE__ */ new Set())).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
function fundingEvent(overrides) {
|
|
37
|
+
return {
|
|
38
|
+
timestamp: 1e3,
|
|
39
|
+
market_id: 1,
|
|
40
|
+
funding_id: 1,
|
|
41
|
+
change: "-2",
|
|
42
|
+
discount: "0",
|
|
43
|
+
rate: "0.0001",
|
|
44
|
+
position_size: "4",
|
|
45
|
+
position_side: "long",
|
|
46
|
+
...overrides
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
|
-
import { fetchTradesSince } from '../../client/lighter/rest';
|
|
2
|
+
import { fetchAccount, fetchTradesSince, fetchPositionFundingSince } from '../../client/lighter/rest';
|
|
3
3
|
import { BaseAccountSyncer } from '../base';
|
|
4
|
+
import { toFundingPaymentEntries } from './funding';
|
|
4
5
|
|
|
5
6
|
const LIGHTER_FEE_TICK_DENOMINATOR = new BigNumber(1e6);
|
|
6
7
|
class LighterAccountSyncer extends BaseAccountSyncer {
|
|
@@ -9,84 +10,135 @@ class LighterAccountSyncer extends BaseAccountSyncer {
|
|
|
9
10
|
apiPrivateKey;
|
|
10
11
|
constructor(config) {
|
|
11
12
|
super(config);
|
|
12
|
-
const
|
|
13
|
+
const credentials = config.credentials;
|
|
13
14
|
this.accountIndex = Number(config.account.exchangeIdentifier);
|
|
14
|
-
this.apiKeyIndex =
|
|
15
|
-
this.apiPrivateKey =
|
|
15
|
+
this.apiKeyIndex = credentials.api_key_index;
|
|
16
|
+
this.apiPrivateKey = credentials.api_private_key;
|
|
17
|
+
}
|
|
18
|
+
async listExternalPositions({
|
|
19
|
+
instruments
|
|
20
|
+
}) {
|
|
21
|
+
const contractSizeByInstrumentId = new Map(
|
|
22
|
+
instruments.map((instrument) => [instrument.id, instrument.precision.quantity.contractSize])
|
|
23
|
+
);
|
|
24
|
+
const response = await fetchAccount(this.accountIndex, this.demo);
|
|
25
|
+
const rawPositions = response.accounts?.[0]?.positions ?? [];
|
|
26
|
+
return rawPositions.reduce((positions, rawPosition) => {
|
|
27
|
+
const instrumentId = rawPosition.market_id.toString();
|
|
28
|
+
const signedSize = new BigNumber(rawPosition.position).times(rawPosition.sign);
|
|
29
|
+
if (signedSize.isZero()) return positions;
|
|
30
|
+
const contractSize = contractSizeByInstrumentId.get(instrumentId) ?? 1;
|
|
31
|
+
const size = signedSize.abs().times(contractSize).toFixed();
|
|
32
|
+
const side = rawPosition.sign === 1 ? "BUY" : "SELL";
|
|
33
|
+
const markPrice = new BigNumber(rawPosition.position_value).abs().div(size).toFixed();
|
|
34
|
+
positions.push({
|
|
35
|
+
symbol: instrumentId,
|
|
36
|
+
size,
|
|
37
|
+
side,
|
|
38
|
+
entryPrice: rawPosition.avg_entry_price,
|
|
39
|
+
markPrice
|
|
40
|
+
});
|
|
41
|
+
return positions;
|
|
42
|
+
}, []);
|
|
16
43
|
}
|
|
17
44
|
async listExternalFills({
|
|
18
45
|
since,
|
|
19
46
|
instruments
|
|
20
47
|
}) {
|
|
48
|
+
this.assertAuthenticatedSyncAvailable();
|
|
21
49
|
const { token } = await this.sdk.core.misc.lighter.createAuthToken({
|
|
22
50
|
accountIndex: this.accountIndex,
|
|
23
51
|
apiKeyIndex: this.apiKeyIndex,
|
|
24
52
|
apiPrivateKey: this.apiPrivateKey
|
|
25
53
|
});
|
|
26
|
-
const trades = await fetchTradesSince(this.accountIndex, token, since);
|
|
54
|
+
const trades = await fetchTradesSince(this.accountIndex, token, since, this.demo);
|
|
27
55
|
const instrumentIds = new Set(instruments.map((instrument) => instrument.id));
|
|
28
|
-
return trades.toSorted((a, b) => a.timestamp - b.timestamp).reduce((
|
|
29
|
-
const instrumentId =
|
|
56
|
+
return trades.toSorted((a, b) => a.timestamp - b.timestamp).reduce((fills, rawFill) => {
|
|
57
|
+
const instrumentId = rawFill.market_id.toString();
|
|
30
58
|
if (!instrumentIds.has(instrumentId)) {
|
|
31
|
-
console.warn(`Lighter market id ${
|
|
32
|
-
return
|
|
59
|
+
console.warn(`Lighter market id ${rawFill.market_id} not found in instrument id map.`);
|
|
60
|
+
return fills;
|
|
33
61
|
}
|
|
34
|
-
const accountSide = this.getAccountSide(
|
|
35
|
-
if (!accountSide) return
|
|
62
|
+
const accountSide = this.getAccountSide(rawFill);
|
|
63
|
+
if (!accountSide) return fills;
|
|
36
64
|
const side = accountSide === "ask" ? "SELL" : "BUY";
|
|
37
|
-
|
|
65
|
+
fills.push({
|
|
38
66
|
connector: "lighter",
|
|
39
|
-
exchangeFillId:
|
|
40
|
-
cloid: this.getClientOrderId(
|
|
67
|
+
exchangeFillId: rawFill.trade_id_str ?? rawFill.trade_id.toString(),
|
|
68
|
+
cloid: this.getClientOrderId(rawFill, accountSide),
|
|
41
69
|
symbol: instrumentId,
|
|
42
|
-
price:
|
|
43
|
-
quantity:
|
|
70
|
+
price: rawFill.price,
|
|
71
|
+
quantity: rawFill.size,
|
|
44
72
|
side,
|
|
45
|
-
reduceOnly: this.isReduceOnly(
|
|
46
|
-
timestamp:
|
|
47
|
-
tradeFee: this.getTradeFee(
|
|
48
|
-
builderFee: this.getIntegratorFee(
|
|
49
|
-
liquidation:
|
|
73
|
+
reduceOnly: this.isReduceOnly(rawFill),
|
|
74
|
+
timestamp: rawFill.timestamp,
|
|
75
|
+
tradeFee: this.getTradeFee(rawFill, accountSide),
|
|
76
|
+
builderFee: this.getIntegratorFee(rawFill, accountSide),
|
|
77
|
+
liquidation: rawFill.type === "liquidation" ? { mark: rawFill.price } : null
|
|
50
78
|
});
|
|
51
|
-
return
|
|
79
|
+
return fills;
|
|
52
80
|
}, []);
|
|
53
81
|
}
|
|
54
|
-
|
|
55
|
-
|
|
82
|
+
async listExternalFunding({
|
|
83
|
+
since,
|
|
84
|
+
instruments
|
|
85
|
+
}) {
|
|
86
|
+
this.assertAuthenticatedSyncAvailable();
|
|
87
|
+
const { token } = await this.sdk.core.misc.lighter.createAuthToken({
|
|
88
|
+
accountIndex: this.accountIndex,
|
|
89
|
+
apiKeyIndex: this.apiKeyIndex,
|
|
90
|
+
apiPrivateKey: this.apiPrivateKey
|
|
91
|
+
});
|
|
92
|
+
const fundingEvents = await fetchPositionFundingSince(this.accountIndex, token, since, this.demo);
|
|
93
|
+
const instrumentIds = new Set(instruments.map((instrument) => instrument.id));
|
|
94
|
+
return toFundingPaymentEntries(fundingEvents, instrumentIds);
|
|
95
|
+
}
|
|
96
|
+
isReduceOnly(rawFill) {
|
|
97
|
+
if (rawFill.type === "liquidation" || rawFill.type === "deleverage" || rawFill.type === "market-settlement") {
|
|
56
98
|
return true;
|
|
57
99
|
}
|
|
58
|
-
const accountSide = this.getAccountSide(
|
|
100
|
+
const accountSide = this.getAccountSide(rawFill);
|
|
59
101
|
if (!accountSide) {
|
|
60
102
|
return false;
|
|
61
103
|
}
|
|
62
|
-
const
|
|
104
|
+
const positionSignChanged = this.isMaker(rawFill, accountSide) ? rawFill.maker_position_sign_changed : rawFill.taker_position_sign_changed;
|
|
105
|
+
if (positionSignChanged) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const positionSizeBefore = this.getPositionSizeBefore(rawFill, accountSide);
|
|
63
109
|
if (positionSizeBefore.isZero()) {
|
|
64
110
|
return false;
|
|
65
111
|
}
|
|
66
112
|
return positionSizeBefore.isPositive() && accountSide === "ask" || positionSizeBefore.isNegative() && accountSide === "bid";
|
|
67
113
|
}
|
|
68
|
-
getAccountSide(
|
|
69
|
-
if (
|
|
70
|
-
if (
|
|
114
|
+
getAccountSide(rawFill) {
|
|
115
|
+
if (rawFill.ask_account_id === this.accountIndex) return "ask";
|
|
116
|
+
if (rawFill.bid_account_id === this.accountIndex) return "bid";
|
|
71
117
|
return null;
|
|
72
118
|
}
|
|
73
|
-
getClientOrderId(
|
|
74
|
-
return (side === "ask" ?
|
|
119
|
+
getClientOrderId(rawFill, side) {
|
|
120
|
+
return (side === "ask" ? rawFill.ask_client_id_str : rawFill.bid_client_id_str) || null;
|
|
75
121
|
}
|
|
76
|
-
isMaker(
|
|
77
|
-
return side === "ask" ?
|
|
122
|
+
isMaker(rawFill, side) {
|
|
123
|
+
return side === "ask" ? rawFill.is_maker_ask : !rawFill.is_maker_ask;
|
|
78
124
|
}
|
|
79
|
-
getPositionSizeBefore(
|
|
80
|
-
const value = this.isMaker(
|
|
125
|
+
getPositionSizeBefore(rawFill, side) {
|
|
126
|
+
const value = this.isMaker(rawFill, side) ? rawFill.maker_position_size_before : rawFill.taker_position_size_before;
|
|
81
127
|
return new BigNumber(value ?? 0);
|
|
82
128
|
}
|
|
83
|
-
getTradeFee(
|
|
84
|
-
const rateTick = this.isMaker(
|
|
85
|
-
return this.feeTickToAmount(rateTick,
|
|
129
|
+
getTradeFee(rawFill, side) {
|
|
130
|
+
const rateTick = this.isMaker(rawFill, side) ? rawFill.maker_fee : rawFill.taker_fee;
|
|
131
|
+
return this.feeTickToAmount(rateTick, rawFill.usd_amount);
|
|
86
132
|
}
|
|
87
|
-
getIntegratorFee(
|
|
88
|
-
const rateTick = this.isMaker(
|
|
89
|
-
return this.feeTickToAmount(rateTick,
|
|
133
|
+
getIntegratorFee(rawFill, side) {
|
|
134
|
+
const rateTick = this.isMaker(rawFill, side) ? rawFill.integrator_maker_fee : rawFill.integrator_taker_fee;
|
|
135
|
+
return this.feeTickToAmount(rateTick, rawFill.usd_amount);
|
|
136
|
+
}
|
|
137
|
+
/** Prevents testnet reads from silently minting a mainnet auth token. */
|
|
138
|
+
assertAuthenticatedSyncAvailable() {
|
|
139
|
+
if (this.demo) {
|
|
140
|
+
throw new Error("Lighter demo authenticated sync is unavailable because backend signing is mainnet-only");
|
|
141
|
+
}
|
|
90
142
|
}
|
|
91
143
|
// Lighter fees come as ppm rates (÷1e6), not amounts — the charged fee is that rate times the trade's USD notional.
|
|
92
144
|
feeTickToAmount(rateTick, usdAmount) {
|