pmxt-core 2.37.14 → 2.38.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/exchanges/hyperliquid/auth.d.ts +30 -0
- package/dist/exchanges/hyperliquid/auth.js +145 -0
- package/dist/exchanges/hyperliquid/config.d.ts +17 -0
- package/dist/exchanges/hyperliquid/config.js +28 -0
- package/dist/exchanges/hyperliquid/errors.d.ts +18 -0
- package/dist/exchanges/hyperliquid/errors.js +61 -0
- package/dist/exchanges/hyperliquid/fetcher.d.ts +140 -0
- package/dist/exchanges/hyperliquid/fetcher.js +137 -0
- package/dist/exchanges/hyperliquid/index.d.ts +31 -0
- package/dist/exchanges/hyperliquid/index.js +219 -0
- package/dist/exchanges/hyperliquid/normalizer.d.ts +18 -0
- package/dist/exchanges/hyperliquid/normalizer.js +339 -0
- package/dist/exchanges/hyperliquid/utils.d.ts +41 -0
- package/dist/exchanges/hyperliquid/utils.js +76 -0
- package/dist/exchanges/kalshi/api.d.ts +1 -1
- package/dist/exchanges/kalshi/api.js +1 -1
- package/dist/exchanges/limitless/api.d.ts +1 -1
- package/dist/exchanges/limitless/api.js +1 -1
- package/dist/exchanges/myriad/api.d.ts +1 -1
- package/dist/exchanges/myriad/api.js +1 -1
- package/dist/exchanges/opinion/api.d.ts +1 -1
- package/dist/exchanges/opinion/api.js +1 -1
- package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
- package/dist/exchanges/polymarket/api-clob.js +1 -1
- package/dist/exchanges/polymarket/api-data.d.ts +1 -1
- package/dist/exchanges/polymarket/api-data.js +1 -1
- package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
- package/dist/exchanges/polymarket/api-gamma.js +1 -1
- package/dist/exchanges/probable/api.d.ts +1 -1
- package/dist/exchanges/probable/api.js +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -1
- package/dist/server/exchange-factory.js +6 -0
- package/dist/server/openapi.yaml +15 -0
- package/package.json +4 -3
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HyperliquidExchange = void 0;
|
|
4
|
+
const BaseExchange_1 = require("../../BaseExchange");
|
|
5
|
+
const errors_1 = require("../../errors");
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
const fetcher_1 = require("./fetcher");
|
|
8
|
+
const normalizer_1 = require("./normalizer");
|
|
9
|
+
const auth_1 = require("./auth");
|
|
10
|
+
const errors_2 = require("./errors");
|
|
11
|
+
class HyperliquidExchange extends BaseExchange_1.PredictionMarketExchange {
|
|
12
|
+
config;
|
|
13
|
+
fetcher;
|
|
14
|
+
normalizer;
|
|
15
|
+
walletAddress;
|
|
16
|
+
auth;
|
|
17
|
+
constructor(credentials) {
|
|
18
|
+
const opts = credentials && 'credentials' in credentials
|
|
19
|
+
? credentials
|
|
20
|
+
: { credentials: credentials };
|
|
21
|
+
super(opts.credentials);
|
|
22
|
+
this.rateLimit = 200;
|
|
23
|
+
const testnet = 'testnet' in opts ? opts.testnet : false;
|
|
24
|
+
this.config = (0, config_1.getHyperliquidConfig)(opts.credentials?.baseUrl, testnet);
|
|
25
|
+
// Initialize auth if privateKey is provided (needed for trading)
|
|
26
|
+
if (opts.credentials?.privateKey) {
|
|
27
|
+
this.auth = new auth_1.HyperliquidAuth(opts.credentials, this.config.testnet);
|
|
28
|
+
this.walletAddress = this.auth.getAddress();
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// For read-only usage, users can pass walletAddress as apiKey
|
|
32
|
+
this.walletAddress = opts.credentials?.apiKey || undefined;
|
|
33
|
+
}
|
|
34
|
+
const ctx = {
|
|
35
|
+
http: this.http,
|
|
36
|
+
callApi: this.callApi.bind(this),
|
|
37
|
+
getHeaders: () => ({}),
|
|
38
|
+
};
|
|
39
|
+
this.fetcher = new fetcher_1.HyperliquidFetcher(ctx, this.config.baseUrl);
|
|
40
|
+
this.normalizer = new normalizer_1.HyperliquidNormalizer();
|
|
41
|
+
}
|
|
42
|
+
get name() {
|
|
43
|
+
return 'Hyperliquid';
|
|
44
|
+
}
|
|
45
|
+
// -------------------------------------------------------------------------
|
|
46
|
+
// Auth helpers
|
|
47
|
+
// -------------------------------------------------------------------------
|
|
48
|
+
requireWallet() {
|
|
49
|
+
if (!this.walletAddress) {
|
|
50
|
+
throw new errors_1.AuthenticationError('This operation requires a wallet address. ' +
|
|
51
|
+
'Initialize HyperliquidExchange with credentials (apiKey = wallet address, or privateKey for trading).', 'Hyperliquid');
|
|
52
|
+
}
|
|
53
|
+
return this.walletAddress;
|
|
54
|
+
}
|
|
55
|
+
requireAuth() {
|
|
56
|
+
if (!this.auth) {
|
|
57
|
+
throw new errors_1.AuthenticationError('Trading requires a privateKey for EIP-712 signing. ' +
|
|
58
|
+
'Initialize HyperliquidExchange with credentials including privateKey.', 'Hyperliquid');
|
|
59
|
+
}
|
|
60
|
+
return this.auth;
|
|
61
|
+
}
|
|
62
|
+
// -------------------------------------------------------------------------
|
|
63
|
+
// Market Data
|
|
64
|
+
// -------------------------------------------------------------------------
|
|
65
|
+
async fetchMarketsImpl(params) {
|
|
66
|
+
const rawOutcomes = await this.fetcher.fetchRawMarkets(params);
|
|
67
|
+
return rawOutcomes
|
|
68
|
+
.map(r => this.normalizer.normalizeMarket(r))
|
|
69
|
+
.filter((m) => m !== null);
|
|
70
|
+
}
|
|
71
|
+
async fetchEventsImpl(params) {
|
|
72
|
+
const [rawQuestions, meta, mids] = await Promise.all([
|
|
73
|
+
this.fetcher.fetchRawEvents(params),
|
|
74
|
+
this.fetcher.fetchOutcomeMeta(),
|
|
75
|
+
this.fetcher.fetchAllMids(),
|
|
76
|
+
]);
|
|
77
|
+
return rawQuestions
|
|
78
|
+
.map(q => this.normalizer.normalizeEventWithMarkets(q, meta, mids))
|
|
79
|
+
.filter((e) => e !== null);
|
|
80
|
+
}
|
|
81
|
+
async fetchOrderBook(outcomeId) {
|
|
82
|
+
const raw = await this.fetcher.fetchRawOrderBook(outcomeId);
|
|
83
|
+
return this.normalizer.normalizeOrderBook(raw, outcomeId);
|
|
84
|
+
}
|
|
85
|
+
async fetchOHLCV(outcomeId, params) {
|
|
86
|
+
const raw = await this.fetcher.fetchRawOHLCV(outcomeId, params);
|
|
87
|
+
return this.normalizer.normalizeOHLCV(raw, params);
|
|
88
|
+
}
|
|
89
|
+
async fetchTrades(outcomeId, params) {
|
|
90
|
+
const raw = await this.fetcher.fetchRawTrades(outcomeId, params || {});
|
|
91
|
+
return raw.map((r, i) => this.normalizer.normalizeTrade(r, i));
|
|
92
|
+
}
|
|
93
|
+
// -------------------------------------------------------------------------
|
|
94
|
+
// User Data
|
|
95
|
+
// -------------------------------------------------------------------------
|
|
96
|
+
async fetchBalance() {
|
|
97
|
+
const wallet = this.requireWallet();
|
|
98
|
+
const raw = await this.fetcher.fetchRawUserState(wallet);
|
|
99
|
+
return this.normalizer.normalizeBalance(raw);
|
|
100
|
+
}
|
|
101
|
+
async fetchPositions() {
|
|
102
|
+
const wallet = this.requireWallet();
|
|
103
|
+
const raw = await this.fetcher.fetchRawUserState(wallet);
|
|
104
|
+
return raw.assetPositions
|
|
105
|
+
.filter(ap => ap.position.coin.startsWith('#'))
|
|
106
|
+
.map(ap => this.normalizer.normalizePosition(ap.position));
|
|
107
|
+
}
|
|
108
|
+
async fetchOpenOrders() {
|
|
109
|
+
const wallet = this.requireWallet();
|
|
110
|
+
const raw = await this.fetcher.fetchRawOpenOrders(wallet);
|
|
111
|
+
return raw
|
|
112
|
+
.filter(o => o.coin.startsWith('#'))
|
|
113
|
+
.map(o => this.normalizer.normalizeOpenOrder(o));
|
|
114
|
+
}
|
|
115
|
+
async fetchMyTrades(params) {
|
|
116
|
+
const wallet = this.requireWallet();
|
|
117
|
+
const raw = await this.fetcher.fetchRawUserFills(wallet);
|
|
118
|
+
return raw
|
|
119
|
+
.filter(f => f.coin.startsWith('#'))
|
|
120
|
+
.map((f, i) => this.normalizer.normalizeUserTrade(f, i));
|
|
121
|
+
}
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
// Trading (EIP-712 signing required)
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
125
|
+
async buildOrder(params) {
|
|
126
|
+
const assetId = parseInt(params.outcomeId, 10);
|
|
127
|
+
// Key order matters for msgpack hash: a, b, p, s, r, t, c
|
|
128
|
+
const orderWire = {
|
|
129
|
+
a: assetId,
|
|
130
|
+
b: params.side === 'buy',
|
|
131
|
+
p: params.price !== undefined ? (0, auth_1.floatToWire)(params.price) : '0.5',
|
|
132
|
+
s: (0, auth_1.floatToWire)(params.amount),
|
|
133
|
+
r: false,
|
|
134
|
+
t: params.type === 'market'
|
|
135
|
+
? { limit: { tif: 'Ioc' } }
|
|
136
|
+
: { limit: { tif: 'Gtc' } },
|
|
137
|
+
};
|
|
138
|
+
// Key order matters for msgpack hash: type, orders, grouping
|
|
139
|
+
const action = {
|
|
140
|
+
type: 'order',
|
|
141
|
+
orders: [orderWire],
|
|
142
|
+
grouping: 'na',
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
exchange: this.name,
|
|
146
|
+
params,
|
|
147
|
+
raw: action,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async submitOrder(built) {
|
|
151
|
+
const auth = this.requireAuth();
|
|
152
|
+
const action = built.raw;
|
|
153
|
+
try {
|
|
154
|
+
const requestBody = await auth.signExchangeRequest(action);
|
|
155
|
+
const response = await this.http.post(`${this.config.baseUrl}/exchange`, requestBody);
|
|
156
|
+
const data = response.data;
|
|
157
|
+
if (data.status === 'err') {
|
|
158
|
+
throw errors_2.hyperliquidErrorMapper.mapError(new Error(data.response || 'Order submission failed'));
|
|
159
|
+
}
|
|
160
|
+
const resting = data.response?.data?.statuses?.[0];
|
|
161
|
+
return {
|
|
162
|
+
id: resting?.resting?.oid ? String(resting.resting.oid) : 'unknown',
|
|
163
|
+
marketId: built.params.marketId,
|
|
164
|
+
outcomeId: built.params.outcomeId,
|
|
165
|
+
side: built.params.side,
|
|
166
|
+
type: built.params.type,
|
|
167
|
+
price: built.params.price,
|
|
168
|
+
amount: built.params.amount,
|
|
169
|
+
status: resting?.resting ? 'open' : 'filled',
|
|
170
|
+
filled: resting?.filled?.totalSz ? parseFloat(resting.filled.totalSz) : 0,
|
|
171
|
+
remaining: built.params.amount - (resting?.filled?.totalSz ? parseFloat(resting.filled.totalSz) : 0),
|
|
172
|
+
timestamp: Date.now(),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
throw errors_2.hyperliquidErrorMapper.mapError(error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async createOrder(params) {
|
|
180
|
+
const built = await this.buildOrder(params);
|
|
181
|
+
return this.submitOrder(built);
|
|
182
|
+
}
|
|
183
|
+
async cancelOrder(orderId) {
|
|
184
|
+
const auth = this.requireAuth();
|
|
185
|
+
// Key order matters for msgpack hash: type, cancels
|
|
186
|
+
// Each cancel entry: a (asset), o (order id)
|
|
187
|
+
const action = {
|
|
188
|
+
type: 'cancel',
|
|
189
|
+
cancels: [{ a: 0, o: parseInt(orderId, 10) }],
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
const requestBody = await auth.signExchangeRequest(action);
|
|
193
|
+
const response = await this.http.post(`${this.config.baseUrl}/exchange`, requestBody);
|
|
194
|
+
const data = response.data;
|
|
195
|
+
if (data.status === 'err') {
|
|
196
|
+
throw new Error(data.response || 'Cancel failed');
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
id: orderId,
|
|
200
|
+
marketId: '',
|
|
201
|
+
outcomeId: '',
|
|
202
|
+
side: 'buy',
|
|
203
|
+
type: 'limit',
|
|
204
|
+
amount: 0,
|
|
205
|
+
status: 'cancelled',
|
|
206
|
+
filled: 0,
|
|
207
|
+
remaining: 0,
|
|
208
|
+
timestamp: Date.now(),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
throw errors_2.hyperliquidErrorMapper.mapError(error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async close() {
|
|
216
|
+
// No persistent connections to clean up
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
exports.HyperliquidExchange = HyperliquidExchange;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { UnifiedMarket, UnifiedEvent, OrderBook, PriceCandle, Trade, UserTrade, Position, Balance, Order } from '../../types';
|
|
2
|
+
import { IExchangeNormalizer } from '../interfaces';
|
|
3
|
+
import { OHLCVParams } from '../../BaseExchange';
|
|
4
|
+
import { HyperliquidRawOutcomeWithQuestion, HyperliquidRawQuestion, HyperliquidRawL2Book, HyperliquidRawCandle, HyperliquidRawTrade, HyperliquidRawFill, HyperliquidRawOpenOrder, HyperliquidRawPosition, HyperliquidRawUserState, HyperliquidRawOutcomeMeta, HyperliquidRawMid } from './fetcher';
|
|
5
|
+
export declare class HyperliquidNormalizer implements IExchangeNormalizer<HyperliquidRawOutcomeWithQuestion, HyperliquidRawQuestion> {
|
|
6
|
+
normalizeMarket(raw: HyperliquidRawOutcomeWithQuestion): UnifiedMarket | null;
|
|
7
|
+
normalizeEvent(raw: HyperliquidRawQuestion): UnifiedEvent | null;
|
|
8
|
+
normalizeEventWithMarkets(raw: HyperliquidRawQuestion, outcomeMeta: HyperliquidRawOutcomeMeta, mids: HyperliquidRawMid): UnifiedEvent | null;
|
|
9
|
+
normalizeOrderBook(raw: HyperliquidRawL2Book, _id: string): OrderBook;
|
|
10
|
+
normalizeOHLCV(raw: HyperliquidRawCandle[], _params: OHLCVParams): PriceCandle[];
|
|
11
|
+
normalizeTrade(raw: HyperliquidRawTrade, _index: number): Trade;
|
|
12
|
+
normalizeUserTrade(raw: HyperliquidRawFill, _index: number): UserTrade;
|
|
13
|
+
normalizeOpenOrder(raw: HyperliquidRawOpenOrder): Order;
|
|
14
|
+
normalizePosition(raw: HyperliquidRawPosition): Position;
|
|
15
|
+
normalizeBalance(raw: HyperliquidRawUserState): Balance[];
|
|
16
|
+
private coinToMarketId;
|
|
17
|
+
private coinToOutcomeId;
|
|
18
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HyperliquidNormalizer = void 0;
|
|
4
|
+
const market_utils_1 = require("../../utils/market-utils");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
function parseDescription(description) {
|
|
8
|
+
// Hyperliquid outcome descriptions use key:value pairs separated by |
|
|
9
|
+
// e.g. "class:priceBinary|underlying:BTC|expiry:20260509-0600|targetPrice:79583|period:1d"
|
|
10
|
+
// or simple values like "other", "index:0"
|
|
11
|
+
const result = { raw: description };
|
|
12
|
+
const parts = description.split('|');
|
|
13
|
+
for (const part of parts) {
|
|
14
|
+
const colonIdx = part.indexOf(':');
|
|
15
|
+
if (colonIdx === -1)
|
|
16
|
+
continue;
|
|
17
|
+
const key = part.slice(0, colonIdx);
|
|
18
|
+
const value = part.slice(colonIdx + 1);
|
|
19
|
+
switch (key) {
|
|
20
|
+
case 'class':
|
|
21
|
+
result.class = value;
|
|
22
|
+
break;
|
|
23
|
+
case 'underlying':
|
|
24
|
+
result.underlying = value;
|
|
25
|
+
break;
|
|
26
|
+
case 'expiry': {
|
|
27
|
+
result.expiry = value;
|
|
28
|
+
result.expiryDate = parseExpiryDate(value);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case 'targetPrice':
|
|
32
|
+
result.targetPrice = value;
|
|
33
|
+
break;
|
|
34
|
+
case 'priceThresholds':
|
|
35
|
+
result.priceThresholds = value.split(',');
|
|
36
|
+
break;
|
|
37
|
+
case 'period':
|
|
38
|
+
result.period = value;
|
|
39
|
+
break;
|
|
40
|
+
case 'index':
|
|
41
|
+
result.index = value;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse Hyperliquid's expiry format "YYYYMMDD-HHmm" into a Date.
|
|
49
|
+
* Example: "20260509-0600" -> 2026-05-09T06:00:00Z
|
|
50
|
+
*/
|
|
51
|
+
function parseExpiryDate(expiry) {
|
|
52
|
+
const match = expiry.match(/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})$/);
|
|
53
|
+
if (!match)
|
|
54
|
+
return undefined;
|
|
55
|
+
const [, year, month, day, hour, minute] = match;
|
|
56
|
+
return new Date(`${year}-${month}-${day}T${hour}:${minute}:00Z`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a human-readable title from the parsed description.
|
|
60
|
+
*/
|
|
61
|
+
function buildTitle(rawName, parsed, questionParsed) {
|
|
62
|
+
// priceBinary: "BTC > $79,583 @ May 9, 2026"
|
|
63
|
+
if (parsed.class === 'priceBinary' && parsed.underlying && parsed.targetPrice) {
|
|
64
|
+
const price = Number(parsed.targetPrice).toLocaleString('en-US');
|
|
65
|
+
const dateStr = parsed.expiryDate
|
|
66
|
+
? parsed.expiryDate.toISOString().replace('T', ' ').replace(':00.000Z', ' UTC')
|
|
67
|
+
: '';
|
|
68
|
+
return `${parsed.underlying} > $${price}${dateStr ? ` @ ${dateStr}` : ''}`;
|
|
69
|
+
}
|
|
70
|
+
// Named outcome in a priceBucket question: use thresholds from question
|
|
71
|
+
if (parsed.index !== undefined && questionParsed?.priceThresholds && questionParsed.underlying) {
|
|
72
|
+
const idx = parseInt(parsed.index, 10);
|
|
73
|
+
const thresholds = questionParsed.priceThresholds;
|
|
74
|
+
const underlying = questionParsed.underlying;
|
|
75
|
+
if (idx === 0 && thresholds.length > 0) {
|
|
76
|
+
return `${underlying} < $${Number(thresholds[0]).toLocaleString('en-US')}`;
|
|
77
|
+
}
|
|
78
|
+
if (idx === thresholds.length) {
|
|
79
|
+
return `${underlying} > $${Number(thresholds[thresholds.length - 1]).toLocaleString('en-US')}`;
|
|
80
|
+
}
|
|
81
|
+
if (idx > 0 && idx <= thresholds.length) {
|
|
82
|
+
const lo = Number(thresholds[idx - 1]).toLocaleString('en-US');
|
|
83
|
+
const hi = Number(thresholds[idx]).toLocaleString('en-US');
|
|
84
|
+
return `${underlying} $${lo} - $${hi}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return rawName;
|
|
88
|
+
}
|
|
89
|
+
function buildMarketUrl(outcomeId) {
|
|
90
|
+
return `https://app.hyperliquid.xyz/trade/${encodeURIComponent(`#${outcomeId * 10}`)}`;
|
|
91
|
+
}
|
|
92
|
+
function buildEventUrl(questionId) {
|
|
93
|
+
return `https://app.hyperliquid.xyz/trade/${encodeURIComponent(`#${questionId}`)}`;
|
|
94
|
+
}
|
|
95
|
+
function isOutcomeCoin(coin) {
|
|
96
|
+
return coin.startsWith('#');
|
|
97
|
+
}
|
|
98
|
+
// ----------------------------------------------------------------------------
|
|
99
|
+
// Normalizer
|
|
100
|
+
// ----------------------------------------------------------------------------
|
|
101
|
+
class HyperliquidNormalizer {
|
|
102
|
+
normalizeMarket(raw) {
|
|
103
|
+
if (!raw || !raw.outcome)
|
|
104
|
+
return null;
|
|
105
|
+
const outcome = raw.outcome;
|
|
106
|
+
const outcomeId = outcome.outcome;
|
|
107
|
+
const parsed = parseDescription(outcome.description);
|
|
108
|
+
// Also parse the question description for context (e.g. priceThresholds)
|
|
109
|
+
const questionParsed = raw.question
|
|
110
|
+
? parseDescription(raw.question.description)
|
|
111
|
+
: undefined;
|
|
112
|
+
const midPrice = raw.midPrice ? parseFloat(raw.midPrice) : 0.5;
|
|
113
|
+
const yesPrice = Math.max(0, Math.min(1, midPrice));
|
|
114
|
+
const noPrice = Math.max(0, Math.min(1, 1 - midPrice));
|
|
115
|
+
const outcomes = [];
|
|
116
|
+
for (const side of outcome.sideSpecs) {
|
|
117
|
+
const sideKey = side.name.toLowerCase() === 'yes' ? 'yes' : 'no';
|
|
118
|
+
outcomes.push({
|
|
119
|
+
outcomeId: (0, utils_1.toOutcomeId)(outcomeId, sideKey),
|
|
120
|
+
marketId: (0, utils_1.toMarketId)(outcomeId),
|
|
121
|
+
label: side.name,
|
|
122
|
+
price: sideKey === 'yes' ? yesPrice : noPrice,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// If no sideSpecs provided, default to Yes/No
|
|
126
|
+
if (outcomes.length === 0) {
|
|
127
|
+
outcomes.push({
|
|
128
|
+
outcomeId: (0, utils_1.toOutcomeId)(outcomeId, 'yes'),
|
|
129
|
+
marketId: (0, utils_1.toMarketId)(outcomeId),
|
|
130
|
+
label: 'Yes',
|
|
131
|
+
price: yesPrice,
|
|
132
|
+
}, {
|
|
133
|
+
outcomeId: (0, utils_1.toOutcomeId)(outcomeId, 'no'),
|
|
134
|
+
marketId: (0, utils_1.toMarketId)(outcomeId),
|
|
135
|
+
label: 'No',
|
|
136
|
+
price: noPrice,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// Resolution date: prefer outcome-level expiry, fall back to question-level
|
|
140
|
+
const expiryDate = parsed.expiryDate
|
|
141
|
+
?? questionParsed?.expiryDate
|
|
142
|
+
?? undefined;
|
|
143
|
+
// Build a descriptive title from the parsed description
|
|
144
|
+
const title = buildTitle(outcome.name, parsed, questionParsed);
|
|
145
|
+
// Derive underlying from outcome or question
|
|
146
|
+
const underlying = parsed.underlying ?? questionParsed?.underlying;
|
|
147
|
+
const tags = [];
|
|
148
|
+
if (underlying) {
|
|
149
|
+
tags.push(underlying);
|
|
150
|
+
}
|
|
151
|
+
tags.push('Outcome Markets');
|
|
152
|
+
const category = underlying ? 'Crypto' : undefined;
|
|
153
|
+
const um = {
|
|
154
|
+
marketId: (0, utils_1.toMarketId)(outcomeId),
|
|
155
|
+
eventId: raw.question ? String(raw.question.question) : undefined,
|
|
156
|
+
title,
|
|
157
|
+
description: outcome.description,
|
|
158
|
+
slug: `hl-${outcomeId}`,
|
|
159
|
+
outcomes,
|
|
160
|
+
resolutionDate: expiryDate ?? new Date(0),
|
|
161
|
+
volume24h: 0,
|
|
162
|
+
liquidity: 0,
|
|
163
|
+
url: buildMarketUrl(outcomeId),
|
|
164
|
+
category,
|
|
165
|
+
tags,
|
|
166
|
+
tickSize: 0.001,
|
|
167
|
+
status: 'active',
|
|
168
|
+
};
|
|
169
|
+
(0, market_utils_1.addBinaryOutcomes)(um);
|
|
170
|
+
return um;
|
|
171
|
+
}
|
|
172
|
+
normalizeEvent(raw) {
|
|
173
|
+
if (!raw)
|
|
174
|
+
return null;
|
|
175
|
+
const parsed = parseDescription(raw.description);
|
|
176
|
+
const underlying = parsed.underlying;
|
|
177
|
+
// Build a more descriptive event title
|
|
178
|
+
let title = raw.name;
|
|
179
|
+
if (parsed.class === 'priceBucket' && underlying && parsed.priceThresholds) {
|
|
180
|
+
const thresholds = parsed.priceThresholds.map(t => `$${Number(t).toLocaleString('en-US')}`).join(', ');
|
|
181
|
+
const dateStr = parsed.expiryDate
|
|
182
|
+
? parsed.expiryDate.toISOString().replace('T', ' ').replace(':00.000Z', ' UTC')
|
|
183
|
+
: '';
|
|
184
|
+
title = `${underlying} Price Bucket [${thresholds}]${dateStr ? ` @ ${dateStr}` : ''}`;
|
|
185
|
+
}
|
|
186
|
+
const tags = ['Outcome Markets'];
|
|
187
|
+
if (underlying)
|
|
188
|
+
tags.push(underlying);
|
|
189
|
+
return {
|
|
190
|
+
id: String(raw.question),
|
|
191
|
+
title,
|
|
192
|
+
description: raw.description,
|
|
193
|
+
slug: `hl-question-${raw.question}`,
|
|
194
|
+
markets: [],
|
|
195
|
+
volume24h: 0,
|
|
196
|
+
url: buildEventUrl(raw.question),
|
|
197
|
+
category: underlying ? 'Crypto' : undefined,
|
|
198
|
+
tags,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
normalizeEventWithMarkets(raw, outcomeMeta, mids) {
|
|
202
|
+
const event = this.normalizeEvent(raw);
|
|
203
|
+
if (!event)
|
|
204
|
+
return null;
|
|
205
|
+
const markets = [];
|
|
206
|
+
for (const outcomeId of raw.namedOutcomes) {
|
|
207
|
+
if (raw.settledNamedOutcomes.includes(outcomeId))
|
|
208
|
+
continue;
|
|
209
|
+
const outcome = outcomeMeta.outcomes.find(o => o.outcome === outcomeId);
|
|
210
|
+
if (!outcome)
|
|
211
|
+
continue;
|
|
212
|
+
const yesCoin = (0, utils_1.toCoinNotation)(outcomeId, 'yes');
|
|
213
|
+
const midPrice = mids[yesCoin];
|
|
214
|
+
const market = this.normalizeMarket({
|
|
215
|
+
outcome,
|
|
216
|
+
question: raw,
|
|
217
|
+
midPrice,
|
|
218
|
+
});
|
|
219
|
+
if (market) {
|
|
220
|
+
markets.push(market);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
...event,
|
|
225
|
+
markets,
|
|
226
|
+
volume24h: markets.reduce((sum, m) => sum + m.volume24h, 0),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
normalizeOrderBook(raw, _id) {
|
|
230
|
+
const [rawBids, rawAsks] = raw.levels;
|
|
231
|
+
const bids = rawBids.map(level => ({
|
|
232
|
+
price: parseFloat(level.px),
|
|
233
|
+
size: parseFloat(level.sz),
|
|
234
|
+
}));
|
|
235
|
+
const asks = rawAsks.map(level => ({
|
|
236
|
+
price: parseFloat(level.px),
|
|
237
|
+
size: parseFloat(level.sz),
|
|
238
|
+
}));
|
|
239
|
+
return {
|
|
240
|
+
bids: [...bids].sort((a, b) => b.price - a.price),
|
|
241
|
+
asks: [...asks].sort((a, b) => a.price - b.price),
|
|
242
|
+
timestamp: raw.time,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
normalizeOHLCV(raw, _params) {
|
|
246
|
+
return raw.map(candle => ({
|
|
247
|
+
timestamp: candle.t,
|
|
248
|
+
open: parseFloat(candle.o),
|
|
249
|
+
high: parseFloat(candle.h),
|
|
250
|
+
low: parseFloat(candle.l),
|
|
251
|
+
close: parseFloat(candle.c),
|
|
252
|
+
volume: parseFloat(candle.v),
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
normalizeTrade(raw, _index) {
|
|
256
|
+
return {
|
|
257
|
+
id: String(raw.tid),
|
|
258
|
+
timestamp: raw.time,
|
|
259
|
+
price: parseFloat(raw.px),
|
|
260
|
+
amount: parseFloat(raw.sz),
|
|
261
|
+
side: raw.side === 'B' ? 'buy' : raw.side === 'A' ? 'sell' : 'unknown',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
normalizeUserTrade(raw, _index) {
|
|
265
|
+
return {
|
|
266
|
+
id: String(raw.tid),
|
|
267
|
+
timestamp: raw.time,
|
|
268
|
+
price: parseFloat(raw.px),
|
|
269
|
+
amount: parseFloat(raw.sz),
|
|
270
|
+
side: raw.side === 'B' ? 'buy' : raw.side === 'A' ? 'sell' : 'unknown',
|
|
271
|
+
orderId: String(raw.oid),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
normalizeOpenOrder(raw) {
|
|
275
|
+
const origSz = parseFloat(raw.origSz);
|
|
276
|
+
const currentSz = parseFloat(raw.sz);
|
|
277
|
+
return {
|
|
278
|
+
id: String(raw.oid),
|
|
279
|
+
marketId: this.coinToMarketId(raw.coin),
|
|
280
|
+
outcomeId: this.coinToOutcomeId(raw.coin),
|
|
281
|
+
side: raw.side === 'B' ? 'buy' : 'sell',
|
|
282
|
+
type: 'limit',
|
|
283
|
+
price: parseFloat(raw.limitPx),
|
|
284
|
+
amount: origSz,
|
|
285
|
+
status: currentSz < origSz ? 'open' : 'pending',
|
|
286
|
+
filled: origSz - currentSz,
|
|
287
|
+
remaining: currentSz,
|
|
288
|
+
timestamp: raw.timestamp,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
normalizePosition(raw) {
|
|
292
|
+
if (!isOutcomeCoin(raw.coin)) {
|
|
293
|
+
return {
|
|
294
|
+
marketId: raw.coin,
|
|
295
|
+
outcomeId: raw.coin,
|
|
296
|
+
outcomeLabel: raw.coin,
|
|
297
|
+
size: parseFloat(raw.szi),
|
|
298
|
+
entryPrice: raw.entryPx ? parseFloat(raw.entryPx) : 0,
|
|
299
|
+
currentPrice: 0,
|
|
300
|
+
unrealizedPnL: parseFloat(raw.unrealizedPnl),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
marketId: this.coinToMarketId(raw.coin),
|
|
305
|
+
outcomeId: this.coinToOutcomeId(raw.coin),
|
|
306
|
+
outcomeLabel: raw.coin,
|
|
307
|
+
size: parseFloat(raw.szi),
|
|
308
|
+
entryPrice: raw.entryPx ? parseFloat(raw.entryPx) : 0,
|
|
309
|
+
currentPrice: 0,
|
|
310
|
+
unrealizedPnL: parseFloat(raw.unrealizedPnl),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
normalizeBalance(raw) {
|
|
314
|
+
const summary = raw.crossMarginSummary;
|
|
315
|
+
const total = parseFloat(summary.accountValue);
|
|
316
|
+
const locked = parseFloat(summary.totalMarginUsed);
|
|
317
|
+
return [{
|
|
318
|
+
currency: 'USDH',
|
|
319
|
+
total,
|
|
320
|
+
available: total - locked,
|
|
321
|
+
locked,
|
|
322
|
+
}];
|
|
323
|
+
}
|
|
324
|
+
// -- Private helpers -------------------------------------------------------
|
|
325
|
+
coinToMarketId(coin) {
|
|
326
|
+
if (!coin.startsWith('#'))
|
|
327
|
+
return coin;
|
|
328
|
+
const encoding = parseInt(coin.slice(1), 10);
|
|
329
|
+
const { outcomeId } = (0, utils_1.decodeAssetId)(config_1.OUTCOME_ASSET_BASE + encoding);
|
|
330
|
+
return (0, utils_1.toMarketId)(outcomeId);
|
|
331
|
+
}
|
|
332
|
+
coinToOutcomeId(coin) {
|
|
333
|
+
if (!coin.startsWith('#'))
|
|
334
|
+
return coin;
|
|
335
|
+
const encoding = parseInt(coin.slice(1), 10);
|
|
336
|
+
return String(config_1.OUTCOME_ASSET_BASE + encoding);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
exports.HyperliquidNormalizer = HyperliquidNormalizer;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode an outcome ID and side into a Hyperliquid asset ID.
|
|
3
|
+
*
|
|
4
|
+
* Formula: 100_000_000 + (10 * outcomeId) + side
|
|
5
|
+
* where side = 0 (Yes) or 1 (No).
|
|
6
|
+
*/
|
|
7
|
+
export declare function encodeAssetId(outcomeId: number, side: 'yes' | 'no'): number;
|
|
8
|
+
/**
|
|
9
|
+
* Decode a Hyperliquid asset ID back into outcome ID and side.
|
|
10
|
+
*/
|
|
11
|
+
export declare function decodeAssetId(assetId: number): {
|
|
12
|
+
outcomeId: number;
|
|
13
|
+
side: 'yes' | 'no';
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Convert an outcome ID and side to the Hyperliquid coin notation (#encoding).
|
|
17
|
+
*
|
|
18
|
+
* Example: outcome 1, Yes -> "#10"
|
|
19
|
+
*/
|
|
20
|
+
export declare function toCoinNotation(outcomeId: number, side: 'yes' | 'no'): string;
|
|
21
|
+
/**
|
|
22
|
+
* Convert an asset encoding (the number after #) to outcome ID and side.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fromCoinEncoding(encoding: number): {
|
|
25
|
+
outcomeId: number;
|
|
26
|
+
side: 'yes' | 'no';
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Build a unique market ID string from an outcome.
|
|
30
|
+
* We use "hl-outcome-{outcomeId}" as the market ID.
|
|
31
|
+
*/
|
|
32
|
+
export declare function toMarketId(outcomeId: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* Extract the numeric outcome ID from our market ID format.
|
|
35
|
+
*/
|
|
36
|
+
export declare function fromMarketId(marketId: string): number;
|
|
37
|
+
/**
|
|
38
|
+
* Build an outcome ID string for the unified type.
|
|
39
|
+
* Format: "{assetId}" which is the full numeric asset ID.
|
|
40
|
+
*/
|
|
41
|
+
export declare function toOutcomeId(outcomeId: number, side: 'yes' | 'no'): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encodeAssetId = encodeAssetId;
|
|
4
|
+
exports.decodeAssetId = decodeAssetId;
|
|
5
|
+
exports.toCoinNotation = toCoinNotation;
|
|
6
|
+
exports.fromCoinEncoding = fromCoinEncoding;
|
|
7
|
+
exports.toMarketId = toMarketId;
|
|
8
|
+
exports.fromMarketId = fromMarketId;
|
|
9
|
+
exports.toOutcomeId = toOutcomeId;
|
|
10
|
+
const config_1 = require("./config");
|
|
11
|
+
/**
|
|
12
|
+
* Encode an outcome ID and side into a Hyperliquid asset ID.
|
|
13
|
+
*
|
|
14
|
+
* Formula: 100_000_000 + (10 * outcomeId) + side
|
|
15
|
+
* where side = 0 (Yes) or 1 (No).
|
|
16
|
+
*/
|
|
17
|
+
function encodeAssetId(outcomeId, side) {
|
|
18
|
+
const sideValue = side === 'yes' ? config_1.SIDE_YES : config_1.SIDE_NO;
|
|
19
|
+
return config_1.OUTCOME_ASSET_BASE + (config_1.OUTCOME_MULTIPLIER * outcomeId) + sideValue;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decode a Hyperliquid asset ID back into outcome ID and side.
|
|
23
|
+
*/
|
|
24
|
+
function decodeAssetId(assetId) {
|
|
25
|
+
const offset = assetId - config_1.OUTCOME_ASSET_BASE;
|
|
26
|
+
const sideValue = offset % config_1.OUTCOME_MULTIPLIER;
|
|
27
|
+
const outcomeId = (offset - sideValue) / config_1.OUTCOME_MULTIPLIER;
|
|
28
|
+
return {
|
|
29
|
+
outcomeId,
|
|
30
|
+
side: sideValue === config_1.SIDE_YES ? 'yes' : 'no',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Convert an outcome ID and side to the Hyperliquid coin notation (#encoding).
|
|
35
|
+
*
|
|
36
|
+
* Example: outcome 1, Yes -> "#10"
|
|
37
|
+
*/
|
|
38
|
+
function toCoinNotation(outcomeId, side) {
|
|
39
|
+
const encoding = config_1.OUTCOME_MULTIPLIER * outcomeId + (side === 'yes' ? config_1.SIDE_YES : config_1.SIDE_NO);
|
|
40
|
+
return `#${encoding}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Convert an asset encoding (the number after #) to outcome ID and side.
|
|
44
|
+
*/
|
|
45
|
+
function fromCoinEncoding(encoding) {
|
|
46
|
+
const sideValue = encoding % config_1.OUTCOME_MULTIPLIER;
|
|
47
|
+
const outcomeId = (encoding - sideValue) / config_1.OUTCOME_MULTIPLIER;
|
|
48
|
+
return {
|
|
49
|
+
outcomeId,
|
|
50
|
+
side: sideValue === config_1.SIDE_YES ? 'yes' : 'no',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build a unique market ID string from an outcome.
|
|
55
|
+
* We use "hl-outcome-{outcomeId}" as the market ID.
|
|
56
|
+
*/
|
|
57
|
+
function toMarketId(outcomeId) {
|
|
58
|
+
return `hl-outcome-${outcomeId}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract the numeric outcome ID from our market ID format.
|
|
62
|
+
*/
|
|
63
|
+
function fromMarketId(marketId) {
|
|
64
|
+
const match = marketId.match(/^hl-outcome-(\d+)$/);
|
|
65
|
+
if (!match) {
|
|
66
|
+
throw new Error(`Invalid Hyperliquid market ID: ${marketId}`);
|
|
67
|
+
}
|
|
68
|
+
return parseInt(match[1], 10);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build an outcome ID string for the unified type.
|
|
72
|
+
* Format: "{assetId}" which is the full numeric asset ID.
|
|
73
|
+
*/
|
|
74
|
+
function toOutcomeId(outcomeId, side) {
|
|
75
|
+
return String(encodeAssetId(outcomeId, side));
|
|
76
|
+
}
|