@pear-protocol/exchanges-sdk 0.1.12 → 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/helper/lighter/helper.js +25 -0
- package/dist/index.d.ts +67 -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
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { parseUnits, parseEther } from 'viem';
|
|
1
2
|
import { fetchApiKeys, fetchAccount, fetchAccountsByL1Address } from '../../client/lighter/rest';
|
|
3
|
+
import { writeWithdraw, writeDeposit } from './ethereum';
|
|
4
|
+
import { getAssetDecimals } from './types';
|
|
2
5
|
|
|
3
6
|
function createLighterHelper(sdk) {
|
|
4
7
|
return {
|
|
@@ -60,6 +63,28 @@ function createLighterHelper(sdk) {
|
|
|
60
63
|
apiKeyIndex: params.apiKeyIndex
|
|
61
64
|
});
|
|
62
65
|
return { txHash };
|
|
66
|
+
},
|
|
67
|
+
async deposit(walletClient, params) {
|
|
68
|
+
const txHash = await writeDeposit(walletClient, {
|
|
69
|
+
to: params.to,
|
|
70
|
+
assetIndex: params.assetId,
|
|
71
|
+
routeType: params.routeType,
|
|
72
|
+
// `amount` is a human string in the asset's own units; scale to base units.
|
|
73
|
+
amount: parseUnits(params.amount, getAssetDecimals(params.assetId)),
|
|
74
|
+
// Native-ETH deposits are payable; `nativeValue` is the ETH sent as
|
|
75
|
+
// msg.value (18-decimal wei), separate from the accounting `amount`.
|
|
76
|
+
value: params.nativeValue ? parseEther(params.nativeValue) : void 0
|
|
77
|
+
});
|
|
78
|
+
return { txHash };
|
|
79
|
+
},
|
|
80
|
+
async withdraw(walletClient, params) {
|
|
81
|
+
const txHash = await writeWithdraw(walletClient, {
|
|
82
|
+
accountIndex: params.accountIndex,
|
|
83
|
+
assetIndex: params.assetId,
|
|
84
|
+
routeType: params.routeType,
|
|
85
|
+
baseAmount: parseUnits(params.amount, getAssetDecimals(params.assetId))
|
|
86
|
+
});
|
|
87
|
+
return { txHash };
|
|
63
88
|
}
|
|
64
89
|
};
|
|
65
90
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -275,11 +275,19 @@ type ApproveIntegratorParams = {
|
|
|
275
275
|
type ApproveIntegratorResult = {
|
|
276
276
|
txHash: string;
|
|
277
277
|
};
|
|
278
|
+
type DepositResult = {
|
|
279
|
+
txHash: string;
|
|
280
|
+
};
|
|
281
|
+
type WithdrawResult = {
|
|
282
|
+
txHash: string;
|
|
283
|
+
};
|
|
278
284
|
interface LighterHelper {
|
|
279
285
|
lookupAccount(l1Address: string): Promise<LighterLookupResult>;
|
|
280
286
|
checkIntegratorApproval(accountIndex: number, integratorAccountIndex: number): Promise<boolean>;
|
|
281
287
|
connectWallet(walletClient: WalletClientWithAccount, params: ConnectWalletParams): Promise<ConnectWalletResult>;
|
|
282
288
|
approveIntegrator(walletClient: WalletClientWithAccount, params: ApproveIntegratorParams): Promise<ApproveIntegratorResult>;
|
|
289
|
+
deposit(walletClient: WalletClientWithAccount, params: DepositParams): Promise<DepositResult>;
|
|
290
|
+
withdraw(walletClient: WalletClientWithAccount, params: WithdrawParams): Promise<WithdrawResult>;
|
|
283
291
|
}
|
|
284
292
|
type LighterLookupResult = {
|
|
285
293
|
mainAccountIndex: number;
|
|
@@ -303,7 +311,8 @@ interface AccountSyncerConfig {
|
|
|
303
311
|
demo: boolean;
|
|
304
312
|
}
|
|
305
313
|
interface AccountSyncer {
|
|
306
|
-
|
|
314
|
+
synchronizeFills(): Promise<void>;
|
|
315
|
+
synchronizeFunding(): Promise<void>;
|
|
307
316
|
}
|
|
308
317
|
interface ListExternalFillsParams {
|
|
309
318
|
since: Date;
|
|
@@ -311,6 +320,13 @@ interface ListExternalFillsParams {
|
|
|
311
320
|
openPositions: HttpTypes.OpenPositionLite[];
|
|
312
321
|
provisionalFills: HttpTypes.ProvisionalFill[];
|
|
313
322
|
}
|
|
323
|
+
interface ListExternalPositionsParams {
|
|
324
|
+
instruments: HttpTypes.Instrument[];
|
|
325
|
+
}
|
|
326
|
+
interface ListExternalFundingParams {
|
|
327
|
+
since: Date;
|
|
328
|
+
instruments: HttpTypes.Instrument[];
|
|
329
|
+
}
|
|
314
330
|
declare abstract class BaseAccountSyncer<ExternalFill> implements AccountSyncer {
|
|
315
331
|
protected sdk: PearSDK;
|
|
316
332
|
protected connector: Connector;
|
|
@@ -319,9 +335,36 @@ declare abstract class BaseAccountSyncer<ExternalFill> implements AccountSyncer
|
|
|
319
335
|
protected ws: ExchangeWsByConnector[Connector];
|
|
320
336
|
protected demo: boolean;
|
|
321
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[]>;
|
|
322
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[]>;
|
|
323
351
|
protected abstract isReduceOnly(fill: ExternalFill): boolean;
|
|
324
|
-
|
|
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>;
|
|
325
368
|
}
|
|
326
369
|
|
|
327
370
|
interface BinanceRawUserTrade {
|
|
@@ -344,8 +387,10 @@ interface BinanceRawUserTrade {
|
|
|
344
387
|
declare class BinanceAccountSyncer extends BaseAccountSyncer<BinanceRawUserTrade> {
|
|
345
388
|
private readonly creds;
|
|
346
389
|
constructor(config: AccountSyncerConfig);
|
|
390
|
+
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
347
391
|
protected listExternalFills({ since, instruments, openPositions, provisionalFills, }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
348
|
-
protected
|
|
392
|
+
protected listExternalFunding(_params: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
393
|
+
protected isReduceOnly(rawFill: BinanceRawUserTrade): boolean;
|
|
349
394
|
private collectRequestedInstrumentIds;
|
|
350
395
|
private mapOrdersBySymbolOrderId;
|
|
351
396
|
private orderKey;
|
|
@@ -368,8 +413,10 @@ interface BybitRawExecution {
|
|
|
368
413
|
declare class BybitAccountSyncer extends BaseAccountSyncer<BybitRawExecution> {
|
|
369
414
|
private readonly creds;
|
|
370
415
|
constructor(config: AccountSyncerConfig);
|
|
416
|
+
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
371
417
|
protected listExternalFills({ since }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
372
|
-
protected
|
|
418
|
+
protected listExternalFunding(_params: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
419
|
+
protected isReduceOnly(rawFill: BybitRawExecution): boolean;
|
|
373
420
|
}
|
|
374
421
|
|
|
375
422
|
declare function createAccountSyncer(exchange: Connector, config: AccountSyncerConfig): AccountSyncer;
|
|
@@ -450,20 +497,26 @@ type UserFillsByTimeResponse = {
|
|
|
450
497
|
};
|
|
451
498
|
};
|
|
452
499
|
|
|
453
|
-
|
|
500
|
+
type HyperliquidRawFill = UserFillsByTimeResponse;
|
|
501
|
+
declare class HyperliquidAccountSyncer extends BaseAccountSyncer<HyperliquidRawFill> {
|
|
454
502
|
private readonly address;
|
|
455
503
|
constructor(config: AccountSyncerConfig);
|
|
504
|
+
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
456
505
|
protected listExternalFills({ since, instruments, }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
457
|
-
protected
|
|
506
|
+
protected listExternalFunding({ since, instruments, }: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
507
|
+
protected isReduceOnly(rawFill: HyperliquidRawFill): boolean;
|
|
458
508
|
}
|
|
459
509
|
|
|
460
|
-
|
|
510
|
+
type RawFill$1 = LighterRawTrade;
|
|
511
|
+
declare class LighterAccountSyncer extends BaseAccountSyncer<RawFill$1> {
|
|
461
512
|
private readonly accountIndex;
|
|
462
513
|
private readonly apiKeyIndex;
|
|
463
514
|
private readonly apiPrivateKey;
|
|
464
515
|
constructor(config: AccountSyncerConfig);
|
|
516
|
+
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
465
517
|
protected listExternalFills({ since, instruments, }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
466
|
-
protected
|
|
518
|
+
protected listExternalFunding(_params: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
519
|
+
protected isReduceOnly(rawFill: RawFill$1): boolean;
|
|
467
520
|
private getAccountSide;
|
|
468
521
|
private getClientOrderId;
|
|
469
522
|
private isMaker;
|
|
@@ -491,11 +544,14 @@ interface OkxRawFill {
|
|
|
491
544
|
ts: string;
|
|
492
545
|
}
|
|
493
546
|
|
|
494
|
-
|
|
547
|
+
type RawFill = OkxRawFill;
|
|
548
|
+
declare class OkxAccountSyncer extends BaseAccountSyncer<RawFill> {
|
|
495
549
|
private readonly creds;
|
|
496
550
|
constructor(config: AccountSyncerConfig);
|
|
551
|
+
protected listExternalPositions({ instruments, }: ListExternalPositionsParams): Promise<HttpTypes.ExternalPosition[]>;
|
|
497
552
|
protected listExternalFills({ since, instruments, }: ListExternalFillsParams): Promise<HttpTypes.UnknownExternalFill[]>;
|
|
498
|
-
protected
|
|
553
|
+
protected listExternalFunding(_params: ListExternalFundingParams): Promise<HttpTypes.FundingPaymentEntry[]>;
|
|
554
|
+
protected isReduceOnly(rawFill: RawFill): boolean;
|
|
499
555
|
private contractsToBase;
|
|
500
556
|
}
|
|
501
557
|
|
|
@@ -814,4 +870,4 @@ declare class ExchangesSDK {
|
|
|
814
870
|
private fetchCredentials;
|
|
815
871
|
}
|
|
816
872
|
|
|
817
|
-
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 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 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"
|