@pear-protocol/exchanges-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -0
- package/dist/account/account-manager.d.ts +34 -0
- package/dist/account/account-manager.js +140 -0
- package/dist/account/exchange-account.d.ts +36 -0
- package/dist/account/exchange-account.js +185 -0
- package/dist/account/exchanges/base.d.ts +49 -0
- package/dist/account/exchanges/base.js +63 -0
- package/dist/account/exchanges/binance/const.d.ts +10 -0
- package/dist/account/exchanges/binance/const.js +10 -0
- package/dist/account/exchanges/binance/mapper.d.ts +9 -0
- package/dist/account/exchanges/binance/mapper.js +96 -0
- package/dist/account/exchanges/binance/orchestrator.d.ts +35 -0
- package/dist/account/exchanges/binance/orchestrator.js +232 -0
- package/dist/account/exchanges/binance/rest.d.ts +13 -0
- package/dist/account/exchanges/binance/rest.js +81 -0
- package/dist/account/exchanges/binance/types.d.ts +77 -0
- package/dist/account/exchanges/binance/types.js +1 -0
- package/dist/account/exchanges/binance/ws.d.ts +21 -0
- package/dist/account/exchanges/binance/ws.js +85 -0
- package/dist/account/exchanges/bybit/const.d.ts +7 -0
- package/dist/account/exchanges/bybit/const.js +7 -0
- package/dist/account/exchanges/bybit/mapper.d.ts +11 -0
- package/dist/account/exchanges/bybit/mapper.js +106 -0
- package/dist/account/exchanges/bybit/orchestrator.d.ts +23 -0
- package/dist/account/exchanges/bybit/orchestrator.js +159 -0
- package/dist/account/exchanges/bybit/rest.d.ts +11 -0
- package/dist/account/exchanges/bybit/rest.js +110 -0
- package/dist/account/exchanges/bybit/types.d.ts +59 -0
- package/dist/account/exchanges/bybit/types.js +1 -0
- package/dist/account/exchanges/bybit/ws.d.ts +18 -0
- package/dist/account/exchanges/bybit/ws.js +74 -0
- package/dist/account/exchanges/index.d.ts +8 -0
- package/dist/account/exchanges/index.js +16 -0
- package/dist/account/index.d.ts +8 -0
- package/dist/account/index.js +4 -0
- package/dist/account/types.d.ts +81 -0
- package/dist/account/types.js +1 -0
- package/dist/account/utils.d.ts +7 -0
- package/dist/account/utils.js +15 -0
- package/dist/credentials.d.ts +28 -0
- package/dist/credentials.js +46 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +21 -0
- package/dist/utils/hmac.d.ts +4 -0
- package/dist/utils/hmac.js +17 -0
- package/dist/utils/ws.d.ts +7 -0
- package/dist/utils/ws.js +25 -0
- package/package.json +36 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare const BASE_URL = "https://fapi.binance.com";
|
|
2
|
+
declare const BASE_URL_TESTNET = "https://testnet.binancefuture.com";
|
|
3
|
+
declare const USER_DATA_STREAM_URL = "wss://fstream.binance.com/ws";
|
|
4
|
+
declare const USER_DATA_STREAM_URL_TESTNET = "wss://stream.binancefuture.com/ws";
|
|
5
|
+
declare const WS_API_URL = "wss://ws-fapi.binance.com/ws-fapi/v1";
|
|
6
|
+
declare const WS_API_URL_TESTNET = "wss://testnet.binancefuture.com/ws-fapi/v1";
|
|
7
|
+
declare const DEFAULT_LISTEN_KEY_REFRESH_MS: number;
|
|
8
|
+
declare const DEFAULT_ACCOUNT_POLL_MS = 1000;
|
|
9
|
+
|
|
10
|
+
export { BASE_URL, BASE_URL_TESTNET, DEFAULT_ACCOUNT_POLL_MS, DEFAULT_LISTEN_KEY_REFRESH_MS, USER_DATA_STREAM_URL, USER_DATA_STREAM_URL_TESTNET, WS_API_URL, WS_API_URL_TESTNET };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const BASE_URL = "https://fapi.binance.com";
|
|
2
|
+
const BASE_URL_TESTNET = "https://testnet.binancefuture.com";
|
|
3
|
+
const USER_DATA_STREAM_URL = "wss://fstream.binance.com/ws";
|
|
4
|
+
const USER_DATA_STREAM_URL_TESTNET = "wss://stream.binancefuture.com/ws";
|
|
5
|
+
const WS_API_URL = "wss://ws-fapi.binance.com/ws-fapi/v1";
|
|
6
|
+
const WS_API_URL_TESTNET = "wss://testnet.binancefuture.com/ws-fapi/v1";
|
|
7
|
+
const DEFAULT_LISTEN_KEY_REFRESH_MS = 25 * 60 * 1e3;
|
|
8
|
+
const DEFAULT_ACCOUNT_POLL_MS = 1e3;
|
|
9
|
+
|
|
10
|
+
export { BASE_URL, BASE_URL_TESTNET, DEFAULT_ACCOUNT_POLL_MS, DEFAULT_LISTEN_KEY_REFRESH_MS, USER_DATA_STREAM_URL, USER_DATA_STREAM_URL_TESTNET, WS_API_URL, WS_API_URL_TESTNET };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AccountBalance, AccountPosition } from '../../types.js';
|
|
2
|
+
import { BinanceRawAccountStatus, BinanceRawPositionRisk } from './types.js';
|
|
3
|
+
import '@pear-protocol/types';
|
|
4
|
+
|
|
5
|
+
declare function mapBalance(raw: BinanceRawAccountStatus, collateralRatios: Map<string, string>): AccountBalance;
|
|
6
|
+
declare function mapPositions(raw: BinanceRawPositionRisk[]): Map<string, AccountPosition>;
|
|
7
|
+
declare function mergePositions(incoming: Map<string, AccountPosition>, existing: Map<string, AccountPosition>): Map<string, AccountPosition>;
|
|
8
|
+
|
|
9
|
+
export { mapBalance, mapPositions, mergePositions };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import '../../types';
|
|
2
|
+
import { positionKey } from '../../utils';
|
|
3
|
+
|
|
4
|
+
function mapBalance(raw, collateralRatios) {
|
|
5
|
+
const free = {};
|
|
6
|
+
const used = {};
|
|
7
|
+
const total = {};
|
|
8
|
+
const assets = [];
|
|
9
|
+
for (const a of raw.assets ?? []) {
|
|
10
|
+
const wb = a.walletBalance ?? "0";
|
|
11
|
+
const ab = a.availableBalance ?? "0";
|
|
12
|
+
const mb = a.marginBalance ?? "0";
|
|
13
|
+
const im = a.initialMargin ?? "0";
|
|
14
|
+
const mm = a.maintMargin ?? "0";
|
|
15
|
+
const up = a.unrealizedProfit ?? "0";
|
|
16
|
+
if (parseFloat(wb) === 0 && parseFloat(ab) === 0 && parseFloat(mb) === 0) continue;
|
|
17
|
+
free[a.asset] = ab;
|
|
18
|
+
used[a.asset] = im;
|
|
19
|
+
total[a.asset] = mb;
|
|
20
|
+
assets.push({
|
|
21
|
+
asset: a.asset,
|
|
22
|
+
free: ab,
|
|
23
|
+
used: im,
|
|
24
|
+
total: mb,
|
|
25
|
+
walletBalance: wb,
|
|
26
|
+
unrealizedPnl: up,
|
|
27
|
+
usdValue: mb,
|
|
28
|
+
initialMargin: im,
|
|
29
|
+
maintenanceMargin: mm,
|
|
30
|
+
isCollateral: a.marginAvailable ?? false,
|
|
31
|
+
collateralRatio: collateralRatios.get(a.asset) ?? "1.0000",
|
|
32
|
+
borrowedAmount: "0"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const totalMarginBalance = raw.totalMarginBalance ?? "0";
|
|
36
|
+
const totalMaintMargin = raw.totalMaintMargin ?? "0";
|
|
37
|
+
const marginBalance = parseFloat(totalMarginBalance);
|
|
38
|
+
const maintMargin = parseFloat(totalMaintMargin);
|
|
39
|
+
const marginRatio = marginBalance > 0 ? (maintMargin / marginBalance).toFixed(4) : "0.0000";
|
|
40
|
+
return {
|
|
41
|
+
exchange: "binance",
|
|
42
|
+
accountType: raw.multiAssetsMargin ? "multi-asset" : "single-asset",
|
|
43
|
+
marginMode: "cross",
|
|
44
|
+
free,
|
|
45
|
+
used,
|
|
46
|
+
total,
|
|
47
|
+
availableToTrade: raw.availableBalance ?? "0",
|
|
48
|
+
totalEquity: totalMarginBalance,
|
|
49
|
+
walletBalance: raw.totalWalletBalance ?? "0",
|
|
50
|
+
unrealizedPnl: raw.totalUnrealizedProfit ?? "0",
|
|
51
|
+
initialMarginUsed: raw.totalInitialMargin ?? "0",
|
|
52
|
+
maintenanceMargin: totalMaintMargin,
|
|
53
|
+
marginRatio,
|
|
54
|
+
assets,
|
|
55
|
+
timestamp: Date.now()
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function mapPositions(raw) {
|
|
59
|
+
const positions = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const p of raw) {
|
|
61
|
+
const amt = p.positionAmt ?? "0";
|
|
62
|
+
if (amt === "0" || amt === "") continue;
|
|
63
|
+
const symbol = p.symbol ?? "";
|
|
64
|
+
const side = amt.startsWith("-") ? "short" : "long";
|
|
65
|
+
positions.set(positionKey(symbol, side), {
|
|
66
|
+
symbol,
|
|
67
|
+
side,
|
|
68
|
+
size: amt,
|
|
69
|
+
entryPrice: p.entryPrice ?? "0",
|
|
70
|
+
unrealizedPnl: p.unRealizedProfit ?? "0",
|
|
71
|
+
leverage: p.leverage ?? "0",
|
|
72
|
+
marginType: p.marginType,
|
|
73
|
+
liquidationPrice: p.liquidationPrice || null
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return positions;
|
|
77
|
+
}
|
|
78
|
+
function mergePositions(incoming, existing) {
|
|
79
|
+
const merged = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const [key, pos] of incoming) {
|
|
81
|
+
const prev = existing.get(key);
|
|
82
|
+
if (prev) {
|
|
83
|
+
merged.set(key, {
|
|
84
|
+
...pos,
|
|
85
|
+
leverage: pos.leverage && pos.leverage !== "0" ? pos.leverage : prev.leverage,
|
|
86
|
+
liquidationPrice: pos.liquidationPrice ?? prev.liquidationPrice,
|
|
87
|
+
entryPrice: pos.entryPrice && pos.entryPrice !== "0" ? pos.entryPrice : prev.entryPrice
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
merged.set(key, pos);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return merged;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { mapBalance, mapPositions, mergePositions };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SupportedCredentials } from '../../types.js';
|
|
2
|
+
import { BaseOrchestrator, OrchestratorConfig } from '../base.js';
|
|
3
|
+
import '@pear-protocol/types';
|
|
4
|
+
|
|
5
|
+
type BinanceOrchestratorConfig = OrchestratorConfig & {
|
|
6
|
+
listenKeyRefreshMs?: number;
|
|
7
|
+
accountPollMs?: number;
|
|
8
|
+
};
|
|
9
|
+
declare class BinanceOrchestrator extends BaseOrchestrator {
|
|
10
|
+
private ws;
|
|
11
|
+
private listenKeyTimer;
|
|
12
|
+
private pollTimer;
|
|
13
|
+
private creds;
|
|
14
|
+
private trackedAssetInfos;
|
|
15
|
+
private currentPositions;
|
|
16
|
+
private collateralRatios;
|
|
17
|
+
private multiAssetsMargin;
|
|
18
|
+
private readonly listenKeyRefreshMs;
|
|
19
|
+
private readonly accountPollMs;
|
|
20
|
+
constructor(config: BinanceOrchestratorConfig);
|
|
21
|
+
start(credentials: SupportedCredentials): Promise<void>;
|
|
22
|
+
stop(): void;
|
|
23
|
+
get isConnected(): boolean;
|
|
24
|
+
onAssetTracked(coin: string): void;
|
|
25
|
+
onAssetUntracked(coin: string): void;
|
|
26
|
+
setLeverage(symbol: string, leverage: string): Promise<void>;
|
|
27
|
+
private stopAll;
|
|
28
|
+
private pollAccountStatus;
|
|
29
|
+
private handleUserDataEvent;
|
|
30
|
+
private handleAccountStatus;
|
|
31
|
+
private handlePositionRisk;
|
|
32
|
+
private refreshAllPositions;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { BinanceOrchestrator };
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { positionKey } from '../../utils';
|
|
2
|
+
import { BaseOrchestrator } from '../base';
|
|
3
|
+
import { DEFAULT_LISTEN_KEY_REFRESH_MS, DEFAULT_ACCOUNT_POLL_MS } from './const';
|
|
4
|
+
import { mapBalance, mapPositions, mergePositions } from './mapper';
|
|
5
|
+
import { fetchAccountStatus, fetchAllPositionRisk, fetchAssetIndex, createListenKey, keepAliveListenKey, fetchPositionRisk, setLeverage } from './rest';
|
|
6
|
+
import { BinanceWs } from './ws';
|
|
7
|
+
|
|
8
|
+
class BinanceOrchestrator extends BaseOrchestrator {
|
|
9
|
+
ws = null;
|
|
10
|
+
listenKeyTimer = null;
|
|
11
|
+
pollTimer = null;
|
|
12
|
+
creds = null;
|
|
13
|
+
trackedAssetInfos = /* @__PURE__ */ new Map();
|
|
14
|
+
currentPositions = /* @__PURE__ */ new Map();
|
|
15
|
+
collateralRatios = /* @__PURE__ */ new Map();
|
|
16
|
+
multiAssetsMargin = false;
|
|
17
|
+
listenKeyRefreshMs;
|
|
18
|
+
accountPollMs;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
super(config);
|
|
21
|
+
this.listenKeyRefreshMs = config.listenKeyRefreshMs ?? DEFAULT_LISTEN_KEY_REFRESH_MS;
|
|
22
|
+
this.accountPollMs = config.accountPollMs ?? DEFAULT_ACCOUNT_POLL_MS;
|
|
23
|
+
}
|
|
24
|
+
async start(credentials) {
|
|
25
|
+
const gen = ++this.startGeneration;
|
|
26
|
+
this.manualClose = false;
|
|
27
|
+
this.credentials = credentials;
|
|
28
|
+
this.creds = credentials;
|
|
29
|
+
this.stopAll();
|
|
30
|
+
try {
|
|
31
|
+
const [snapshot, positionRisk] = await Promise.all([
|
|
32
|
+
fetchAccountStatus(this.creds, this.demo),
|
|
33
|
+
fetchAllPositionRisk(this.creds, this.demo)
|
|
34
|
+
]);
|
|
35
|
+
if (gen !== this.startGeneration) return;
|
|
36
|
+
this.multiAssetsMargin = snapshot.multiAssetsMargin ?? false;
|
|
37
|
+
if (this.multiAssetsMargin) {
|
|
38
|
+
const indexData = await fetchAssetIndex(this.demo);
|
|
39
|
+
if (gen !== this.startGeneration) return;
|
|
40
|
+
this.collateralRatios.clear();
|
|
41
|
+
for (const item of indexData) {
|
|
42
|
+
const asset = item.symbol.replace(/USD$/, "");
|
|
43
|
+
if (item.bidRate && parseFloat(item.bidRate) > 0) {
|
|
44
|
+
this.collateralRatios.set(asset, item.bidRate);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
this.handleAccountStatus(snapshot);
|
|
49
|
+
this.handlePositionRisk(positionRisk, false);
|
|
50
|
+
const listenKey = await createListenKey(this.creds.api_key, this.demo);
|
|
51
|
+
this.ws = new BinanceWs(
|
|
52
|
+
{
|
|
53
|
+
onUserDataEvent: (event) => this.handleUserDataEvent(event),
|
|
54
|
+
onWsApiResponse: (_id, result) => this.handleAccountStatus(result),
|
|
55
|
+
onClose: () => {
|
|
56
|
+
if (!this.manualClose) {
|
|
57
|
+
this.stopAll();
|
|
58
|
+
this.scheduleReconnect();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
this.demo
|
|
63
|
+
);
|
|
64
|
+
await this.ws.connectUserDataStream(listenKey);
|
|
65
|
+
this.listenKeyTimer = setInterval(() => {
|
|
66
|
+
if (this.creds) {
|
|
67
|
+
keepAliveListenKey(this.creds.api_key, this.demo).catch(
|
|
68
|
+
(err) => this.onError(err instanceof Error ? err : new Error(String(err)))
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}, this.listenKeyRefreshMs);
|
|
72
|
+
await this.ws.connectWsApi(this.creds);
|
|
73
|
+
this.reconnectAttempts = 0;
|
|
74
|
+
this.pollAccountStatus();
|
|
75
|
+
this.pollTimer = setInterval(() => this.pollAccountStatus(), this.accountPollMs);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (gen !== this.startGeneration) return;
|
|
78
|
+
this.stopAll();
|
|
79
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
80
|
+
this.scheduleReconnect();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
stop() {
|
|
84
|
+
this.manualClose = true;
|
|
85
|
+
this.stopAll();
|
|
86
|
+
this.credentials = null;
|
|
87
|
+
this.creds = null;
|
|
88
|
+
this.trackedAssetInfos.clear();
|
|
89
|
+
this.currentPositions.clear();
|
|
90
|
+
this.collateralRatios.clear();
|
|
91
|
+
this.multiAssetsMargin = false;
|
|
92
|
+
}
|
|
93
|
+
get isConnected() {
|
|
94
|
+
return this.ws?.isConnected ?? false;
|
|
95
|
+
}
|
|
96
|
+
onAssetTracked(coin) {
|
|
97
|
+
if (!this.creds) return;
|
|
98
|
+
fetchPositionRisk(this.creds, coin, this.demo).then((risk) => {
|
|
99
|
+
if (!risk) return;
|
|
100
|
+
const leverage = risk?.leverage;
|
|
101
|
+
const marginType = risk?.marginType === "isolated" ? "isolated" : "cross";
|
|
102
|
+
const info = {
|
|
103
|
+
coin,
|
|
104
|
+
leverage,
|
|
105
|
+
marginType,
|
|
106
|
+
availableToTrade: null,
|
|
107
|
+
maxTradeSzs: null,
|
|
108
|
+
collateralToken: null
|
|
109
|
+
};
|
|
110
|
+
this.trackedAssetInfos.set(coin, info);
|
|
111
|
+
const trackedAssets = /* @__PURE__ */ new Map();
|
|
112
|
+
trackedAssets.set(coin, info);
|
|
113
|
+
this.onStateUpdate({ trackedAssets });
|
|
114
|
+
}).catch((err) => {
|
|
115
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
onAssetUntracked(coin) {
|
|
119
|
+
this.trackedAssetInfos.delete(coin);
|
|
120
|
+
this.onStateUpdate({ trackedAssets: new Map(this.trackedAssetInfos) });
|
|
121
|
+
}
|
|
122
|
+
async setLeverage(symbol, leverage) {
|
|
123
|
+
if (!this.creds) throw new Error("Not connected");
|
|
124
|
+
await setLeverage(this.creds, symbol, leverage, this.demo);
|
|
125
|
+
}
|
|
126
|
+
stopAll() {
|
|
127
|
+
if (this.listenKeyTimer) {
|
|
128
|
+
clearInterval(this.listenKeyTimer);
|
|
129
|
+
this.listenKeyTimer = null;
|
|
130
|
+
}
|
|
131
|
+
if (this.pollTimer) {
|
|
132
|
+
clearInterval(this.pollTimer);
|
|
133
|
+
this.pollTimer = null;
|
|
134
|
+
}
|
|
135
|
+
this.clearReconnectTimer();
|
|
136
|
+
this.ws?.close();
|
|
137
|
+
this.ws = null;
|
|
138
|
+
}
|
|
139
|
+
pollAccountStatus() {
|
|
140
|
+
if (!this.ws?.isConnected || !this.creds) return;
|
|
141
|
+
const id = this.ws.nextRequestId();
|
|
142
|
+
this.ws.sendSignedRequest(id, "v3/account.status");
|
|
143
|
+
}
|
|
144
|
+
handleUserDataEvent(event) {
|
|
145
|
+
if (event.e === "ACCOUNT_CONFIG_UPDATE" && event.ac) {
|
|
146
|
+
const symbol = event.ac.s ?? "";
|
|
147
|
+
const leverage = event.ac.l ?? "0";
|
|
148
|
+
if (symbol && leverage !== "0") {
|
|
149
|
+
const leverageSettings = /* @__PURE__ */ new Map();
|
|
150
|
+
leverageSettings.set(symbol, leverage);
|
|
151
|
+
const existing = this.trackedAssetInfos.get(symbol);
|
|
152
|
+
if (existing) {
|
|
153
|
+
const updated = { ...existing, leverage };
|
|
154
|
+
this.trackedAssetInfos.set(symbol, updated);
|
|
155
|
+
const trackedAssets = /* @__PURE__ */ new Map();
|
|
156
|
+
trackedAssets.set(symbol, updated);
|
|
157
|
+
this.onStateUpdate({ leverageSettings, trackedAssets });
|
|
158
|
+
} else {
|
|
159
|
+
this.onStateUpdate({ leverageSettings });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.refreshAllPositions();
|
|
163
|
+
}
|
|
164
|
+
if (event.e === "ACCOUNT_UPDATE") {
|
|
165
|
+
this.refreshAllPositions();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
handleAccountStatus(result) {
|
|
169
|
+
const balance = mapBalance(result, this.collateralRatios);
|
|
170
|
+
this.onStateUpdate({ balance });
|
|
171
|
+
}
|
|
172
|
+
handlePositionRisk(risk, merge) {
|
|
173
|
+
let positions = mapPositions(risk);
|
|
174
|
+
if (merge && this.currentPositions.size > 0) {
|
|
175
|
+
positions = mergePositions(positions, this.currentPositions);
|
|
176
|
+
}
|
|
177
|
+
this.currentPositions = positions;
|
|
178
|
+
const leverageSettings = /* @__PURE__ */ new Map();
|
|
179
|
+
for (const p of risk) {
|
|
180
|
+
if (p.symbol && p.leverage) {
|
|
181
|
+
leverageSettings.set(p.symbol, p.leverage);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this.onStateUpdate({ positions, leverageSettings });
|
|
185
|
+
}
|
|
186
|
+
refreshAllPositions() {
|
|
187
|
+
if (!this.creds) return;
|
|
188
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
189
|
+
for (const pos of this.currentPositions.values()) {
|
|
190
|
+
symbols.add(pos.symbol);
|
|
191
|
+
}
|
|
192
|
+
for (const coin of this.trackedCoins) {
|
|
193
|
+
symbols.add(coin);
|
|
194
|
+
}
|
|
195
|
+
if (symbols.size === 0) return;
|
|
196
|
+
const creds = this.creds;
|
|
197
|
+
const promises = Array.from(symbols).map(
|
|
198
|
+
(symbol) => fetchPositionRisk(creds, symbol, this.demo).catch(() => void 0)
|
|
199
|
+
);
|
|
200
|
+
Promise.all(promises).then((results) => {
|
|
201
|
+
const freshPositions = /* @__PURE__ */ new Map();
|
|
202
|
+
for (const risk of results) {
|
|
203
|
+
if (!risk) continue;
|
|
204
|
+
const amt = risk.positionAmt ?? "0";
|
|
205
|
+
if (amt === "0" || amt === "") continue;
|
|
206
|
+
const symbol = risk.symbol ?? "";
|
|
207
|
+
const side = amt.startsWith("-") ? "short" : "long";
|
|
208
|
+
freshPositions.set(positionKey(symbol, side), {
|
|
209
|
+
symbol,
|
|
210
|
+
side,
|
|
211
|
+
size: amt,
|
|
212
|
+
entryPrice: risk.entryPrice ?? "0",
|
|
213
|
+
unrealizedPnl: risk.unRealizedProfit ?? "0",
|
|
214
|
+
leverage: risk.leverage ?? "0",
|
|
215
|
+
marginType: risk.marginType === "isolated" ? "isolated" : "cross",
|
|
216
|
+
liquidationPrice: risk.liquidationPrice || null
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
for (const [key, pos] of this.currentPositions) {
|
|
220
|
+
if (!symbols.has(pos.symbol) && !freshPositions.has(key)) {
|
|
221
|
+
freshPositions.set(key, pos);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.currentPositions = freshPositions;
|
|
225
|
+
this.onStateUpdate({ positions: freshPositions });
|
|
226
|
+
}).catch((err) => {
|
|
227
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export { BinanceOrchestrator };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BinanceUSDMCredentialsDTO } from '@pear-protocol/types';
|
|
2
|
+
import { BinanceRawAccountStatus, BinanceRawPositionRisk, BinanceAssetIndex, BinanceRawLeverageResponse } from './types.js';
|
|
3
|
+
import '../../types.js';
|
|
4
|
+
|
|
5
|
+
declare function createListenKey(apiKey: string, demo?: boolean): Promise<string>;
|
|
6
|
+
declare function keepAliveListenKey(apiKey: string, demo?: boolean): Promise<void>;
|
|
7
|
+
declare function fetchAccountStatus(creds: BinanceUSDMCredentialsDTO, demo: boolean): Promise<BinanceRawAccountStatus>;
|
|
8
|
+
declare function fetchPositionRisk(creds: BinanceUSDMCredentialsDTO, symbol: string, demo: boolean): Promise<BinanceRawPositionRisk | undefined>;
|
|
9
|
+
declare function fetchAllPositionRisk(creds: BinanceUSDMCredentialsDTO, demo: boolean): Promise<BinanceRawPositionRisk[]>;
|
|
10
|
+
declare function setLeverage(creds: BinanceUSDMCredentialsDTO, symbol: string, leverage: string, demo: boolean): Promise<BinanceRawLeverageResponse>;
|
|
11
|
+
declare function fetchAssetIndex(demo: boolean): Promise<BinanceAssetIndex[]>;
|
|
12
|
+
|
|
13
|
+
export { createListenKey, fetchAccountStatus, fetchAllPositionRisk, fetchAssetIndex, fetchPositionRisk, keepAliveListenKey, setLeverage };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { hmacSha256Hex } from '../../../utils/hmac';
|
|
2
|
+
import { BASE_URL_TESTNET, BASE_URL } from './const';
|
|
3
|
+
|
|
4
|
+
async function signedGet(creds, path, params, demo) {
|
|
5
|
+
const baseUrl = demo ? BASE_URL_TESTNET : BASE_URL;
|
|
6
|
+
const timestamp = Date.now().toString();
|
|
7
|
+
const allParams = { ...params, timestamp, recvWindow: "5000" };
|
|
8
|
+
const queryString = new URLSearchParams(allParams).toString();
|
|
9
|
+
const signature = await hmacSha256Hex(creds.api_secret, queryString);
|
|
10
|
+
const response = await fetch(`${baseUrl}${path}?${queryString}&signature=${signature}`, {
|
|
11
|
+
headers: { "X-MBX-APIKEY": creds.api_key }
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const body = await response.json().catch(() => null);
|
|
15
|
+
const msg = body?.msg ?? response.statusText;
|
|
16
|
+
throw new Error(`Binance API error ${response.status}: ${msg}`);
|
|
17
|
+
}
|
|
18
|
+
return await response.json();
|
|
19
|
+
}
|
|
20
|
+
async function signedPost(creds, path, params, demo) {
|
|
21
|
+
const baseUrl = demo ? BASE_URL_TESTNET : BASE_URL;
|
|
22
|
+
const timestamp = Date.now().toString();
|
|
23
|
+
const allParams = { ...params, timestamp, recvWindow: "5000" };
|
|
24
|
+
const queryString = new URLSearchParams(allParams).toString();
|
|
25
|
+
const signature = await hmacSha256Hex(creds.api_secret, queryString);
|
|
26
|
+
const response = await fetch(`${baseUrl}${path}?${queryString}&signature=${signature}`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "X-MBX-APIKEY": creds.api_key }
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const body = await response.json().catch(() => null);
|
|
32
|
+
const msg = body?.msg ?? response.statusText;
|
|
33
|
+
throw new Error(`Binance API error ${response.status}: ${msg}`);
|
|
34
|
+
}
|
|
35
|
+
return await response.json();
|
|
36
|
+
}
|
|
37
|
+
async function createListenKey(apiKey, demo = false) {
|
|
38
|
+
const baseUrl = demo ? BASE_URL_TESTNET : BASE_URL;
|
|
39
|
+
const response = await fetch(`${baseUrl}/fapi/v1/listenKey`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "X-MBX-APIKEY": apiKey }
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`Failed to create listenKey: ${response.status} ${response.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
return data.listenKey;
|
|
48
|
+
}
|
|
49
|
+
async function keepAliveListenKey(apiKey, demo = false) {
|
|
50
|
+
const baseUrl = demo ? BASE_URL_TESTNET : BASE_URL;
|
|
51
|
+
const response = await fetch(`${baseUrl}/fapi/v1/listenKey`, {
|
|
52
|
+
method: "PUT",
|
|
53
|
+
headers: { "X-MBX-APIKEY": apiKey }
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`Failed to keep-alive listenKey: ${response.status} ${response.statusText}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function fetchAccountStatus(creds, demo) {
|
|
60
|
+
return signedGet(creds, "/fapi/v3/account", {}, demo);
|
|
61
|
+
}
|
|
62
|
+
async function fetchPositionRisk(creds, symbol, demo) {
|
|
63
|
+
const data = await signedGet(creds, "/fapi/v2/positionRisk", { symbol }, demo);
|
|
64
|
+
return data[0];
|
|
65
|
+
}
|
|
66
|
+
async function fetchAllPositionRisk(creds, demo) {
|
|
67
|
+
return signedGet(creds, "/fapi/v2/positionRisk", {}, demo);
|
|
68
|
+
}
|
|
69
|
+
async function setLeverage(creds, symbol, leverage, demo) {
|
|
70
|
+
return signedPost(creds, "/fapi/v1/leverage", { symbol, leverage }, demo);
|
|
71
|
+
}
|
|
72
|
+
async function fetchAssetIndex(demo) {
|
|
73
|
+
const baseUrl = demo ? BASE_URL_TESTNET : BASE_URL;
|
|
74
|
+
const response = await fetch(`${baseUrl}/fapi/v1/assetIndex`);
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`Binance API error: ${response.status} ${response.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
return await response.json();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { createListenKey, fetchAccountStatus, fetchAllPositionRisk, fetchAssetIndex, fetchPositionRisk, keepAliveListenKey, setLeverage };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { MarginMode } from '../../types.js';
|
|
2
|
+
import '@pear-protocol/types';
|
|
3
|
+
|
|
4
|
+
interface BinanceRawPositionRisk {
|
|
5
|
+
symbol: string;
|
|
6
|
+
leverage: string;
|
|
7
|
+
marginType: MarginMode;
|
|
8
|
+
isAutoAddMargin: string;
|
|
9
|
+
isolatedMargin: string;
|
|
10
|
+
positionAmt: string;
|
|
11
|
+
entryPrice: string;
|
|
12
|
+
breakEvenPrice: string;
|
|
13
|
+
unRealizedProfit: string;
|
|
14
|
+
liquidationPrice: string;
|
|
15
|
+
markPrice: string;
|
|
16
|
+
maxNotionalValue: string;
|
|
17
|
+
positionSide: string;
|
|
18
|
+
notional: string;
|
|
19
|
+
isolatedWallet: string;
|
|
20
|
+
updateTime: number;
|
|
21
|
+
}
|
|
22
|
+
interface BinanceRawAsset {
|
|
23
|
+
asset: string;
|
|
24
|
+
walletBalance: string;
|
|
25
|
+
availableBalance: string;
|
|
26
|
+
unrealizedProfit: string;
|
|
27
|
+
marginBalance: string;
|
|
28
|
+
initialMargin: string;
|
|
29
|
+
maintMargin: string;
|
|
30
|
+
marginAvailable: boolean;
|
|
31
|
+
}
|
|
32
|
+
interface BinanceRawAccountStatus {
|
|
33
|
+
totalWalletBalance: string;
|
|
34
|
+
totalMarginBalance: string;
|
|
35
|
+
availableBalance: string;
|
|
36
|
+
totalUnrealizedProfit: string;
|
|
37
|
+
totalInitialMargin: string;
|
|
38
|
+
totalMaintMargin: string;
|
|
39
|
+
multiAssetsMargin: boolean;
|
|
40
|
+
assets?: BinanceRawAsset[];
|
|
41
|
+
}
|
|
42
|
+
interface BinanceAssetIndex {
|
|
43
|
+
symbol: string;
|
|
44
|
+
time: number;
|
|
45
|
+
index: string;
|
|
46
|
+
bidBuffer: string;
|
|
47
|
+
askBuffer: string;
|
|
48
|
+
bidRate: string;
|
|
49
|
+
askRate: string;
|
|
50
|
+
autoExchangeBidBuffer: string;
|
|
51
|
+
autoExchangeAskBuffer: string;
|
|
52
|
+
autoExchangeBidRate: string;
|
|
53
|
+
autoExchangeAskRate: string;
|
|
54
|
+
}
|
|
55
|
+
interface BinanceRawUserDataEvent {
|
|
56
|
+
e?: string;
|
|
57
|
+
ac?: {
|
|
58
|
+
s: string;
|
|
59
|
+
l: string;
|
|
60
|
+
};
|
|
61
|
+
a?: {
|
|
62
|
+
B?: unknown[];
|
|
63
|
+
P?: unknown[];
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
interface BinanceRawLeverageResponse {
|
|
67
|
+
leverage: string;
|
|
68
|
+
maxNotionalValue: string;
|
|
69
|
+
symbol: string;
|
|
70
|
+
}
|
|
71
|
+
interface BinanceWsHandlers {
|
|
72
|
+
onUserDataEvent: (event: unknown) => void;
|
|
73
|
+
onWsApiResponse: (id: number, result: unknown) => void;
|
|
74
|
+
onClose: () => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type { BinanceAssetIndex, BinanceRawAccountStatus, BinanceRawAsset, BinanceRawLeverageResponse, BinanceRawPositionRisk, BinanceRawUserDataEvent, BinanceWsHandlers };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BinanceUSDMCredentialsDTO } from '@pear-protocol/types';
|
|
2
|
+
import { BinanceWsHandlers } from './types.js';
|
|
3
|
+
import '../../types.js';
|
|
4
|
+
|
|
5
|
+
declare class BinanceWs {
|
|
6
|
+
private userDataWs;
|
|
7
|
+
private wsApiWs;
|
|
8
|
+
private requestId;
|
|
9
|
+
private handlers;
|
|
10
|
+
private demo;
|
|
11
|
+
private creds;
|
|
12
|
+
constructor(handlers: BinanceWsHandlers, demo: boolean);
|
|
13
|
+
connectUserDataStream(listenKey: string): Promise<void>;
|
|
14
|
+
connectWsApi(creds: BinanceUSDMCredentialsDTO): Promise<void>;
|
|
15
|
+
nextRequestId(): number;
|
|
16
|
+
sendSignedRequest(id: number, method: string, params?: Record<string, unknown>): Promise<void>;
|
|
17
|
+
close(): void;
|
|
18
|
+
get isConnected(): boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { BinanceWs };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { hmacSha256Hex } from '../../../utils/hmac';
|
|
2
|
+
import { connectWs, sendWs } from '../../../utils/ws';
|
|
3
|
+
import { USER_DATA_STREAM_URL_TESTNET, USER_DATA_STREAM_URL, WS_API_URL_TESTNET, WS_API_URL } from './const';
|
|
4
|
+
|
|
5
|
+
class BinanceWs {
|
|
6
|
+
userDataWs = null;
|
|
7
|
+
wsApiWs = null;
|
|
8
|
+
requestId = 1;
|
|
9
|
+
handlers;
|
|
10
|
+
demo;
|
|
11
|
+
creds = null;
|
|
12
|
+
constructor(handlers, demo) {
|
|
13
|
+
this.handlers = handlers;
|
|
14
|
+
this.demo = demo;
|
|
15
|
+
}
|
|
16
|
+
async connectUserDataStream(listenKey) {
|
|
17
|
+
const url = this.demo ? USER_DATA_STREAM_URL_TESTNET : USER_DATA_STREAM_URL;
|
|
18
|
+
this.userDataWs = await connectWs(`${url}/${listenKey}`);
|
|
19
|
+
this.userDataWs.addEventListener("message", (event) => {
|
|
20
|
+
try {
|
|
21
|
+
this.handlers.onUserDataEvent(JSON.parse(event.data));
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
this.userDataWs.onclose = () => {
|
|
26
|
+
if (this.userDataWs) {
|
|
27
|
+
this.userDataWs = null;
|
|
28
|
+
this.handlers.onClose();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
this.userDataWs.onerror = () => {
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async connectWsApi(creds) {
|
|
35
|
+
this.creds = creds;
|
|
36
|
+
const url = this.demo ? WS_API_URL_TESTNET : WS_API_URL;
|
|
37
|
+
this.wsApiWs = await connectWs(url);
|
|
38
|
+
this.wsApiWs.addEventListener("message", (event) => {
|
|
39
|
+
try {
|
|
40
|
+
const msg = JSON.parse(event.data);
|
|
41
|
+
if (msg.id !== void 0 && msg.result !== void 0) {
|
|
42
|
+
this.handlers.onWsApiResponse(msg.id, msg.result);
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
this.wsApiWs.onclose = () => {
|
|
48
|
+
if (this.wsApiWs) {
|
|
49
|
+
this.wsApiWs = null;
|
|
50
|
+
this.handlers.onClose();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
this.wsApiWs.onerror = () => {
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
nextRequestId() {
|
|
57
|
+
return this.requestId++;
|
|
58
|
+
}
|
|
59
|
+
async sendSignedRequest(id, method, params = {}) {
|
|
60
|
+
if (!this.creds) return;
|
|
61
|
+
const timestamp = Date.now();
|
|
62
|
+
const allParams = { ...params, apiKey: this.creds.api_key, timestamp };
|
|
63
|
+
const queryString = Object.keys(allParams).sort().map((k) => `${k}=${allParams[k]}`).join("&");
|
|
64
|
+
const signature = await hmacSha256Hex(this.creds.api_secret, queryString);
|
|
65
|
+
sendWs(this.wsApiWs, { id, method, params: { ...allParams, signature } });
|
|
66
|
+
}
|
|
67
|
+
close() {
|
|
68
|
+
this.creds = null;
|
|
69
|
+
if (this.userDataWs) {
|
|
70
|
+
this.userDataWs.onclose = null;
|
|
71
|
+
this.userDataWs.close();
|
|
72
|
+
this.userDataWs = null;
|
|
73
|
+
}
|
|
74
|
+
if (this.wsApiWs) {
|
|
75
|
+
this.wsApiWs.onclose = null;
|
|
76
|
+
this.wsApiWs.close();
|
|
77
|
+
this.wsApiWs = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
get isConnected() {
|
|
81
|
+
return this.userDataWs?.readyState === WebSocket.OPEN && this.wsApiWs?.readyState === WebSocket.OPEN;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { BinanceWs };
|