@pear-protocol/exchanges-sdk 0.2.0 → 0.3.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/dist/index.d.ts +59 -11
- package/dist/syncer/base.js +107 -23
- package/dist/syncer/base.test.js +132 -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 +130 -24
- package/dist/syncer/lighter/syncer.js +68 -40
- package/dist/syncer/okx/syncer.js +41 -17
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -311,7 +311,8 @@ interface AccountSyncerConfig {
|
|
|
311
311
|
demo: boolean;
|
|
312
312
|
}
|
|
313
313
|
interface AccountSyncer {
|
|
314
|
-
|
|
314
|
+
synchronizeFills(): Promise<void>;
|
|
315
|
+
synchronizeFunding(): Promise<void>;
|
|
315
316
|
}
|
|
316
317
|
interface ListExternalFillsParams {
|
|
317
318
|
since: Date;
|
|
@@ -319,6 +320,13 @@ interface ListExternalFillsParams {
|
|
|
319
320
|
openPositions: HttpTypes.OpenPositionLite[];
|
|
320
321
|
provisionalFills: HttpTypes.ProvisionalFill[];
|
|
321
322
|
}
|
|
323
|
+
interface ListExternalPositionsParams {
|
|
324
|
+
instruments: HttpTypes.Instrument[];
|
|
325
|
+
}
|
|
326
|
+
interface ListExternalFundingParams {
|
|
327
|
+
since: Date;
|
|
328
|
+
instruments: HttpTypes.Instrument[];
|
|
329
|
+
}
|
|
322
330
|
declare abstract class BaseAccountSyncer<ExternalFill> implements AccountSyncer {
|
|
323
331
|
protected sdk: PearSDK;
|
|
324
332
|
protected connector: Connector;
|
|
@@ -327,9 +335,36 @@ declare abstract class BaseAccountSyncer<ExternalFill> implements AccountSyncer
|
|
|
327
335
|
protected ws: ExchangeWsByConnector[Connector];
|
|
328
336
|
protected demo: boolean;
|
|
329
337
|
constructor(config: AccountSyncerConfig);
|
|
338
|
+
/**
|
|
339
|
+
* Fetch the account's currently-open positions from the exchange, normalized
|
|
340
|
+
* to BASE units (contractSize applied). The exchange position API is the
|
|
341
|
+
* source of truth for current exposure. Account-wide; no symbol scoping.
|
|
342
|
+
* Hedge-mode (LONG/SHORT legs on one symbol) is rejected — skipped this cycle.
|
|
343
|
+
*/
|
|
344
|
+
protected abstract listExternalPositions(params: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
330
345
|
protected abstract listExternalFills(params: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
346
|
+
/**
|
|
347
|
+
* Fetch the account's historical funding payments since the watermark,
|
|
348
|
+
* applied-rate-only and mapped to instrument ids. Account-wide, no symbol scoping.
|
|
349
|
+
*/
|
|
350
|
+
protected abstract listExternalFunding(params: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
331
351
|
protected abstract isReduceOnly(fill: ExternalFill): boolean;
|
|
332
|
-
|
|
352
|
+
private loadPearState;
|
|
353
|
+
/**
|
|
354
|
+
* Global per-symbol cost-basis entry of existing open positions, derived by pulling each
|
|
355
|
+
* position's fills and folding them (mirrors example-ui). One call per open position today —
|
|
356
|
+
* can collapse to a single endpoint later.
|
|
357
|
+
*/
|
|
358
|
+
private loadEntryBySymbol;
|
|
359
|
+
private snapshotExchange;
|
|
360
|
+
private computeAndPostSync;
|
|
361
|
+
/**
|
|
362
|
+
* Pull historical funding payments since the funding watermark and post them.
|
|
363
|
+
* Independent of the fill sync cycle (`synchronizeFills`) — funding rows are
|
|
364
|
+
* immutable and upsert idempotently, so this is driven on its own cadence.
|
|
365
|
+
*/
|
|
366
|
+
synchronizeFunding(): Promise<void>;
|
|
367
|
+
synchronizeFills(): Promise<void>;
|
|
333
368
|
}
|
|
334
369
|
|
|
335
370
|
interface BinanceRawUserTrade {
|
|
@@ -352,8 +387,10 @@ interface BinanceRawUserTrade {
|
|
|
352
387
|
declare class BinanceAccountSyncer extends BaseAccountSyncer<BinanceRawUserTrade> {
|
|
353
388
|
private readonly creds;
|
|
354
389
|
constructor(config: AccountSyncerConfig);
|
|
390
|
+
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
355
391
|
protected listExternalFills({ since, instruments, openPositions, provisionalFills, }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
356
|
-
protected
|
|
392
|
+
protected listExternalFunding(_params: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
393
|
+
protected isReduceOnly(rawFill: BinanceRawUserTrade): boolean;
|
|
357
394
|
private collectRequestedInstrumentIds;
|
|
358
395
|
private mapOrdersBySymbolOrderId;
|
|
359
396
|
private orderKey;
|
|
@@ -376,8 +413,10 @@ interface BybitRawExecution {
|
|
|
376
413
|
declare class BybitAccountSyncer extends BaseAccountSyncer<BybitRawExecution> {
|
|
377
414
|
private readonly creds;
|
|
378
415
|
constructor(config: AccountSyncerConfig);
|
|
416
|
+
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
379
417
|
protected listExternalFills({ since }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
380
|
-
protected
|
|
418
|
+
protected listExternalFunding(_params: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
419
|
+
protected isReduceOnly(rawFill: BybitRawExecution): boolean;
|
|
381
420
|
}
|
|
382
421
|
|
|
383
422
|
declare function createAccountSyncer(exchange: Connector, config: AccountSyncerConfig): AccountSyncer;
|
|
@@ -458,20 +497,26 @@ type UserFillsByTimeResponse = {
|
|
|
458
497
|
};
|
|
459
498
|
};
|
|
460
499
|
|
|
461
|
-
|
|
500
|
+
type HyperliquidRawFill = UserFillsByTimeResponse;
|
|
501
|
+
declare class HyperliquidAccountSyncer extends BaseAccountSyncer<HyperliquidRawFill> {
|
|
462
502
|
private readonly address;
|
|
463
503
|
constructor(config: AccountSyncerConfig);
|
|
504
|
+
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
464
505
|
protected listExternalFills({ since, instruments, }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
465
|
-
protected
|
|
506
|
+
protected listExternalFunding({ since, instruments, }: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
507
|
+
protected isReduceOnly(rawFill: HyperliquidRawFill): boolean;
|
|
466
508
|
}
|
|
467
509
|
|
|
468
|
-
|
|
510
|
+
type RawFill$1 = LighterRawTrade;
|
|
511
|
+
declare class LighterAccountSyncer extends BaseAccountSyncer<RawFill$1> {
|
|
469
512
|
private readonly accountIndex;
|
|
470
513
|
private readonly apiKeyIndex;
|
|
471
514
|
private readonly apiPrivateKey;
|
|
472
515
|
constructor(config: AccountSyncerConfig);
|
|
516
|
+
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
473
517
|
protected listExternalFills({ since, instruments, }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
474
|
-
protected
|
|
518
|
+
protected listExternalFunding(_params: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
519
|
+
protected isReduceOnly(rawFill: RawFill$1): boolean;
|
|
475
520
|
private getAccountSide;
|
|
476
521
|
private getClientOrderId;
|
|
477
522
|
private isMaker;
|
|
@@ -499,11 +544,14 @@ interface OkxRawFill {
|
|
|
499
544
|
ts: string;
|
|
500
545
|
}
|
|
501
546
|
|
|
502
|
-
|
|
547
|
+
type RawFill = OkxRawFill;
|
|
548
|
+
declare class OkxAccountSyncer extends BaseAccountSyncer<RawFill> {
|
|
503
549
|
private readonly creds;
|
|
504
550
|
constructor(config: AccountSyncerConfig);
|
|
551
|
+
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
505
552
|
protected listExternalFills({ since, instruments, }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
506
|
-
protected
|
|
553
|
+
protected listExternalFunding(_params: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
554
|
+
protected isReduceOnly(rawFill: RawFill): boolean;
|
|
507
555
|
private contractsToBase;
|
|
508
556
|
}
|
|
509
557
|
|
|
@@ -822,4 +870,4 @@ declare class ExchangesSDK {
|
|
|
822
870
|
private fetchCredentials;
|
|
823
871
|
}
|
|
824
872
|
|
|
825
|
-
export { ACCOUNT_TYPE_LABELS, ASSET_DECIMALS, type AccountBalance, type AccountHealthStatus, type AccountPosition, type AccountStatus, type AccountSyncer, type AccountSyncerConfig, type AccountTracker, type AccountTrackerConfig, type AccountType, type AccountWarningReason, type ApiKey, type ApproveIntegratorParams, type ApproveIntegratorResult, type AssetBalance, AssetId, type BalanceCallback, BaseAccountSyncer, BaseAccountTracker, BaseWs, BinanceAccountSyncer, BinanceAccountTracker, type BinanceAccountTrackerConfig, BinanceAccountType, BinanceExchangeWs, BinanceUserDataWs, type BinanceUserDataWsConfig, BinanceWsApi, type BinanceWsApiConfig, BybitAccountSyncer, BybitAccountTracker, type BybitAccountTrackerConfig, BybitAccountType, BybitExchangeWs, BybitWs, type BybitWsConfig, type CheckApiWalletParams, type ConnectOptions, type ConnectWalletParams, type ConnectWalletResult, type CreateAccountTrackerConfig, type CreateExchangeWsConfig, type DepositParams, type DepositResult, type ExchangeConnection, type ExchangeHelperExchange, type ExchangeHelperMap, type ExchangeState, ExchangeWs, type ExchangeWsByConnector, ExchangesSDK, type ExchangesSDKConfig, HyperliquidAccountSyncer, HyperliquidAccountTracker, type HyperliquidAccountTrackerConfig, HyperliquidAccountType, HyperliquidExchangeWs, type HyperliquidHelper, HyperliquidWs, LighterAccountSyncer, LighterAccountTracker, type LighterAccountTrackerConfig, LighterAccountType, LighterExchangeWs, type LighterHelper, type LighterLookupResult, LighterWs, type ListExternalFillsParams, type MarginMode, type MessageListener, OkxAccountSyncer, OkxAccountTracker, type OkxAccountTrackerConfig, OkxAccountType, OkxExchangeWs, OkxWs, type OkxWsConfig, type OpenListener, type PositionsCallback, RouteType, type StateUpdate, type SupportedCredentials, type TrackedAssetCallback, type TrackedAssetInfo, type WalletClientWithAccount, type WithdrawParams, type WithdrawResult, type WsMessage, createAccountSyncer, createAccountTracker, createExchangeWs, createHyperliquidHelper, createLighterHelper, getAssetDecimals };
|
|
873
|
+
export { ACCOUNT_TYPE_LABELS, ASSET_DECIMALS, type AccountBalance, type AccountHealthStatus, type AccountPosition, type AccountStatus, type AccountSyncer, type AccountSyncerConfig, type AccountTracker, type AccountTrackerConfig, type AccountType, type AccountWarningReason, type ApiKey, type ApproveIntegratorParams, type ApproveIntegratorResult, type AssetBalance, AssetId, type BalanceCallback, BaseAccountSyncer, BaseAccountTracker, BaseWs, BinanceAccountSyncer, BinanceAccountTracker, type BinanceAccountTrackerConfig, BinanceAccountType, BinanceExchangeWs, BinanceUserDataWs, type BinanceUserDataWsConfig, BinanceWsApi, type BinanceWsApiConfig, BybitAccountSyncer, BybitAccountTracker, type BybitAccountTrackerConfig, BybitAccountType, BybitExchangeWs, BybitWs, type BybitWsConfig, type CheckApiWalletParams, type ConnectOptions, type ConnectWalletParams, type ConnectWalletResult, type CreateAccountTrackerConfig, type CreateExchangeWsConfig, type DepositParams, type DepositResult, type ExchangeConnection, type ExchangeHelperExchange, type ExchangeHelperMap, type ExchangeState, ExchangeWs, type ExchangeWsByConnector, ExchangesSDK, type ExchangesSDKConfig, HyperliquidAccountSyncer, HyperliquidAccountTracker, type HyperliquidAccountTrackerConfig, HyperliquidAccountType, HyperliquidExchangeWs, type HyperliquidHelper, HyperliquidWs, LighterAccountSyncer, LighterAccountTracker, type LighterAccountTrackerConfig, LighterAccountType, LighterExchangeWs, type LighterHelper, type LighterLookupResult, LighterWs, type ListExternalFillsParams, type ListExternalFundingParams, type ListExternalPositionsParams, type MarginMode, type MessageListener, OkxAccountSyncer, OkxAccountTracker, type OkxAccountTrackerConfig, OkxAccountType, OkxExchangeWs, OkxWs, type OkxWsConfig, type OpenListener, type PositionsCallback, RouteType, type StateUpdate, type SupportedCredentials, type TrackedAssetCallback, type TrackedAssetInfo, type WalletClientWithAccount, type WithdrawParams, type WithdrawResult, type WsMessage, createAccountSyncer, createAccountTracker, createExchangeWs, createHyperliquidHelper, createLighterHelper, getAssetDecimals };
|
package/dist/syncer/base.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { computeSyncPayload } from '@pear-protocol/utils';
|
|
1
|
+
import { computeEntryPriceForAsset, computeSyncPayload } from '@pear-protocol/utils';
|
|
2
2
|
|
|
3
|
+
const OVERLAP_MS = 6e4;
|
|
3
4
|
class BaseAccountSyncer {
|
|
4
5
|
sdk;
|
|
5
6
|
connector;
|
|
@@ -15,46 +16,129 @@ class BaseAccountSyncer {
|
|
|
15
16
|
this.ws = config.ws;
|
|
16
17
|
this.demo = config.demo;
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// Phase A: Load current Pear state (positions, fills, instruments, watermark).
|
|
20
|
+
async loadPearState() {
|
|
21
|
+
const [{ positions: openPositions }, { fills: provisionalFills }, { instruments }, watermark] = await Promise.all([
|
|
20
22
|
this.sdk.core.positions.openLite(),
|
|
21
|
-
this.sdk.core.fills.provisionals()
|
|
22
|
-
]);
|
|
23
|
-
if (!positions.length && !provisionalFills.length) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const [{ instruments }, since] = await Promise.all([
|
|
23
|
+
this.sdk.core.fills.provisionals(),
|
|
27
24
|
this.sdk.core.instruments.list({ connector: this.connector }),
|
|
28
|
-
this.sdk.core.sync.since({
|
|
29
|
-
overlapMs: 6e4,
|
|
30
|
-
nowMs: Date.now(),
|
|
31
|
-
bootstrapLookbackMs: 30 * 24 * 60 * 60 * 1e3
|
|
32
|
-
})
|
|
25
|
+
this.sdk.core.sync.fills.since({ overlapMs: OVERLAP_MS })
|
|
33
26
|
]);
|
|
27
|
+
const entryBySymbol = await this.loadEntryBySymbol(openPositions);
|
|
28
|
+
return { openPositions, entryBySymbol, provisionalFills, instruments, watermark };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Global per-symbol cost-basis entry of existing open positions, derived by pulling each
|
|
32
|
+
* position's fills and folding them (mirrors example-ui). One call per open position today —
|
|
33
|
+
* can collapse to a single endpoint later.
|
|
34
|
+
*/
|
|
35
|
+
async loadEntryBySymbol(openPositions) {
|
|
36
|
+
const responses = await Promise.all(openPositions.map((position) => this.sdk.core.fills.details(position.id)));
|
|
37
|
+
const fills = responses.flatMap((response) => response.fills).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
38
|
+
const fillsBySymbol = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const fill of fills) {
|
|
40
|
+
const list = fillsBySymbol.get(fill.symbol) ?? [];
|
|
41
|
+
list.push(fill);
|
|
42
|
+
fillsBySymbol.set(fill.symbol, list);
|
|
43
|
+
}
|
|
44
|
+
const entryBySymbol = {};
|
|
45
|
+
for (const [symbol, symbolFills] of fillsBySymbol) {
|
|
46
|
+
const entry = computeEntryPriceForAsset(symbolFills);
|
|
47
|
+
if (!entry.isZero()) entryBySymbol[symbol] = entry.toFixed();
|
|
48
|
+
}
|
|
49
|
+
return entryBySymbol;
|
|
50
|
+
}
|
|
51
|
+
// Phase B: Snapshot the exchange — fetch positions and fills since the watermark.
|
|
52
|
+
async snapshotExchange(state) {
|
|
53
|
+
const { openPositions, provisionalFills, instruments, watermark } = state;
|
|
34
54
|
const externalFills = await this.listExternalFills({
|
|
35
|
-
since: new Date(
|
|
55
|
+
since: new Date(watermark.sinceMs),
|
|
36
56
|
instruments,
|
|
37
|
-
openPositions
|
|
57
|
+
openPositions,
|
|
38
58
|
provisionalFills
|
|
39
59
|
});
|
|
60
|
+
const exchangePositions = await this.listExternalPositions({ instruments });
|
|
61
|
+
return { exchangePositions, externalFills };
|
|
62
|
+
}
|
|
63
|
+
// Phase C: Compute the sync payload and post it across the three sync endpoints.
|
|
64
|
+
async computeAndPostSync(state, snapshot) {
|
|
65
|
+
const { openPositions, entryBySymbol, provisionalFills, watermark } = state;
|
|
66
|
+
const { exchangePositions, externalFills } = snapshot;
|
|
40
67
|
const payload = await computeSyncPayload({
|
|
41
|
-
|
|
68
|
+
tradeAccountId: this.account.id,
|
|
69
|
+
connector: this.connector,
|
|
70
|
+
openPositions,
|
|
71
|
+
entryBySymbol,
|
|
42
72
|
provisionalFills,
|
|
43
|
-
confirmedExchangeFillIds:
|
|
44
|
-
externalFills
|
|
73
|
+
confirmedExchangeFillIds: watermark.confirmedExchangeFillIds ?? [],
|
|
74
|
+
externalFills,
|
|
75
|
+
exchangePositions,
|
|
76
|
+
neverSynced: watermark.neverSynced
|
|
45
77
|
});
|
|
46
78
|
if (!payload) {
|
|
47
79
|
return;
|
|
48
80
|
}
|
|
49
81
|
if (payload.reconcilables) {
|
|
50
|
-
await this.sdk.core.sync.reconcile(payload.reconcilables);
|
|
82
|
+
await this.sdk.core.sync.fills.reconcile(payload.reconcilables);
|
|
51
83
|
}
|
|
52
84
|
if (payload.confirmables) {
|
|
53
|
-
await this.sdk.core.sync.confirm(payload.confirmables);
|
|
85
|
+
await this.sdk.core.sync.fills.confirm(payload.confirmables);
|
|
86
|
+
}
|
|
87
|
+
const { firstSyncTrim } = payload;
|
|
88
|
+
let meta = payload.meta;
|
|
89
|
+
if (firstSyncTrim) {
|
|
90
|
+
console.log(
|
|
91
|
+
`${this.connector}: first-sync trim \u2014 dropped ${firstSyncTrim.trimmedCount} pre-cutoff fills (cutoffTs=${firstSyncTrim.cutoffTs})`
|
|
92
|
+
);
|
|
93
|
+
if (meta.externalMaxTs !== null && firstSyncTrim.trimmedMaxTs >= meta.externalMaxTs - OVERLAP_MS) {
|
|
94
|
+
meta = { ...meta, externalMaxTs: meta.externalMaxTs + OVERLAP_MS + 1 };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const createBody = {
|
|
98
|
+
applicables: payload.applicables,
|
|
99
|
+
exchangePositions: payload.exchangePositions,
|
|
100
|
+
meta
|
|
101
|
+
};
|
|
102
|
+
const bytes = new Blob([JSON.stringify(createBody)]).size;
|
|
103
|
+
if (bytes > 50 * 1024) {
|
|
104
|
+
await this.sdk.core.sync.fills.createBulk(createBody);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await this.sdk.core.sync.fills.create(createBody);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Pull historical funding payments since the funding watermark and post them.
|
|
111
|
+
* Independent of the fill sync cycle (`synchronizeFills`) — funding rows are
|
|
112
|
+
* immutable and upsert idempotently, so this is driven on its own cadence.
|
|
113
|
+
*/
|
|
114
|
+
async synchronizeFunding() {
|
|
115
|
+
const [{ instruments }, watermark] = await Promise.all([
|
|
116
|
+
this.sdk.core.instruments.list({ connector: this.connector }),
|
|
117
|
+
this.sdk.core.sync.funding.since({ overlapMs: 6e4 })
|
|
118
|
+
]);
|
|
119
|
+
const payments = await this.listExternalFunding({ since: new Date(watermark.sinceMs), instruments });
|
|
120
|
+
if (payments.length === 0) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const externalMaxTs = Math.max(...payments.map((payment) => payment.timestamp));
|
|
124
|
+
const createBody = { payments, meta: { externalMaxTs } };
|
|
125
|
+
const bytes = new Blob([JSON.stringify(createBody)]).size;
|
|
126
|
+
if (bytes > 50 * 1024) {
|
|
127
|
+
await this.sdk.core.sync.funding.createBulk(createBody);
|
|
128
|
+
return;
|
|
54
129
|
}
|
|
55
|
-
|
|
56
|
-
|
|
130
|
+
await this.sdk.core.sync.funding.create(createBody);
|
|
131
|
+
}
|
|
132
|
+
async synchronizeFills() {
|
|
133
|
+
const state = await this.loadPearState();
|
|
134
|
+
let snapshot;
|
|
135
|
+
try {
|
|
136
|
+
snapshot = await this.snapshotExchange(state);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`${this.connector}: position/fill fetch failed \u2014 sync cycle aborted, retry next tick`, error);
|
|
139
|
+
return;
|
|
57
140
|
}
|
|
141
|
+
await this.computeAndPostSync(state, snapshot);
|
|
58
142
|
}
|
|
59
143
|
}
|
|
60
144
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, afterEach, vi, it, expect } from 'vitest';
|
|
2
|
+
import { BaseAccountSyncer } from './base';
|
|
3
|
+
|
|
4
|
+
function extFill(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
exchangeFillId: "tx-default",
|
|
7
|
+
cloid: null,
|
|
8
|
+
connector: "hyperliquid",
|
|
9
|
+
symbol: "BTC",
|
|
10
|
+
price: "100",
|
|
11
|
+
quantity: "1",
|
|
12
|
+
side: "BUY",
|
|
13
|
+
reduceOnly: false,
|
|
14
|
+
timestamp: 1e3,
|
|
15
|
+
tradeFee: "0",
|
|
16
|
+
builderFee: "0",
|
|
17
|
+
liquidation: null,
|
|
18
|
+
...overrides
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function exchangePos(overrides = {}) {
|
|
22
|
+
return { symbol: "BTC", size: "1", side: "BUY", entryPrice: "100", markPrice: "100", ...overrides };
|
|
23
|
+
}
|
|
24
|
+
class FakeSyncer extends BaseAccountSyncer {
|
|
25
|
+
positionsFn = vi.fn();
|
|
26
|
+
fillsFn = vi.fn();
|
|
27
|
+
listExternalPositions(params) {
|
|
28
|
+
return this.positionsFn(params);
|
|
29
|
+
}
|
|
30
|
+
listExternalFills(params) {
|
|
31
|
+
return this.fillsFn(params);
|
|
32
|
+
}
|
|
33
|
+
listExternalFunding() {
|
|
34
|
+
return Promise.resolve([]);
|
|
35
|
+
}
|
|
36
|
+
isReduceOnly() {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function makeSyncer(args) {
|
|
41
|
+
const fills = {
|
|
42
|
+
since: vi.fn().mockResolvedValue({ sinceMs: 0, neverSynced: args.neverSynced, confirmedExchangeFillIds: [] }),
|
|
43
|
+
reconcile: vi.fn().mockResolvedValue({}),
|
|
44
|
+
confirm: vi.fn().mockResolvedValue({}),
|
|
45
|
+
create: vi.fn().mockResolvedValue({}),
|
|
46
|
+
createBulk: vi.fn().mockResolvedValue({})
|
|
47
|
+
};
|
|
48
|
+
const sync = { fills };
|
|
49
|
+
const sdk = {
|
|
50
|
+
core: {
|
|
51
|
+
positions: { openLite: vi.fn().mockResolvedValue({ positions: [] }) },
|
|
52
|
+
fills: { provisionals: vi.fn().mockResolvedValue({ fills: [] }), details: vi.fn() },
|
|
53
|
+
instruments: { list: vi.fn().mockResolvedValue({ instruments: [] }) },
|
|
54
|
+
sync
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const config = {
|
|
58
|
+
sdk,
|
|
59
|
+
connector: "hyperliquid",
|
|
60
|
+
account: { id: "acct-1" },
|
|
61
|
+
credentials: {},
|
|
62
|
+
ws: {},
|
|
63
|
+
demo: false
|
|
64
|
+
};
|
|
65
|
+
return { syncer: new FakeSyncer(config), sync };
|
|
66
|
+
}
|
|
67
|
+
const NEWEST_TS = 105e4;
|
|
68
|
+
function trimmableFills(btcEraTs) {
|
|
69
|
+
return [
|
|
70
|
+
extFill({ exchangeFillId: "a", symbol: "BTC", side: "BUY", quantity: "1", timestamp: btcEraTs.open }),
|
|
71
|
+
extFill({ exchangeFillId: "b", symbol: "BTC", side: "SELL", quantity: "1", timestamp: btcEraTs.close }),
|
|
72
|
+
extFill({ exchangeFillId: "e", symbol: "SOL", side: "BUY", quantity: "2", timestamp: NEWEST_TS })
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
const SOL_SNAPSHOT = [exchangePos({ symbol: "SOL", size: "2" })];
|
|
76
|
+
describe("BaseAccountSyncer \u2014 first-sync plumbing", () => {
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
vi.restoreAllMocks();
|
|
79
|
+
});
|
|
80
|
+
it("neverSynced=true: the posted snapshot is fetched AFTER the fill stream", async () => {
|
|
81
|
+
const { syncer, sync } = makeSyncer({ neverSynced: true });
|
|
82
|
+
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
83
|
+
syncer.fillsFn.mockResolvedValue([
|
|
84
|
+
extFill({ exchangeFillId: "e", symbol: "SOL", side: "BUY", quantity: "2", timestamp: NEWEST_TS })
|
|
85
|
+
]);
|
|
86
|
+
await syncer.synchronizeFills();
|
|
87
|
+
expect(syncer.positionsFn).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(syncer.fillsFn.mock.invocationCallOrder[0]).toBeLessThan(
|
|
89
|
+
syncer.positionsFn.mock.invocationCallOrder[0] ?? 0
|
|
90
|
+
);
|
|
91
|
+
expect(sync.fills.create).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(sync.fills.create.mock.calls[0]?.[0]?.exchangePositions).toBe(SOL_SNAPSHOT);
|
|
93
|
+
});
|
|
94
|
+
it("bumps the posted watermark past the overlap window when trimmed fills fall inside it", async () => {
|
|
95
|
+
const { syncer, sync } = makeSyncer({ neverSynced: true });
|
|
96
|
+
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
97
|
+
syncer.fillsFn.mockResolvedValue(trimmableFills({ open: 1e6, close: NEWEST_TS - 2e4 }));
|
|
98
|
+
await syncer.synchronizeFills();
|
|
99
|
+
expect(sync.fills.create.mock.calls[0]?.[0]?.meta.externalMaxTs).toBe(NEWEST_TS + 6e4 + 1);
|
|
100
|
+
});
|
|
101
|
+
it("leaves the watermark unbumped when the trimmed era is entirely older than the overlap window", async () => {
|
|
102
|
+
const { syncer, sync } = makeSyncer({ neverSynced: true });
|
|
103
|
+
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
104
|
+
syncer.fillsFn.mockResolvedValue(trimmableFills({ open: 1e5, close: 2e5 }));
|
|
105
|
+
await syncer.synchronizeFills();
|
|
106
|
+
expect(sync.fills.create.mock.calls[0]?.[0]?.meta.externalMaxTs).toBe(NEWEST_TS);
|
|
107
|
+
});
|
|
108
|
+
it("logs the trim exactly once when a trim happened", async () => {
|
|
109
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {
|
|
110
|
+
});
|
|
111
|
+
const { syncer } = makeSyncer({ neverSynced: true });
|
|
112
|
+
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
113
|
+
syncer.fillsFn.mockResolvedValue(trimmableFills({ open: 1e5, close: 2e5 }));
|
|
114
|
+
await syncer.synchronizeFills();
|
|
115
|
+
const trimLogs = logSpy.mock.calls.filter((call) => String(call[0]).includes("first-sync trim"));
|
|
116
|
+
expect(trimLogs).toHaveLength(1);
|
|
117
|
+
expect(String(trimLogs[0]?.[0])).toContain("hyperliquid");
|
|
118
|
+
});
|
|
119
|
+
it("neverSynced=false: positions fetched once, no bump, no trim log", async () => {
|
|
120
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {
|
|
121
|
+
});
|
|
122
|
+
const { syncer, sync } = makeSyncer({ neverSynced: false });
|
|
123
|
+
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
124
|
+
syncer.fillsFn.mockResolvedValue([
|
|
125
|
+
extFill({ exchangeFillId: "e", symbol: "SOL", side: "BUY", quantity: "2", timestamp: NEWEST_TS })
|
|
126
|
+
]);
|
|
127
|
+
await syncer.synchronizeFills();
|
|
128
|
+
expect(syncer.positionsFn).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(sync.fills.create.mock.calls[0]?.[0]?.meta.externalMaxTs).toBe(NEWEST_TS);
|
|
130
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
|
-
import { fetchUserTradesSince, fetchAllOrdersSince } from '../../client/binance/rest';
|
|
2
|
+
import { fetchAllPositionRisk, fetchUserTradesSince, fetchAllOrdersSince } from '../../client/binance/rest';
|
|
3
3
|
import { BaseAccountSyncer } from '../base';
|
|
4
4
|
|
|
5
5
|
class BinanceAccountSyncer extends BaseAccountSyncer {
|
|
@@ -8,6 +8,31 @@ class BinanceAccountSyncer extends BaseAccountSyncer {
|
|
|
8
8
|
super(config);
|
|
9
9
|
this.creds = config.credentials;
|
|
10
10
|
}
|
|
11
|
+
async listExternalPositions({
|
|
12
|
+
instruments
|
|
13
|
+
}) {
|
|
14
|
+
const contractSizeByInstrumentId = new Map(
|
|
15
|
+
instruments.map((instrument) => [instrument.id, instrument.precision.quantity.contractSize])
|
|
16
|
+
);
|
|
17
|
+
const rawPositions = await fetchAllPositionRisk(this.creds, this.demo);
|
|
18
|
+
return rawPositions.reduce((positions, rawPosition) => {
|
|
19
|
+
const signedSize = new BigNumber(rawPosition.positionAmt);
|
|
20
|
+
if (signedSize.isZero()) {
|
|
21
|
+
return positions;
|
|
22
|
+
}
|
|
23
|
+
const contractSize = contractSizeByInstrumentId.get(rawPosition.symbol) ?? 1;
|
|
24
|
+
const size = signedSize.abs().times(contractSize).toFixed();
|
|
25
|
+
const side = signedSize.isGreaterThan(0) ? "BUY" : "SELL";
|
|
26
|
+
positions.push({
|
|
27
|
+
symbol: rawPosition.symbol,
|
|
28
|
+
size,
|
|
29
|
+
side,
|
|
30
|
+
entryPrice: rawPosition.entryPrice,
|
|
31
|
+
markPrice: rawPosition.markPrice
|
|
32
|
+
});
|
|
33
|
+
return positions;
|
|
34
|
+
}, []);
|
|
35
|
+
}
|
|
11
36
|
async listExternalFills({
|
|
12
37
|
since,
|
|
13
38
|
instruments,
|
|
@@ -25,33 +50,36 @@ class BinanceAccountSyncer extends BaseAccountSyncer {
|
|
|
25
50
|
fetchAllOrdersSince(this.creds, instrumentIds, since, this.demo)
|
|
26
51
|
]);
|
|
27
52
|
const ordersBySymbolOrderId = this.mapOrdersBySymbolOrderId(orders);
|
|
28
|
-
return trades.reduce((
|
|
29
|
-
if (!requestedInstrumentIds.has(
|
|
30
|
-
return
|
|
53
|
+
return trades.reduce((fills, rawFill) => {
|
|
54
|
+
if (!requestedInstrumentIds.has(rawFill.symbol)) {
|
|
55
|
+
return fills;
|
|
31
56
|
}
|
|
32
|
-
const order = ordersBySymbolOrderId.get(this.orderKey(
|
|
33
|
-
|
|
57
|
+
const order = ordersBySymbolOrderId.get(this.orderKey(rawFill.symbol, rawFill.orderId));
|
|
58
|
+
fills.push({
|
|
34
59
|
connector: "binance",
|
|
35
|
-
exchangeFillId:
|
|
60
|
+
exchangeFillId: rawFill.id.toString(),
|
|
36
61
|
cloid: order?.clientOrderId || null,
|
|
37
|
-
symbol:
|
|
38
|
-
price:
|
|
39
|
-
quantity:
|
|
40
|
-
side:
|
|
41
|
-
reduceOnly: this.isReduceOnly(
|
|
42
|
-
timestamp:
|
|
43
|
-
tradeFee: new BigNumber(
|
|
62
|
+
symbol: rawFill.symbol,
|
|
63
|
+
price: rawFill.price,
|
|
64
|
+
quantity: rawFill.qty,
|
|
65
|
+
side: rawFill.side,
|
|
66
|
+
reduceOnly: this.isReduceOnly(rawFill),
|
|
67
|
+
timestamp: rawFill.time,
|
|
68
|
+
tradeFee: new BigNumber(rawFill.commission || 0).abs().toFixed(),
|
|
44
69
|
builderFee: "0",
|
|
45
70
|
liquidation: null
|
|
46
71
|
});
|
|
47
|
-
return
|
|
72
|
+
return fills;
|
|
48
73
|
}, []);
|
|
49
74
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
async listExternalFunding(_params) {
|
|
76
|
+
throw new Error("funding sync is not implemented for binance");
|
|
77
|
+
}
|
|
78
|
+
isReduceOnly(rawFill) {
|
|
79
|
+
if (rawFill.positionSide === "LONG") return rawFill.side === "SELL";
|
|
80
|
+
if (rawFill.positionSide === "SHORT") return rawFill.side === "BUY";
|
|
81
|
+
if (rawFill.positionSide === "BOTH") {
|
|
82
|
+
return !new BigNumber(rawFill.realizedPnl || 0).isZero();
|
|
55
83
|
}
|
|
56
84
|
return false;
|
|
57
85
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
|
-
import { fetchExecutionsSince } from '../../client/bybit/rest';
|
|
2
|
+
import { fetchPositions, fetchExecutionsSince } from '../../client/bybit/rest';
|
|
3
3
|
import { BaseAccountSyncer } from '../base';
|
|
4
4
|
|
|
5
5
|
class BybitAccountSyncer extends BaseAccountSyncer {
|
|
@@ -8,28 +8,52 @@ class BybitAccountSyncer extends BaseAccountSyncer {
|
|
|
8
8
|
super(config);
|
|
9
9
|
this.creds = config.credentials;
|
|
10
10
|
}
|
|
11
|
+
async listExternalPositions({
|
|
12
|
+
instruments
|
|
13
|
+
}) {
|
|
14
|
+
const rawPositions = await fetchPositions(this.creds, "USDT", this.demo);
|
|
15
|
+
const contractSizeByInstrumentId = new Map(
|
|
16
|
+
instruments.map((instrument) => [instrument.id, instrument.precision.quantity.contractSize])
|
|
17
|
+
);
|
|
18
|
+
return rawPositions.reduce((positions, rawPosition) => {
|
|
19
|
+
if (!rawPosition.size || rawPosition.size === "0" || !rawPosition.side) return positions;
|
|
20
|
+
const contractSize = contractSizeByInstrumentId.get(rawPosition.symbol) ?? 1;
|
|
21
|
+
const size = new BigNumber(rawPosition.size).abs().times(contractSize).toFixed();
|
|
22
|
+
positions.push({
|
|
23
|
+
symbol: rawPosition.symbol,
|
|
24
|
+
size,
|
|
25
|
+
side: rawPosition.side === "Buy" ? "BUY" : "SELL",
|
|
26
|
+
entryPrice: rawPosition.avgPrice,
|
|
27
|
+
markPrice: rawPosition.avgPrice
|
|
28
|
+
});
|
|
29
|
+
return positions;
|
|
30
|
+
}, []);
|
|
31
|
+
}
|
|
11
32
|
async listExternalFills({ since }) {
|
|
12
33
|
const executions = await fetchExecutionsSince(this.creds, since, this.demo);
|
|
13
|
-
return executions.reduce((
|
|
14
|
-
|
|
34
|
+
return executions.reduce((fills, rawFill) => {
|
|
35
|
+
fills.push({
|
|
15
36
|
connector: "bybit",
|
|
16
|
-
exchangeFillId:
|
|
17
|
-
cloid:
|
|
18
|
-
symbol:
|
|
19
|
-
price:
|
|
20
|
-
quantity:
|
|
21
|
-
side:
|
|
22
|
-
reduceOnly: this.isReduceOnly(
|
|
23
|
-
timestamp: Number(
|
|
24
|
-
tradeFee: new BigNumber(
|
|
37
|
+
exchangeFillId: rawFill.execId,
|
|
38
|
+
cloid: rawFill.orderLinkId || null,
|
|
39
|
+
symbol: rawFill.symbol,
|
|
40
|
+
price: rawFill.execPrice,
|
|
41
|
+
quantity: rawFill.execQty,
|
|
42
|
+
side: rawFill.side === "Buy" ? "BUY" : "SELL",
|
|
43
|
+
reduceOnly: this.isReduceOnly(rawFill),
|
|
44
|
+
timestamp: Number(rawFill.execTime),
|
|
45
|
+
tradeFee: new BigNumber(rawFill.execFee || 0).abs().toFixed(),
|
|
25
46
|
builderFee: "0",
|
|
26
47
|
liquidation: null
|
|
27
48
|
});
|
|
28
|
-
return
|
|
49
|
+
return fills;
|
|
29
50
|
}, []);
|
|
30
51
|
}
|
|
31
|
-
|
|
32
|
-
|
|
52
|
+
async listExternalFunding(_params) {
|
|
53
|
+
throw new Error("funding sync is not implemented for bybit");
|
|
54
|
+
}
|
|
55
|
+
isReduceOnly(rawFill) {
|
|
56
|
+
return Number(rawFill.closedSize || "0") > 0;
|
|
33
57
|
}
|
|
34
58
|
}
|
|
35
59
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
function toFundingPaymentEntries(events, symbolToInstrumentId) {
|
|
4
|
+
return events.reduce((entries, event) => {
|
|
5
|
+
const instrumentId = symbolToInstrumentId[event.delta.coin];
|
|
6
|
+
if (!instrumentId) {
|
|
7
|
+
console.warn(`Instrument symbol ${event.delta.coin} not found in instrument symbol to id map.`);
|
|
8
|
+
return entries;
|
|
9
|
+
}
|
|
10
|
+
const size = new BigNumber(event.delta.szi);
|
|
11
|
+
const fundingPerUnit = size.isZero() ? "0" : new BigNumber(event.delta.usdc).div(size).toFixed();
|
|
12
|
+
entries.push({
|
|
13
|
+
connector: "hyperliquid",
|
|
14
|
+
instrumentId,
|
|
15
|
+
fundingRate: event.delta.fundingRate,
|
|
16
|
+
notional: event.delta.usdc,
|
|
17
|
+
fundingPerUnit,
|
|
18
|
+
timestamp: event.time
|
|
19
|
+
});
|
|
20
|
+
return entries;
|
|
21
|
+
}, []);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { toFundingPaymentEntries };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { toFundingPaymentEntries } from './funding';
|
|
3
|
+
|
|
4
|
+
const fundingEvent = (over) => ({
|
|
5
|
+
time: over.time,
|
|
6
|
+
hash: "0xabc",
|
|
7
|
+
delta: {
|
|
8
|
+
type: "funding",
|
|
9
|
+
coin: over.coin,
|
|
10
|
+
usdc: over.usdc ?? "1.5",
|
|
11
|
+
szi: over.szi ?? "10",
|
|
12
|
+
fundingRate: over.fundingRate,
|
|
13
|
+
nSamples: 1
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
describe("toFundingPaymentEntries", () => {
|
|
17
|
+
const map = {
|
|
18
|
+
BTC: "HL-BTC",
|
|
19
|
+
ETH: "HL-ETH"
|
|
20
|
+
};
|
|
21
|
+
it("maps a funding event to a funding-payment entry (rate + notional + per-size)", () => {
|
|
22
|
+
const result = toFundingPaymentEntries(
|
|
23
|
+
[fundingEvent({ coin: "BTC", fundingRate: "-0.0001", usdc: "-2.5", szi: "5", time: 1e3 })],
|
|
24
|
+
map
|
|
25
|
+
);
|
|
26
|
+
expect(result).toEqual([
|
|
27
|
+
{
|
|
28
|
+
connector: "hyperliquid",
|
|
29
|
+
instrumentId: "HL-BTC",
|
|
30
|
+
fundingRate: "-0.0001",
|
|
31
|
+
notional: "-2.5",
|
|
32
|
+
// notional / szi = -2.5 / 5
|
|
33
|
+
fundingPerUnit: "-0.5",
|
|
34
|
+
timestamp: 1e3
|
|
35
|
+
}
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
it("uses signed division by szi (per-size is the attribution coefficient)", () => {
|
|
39
|
+
const result = toFundingPaymentEntries(
|
|
40
|
+
[fundingEvent({ coin: "BTC", fundingRate: "0.0001", usdc: "2.5", szi: "-5", time: 1e3 })],
|
|
41
|
+
map
|
|
42
|
+
);
|
|
43
|
+
expect(result[0]?.fundingPerUnit).toBe("-0.5");
|
|
44
|
+
});
|
|
45
|
+
it("uses zero per-size when size is zero (no divide by zero)", () => {
|
|
46
|
+
const result = toFundingPaymentEntries(
|
|
47
|
+
[fundingEvent({ coin: "BTC", fundingRate: "0.0001", usdc: "0", szi: "0", time: 1e3 })],
|
|
48
|
+
map
|
|
49
|
+
);
|
|
50
|
+
expect(result[0]?.fundingPerUnit).toBe("0");
|
|
51
|
+
});
|
|
52
|
+
it("skips events whose coin is not in the instrument map", () => {
|
|
53
|
+
const result = toFundingPaymentEntries(
|
|
54
|
+
[
|
|
55
|
+
fundingEvent({ coin: "BTC", fundingRate: "0.0001", time: 1e3 }),
|
|
56
|
+
fundingEvent({ coin: "DOGE", fundingRate: "0.0002", time: 2e3 }),
|
|
57
|
+
fundingEvent({ coin: "ETH", fundingRate: "0.0003", time: 3e3 })
|
|
58
|
+
],
|
|
59
|
+
map
|
|
60
|
+
);
|
|
61
|
+
expect(result.map((entry) => entry.instrumentId)).toEqual(["HL-BTC", "HL-ETH"]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
1
2
|
import { fetchInfo } from '../../client/hyperliquid/rest';
|
|
2
3
|
import { BaseAccountSyncer } from '../base';
|
|
4
|
+
import { toFundingPaymentEntries } from './funding';
|
|
3
5
|
|
|
4
6
|
class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
5
7
|
address;
|
|
@@ -7,6 +9,47 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
7
9
|
super(config);
|
|
8
10
|
this.address = config.account.exchangeIdentifier;
|
|
9
11
|
}
|
|
12
|
+
async listExternalPositions({
|
|
13
|
+
instruments
|
|
14
|
+
}) {
|
|
15
|
+
const instrumentByBase = new Map(
|
|
16
|
+
instruments.map((instrument) => [
|
|
17
|
+
instrument.base,
|
|
18
|
+
{ id: instrument.id, contractSize: instrument.precision.quantity.contractSize }
|
|
19
|
+
])
|
|
20
|
+
);
|
|
21
|
+
const perpDexs = await fetchInfo({ type: "perpDexs" });
|
|
22
|
+
const dexNames = perpDexs.map((perpDex) => perpDex?.name ?? "");
|
|
23
|
+
const states = await Promise.all(
|
|
24
|
+
dexNames.map(
|
|
25
|
+
(dex) => fetchInfo({
|
|
26
|
+
type: "clearinghouseState",
|
|
27
|
+
user: this.address,
|
|
28
|
+
dex
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
const assetPositions = states.flatMap((state) => state.assetPositions);
|
|
33
|
+
return assetPositions.reduce((positions, { position: rawPosition }) => {
|
|
34
|
+
const szi = new BigNumber(rawPosition.szi);
|
|
35
|
+
if (szi.isZero()) {
|
|
36
|
+
return positions;
|
|
37
|
+
}
|
|
38
|
+
const instrument = instrumentByBase.get(rawPosition.coin);
|
|
39
|
+
if (!instrument) {
|
|
40
|
+
console.warn(`Instrument for coin ${rawPosition.coin} not found.`);
|
|
41
|
+
return positions;
|
|
42
|
+
}
|
|
43
|
+
positions.push({
|
|
44
|
+
symbol: instrument.id,
|
|
45
|
+
size: szi.abs().times(instrument.contractSize).toFixed(),
|
|
46
|
+
side: szi.isPositive() ? "BUY" : "SELL",
|
|
47
|
+
entryPrice: rawPosition.entryPx,
|
|
48
|
+
markPrice: rawPosition.entryPx
|
|
49
|
+
});
|
|
50
|
+
return positions;
|
|
51
|
+
}, []);
|
|
52
|
+
}
|
|
10
53
|
async listExternalFills({
|
|
11
54
|
since,
|
|
12
55
|
instruments
|
|
@@ -15,37 +58,100 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
15
58
|
acc[instrument.base] = instrument.id;
|
|
16
59
|
return acc;
|
|
17
60
|
}, {});
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
61
|
+
const PAGE_LIMIT = 2e3;
|
|
62
|
+
const allFills = [];
|
|
63
|
+
const seenTids = /* @__PURE__ */ new Set();
|
|
64
|
+
const addFill = (fill) => {
|
|
65
|
+
if (!seenTids.has(fill.tid)) {
|
|
66
|
+
seenTids.add(fill.tid);
|
|
67
|
+
allFills.push(fill);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
let cursor = since.getTime();
|
|
71
|
+
let batchSize;
|
|
72
|
+
do {
|
|
73
|
+
const batch = await fetchInfo({
|
|
74
|
+
type: "userFillsByTime",
|
|
75
|
+
user: this.address,
|
|
76
|
+
startTime: cursor,
|
|
77
|
+
aggregateByTime: true
|
|
78
|
+
});
|
|
79
|
+
batchSize = batch.length;
|
|
80
|
+
for (const rawFill of batch) {
|
|
81
|
+
addFill(rawFill);
|
|
82
|
+
}
|
|
83
|
+
if (batchSize === PAGE_LIMIT) {
|
|
84
|
+
cursor = batch[batch.length - 1]?.time ?? cursor;
|
|
85
|
+
}
|
|
86
|
+
} while (batchSize === PAGE_LIMIT);
|
|
87
|
+
cursor = since.getTime();
|
|
88
|
+
do {
|
|
89
|
+
const twapBatch = await fetchInfo({
|
|
90
|
+
type: "userTwapSliceFillsByTime",
|
|
91
|
+
user: this.address,
|
|
92
|
+
startTime: cursor,
|
|
93
|
+
aggregateByTime: true
|
|
94
|
+
});
|
|
95
|
+
batchSize = twapBatch.length;
|
|
96
|
+
for (const item of twapBatch) {
|
|
97
|
+
addFill(item.fill);
|
|
98
|
+
}
|
|
99
|
+
if (batchSize === PAGE_LIMIT) {
|
|
100
|
+
cursor = twapBatch[twapBatch.length - 1]?.fill.time ?? cursor;
|
|
101
|
+
}
|
|
102
|
+
} while (batchSize === PAGE_LIMIT);
|
|
103
|
+
allFills.sort((a, b) => a.time - b.time);
|
|
104
|
+
return allFills.reduce((fills, rawFill) => {
|
|
105
|
+
const instrumentId = instrumentSymbolToIdMap[rawFill.coin];
|
|
26
106
|
if (!instrumentId) {
|
|
27
|
-
console.warn(`Instrument symbol ${
|
|
28
|
-
return
|
|
107
|
+
console.warn(`Instrument symbol ${rawFill.coin} not found in instrument symbol to id map.`);
|
|
108
|
+
return fills;
|
|
29
109
|
}
|
|
30
|
-
|
|
110
|
+
fills.push({
|
|
31
111
|
connector: "hyperliquid",
|
|
32
|
-
exchangeFillId:
|
|
33
|
-
cloid:
|
|
112
|
+
exchangeFillId: rawFill.tid.toFixed(),
|
|
113
|
+
cloid: rawFill.cloid ?? null,
|
|
34
114
|
symbol: instrumentId,
|
|
35
|
-
price:
|
|
36
|
-
quantity:
|
|
37
|
-
side:
|
|
38
|
-
reduceOnly: this.isReduceOnly(
|
|
39
|
-
timestamp:
|
|
40
|
-
tradeFee:
|
|
41
|
-
builderFee:
|
|
42
|
-
liquidation:
|
|
115
|
+
price: rawFill.px,
|
|
116
|
+
quantity: rawFill.sz,
|
|
117
|
+
side: rawFill.side === "B" ? "BUY" : "SELL",
|
|
118
|
+
reduceOnly: this.isReduceOnly(rawFill),
|
|
119
|
+
timestamp: rawFill.time,
|
|
120
|
+
tradeFee: rawFill.fee,
|
|
121
|
+
builderFee: rawFill.builderFee ?? "0",
|
|
122
|
+
liquidation: rawFill.liquidation ? { mark: rawFill.liquidation.markPx } : null
|
|
43
123
|
});
|
|
44
|
-
return
|
|
124
|
+
return fills;
|
|
45
125
|
}, []);
|
|
46
126
|
}
|
|
47
|
-
|
|
48
|
-
|
|
127
|
+
async listExternalFunding({
|
|
128
|
+
since,
|
|
129
|
+
instruments
|
|
130
|
+
}) {
|
|
131
|
+
const instrumentSymbolToIdMap = instruments.reduce((acc, instrument) => {
|
|
132
|
+
acc[instrument.base] = instrument.id;
|
|
133
|
+
return acc;
|
|
134
|
+
}, {});
|
|
135
|
+
const PAGE_LIMIT = 500;
|
|
136
|
+
const allEvents = [];
|
|
137
|
+
let cursor = since.getTime();
|
|
138
|
+
let batchSize;
|
|
139
|
+
do {
|
|
140
|
+
const batch = await fetchInfo({
|
|
141
|
+
type: "userFunding",
|
|
142
|
+
user: this.address,
|
|
143
|
+
startTime: cursor
|
|
144
|
+
});
|
|
145
|
+
batchSize = batch.length;
|
|
146
|
+
allEvents.push(...batch);
|
|
147
|
+
if (batchSize === PAGE_LIMIT) {
|
|
148
|
+
cursor = batch[batch.length - 1].time;
|
|
149
|
+
}
|
|
150
|
+
} while (batchSize === PAGE_LIMIT);
|
|
151
|
+
return toFundingPaymentEntries(allEvents, instrumentSymbolToIdMap);
|
|
152
|
+
}
|
|
153
|
+
isReduceOnly(rawFill) {
|
|
154
|
+
return rawFill.dir.includes("Close");
|
|
49
155
|
}
|
|
50
156
|
}
|
|
51
157
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
|
-
import { fetchTradesSince } from '../../client/lighter/rest';
|
|
2
|
+
import { fetchAccount, fetchTradesSince } from '../../client/lighter/rest';
|
|
3
3
|
import { BaseAccountSyncer } from '../base';
|
|
4
4
|
|
|
5
5
|
const LIGHTER_FEE_TICK_DENOMINATOR = new BigNumber(1e6);
|
|
@@ -9,10 +9,35 @@ class LighterAccountSyncer extends BaseAccountSyncer {
|
|
|
9
9
|
apiPrivateKey;
|
|
10
10
|
constructor(config) {
|
|
11
11
|
super(config);
|
|
12
|
-
const
|
|
12
|
+
const credentials = config.credentials;
|
|
13
13
|
this.accountIndex = Number(config.account.exchangeIdentifier);
|
|
14
|
-
this.apiKeyIndex =
|
|
15
|
-
this.apiPrivateKey =
|
|
14
|
+
this.apiKeyIndex = credentials.api_key_index;
|
|
15
|
+
this.apiPrivateKey = credentials.api_private_key;
|
|
16
|
+
}
|
|
17
|
+
async listExternalPositions({
|
|
18
|
+
instruments
|
|
19
|
+
}) {
|
|
20
|
+
const contractSizeByInstrumentId = new Map(
|
|
21
|
+
instruments.map((instrument) => [instrument.id, instrument.precision.quantity.contractSize])
|
|
22
|
+
);
|
|
23
|
+
const response = await fetchAccount(this.accountIndex);
|
|
24
|
+
const rawPositions = response.accounts?.[0]?.positions ?? [];
|
|
25
|
+
return rawPositions.reduce((positions, rawPosition) => {
|
|
26
|
+
const instrumentId = rawPosition.market_id.toString();
|
|
27
|
+
const signedSize = new BigNumber(rawPosition.position).times(rawPosition.sign);
|
|
28
|
+
if (signedSize.isZero()) return positions;
|
|
29
|
+
const contractSize = contractSizeByInstrumentId.get(instrumentId) ?? 1;
|
|
30
|
+
const size = signedSize.abs().times(contractSize).toFixed();
|
|
31
|
+
const side = rawPosition.sign === 1 ? "BUY" : "SELL";
|
|
32
|
+
positions.push({
|
|
33
|
+
symbol: instrumentId,
|
|
34
|
+
size,
|
|
35
|
+
side,
|
|
36
|
+
entryPrice: rawPosition.avg_entry_price,
|
|
37
|
+
markPrice: rawPosition.avg_entry_price
|
|
38
|
+
});
|
|
39
|
+
return positions;
|
|
40
|
+
}, []);
|
|
16
41
|
}
|
|
17
42
|
async listExternalFills({
|
|
18
43
|
since,
|
|
@@ -25,68 +50,71 @@ class LighterAccountSyncer extends BaseAccountSyncer {
|
|
|
25
50
|
});
|
|
26
51
|
const trades = await fetchTradesSince(this.accountIndex, token, since);
|
|
27
52
|
const instrumentIds = new Set(instruments.map((instrument) => instrument.id));
|
|
28
|
-
return trades.toSorted((a, b) => a.timestamp - b.timestamp).reduce((
|
|
29
|
-
const instrumentId =
|
|
53
|
+
return trades.toSorted((a, b) => a.timestamp - b.timestamp).reduce((fills, rawFill) => {
|
|
54
|
+
const instrumentId = rawFill.market_id.toString();
|
|
30
55
|
if (!instrumentIds.has(instrumentId)) {
|
|
31
|
-
console.warn(`Lighter market id ${
|
|
32
|
-
return
|
|
56
|
+
console.warn(`Lighter market id ${rawFill.market_id} not found in instrument id map.`);
|
|
57
|
+
return fills;
|
|
33
58
|
}
|
|
34
|
-
const accountSide = this.getAccountSide(
|
|
35
|
-
if (!accountSide) return
|
|
59
|
+
const accountSide = this.getAccountSide(rawFill);
|
|
60
|
+
if (!accountSide) return fills;
|
|
36
61
|
const side = accountSide === "ask" ? "SELL" : "BUY";
|
|
37
|
-
|
|
62
|
+
fills.push({
|
|
38
63
|
connector: "lighter",
|
|
39
|
-
exchangeFillId:
|
|
40
|
-
cloid: this.getClientOrderId(
|
|
64
|
+
exchangeFillId: rawFill.trade_id_str ?? rawFill.trade_id.toString(),
|
|
65
|
+
cloid: this.getClientOrderId(rawFill, accountSide),
|
|
41
66
|
symbol: instrumentId,
|
|
42
|
-
price:
|
|
43
|
-
quantity:
|
|
67
|
+
price: rawFill.price,
|
|
68
|
+
quantity: rawFill.size,
|
|
44
69
|
side,
|
|
45
|
-
reduceOnly: this.isReduceOnly(
|
|
46
|
-
timestamp:
|
|
47
|
-
tradeFee: this.getTradeFee(
|
|
48
|
-
builderFee: this.getIntegratorFee(
|
|
49
|
-
liquidation:
|
|
70
|
+
reduceOnly: this.isReduceOnly(rawFill),
|
|
71
|
+
timestamp: rawFill.timestamp,
|
|
72
|
+
tradeFee: this.getTradeFee(rawFill, accountSide),
|
|
73
|
+
builderFee: this.getIntegratorFee(rawFill, accountSide),
|
|
74
|
+
liquidation: rawFill.type === "liquidation" ? { mark: rawFill.price } : null
|
|
50
75
|
});
|
|
51
|
-
return
|
|
76
|
+
return fills;
|
|
52
77
|
}, []);
|
|
53
78
|
}
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
async listExternalFunding(_params) {
|
|
80
|
+
throw new Error("funding sync is not implemented for lighter");
|
|
81
|
+
}
|
|
82
|
+
isReduceOnly(rawFill) {
|
|
83
|
+
if (rawFill.type === "liquidation" || rawFill.type === "deleverage" || rawFill.type === "market-settlement") {
|
|
56
84
|
return true;
|
|
57
85
|
}
|
|
58
|
-
const accountSide = this.getAccountSide(
|
|
86
|
+
const accountSide = this.getAccountSide(rawFill);
|
|
59
87
|
if (!accountSide) {
|
|
60
88
|
return false;
|
|
61
89
|
}
|
|
62
|
-
const positionSizeBefore = this.getPositionSizeBefore(
|
|
90
|
+
const positionSizeBefore = this.getPositionSizeBefore(rawFill, accountSide);
|
|
63
91
|
if (positionSizeBefore.isZero()) {
|
|
64
92
|
return false;
|
|
65
93
|
}
|
|
66
94
|
return positionSizeBefore.isPositive() && accountSide === "ask" || positionSizeBefore.isNegative() && accountSide === "bid";
|
|
67
95
|
}
|
|
68
|
-
getAccountSide(
|
|
69
|
-
if (
|
|
70
|
-
if (
|
|
96
|
+
getAccountSide(rawFill) {
|
|
97
|
+
if (rawFill.ask_account_id === this.accountIndex) return "ask";
|
|
98
|
+
if (rawFill.bid_account_id === this.accountIndex) return "bid";
|
|
71
99
|
return null;
|
|
72
100
|
}
|
|
73
|
-
getClientOrderId(
|
|
74
|
-
return (side === "ask" ?
|
|
101
|
+
getClientOrderId(rawFill, side) {
|
|
102
|
+
return (side === "ask" ? rawFill.ask_client_id_str : rawFill.bid_client_id_str) || null;
|
|
75
103
|
}
|
|
76
|
-
isMaker(
|
|
77
|
-
return side === "ask" ?
|
|
104
|
+
isMaker(rawFill, side) {
|
|
105
|
+
return side === "ask" ? rawFill.is_maker_ask : !rawFill.is_maker_ask;
|
|
78
106
|
}
|
|
79
|
-
getPositionSizeBefore(
|
|
80
|
-
const value = this.isMaker(
|
|
107
|
+
getPositionSizeBefore(rawFill, side) {
|
|
108
|
+
const value = this.isMaker(rawFill, side) ? rawFill.maker_position_size_before : rawFill.taker_position_size_before;
|
|
81
109
|
return new BigNumber(value ?? 0);
|
|
82
110
|
}
|
|
83
|
-
getTradeFee(
|
|
84
|
-
const rateTick = this.isMaker(
|
|
85
|
-
return this.feeTickToAmount(rateTick,
|
|
111
|
+
getTradeFee(rawFill, side) {
|
|
112
|
+
const rateTick = this.isMaker(rawFill, side) ? rawFill.maker_fee : rawFill.taker_fee;
|
|
113
|
+
return this.feeTickToAmount(rateTick, rawFill.usd_amount);
|
|
86
114
|
}
|
|
87
|
-
getIntegratorFee(
|
|
88
|
-
const rateTick = this.isMaker(
|
|
89
|
-
return this.feeTickToAmount(rateTick,
|
|
115
|
+
getIntegratorFee(rawFill, side) {
|
|
116
|
+
const rateTick = this.isMaker(rawFill, side) ? rawFill.integrator_maker_fee : rawFill.integrator_taker_fee;
|
|
117
|
+
return this.feeTickToAmount(rateTick, rawFill.usd_amount);
|
|
90
118
|
}
|
|
91
119
|
// Lighter fees come as ppm rates (÷1e6), not amounts — the charged fee is that rate times the trade's USD notional.
|
|
92
120
|
feeTickToAmount(rateTick, usdAmount) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
|
-
import { fetchFillsHistorySince } from '../../client/okx/rest';
|
|
2
|
+
import { fetchPositions, fetchFillsHistorySince } from '../../client/okx/rest';
|
|
3
3
|
import { BaseAccountSyncer } from '../base';
|
|
4
4
|
|
|
5
5
|
class OkxAccountSyncer extends BaseAccountSyncer {
|
|
@@ -8,6 +8,27 @@ class OkxAccountSyncer extends BaseAccountSyncer {
|
|
|
8
8
|
super(config);
|
|
9
9
|
this.creds = config.credentials;
|
|
10
10
|
}
|
|
11
|
+
async listExternalPositions({
|
|
12
|
+
instruments
|
|
13
|
+
}) {
|
|
14
|
+
const rawPositions = await fetchPositions(this.creds, this.demo);
|
|
15
|
+
const contractSizeByInstrumentId = new Map(
|
|
16
|
+
instruments.map((instrument) => [instrument.id, instrument.precision.quantity.contractSize])
|
|
17
|
+
);
|
|
18
|
+
return rawPositions.reduce((positions, rawPosition) => {
|
|
19
|
+
if (!rawPosition.pos || rawPosition.pos === "0") return positions;
|
|
20
|
+
const contractSize = contractSizeByInstrumentId.get(rawPosition.instId) ?? 1;
|
|
21
|
+
const signedSize = new BigNumber(rawPosition.pos);
|
|
22
|
+
positions.push({
|
|
23
|
+
symbol: rawPosition.instId,
|
|
24
|
+
size: signedSize.abs().times(contractSize).toFixed(),
|
|
25
|
+
side: signedSize.isGreaterThan(0) ? "BUY" : "SELL",
|
|
26
|
+
entryPrice: rawPosition.avgPx,
|
|
27
|
+
markPrice: rawPosition.avgPx
|
|
28
|
+
});
|
|
29
|
+
return positions;
|
|
30
|
+
}, []);
|
|
31
|
+
}
|
|
11
32
|
async listExternalFills({
|
|
12
33
|
since,
|
|
13
34
|
instruments
|
|
@@ -16,28 +37,31 @@ class OkxAccountSyncer extends BaseAccountSyncer {
|
|
|
16
37
|
const contractSizeByInstrumentId = new Map(
|
|
17
38
|
instruments.map((instrument) => [instrument.id, instrument.precision.quantity.contractSize])
|
|
18
39
|
);
|
|
19
|
-
return fills.reduce((
|
|
20
|
-
const contractSize = contractSizeByInstrumentId.get(
|
|
21
|
-
|
|
40
|
+
return fills.reduce((result, rawFill) => {
|
|
41
|
+
const contractSize = contractSizeByInstrumentId.get(rawFill.instId) ?? 1;
|
|
42
|
+
result.push({
|
|
22
43
|
connector: "okx",
|
|
23
|
-
exchangeFillId:
|
|
24
|
-
cloid:
|
|
25
|
-
symbol:
|
|
26
|
-
price:
|
|
27
|
-
quantity: this.contractsToBase(
|
|
28
|
-
side:
|
|
29
|
-
reduceOnly: this.isReduceOnly(
|
|
30
|
-
timestamp: Number(
|
|
31
|
-
tradeFee: new BigNumber(
|
|
44
|
+
exchangeFillId: rawFill.billId || rawFill.tradeId,
|
|
45
|
+
cloid: rawFill.clOrdId || null,
|
|
46
|
+
symbol: rawFill.instId,
|
|
47
|
+
price: rawFill.fillPx,
|
|
48
|
+
quantity: this.contractsToBase(rawFill.fillSz, contractSize),
|
|
49
|
+
side: rawFill.side === "buy" ? "BUY" : "SELL",
|
|
50
|
+
reduceOnly: this.isReduceOnly(rawFill),
|
|
51
|
+
timestamp: Number(rawFill.fillTime || rawFill.ts),
|
|
52
|
+
tradeFee: new BigNumber(rawFill.fee || 0).abs().toFixed(),
|
|
32
53
|
builderFee: "0",
|
|
33
54
|
liquidation: null
|
|
34
55
|
});
|
|
35
|
-
return
|
|
56
|
+
return result;
|
|
36
57
|
}, []);
|
|
37
58
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
async listExternalFunding(_params) {
|
|
60
|
+
throw new Error("funding sync is not implemented for okx");
|
|
61
|
+
}
|
|
62
|
+
isReduceOnly(rawFill) {
|
|
63
|
+
if (rawFill.posSide === "long") return rawFill.side === "sell";
|
|
64
|
+
if (rawFill.posSide === "short") return rawFill.side === "buy";
|
|
41
65
|
return false;
|
|
42
66
|
}
|
|
43
67
|
contractsToBase(quantity, contractSize) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pear-protocol/exchanges-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pear Protocol Exchanges SDK",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"typecheck": "tsc --noEmit"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@pear-protocol/core-sdk": "^1.
|
|
30
|
-
"@pear-protocol/types": "^1.
|
|
31
|
-
"@pear-protocol/utils": "^0.0
|
|
29
|
+
"@pear-protocol/core-sdk": "^1.5.0",
|
|
30
|
+
"@pear-protocol/types": "^1.6.0",
|
|
31
|
+
"@pear-protocol/utils": "^0.1.0",
|
|
32
32
|
"bignumber.js": "^11.1.1",
|
|
33
33
|
"partysocket": "^1.0.3",
|
|
34
34
|
"viem": "^2.48.8"
|