pmxt-core 2.24.0 → 2.25.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.
Files changed (44) hide show
  1. package/dist/exchanges/kalshi/api.d.ts +1 -1
  2. package/dist/exchanges/kalshi/api.js +1 -1
  3. package/dist/exchanges/limitless/api.d.ts +1 -1
  4. package/dist/exchanges/limitless/api.js +1 -1
  5. package/dist/exchanges/myriad/api.d.ts +1 -1
  6. package/dist/exchanges/myriad/api.js +1 -1
  7. package/dist/exchanges/opinion/api.d.ts +1 -1
  8. package/dist/exchanges/opinion/api.js +1 -1
  9. package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
  10. package/dist/exchanges/polymarket/api-clob.js +1 -1
  11. package/dist/exchanges/polymarket/api-data.d.ts +1 -1
  12. package/dist/exchanges/polymarket/api-data.js +1 -1
  13. package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
  14. package/dist/exchanges/polymarket/api-gamma.js +1 -1
  15. package/dist/exchanges/polymarket/utils.js +17 -1
  16. package/dist/exchanges/polymarket_us/config.d.ts +18 -0
  17. package/dist/exchanges/polymarket_us/config.js +22 -0
  18. package/dist/exchanges/polymarket_us/errors.d.ts +19 -0
  19. package/dist/exchanges/polymarket_us/errors.js +123 -0
  20. package/dist/exchanges/polymarket_us/errors.test.d.ts +1 -0
  21. package/dist/exchanges/polymarket_us/errors.test.js +54 -0
  22. package/dist/exchanges/polymarket_us/index.d.ts +90 -0
  23. package/dist/exchanges/polymarket_us/index.js +366 -0
  24. package/dist/exchanges/polymarket_us/index.test.d.ts +8 -0
  25. package/dist/exchanges/polymarket_us/index.test.js +237 -0
  26. package/dist/exchanges/polymarket_us/normalizer.d.ts +55 -0
  27. package/dist/exchanges/polymarket_us/normalizer.js +385 -0
  28. package/dist/exchanges/polymarket_us/normalizer.test.d.ts +1 -0
  29. package/dist/exchanges/polymarket_us/normalizer.test.js +224 -0
  30. package/dist/exchanges/polymarket_us/price.d.ts +94 -0
  31. package/dist/exchanges/polymarket_us/price.js +149 -0
  32. package/dist/exchanges/polymarket_us/price.test.d.ts +1 -0
  33. package/dist/exchanges/polymarket_us/price.test.js +131 -0
  34. package/dist/exchanges/polymarket_us/websocket.d.ts +39 -0
  35. package/dist/exchanges/polymarket_us/websocket.js +181 -0
  36. package/dist/exchanges/polymarket_us/websocket.test.d.ts +8 -0
  37. package/dist/exchanges/polymarket_us/websocket.test.js +162 -0
  38. package/dist/exchanges/probable/api.d.ts +1 -1
  39. package/dist/exchanges/probable/api.js +1 -1
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.js +5 -1
  42. package/dist/server/app.js +6 -0
  43. package/dist/types.d.ts +4 -0
  44. package/package.json +4 -3
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PolymarketUSNormalizer = void 0;
4
+ const market_utils_1 = require("../../utils/market-utils");
5
+ const price_1 = require("./price");
6
+ // ----------------------------------------------------------------------------
7
+ // Helpers
8
+ // ----------------------------------------------------------------------------
9
+ const POLYMARKET_US_BASE_URL = 'https://polymarket.us';
10
+ function buildMarketUrl(slug) {
11
+ return `${POLYMARKET_US_BASE_URL}/market/${slug}`;
12
+ }
13
+ function buildEventUrl(slug) {
14
+ return `${POLYMARKET_US_BASE_URL}/event/${slug}`;
15
+ }
16
+ /**
17
+ * Parse a stringified price from the gateway. Returns `undefined` when
18
+ * the input is missing, empty, or non-numeric so downstream logic can
19
+ * distinguish "no quote" from a legitimate 0.
20
+ */
21
+ function parsePriceString(value) {
22
+ if (value == null || value === '')
23
+ return undefined;
24
+ const n = parseFloat(value);
25
+ return Number.isFinite(n) ? n : undefined;
26
+ }
27
+ function buildBinaryOutcomes(slug, longPrice, shortPrice) {
28
+ return [
29
+ {
30
+ outcomeId: `${slug}:long`,
31
+ marketId: slug,
32
+ label: 'long',
33
+ price: longPrice,
34
+ },
35
+ {
36
+ outcomeId: `${slug}:short`,
37
+ marketId: slug,
38
+ label: 'short',
39
+ price: shortPrice,
40
+ },
41
+ ];
42
+ }
43
+ /**
44
+ * Build binary outcomes from the gateway's `marketSides[]` array. The side
45
+ * ordering is normalized to [long, short] regardless of input order. Human
46
+ * labels from `description` (e.g. team names) are stashed under
47
+ * `metadata.sideDescription`; `label` is kept canonical ("long" / "short")
48
+ * so downstream helpers like `addBinaryOutcomes` continue to work.
49
+ *
50
+ * Prices are sourced with this precedence:
51
+ * 1. `marketSides[i].price` (per-side price on the gateway payload)
52
+ * 2. `outcomePrices[0|1]` (legacy 2-element fallback)
53
+ * 3. 0 (no quote available)
54
+ *
55
+ * The short-side price is derived as `1 - longPrice` when only the long
56
+ * side is quoted (and vice versa), since Polymarket US is fully binary.
57
+ */
58
+ function buildOutcomes(slug, sides, outcomePrices) {
59
+ let longPrice;
60
+ let shortPrice;
61
+ let longDescription;
62
+ let shortDescription;
63
+ if (sides && sides.length > 0) {
64
+ const longSide = sides.find(s => s.long === true);
65
+ const shortSide = sides.find(s => s.long === false);
66
+ if (longSide) {
67
+ longPrice = parsePriceString(longSide.price);
68
+ longDescription = longSide.description;
69
+ }
70
+ if (shortSide) {
71
+ shortPrice = parsePriceString(shortSide.price);
72
+ shortDescription = shortSide.description;
73
+ }
74
+ }
75
+ if (longPrice === undefined && outcomePrices && outcomePrices.length >= 1) {
76
+ longPrice = parsePriceString(outcomePrices[0]);
77
+ }
78
+ if (shortPrice === undefined && outcomePrices && outcomePrices.length >= 2) {
79
+ shortPrice = parsePriceString(outcomePrices[1]);
80
+ }
81
+ // Fill in the complementary side from the binary identity when only
82
+ // one side was quoted.
83
+ if (longPrice !== undefined && shortPrice === undefined) {
84
+ shortPrice = 1 - longPrice;
85
+ }
86
+ if (shortPrice !== undefined && longPrice === undefined) {
87
+ longPrice = 1 - shortPrice;
88
+ }
89
+ const outcomes = buildBinaryOutcomes(slug, longPrice ?? 0, shortPrice ?? 0);
90
+ if (longDescription) {
91
+ outcomes[0].metadata = { sideDescription: longDescription };
92
+ }
93
+ if (shortDescription) {
94
+ outcomes[1].metadata = { sideDescription: shortDescription };
95
+ }
96
+ return outcomes;
97
+ }
98
+ /**
99
+ * Accepts the `tags` field in whatever shape the gateway returns it
100
+ * (string[], array of {label|name|slug} objects, or undefined) and
101
+ * produces a flat `string[]`.
102
+ */
103
+ function coerceTags(raw) {
104
+ if (!Array.isArray(raw))
105
+ return [];
106
+ const out = [];
107
+ for (const item of raw) {
108
+ if (typeof item === 'string') {
109
+ if (item)
110
+ out.push(item);
111
+ }
112
+ else if (item && typeof item === 'object') {
113
+ const obj = item;
114
+ const label = (typeof obj.label === 'string' && obj.label) ||
115
+ (typeof obj.name === 'string' && obj.name) ||
116
+ (typeof obj.slug === 'string' && obj.slug) ||
117
+ '';
118
+ if (label)
119
+ out.push(label);
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ function parseTimeToMs(value) {
125
+ if (!value)
126
+ return 0;
127
+ const ms = new Date(value).getTime();
128
+ return Number.isFinite(ms) ? ms : 0;
129
+ }
130
+ function intentToSide(intent) {
131
+ switch (intent) {
132
+ case 'ORDER_INTENT_BUY_LONG':
133
+ case 'ORDER_INTENT_BUY_SHORT':
134
+ return 'buy';
135
+ case 'ORDER_INTENT_SELL_LONG':
136
+ case 'ORDER_INTENT_SELL_SHORT':
137
+ return 'sell';
138
+ default:
139
+ return 'buy';
140
+ }
141
+ }
142
+ function intentToOutcomeId(intent, slug) {
143
+ switch (intent) {
144
+ case 'ORDER_INTENT_BUY_LONG':
145
+ case 'ORDER_INTENT_SELL_LONG':
146
+ return `${slug}:long`;
147
+ case 'ORDER_INTENT_BUY_SHORT':
148
+ case 'ORDER_INTENT_SELL_SHORT':
149
+ return `${slug}:short`;
150
+ default:
151
+ return `${slug}:long`;
152
+ }
153
+ }
154
+ function mapOrderType(type) {
155
+ return type === 'ORDER_TYPE_MARKET' ? 'market' : 'limit';
156
+ }
157
+ // PMXT Order.status values: 'pending' | 'open' | 'filled' | 'cancelled' | 'rejected'
158
+ // Note: PMXT has no 'expired' status; expired orders are mapped to 'cancelled'.
159
+ function mapOrderStatus(state) {
160
+ switch (state) {
161
+ case 'ORDER_STATE_FILLED':
162
+ return 'filled';
163
+ case 'ORDER_STATE_CANCELED':
164
+ case 'ORDER_STATE_EXPIRED':
165
+ return 'cancelled';
166
+ case 'ORDER_STATE_REJECTED':
167
+ return 'rejected';
168
+ case 'ORDER_STATE_NEW':
169
+ case 'ORDER_STATE_PENDING_NEW':
170
+ case 'ORDER_STATE_PENDING_REPLACE':
171
+ case 'ORDER_STATE_PENDING_CANCEL':
172
+ case 'ORDER_STATE_PENDING_RISK':
173
+ case 'ORDER_STATE_PARTIALLY_FILLED':
174
+ case 'ORDER_STATE_REPLACED':
175
+ default:
176
+ return 'open';
177
+ }
178
+ }
179
+ // ----------------------------------------------------------------------------
180
+ // Normalizer
181
+ // ----------------------------------------------------------------------------
182
+ class PolymarketUSNormalizer {
183
+ /**
184
+ * Normalize a single MarketDetail into a UnifiedMarket.
185
+ * The slug is the canonical PMXT marketId for Polymarket US.
186
+ */
187
+ normalizeMarket(detail) {
188
+ const real = detail;
189
+ return this.buildUnifiedMarket(real);
190
+ }
191
+ /**
192
+ * Flatten an SDK Event's markets into UnifiedMarkets.
193
+ * Each market inherits the parent event's metadata.
194
+ */
195
+ normalizeMarketsFromEvent(event) {
196
+ const realEvent = event;
197
+ const nested = realEvent.markets;
198
+ if (!nested)
199
+ return [];
200
+ const parentTags = coerceTags(realEvent.tags);
201
+ const parentCategory = realEvent.category || (parentTags.length > 0 ? parentTags[0] : '');
202
+ const parentEndDate = realEvent.endDate || realEvent.endTime;
203
+ return nested.map(m => this.buildUnifiedMarket(m, {
204
+ eventSlug: realEvent.slug,
205
+ category: parentCategory,
206
+ tags: parentTags,
207
+ endDate: parentEndDate,
208
+ }));
209
+ }
210
+ /**
211
+ * Normalize an SDK Event into a UnifiedEvent.
212
+ */
213
+ normalizeEvent(event) {
214
+ const real = event;
215
+ const markets = this.normalizeMarketsFromEvent(event);
216
+ const tags = coerceTags(real.tags);
217
+ const category = real.category || (tags.length > 0 ? tags[0] : '');
218
+ return {
219
+ id: real.slug,
220
+ title: real.title || '',
221
+ description: real.description || '',
222
+ slug: real.slug,
223
+ markets,
224
+ volume24h: 0,
225
+ volume: real.volume,
226
+ url: buildEventUrl(real.slug),
227
+ category,
228
+ tags,
229
+ };
230
+ }
231
+ /**
232
+ * Internal helper that builds a UnifiedMarket from the gateway's real
233
+ * runtime shape. Handles inheritance from a parent event when the market
234
+ * is nested (fields like category/tags/endDate fall back to the parent).
235
+ */
236
+ buildUnifiedMarket(market, parent) {
237
+ const slug = market.slug;
238
+ const title = market.question || market.title || slug;
239
+ const tags = coerceTags(market.tags);
240
+ const effectiveTags = tags.length > 0 ? tags : (parent?.tags || []);
241
+ const category = market.category || parent?.category || (effectiveTags.length > 0 ? effectiveTags[0] : '');
242
+ const endDate = market.endDate || parent?.endDate;
243
+ const resolutionDate = endDate ? new Date(endDate) : new Date(0);
244
+ const outcomes = buildOutcomes(slug, market.marketSides, market.outcomePrices);
245
+ const um = {
246
+ marketId: slug,
247
+ eventId: market.eventSlug || parent?.eventSlug,
248
+ title,
249
+ description: market.description || '',
250
+ slug,
251
+ outcomes,
252
+ resolutionDate,
253
+ volume24h: 0,
254
+ volume: market.volume,
255
+ liquidity: market.liquidity ?? 0,
256
+ url: buildMarketUrl(slug),
257
+ category,
258
+ tags: [...effectiveTags],
259
+ tickSize: typeof market.orderPriceMinTickSize === 'number'
260
+ ? market.orderPriceMinTickSize
261
+ : undefined,
262
+ };
263
+ (0, market_utils_1.addBinaryOutcomes)(um);
264
+ return um;
265
+ }
266
+ /**
267
+ * Normalize a MarketBook into a PMXT OrderBook.
268
+ *
269
+ * IMPORTANT: Polymarket US books are quoted in long-side prices. PMXT
270
+ * exposes the book as the LONG side directly:
271
+ * - bids: levels where someone is bidding to BUY LONG (price = fromAmount(level.px))
272
+ * - asks: levels where someone is offering to SELL LONG (price = fromAmount(level.px))
273
+ * The implicit short-side book is `1 - longPrice` for each level. Callers
274
+ * needing the short-side view must invert prices themselves.
275
+ */
276
+ normalizeOrderBook(book, _marketId) {
277
+ const bids = (book.bids || []).map(level => ({
278
+ price: (0, price_1.fromAmount)(level.px),
279
+ size: parseFloat(level.qty || '0'),
280
+ }));
281
+ const asks = (book.offers || []).map(level => ({
282
+ price: (0, price_1.fromAmount)(level.px),
283
+ size: parseFloat(level.qty || '0'),
284
+ }));
285
+ return {
286
+ bids,
287
+ asks,
288
+ timestamp: parseTimeToMs(book.transactTime) || Date.now(),
289
+ };
290
+ }
291
+ /**
292
+ * Normalize an SDK Order into a PMXT Order.
293
+ * Prices are converted from long-side to user-facing using the order's intent.
294
+ */
295
+ normalizeOrder(order) {
296
+ const slug = order.marketSlug;
297
+ const longPrice = (0, price_1.fromAmount)(order.price);
298
+ const userFacingPrice = (0, price_1.fromLongSidePrice)(order.intent, longPrice);
299
+ const fee = order.commissionNotionalTotalCollected
300
+ ? (0, price_1.fromAmount)(order.commissionNotionalTotalCollected)
301
+ : undefined;
302
+ const timestamp = parseTimeToMs(order.createTime) || parseTimeToMs(order.insertTime);
303
+ return {
304
+ id: order.id,
305
+ marketId: slug,
306
+ outcomeId: intentToOutcomeId(order.intent, slug),
307
+ side: intentToSide(order.intent),
308
+ type: mapOrderType(order.type),
309
+ price: userFacingPrice,
310
+ amount: order.quantity,
311
+ status: mapOrderStatus(order.state),
312
+ filled: order.cumQuantity,
313
+ remaining: order.leavesQuantity,
314
+ timestamp,
315
+ fee,
316
+ };
317
+ }
318
+ /**
319
+ * Normalize the SDK's positions map into an array of PMXT Positions.
320
+ * Positive netPosition -> long outcome; negative -> short outcome.
321
+ */
322
+ normalizePositions(positions) {
323
+ const results = [];
324
+ for (const slug of Object.keys(positions)) {
325
+ const pos = positions[slug];
326
+ if (!pos)
327
+ continue;
328
+ const net = parseFloat(pos.netPosition || '0');
329
+ const isLong = net >= 0;
330
+ const size = Math.abs(net);
331
+ // Approximate cost basis per share. The SDK does not expose a
332
+ // running average price; we derive it from total cost / size.
333
+ const totalCost = (0, price_1.fromAmount)(pos.cost);
334
+ const entryPrice = size > 0 ? Math.abs(totalCost) / size : 0;
335
+ results.push({
336
+ marketId: slug,
337
+ outcomeId: isLong ? `${slug}:long` : `${slug}:short`,
338
+ outcomeLabel: isLong ? 'long' : 'short',
339
+ size,
340
+ entryPrice,
341
+ // SDK does not expose live mark on the position object.
342
+ // Callers must enrich with current price if needed.
343
+ currentPrice: 0,
344
+ unrealizedPnL: 0,
345
+ realizedPnL: (0, price_1.fromAmount)(pos.realized),
346
+ });
347
+ }
348
+ return results;
349
+ }
350
+ /**
351
+ * Normalize a UserBalance into PMXT Balance[].
352
+ * Locked = total - available (clamped to >= 0).
353
+ */
354
+ normalizeBalance(balance) {
355
+ const total = balance.currentBalance;
356
+ const available = balance.buyingPower;
357
+ const locked = Math.max(0, total - available);
358
+ return [{
359
+ currency: 'USD',
360
+ total,
361
+ available,
362
+ locked,
363
+ }];
364
+ }
365
+ /**
366
+ * Normalize a single Activity into a UserTrade.
367
+ * Returns null for non-trade activities.
368
+ */
369
+ normalizeUserTradeFromActivity(activity, _index) {
370
+ if (activity.type !== 'ACTIVITY_TYPE_TRADE')
371
+ return null;
372
+ const trade = activity.trade;
373
+ if (!trade)
374
+ return null;
375
+ return {
376
+ id: trade.id,
377
+ timestamp: parseTimeToMs(trade.createTime),
378
+ price: (0, price_1.fromAmount)(trade.price),
379
+ amount: parseFloat(trade.qty || '0'),
380
+ side: 'unknown',
381
+ outcomeId: undefined,
382
+ };
383
+ }
384
+ }
385
+ exports.PolymarketUSNormalizer = PolymarketUSNormalizer;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // Mock the price helpers so this test does not depend on price.ts being
4
+ // implemented yet (it is created in parallel by another agent).
5
+ jest.mock('./price', () => ({
6
+ fromAmount: (a) => a ? parseFloat(a.value) : 0,
7
+ fromLongSidePrice: (intent, longPrice) => intent.endsWith('SHORT') ? 1 - longPrice : longPrice,
8
+ toAmount: (p) => ({ value: p.toFixed(2), currency: 'USD' }),
9
+ }));
10
+ const normalizer_1 = require("./normalizer");
11
+ function makeOrder(overrides = {}) {
12
+ return {
13
+ id: 'order-1',
14
+ marketSlug: 'btc-100k',
15
+ side: 'ORDER_SIDE_BUY',
16
+ type: 'ORDER_TYPE_LIMIT',
17
+ price: { value: '0.55', currency: 'USD' },
18
+ quantity: 10,
19
+ cumQuantity: 0,
20
+ leavesQuantity: 10,
21
+ tif: 'TIME_IN_FORCE_GOOD_TILL_CANCEL',
22
+ intent: 'ORDER_INTENT_BUY_LONG',
23
+ state: 'ORDER_STATE_NEW',
24
+ createTime: '2026-01-01T00:00:00.000Z',
25
+ ...overrides,
26
+ };
27
+ }
28
+ describe('PolymarketUSNormalizer', () => {
29
+ const normalizer = new normalizer_1.PolymarketUSNormalizer();
30
+ describe('normalizeMarket', () => {
31
+ it('uses the slug as marketId and synthesizes long/short outcomes', () => {
32
+ const detail = {
33
+ id: 1,
34
+ slug: 'btc-100k',
35
+ title: 'BTC reaches $100k',
36
+ outcome: 'yes',
37
+ description: 'Will BTC hit 100k by year end?',
38
+ active: true,
39
+ closed: false,
40
+ liquidity: 5000,
41
+ volume: 12345,
42
+ eventSlug: 'btc-events',
43
+ };
44
+ const um = normalizer.normalizeMarket(detail);
45
+ expect(um.marketId).toBe('btc-100k');
46
+ expect(um.slug).toBe('btc-100k');
47
+ expect(um.title).toBe('BTC reaches $100k');
48
+ expect(um.description).toBe('Will BTC hit 100k by year end?');
49
+ expect(um.outcomes).toHaveLength(2);
50
+ expect(um.outcomes[0].outcomeId).toBe('btc-100k:long');
51
+ expect(um.outcomes[0].label).toBe('long');
52
+ expect(um.outcomes[1].outcomeId).toBe('btc-100k:short');
53
+ expect(um.outcomes[1].label).toBe('short');
54
+ expect(um.liquidity).toBe(5000);
55
+ expect(um.volume).toBe(12345);
56
+ });
57
+ it('populates outcome prices from marketSides[].price and tickSize from orderPriceMinTickSize', () => {
58
+ const detail = {
59
+ slug: 'nfl-sf-at-kc',
60
+ question: 'SF at KC',
61
+ marketSides: [
62
+ { description: 'Kansas City Chiefs', long: true, price: '0.864' },
63
+ { description: 'San Francisco 49ers', long: false, price: '0.136' },
64
+ ],
65
+ orderPriceMinTickSize: 0.001,
66
+ };
67
+ const um = normalizer.normalizeMarket(detail);
68
+ expect(um.outcomes[0].price).toBeCloseTo(0.864);
69
+ expect(um.outcomes[0].metadata?.sideDescription).toBe('Kansas City Chiefs');
70
+ expect(um.outcomes[1].price).toBeCloseTo(0.136);
71
+ expect(um.outcomes[1].metadata?.sideDescription).toBe('San Francisco 49ers');
72
+ expect(um.tickSize).toBe(0.001);
73
+ });
74
+ it('derives the short-side price from 1 - longPrice when only the long side is quoted', () => {
75
+ const detail = {
76
+ slug: 'btc-100k',
77
+ question: 'BTC 100k',
78
+ marketSides: [
79
+ { long: true, price: '0.72' },
80
+ { long: false },
81
+ ],
82
+ };
83
+ const um = normalizer.normalizeMarket(detail);
84
+ expect(um.outcomes[0].price).toBeCloseTo(0.72);
85
+ expect(um.outcomes[1].price).toBeCloseTo(0.28);
86
+ });
87
+ it('falls back to outcomePrices[] when marketSides is missing', () => {
88
+ const detail = {
89
+ slug: 'eth-5k',
90
+ question: 'ETH 5k',
91
+ outcomePrices: ['0.30', '0.70'],
92
+ };
93
+ const um = normalizer.normalizeMarket(detail);
94
+ expect(um.outcomes[0].price).toBeCloseTo(0.30);
95
+ expect(um.outcomes[1].price).toBeCloseTo(0.70);
96
+ });
97
+ });
98
+ describe('normalizeOrderBook', () => {
99
+ it('preserves bid/offer levels with parsed numeric prices', () => {
100
+ const book = {
101
+ marketSlug: 'btc-100k',
102
+ bids: [
103
+ { px: { value: '0.55', currency: 'USD' }, qty: '100' },
104
+ { px: { value: '0.54', currency: 'USD' }, qty: '50' },
105
+ ],
106
+ offers: [
107
+ { px: { value: '0.56', currency: 'USD' }, qty: '75' },
108
+ { px: { value: '0.57', currency: 'USD' }, qty: '25' },
109
+ ],
110
+ state: 'MARKET_STATE_OPEN',
111
+ };
112
+ const ob = normalizer.normalizeOrderBook(book, 'btc-100k');
113
+ expect(ob.bids).toEqual([
114
+ { price: 0.55, size: 100 },
115
+ { price: 0.54, size: 50 },
116
+ ]);
117
+ expect(ob.asks).toEqual([
118
+ { price: 0.56, size: 75 },
119
+ { price: 0.57, size: 25 },
120
+ ]);
121
+ expect(typeof ob.timestamp).toBe('number');
122
+ });
123
+ });
124
+ describe('normalizeOrder - intent mapping', () => {
125
+ const cases = [
126
+ { intent: 'ORDER_INTENT_BUY_LONG', expectedSide: 'buy', expectedOutcomeSuffix: ':long' },
127
+ { intent: 'ORDER_INTENT_SELL_LONG', expectedSide: 'sell', expectedOutcomeSuffix: ':long' },
128
+ { intent: 'ORDER_INTENT_BUY_SHORT', expectedSide: 'buy', expectedOutcomeSuffix: ':short' },
129
+ { intent: 'ORDER_INTENT_SELL_SHORT', expectedSide: 'sell', expectedOutcomeSuffix: ':short' },
130
+ ];
131
+ for (const { intent, expectedSide, expectedOutcomeSuffix } of cases) {
132
+ it(`maps ${intent} to side=${expectedSide} and outcomeId ending in ${expectedOutcomeSuffix}`, () => {
133
+ const order = normalizer.normalizeOrder(makeOrder({ intent }));
134
+ expect(order.side).toBe(expectedSide);
135
+ expect(order.outcomeId.endsWith(expectedOutcomeSuffix)).toBe(true);
136
+ expect(order.marketId).toBe('btc-100k');
137
+ });
138
+ }
139
+ it('keeps long-side price unchanged for BUY_LONG', () => {
140
+ const order = normalizer.normalizeOrder(makeOrder({
141
+ intent: 'ORDER_INTENT_BUY_LONG',
142
+ price: { value: '0.60', currency: 'USD' },
143
+ }));
144
+ expect(order.price).toBeCloseTo(0.60);
145
+ });
146
+ it('flips long-side price to short-side for BUY_SHORT', () => {
147
+ const order = normalizer.normalizeOrder(makeOrder({
148
+ intent: 'ORDER_INTENT_BUY_SHORT',
149
+ price: { value: '0.60', currency: 'USD' },
150
+ }));
151
+ expect(order.price).toBeCloseTo(0.40);
152
+ });
153
+ });
154
+ describe('normalizeOrder - state mapping', () => {
155
+ const cases = [
156
+ { state: 'ORDER_STATE_NEW', expected: 'open' },
157
+ { state: 'ORDER_STATE_PARTIALLY_FILLED', expected: 'open' },
158
+ { state: 'ORDER_STATE_FILLED', expected: 'filled' },
159
+ { state: 'ORDER_STATE_CANCELED', expected: 'cancelled' },
160
+ { state: 'ORDER_STATE_REJECTED', expected: 'rejected' },
161
+ { state: 'ORDER_STATE_EXPIRED', expected: 'cancelled' },
162
+ ];
163
+ for (const { state, expected } of cases) {
164
+ it(`maps ${state} to ${expected}`, () => {
165
+ const order = normalizer.normalizeOrder(makeOrder({ state }));
166
+ expect(order.status).toBe(expected);
167
+ });
168
+ }
169
+ });
170
+ describe('normalizePositions', () => {
171
+ it('maps positive netPosition to long and negative to short with absolute sizes', () => {
172
+ const positions = {
173
+ 'btc-100k': {
174
+ netPosition: '15',
175
+ qtyBought: '20',
176
+ qtySold: '5',
177
+ cost: { value: '7.50', currency: 'USD' },
178
+ realized: { value: '0.00', currency: 'USD' },
179
+ bodPosition: '0',
180
+ expired: false,
181
+ },
182
+ 'eth-5k': {
183
+ netPosition: '-8',
184
+ qtyBought: '2',
185
+ qtySold: '10',
186
+ cost: { value: '4.00', currency: 'USD' },
187
+ realized: { value: '1.25', currency: 'USD' },
188
+ bodPosition: '0',
189
+ expired: false,
190
+ },
191
+ };
192
+ const result = normalizer.normalizePositions(positions);
193
+ expect(result).toHaveLength(2);
194
+ const btc = result.find(p => p.marketId === 'btc-100k');
195
+ expect(btc.outcomeId).toBe('btc-100k:long');
196
+ expect(btc.outcomeLabel).toBe('long');
197
+ expect(btc.size).toBe(15);
198
+ expect(btc.entryPrice).toBeCloseTo(0.5);
199
+ const eth = result.find(p => p.marketId === 'eth-5k');
200
+ expect(eth.outcomeId).toBe('eth-5k:short');
201
+ expect(eth.outcomeLabel).toBe('short');
202
+ expect(eth.size).toBe(8);
203
+ expect(eth.entryPrice).toBeCloseTo(0.5);
204
+ expect(eth.realizedPnL).toBeCloseTo(1.25);
205
+ });
206
+ });
207
+ describe('normalizeBalance', () => {
208
+ it('maps current/buyingPower to total/available/locked', () => {
209
+ const balance = {
210
+ currentBalance: 1000,
211
+ currency: 'USD',
212
+ buyingPower: 800,
213
+ };
214
+ const result = normalizer.normalizeBalance(balance);
215
+ expect(result).toHaveLength(1);
216
+ expect(result[0]).toEqual({
217
+ currency: 'USD',
218
+ total: 1000,
219
+ available: 800,
220
+ locked: 200,
221
+ });
222
+ });
223
+ });
224
+ });