pmxt-core 2.9.2 → 2.9.3
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/BaseExchange.d.ts +118 -4
- package/dist/BaseExchange.js +160 -7
- package/dist/exchanges/baozi/fetchEvents.js +16 -11
- package/dist/exchanges/baozi/index.d.ts +5 -0
- package/dist/exchanges/baozi/index.js +6 -0
- package/dist/exchanges/kalshi/api.d.ts +7 -1
- package/dist/exchanges/kalshi/api.js +11 -2
- package/dist/exchanges/kalshi/config.d.ts +103 -0
- package/dist/exchanges/kalshi/config.js +144 -0
- package/dist/exchanges/kalshi/fetchEvents.d.ts +2 -2
- package/dist/exchanges/kalshi/fetchEvents.js +138 -67
- package/dist/exchanges/kalshi/fetchMarkets.d.ts +2 -2
- package/dist/exchanges/kalshi/fetchMarkets.js +36 -25
- package/dist/exchanges/kalshi/fetchOHLCV.d.ts +3 -3
- package/dist/exchanges/kalshi/fetchOHLCV.js +20 -17
- package/dist/exchanges/kalshi/fetchOrderBook.d.ts +2 -0
- package/dist/exchanges/kalshi/fetchOrderBook.js +60 -0
- package/dist/exchanges/kalshi/fetchTrades.d.ts +3 -0
- package/dist/exchanges/kalshi/fetchTrades.js +32 -0
- package/dist/exchanges/kalshi/index.d.ts +20 -4
- package/dist/exchanges/kalshi/index.js +171 -90
- package/dist/exchanges/kalshi/kalshi.test.js +440 -157
- package/dist/exchanges/kalshi/utils.d.ts +1 -3
- package/dist/exchanges/kalshi/utils.js +15 -16
- package/dist/exchanges/kalshi/websocket.d.ts +4 -3
- package/dist/exchanges/kalshi/websocket.js +87 -61
- package/dist/exchanges/kalshi-demo/index.d.ts +10 -0
- package/dist/exchanges/kalshi-demo/index.js +23 -0
- package/dist/exchanges/limitless/api.d.ts +1 -1
- package/dist/exchanges/limitless/api.js +1 -1
- package/dist/exchanges/limitless/fetchEvents.d.ts +2 -1
- package/dist/exchanges/limitless/fetchEvents.js +95 -49
- package/dist/exchanges/limitless/fetchOHLCV.d.ts +2 -2
- package/dist/exchanges/limitless/index.d.ts +11 -3
- package/dist/exchanges/limitless/index.js +69 -1
- package/dist/exchanges/limitless/utils.js +1 -0
- package/dist/exchanges/myriad/api.d.ts +1 -1
- package/dist/exchanges/myriad/api.js +1 -1
- package/dist/exchanges/myriad/fetchOHLCV.d.ts +2 -2
- package/dist/exchanges/myriad/index.d.ts +9 -3
- package/dist/exchanges/myriad/index.js +34 -0
- package/dist/exchanges/myriad/utils.js +5 -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/polymarket/auth.js +3 -1
- package/dist/exchanges/polymarket/fetchEvents.js +116 -80
- package/dist/exchanges/polymarket/fetchOHLCV.d.ts +2 -2
- package/dist/exchanges/polymarket/index.d.ts +30 -6
- package/dist/exchanges/polymarket/index.js +101 -31
- package/dist/exchanges/polymarket/utils.js +1 -0
- package/dist/exchanges/probable/api.d.ts +1 -1
- package/dist/exchanges/probable/api.js +1 -1
- package/dist/exchanges/probable/index.d.ts +45 -3
- package/dist/exchanges/probable/index.js +61 -0
- package/dist/exchanges/probable/utils.js +5 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -1
- package/dist/server/app.js +56 -48
- package/dist/server/utils/port-manager.js +1 -1
- package/dist/types.d.ts +29 -0
- package/dist/utils/throttler.d.ts +17 -0
- package/dist/utils/throttler.js +50 -0
- package/package.json +7 -4
|
@@ -6,10 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const kalshi_1 = require("../kalshi");
|
|
7
7
|
const axios_1 = __importDefault(require("axios"));
|
|
8
8
|
const auth_1 = require("./auth");
|
|
9
|
+
const config_1 = require("./config");
|
|
9
10
|
// Jest hoisting means we can't use outer variables in jest.mock factory
|
|
10
11
|
// unless they start with 'mock'. However, let's just define it inline to be safe and simple.
|
|
11
12
|
// To access the inner methods, we'll grab the instance returned by axios.create().
|
|
12
|
-
jest.mock(
|
|
13
|
+
jest.mock("axios", () => {
|
|
13
14
|
const mockInstance = {
|
|
14
15
|
interceptors: {
|
|
15
16
|
request: { use: jest.fn() },
|
|
@@ -21,7 +22,7 @@ jest.mock('axios', () => {
|
|
|
21
22
|
request: jest.fn(),
|
|
22
23
|
defaults: { headers: { common: {} } },
|
|
23
24
|
};
|
|
24
|
-
const actualAxios = jest.requireActual(
|
|
25
|
+
const actualAxios = jest.requireActual("axios");
|
|
25
26
|
const mockAxios = {
|
|
26
27
|
create: jest.fn(() => mockInstance),
|
|
27
28
|
isAxiosError: actualAxios.isAxiosError,
|
|
@@ -38,13 +39,13 @@ jest.mock('axios', () => {
|
|
|
38
39
|
const mockAxiosInstance = axios_1.default.create();
|
|
39
40
|
const mockedAxios = axios_1.default;
|
|
40
41
|
// Mock KalshiAuth
|
|
41
|
-
jest.mock(
|
|
42
|
+
jest.mock("./auth");
|
|
42
43
|
const MockedKalshiAuth = auth_1.KalshiAuth;
|
|
43
|
-
describe(
|
|
44
|
+
describe("KalshiExchange", () => {
|
|
44
45
|
let exchange;
|
|
45
46
|
const mockCredentials = {
|
|
46
|
-
apiKey:
|
|
47
|
-
privateKey:
|
|
47
|
+
apiKey: "test-api-key",
|
|
48
|
+
privateKey: "mock-private-key",
|
|
48
49
|
};
|
|
49
50
|
beforeEach(() => {
|
|
50
51
|
jest.clearAllMocks();
|
|
@@ -55,181 +56,184 @@ describe('KalshiExchange', () => {
|
|
|
55
56
|
mockAxiosInstance.request.mockReset();
|
|
56
57
|
// Mock the getHeaders method
|
|
57
58
|
MockedKalshiAuth.prototype.getHeaders = jest.fn().mockReturnValue({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
"KALSHI-ACCESS-KEY": "test-api-key",
|
|
60
|
+
"KALSHI-ACCESS-TIMESTAMP": "1234567890",
|
|
61
|
+
"KALSHI-ACCESS-SIGNATURE": "mock-signature",
|
|
62
|
+
"Content-Type": "application/json",
|
|
62
63
|
});
|
|
63
64
|
});
|
|
64
|
-
describe(
|
|
65
|
-
it(
|
|
65
|
+
describe("Authentication", () => {
|
|
66
|
+
it("should throw error when trading without credentials", async () => {
|
|
66
67
|
exchange = new kalshi_1.KalshiExchange();
|
|
67
|
-
await expect(exchange.fetchBalance()).rejects.toThrow(
|
|
68
|
+
await expect(exchange.fetchBalance()).rejects.toThrow("Trading operations require authentication");
|
|
68
69
|
});
|
|
69
|
-
it(
|
|
70
|
+
it("should initialize with credentials", () => {
|
|
70
71
|
exchange = new kalshi_1.KalshiExchange(mockCredentials);
|
|
71
72
|
expect(exchange).toBeDefined();
|
|
72
73
|
});
|
|
73
74
|
});
|
|
74
|
-
describe(
|
|
75
|
+
describe("Market Data Methods", () => {
|
|
75
76
|
beforeEach(() => {
|
|
76
77
|
exchange = new kalshi_1.KalshiExchange();
|
|
77
78
|
});
|
|
78
|
-
it(
|
|
79
|
+
it("should fetch markets", async () => {
|
|
79
80
|
const mockResponse = {
|
|
80
81
|
data: {
|
|
81
82
|
markets: [
|
|
82
83
|
{
|
|
83
|
-
ticker:
|
|
84
|
-
title:
|
|
84
|
+
ticker: "TEST-MARKET",
|
|
85
|
+
title: "Test Market",
|
|
85
86
|
yes_bid: 50,
|
|
86
87
|
yes_ask: 52,
|
|
87
|
-
volume: 1000
|
|
88
|
-
}
|
|
89
|
-
]
|
|
90
|
-
}
|
|
88
|
+
volume: 1000,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
91
92
|
};
|
|
92
93
|
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
93
94
|
const markets = await exchange.fetchMarkets();
|
|
94
95
|
expect(markets).toBeDefined();
|
|
95
96
|
});
|
|
96
97
|
});
|
|
97
|
-
describe(
|
|
98
|
+
describe("Trading Methods", () => {
|
|
98
99
|
beforeEach(() => {
|
|
99
100
|
exchange = new kalshi_1.KalshiExchange(mockCredentials);
|
|
100
101
|
});
|
|
101
|
-
describe(
|
|
102
|
-
it(
|
|
102
|
+
describe("createOrder", () => {
|
|
103
|
+
it("should create buy order with yes_price for buy side", async () => {
|
|
103
104
|
const orderParams = {
|
|
104
|
-
marketId:
|
|
105
|
-
outcomeId:
|
|
106
|
-
side:
|
|
107
|
-
type:
|
|
105
|
+
marketId: "TEST-MARKET",
|
|
106
|
+
outcomeId: "yes",
|
|
107
|
+
side: "buy",
|
|
108
|
+
type: "limit",
|
|
108
109
|
amount: 10,
|
|
109
|
-
price: 0.55
|
|
110
|
+
price: 0.55,
|
|
110
111
|
};
|
|
111
112
|
const mockResponse = {
|
|
112
113
|
data: {
|
|
113
114
|
order: {
|
|
114
|
-
order_id:
|
|
115
|
-
ticker:
|
|
116
|
-
status:
|
|
115
|
+
order_id: "order-123",
|
|
116
|
+
ticker: "TEST-MARKET",
|
|
117
|
+
status: "resting",
|
|
117
118
|
count: 10,
|
|
118
119
|
remaining_count: 10,
|
|
119
|
-
created_time:
|
|
120
|
-
queue_position: 1
|
|
121
|
-
}
|
|
122
|
-
}
|
|
120
|
+
created_time: "2026-01-13T12:00:00Z",
|
|
121
|
+
queue_position: 1,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
123
124
|
};
|
|
124
125
|
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
125
126
|
const order = await exchange.createOrder(orderParams);
|
|
126
127
|
expect(mockAxiosInstance.request).toHaveBeenCalledWith(expect.objectContaining({
|
|
127
|
-
method:
|
|
128
|
-
url:
|
|
128
|
+
method: "POST",
|
|
129
|
+
url: "https://api.elections.kalshi.com/trade-api/v2/portfolio/orders",
|
|
129
130
|
data: expect.objectContaining({
|
|
130
|
-
ticker:
|
|
131
|
-
side:
|
|
132
|
-
action:
|
|
131
|
+
ticker: "TEST-MARKET",
|
|
132
|
+
side: "yes",
|
|
133
|
+
action: "buy",
|
|
133
134
|
count: 10,
|
|
134
|
-
type:
|
|
135
|
-
yes_price: 55 // 0.55 * 100
|
|
135
|
+
type: "limit",
|
|
136
|
+
yes_price: 55, // 0.55 * 100
|
|
136
137
|
}),
|
|
137
138
|
}));
|
|
138
|
-
expect(order.id).toBe(
|
|
139
|
-
expect(order.status).toBe(
|
|
139
|
+
expect(order.id).toBe("order-123");
|
|
140
|
+
expect(order.status).toBe("open");
|
|
140
141
|
});
|
|
141
|
-
it(
|
|
142
|
+
it("should create sell order with no_price for sell side", async () => {
|
|
142
143
|
const orderParams = {
|
|
143
|
-
marketId:
|
|
144
|
-
outcomeId:
|
|
145
|
-
side:
|
|
146
|
-
type:
|
|
144
|
+
marketId: "TEST-MARKET",
|
|
145
|
+
outcomeId: "no",
|
|
146
|
+
side: "sell",
|
|
147
|
+
type: "limit",
|
|
147
148
|
amount: 5,
|
|
148
|
-
price: 0.45
|
|
149
|
+
price: 0.45,
|
|
149
150
|
};
|
|
150
151
|
const mockResponse = {
|
|
151
152
|
data: {
|
|
152
153
|
order: {
|
|
153
|
-
order_id:
|
|
154
|
-
ticker:
|
|
155
|
-
status:
|
|
154
|
+
order_id: "order-456",
|
|
155
|
+
ticker: "TEST-MARKET",
|
|
156
|
+
status: "resting",
|
|
156
157
|
count: 5,
|
|
157
158
|
remaining_count: 5,
|
|
158
|
-
created_time:
|
|
159
|
-
queue_position: 1
|
|
160
|
-
}
|
|
161
|
-
}
|
|
159
|
+
created_time: "2026-01-13T12:00:00Z",
|
|
160
|
+
queue_position: 1,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
162
163
|
};
|
|
163
164
|
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
164
165
|
await exchange.createOrder(orderParams);
|
|
165
166
|
expect(mockAxiosInstance.request).toHaveBeenCalledWith(expect.objectContaining({
|
|
166
|
-
method:
|
|
167
|
-
url:
|
|
167
|
+
method: "POST",
|
|
168
|
+
url: "https://api.elections.kalshi.com/trade-api/v2/portfolio/orders",
|
|
168
169
|
data: expect.objectContaining({
|
|
169
|
-
ticker:
|
|
170
|
-
side:
|
|
171
|
-
action:
|
|
170
|
+
ticker: "TEST-MARKET",
|
|
171
|
+
side: "no",
|
|
172
|
+
action: "sell",
|
|
172
173
|
count: 5,
|
|
173
|
-
type:
|
|
174
|
-
no_price: 45 // 0.45 * 100
|
|
174
|
+
type: "limit",
|
|
175
|
+
no_price: 45, // 0.45 * 100
|
|
175
176
|
}),
|
|
176
177
|
}));
|
|
177
178
|
});
|
|
178
179
|
});
|
|
179
|
-
describe(
|
|
180
|
-
it(
|
|
180
|
+
describe("fetchOpenOrders", () => {
|
|
181
|
+
it("should sign request without query parameters", async () => {
|
|
181
182
|
const mockResponse = {
|
|
182
183
|
data: {
|
|
183
184
|
orders: [
|
|
184
185
|
{
|
|
185
|
-
order_id:
|
|
186
|
-
ticker:
|
|
187
|
-
side:
|
|
188
|
-
type:
|
|
186
|
+
order_id: "order-123",
|
|
187
|
+
ticker: "TEST-MARKET",
|
|
188
|
+
side: "yes",
|
|
189
|
+
type: "limit",
|
|
189
190
|
yes_price: 55,
|
|
190
191
|
count: 10,
|
|
191
192
|
remaining_count: 10,
|
|
192
|
-
created_time:
|
|
193
|
-
}
|
|
194
|
-
]
|
|
195
|
-
}
|
|
193
|
+
created_time: "2026-01-13T12:00:00Z",
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
},
|
|
196
197
|
};
|
|
197
198
|
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
198
199
|
await exchange.fetchOpenOrders();
|
|
199
200
|
// Verify the request includes the correct params
|
|
200
201
|
expect(mockAxiosInstance.request).toHaveBeenCalledWith(expect.objectContaining({
|
|
201
|
-
method:
|
|
202
|
-
url:
|
|
203
|
-
params: expect.objectContaining({ status:
|
|
202
|
+
method: "GET",
|
|
203
|
+
url: "https://api.elections.kalshi.com/trade-api/v2/portfolio/orders",
|
|
204
|
+
params: expect.objectContaining({ status: "resting" }),
|
|
204
205
|
}));
|
|
205
206
|
// Verify getHeaders was called with base path only (no query params)
|
|
206
|
-
expect(MockedKalshiAuth.prototype.getHeaders).toHaveBeenCalledWith(
|
|
207
|
+
expect(MockedKalshiAuth.prototype.getHeaders).toHaveBeenCalledWith("GET", (0, config_1.getApiPath)("/portfolio/orders"));
|
|
207
208
|
});
|
|
208
|
-
it(
|
|
209
|
+
it("should include ticker in query params when marketId provided", async () => {
|
|
209
210
|
const mockResponse = { data: { orders: [] } };
|
|
210
211
|
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
211
|
-
await exchange.fetchOpenOrders(
|
|
212
|
+
await exchange.fetchOpenOrders("TEST-MARKET");
|
|
212
213
|
expect(mockAxiosInstance.request).toHaveBeenCalledWith(expect.objectContaining({
|
|
213
|
-
method:
|
|
214
|
-
url:
|
|
215
|
-
params: expect.objectContaining({
|
|
214
|
+
method: "GET",
|
|
215
|
+
url: "https://api.elections.kalshi.com/trade-api/v2/portfolio/orders",
|
|
216
|
+
params: expect.objectContaining({
|
|
217
|
+
status: "resting",
|
|
218
|
+
ticker: "TEST-MARKET",
|
|
219
|
+
}),
|
|
216
220
|
}));
|
|
217
221
|
});
|
|
218
222
|
});
|
|
219
|
-
describe(
|
|
220
|
-
it(
|
|
223
|
+
describe("fetchPositions", () => {
|
|
224
|
+
it("should handle positions with zero contracts", async () => {
|
|
221
225
|
const mockResponse = {
|
|
222
226
|
data: {
|
|
223
227
|
market_positions: [
|
|
224
228
|
{
|
|
225
|
-
ticker:
|
|
229
|
+
ticker: "TEST-MARKET",
|
|
226
230
|
position: 0,
|
|
227
231
|
total_cost: 0,
|
|
228
232
|
market_exposure: 0,
|
|
229
|
-
realized_pnl: 0
|
|
230
|
-
}
|
|
231
|
-
]
|
|
232
|
-
}
|
|
233
|
+
realized_pnl: 0,
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
233
237
|
};
|
|
234
238
|
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
235
239
|
const positions = await exchange.fetchPositions();
|
|
@@ -237,41 +241,41 @@ describe('KalshiExchange', () => {
|
|
|
237
241
|
expect(positions[0].size).toBe(0);
|
|
238
242
|
expect(positions[0].entryPrice).toBe(0); // Should not throw division by zero
|
|
239
243
|
});
|
|
240
|
-
it(
|
|
244
|
+
it("should correctly calculate average price and PnL", async () => {
|
|
241
245
|
const mockResponse = {
|
|
242
246
|
data: {
|
|
243
247
|
market_positions: [
|
|
244
248
|
{
|
|
245
|
-
ticker:
|
|
249
|
+
ticker: "TEST-MARKET",
|
|
246
250
|
position: 10,
|
|
247
251
|
total_cost: 550, // 10 contracts at $0.55 each = $5.50 = 550 cents
|
|
248
252
|
market_exposure: 100, // $1.00 unrealized PnL
|
|
249
|
-
realized_pnl: 50 // $0.50 realized PnL
|
|
250
|
-
}
|
|
251
|
-
]
|
|
252
|
-
}
|
|
253
|
+
realized_pnl: 50, // $0.50 realized PnL
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
},
|
|
253
257
|
};
|
|
254
258
|
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
255
259
|
const positions = await exchange.fetchPositions();
|
|
256
260
|
expect(positions).toHaveLength(1);
|
|
257
261
|
expect(positions[0].size).toBe(10);
|
|
258
262
|
expect(positions[0].entryPrice).toBe(0.55); // 550 / 10 / 100
|
|
259
|
-
expect(positions[0].unrealizedPnL).toBe(1.
|
|
260
|
-
expect(positions[0].realizedPnL).toBe(0.
|
|
263
|
+
expect(positions[0].unrealizedPnL).toBe(1.0); // 100 / 100
|
|
264
|
+
expect(positions[0].realizedPnL).toBe(0.5); // 50 / 100
|
|
261
265
|
});
|
|
262
|
-
it(
|
|
266
|
+
it("should handle short positions", async () => {
|
|
263
267
|
const mockResponse = {
|
|
264
268
|
data: {
|
|
265
269
|
market_positions: [
|
|
266
270
|
{
|
|
267
|
-
ticker:
|
|
271
|
+
ticker: "TEST-MARKET",
|
|
268
272
|
position: -5, // Short position
|
|
269
273
|
total_cost: 250,
|
|
270
274
|
market_exposure: -50,
|
|
271
|
-
realized_pnl: 25
|
|
272
|
-
}
|
|
273
|
-
]
|
|
274
|
-
}
|
|
275
|
+
realized_pnl: 25,
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
275
279
|
};
|
|
276
280
|
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
277
281
|
const positions = await exchange.fetchPositions();
|
|
@@ -279,80 +283,359 @@ describe('KalshiExchange', () => {
|
|
|
279
283
|
expect(Math.abs(positions[0].size)).toBe(5); // Absolute value
|
|
280
284
|
});
|
|
281
285
|
});
|
|
282
|
-
describe(
|
|
283
|
-
it(
|
|
286
|
+
describe("fetchBalance", () => {
|
|
287
|
+
it("should correctly convert cents to dollars", async () => {
|
|
284
288
|
const mockResponse = {
|
|
285
289
|
data: {
|
|
286
290
|
balance: 10000, // $100.00 available
|
|
287
|
-
portfolio_value: 15000 // $150.00 total
|
|
288
|
-
}
|
|
291
|
+
portfolio_value: 15000, // $150.00 total
|
|
292
|
+
},
|
|
289
293
|
};
|
|
290
294
|
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
291
295
|
const balances = await exchange.fetchBalance();
|
|
292
296
|
expect(balances).toHaveLength(1);
|
|
293
|
-
expect(balances[0].currency).toBe(
|
|
294
|
-
expect(balances[0].available).toBe(100.
|
|
295
|
-
expect(balances[0].total).toBe(150.
|
|
296
|
-
expect(balances[0].locked).toBe(50.
|
|
297
|
+
expect(balances[0].currency).toBe("USD");
|
|
298
|
+
expect(balances[0].available).toBe(100.0);
|
|
299
|
+
expect(balances[0].total).toBe(150.0);
|
|
300
|
+
expect(balances[0].locked).toBe(50.0); // 150 - 100
|
|
297
301
|
});
|
|
298
302
|
});
|
|
299
|
-
describe(
|
|
300
|
-
it(
|
|
303
|
+
describe("cancelOrder", () => {
|
|
304
|
+
it("should cancel order successfully", async () => {
|
|
301
305
|
const mockResponse = {
|
|
302
306
|
data: {
|
|
303
307
|
order: {
|
|
304
|
-
order_id:
|
|
305
|
-
ticker:
|
|
306
|
-
side:
|
|
308
|
+
order_id: "order-123",
|
|
309
|
+
ticker: "TEST-MARKET",
|
|
310
|
+
side: "yes",
|
|
307
311
|
count: 10,
|
|
308
312
|
remaining_count: 5,
|
|
309
|
-
created_time:
|
|
310
|
-
}
|
|
311
|
-
}
|
|
313
|
+
created_time: "2026-01-13T12:00:00Z",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
312
316
|
};
|
|
313
317
|
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
314
|
-
const order = await exchange.cancelOrder(
|
|
315
|
-
expect(order.status).toBe(
|
|
318
|
+
const order = await exchange.cancelOrder("order-123");
|
|
319
|
+
expect(order.status).toBe("cancelled");
|
|
316
320
|
expect(order.filled).toBe(5); // count - remaining_count
|
|
317
321
|
expect(order.remaining).toBe(0);
|
|
318
322
|
});
|
|
319
323
|
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
+
describe("Trading History Methods", () => {
|
|
325
|
+
it("should map GetFills response to UserTrade array", async () => {
|
|
326
|
+
const mockResponse = {
|
|
327
|
+
data: {
|
|
328
|
+
fills: [
|
|
329
|
+
{
|
|
330
|
+
fill_id: "fill-abc",
|
|
331
|
+
order_id: "order-123",
|
|
332
|
+
created_time: "2026-01-13T12:00:00Z",
|
|
333
|
+
yes_price: 55,
|
|
334
|
+
count: 10,
|
|
335
|
+
side: "yes",
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
fill_id: "fill-def",
|
|
339
|
+
order_id: "order-456",
|
|
340
|
+
created_time: "2026-01-13T13:00:00Z",
|
|
341
|
+
yes_price: 45,
|
|
342
|
+
count: 5,
|
|
343
|
+
side: "no",
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
349
|
+
const trades = await exchange.fetchMyTrades();
|
|
350
|
+
expect(Array.isArray(trades)).toBe(true);
|
|
351
|
+
expect(trades).toHaveLength(2);
|
|
352
|
+
expect(trades[0].id).toBe("fill-abc");
|
|
353
|
+
expect(trades[0].orderId).toBe("order-123");
|
|
354
|
+
expect(trades[0].price).toBe(0.55); // 55 / 100
|
|
355
|
+
expect(trades[0].amount).toBe(10);
|
|
356
|
+
expect(trades[0].side).toBe("buy"); // 'yes' => 'buy'
|
|
357
|
+
expect(trades[1].id).toBe("fill-def");
|
|
358
|
+
expect(trades[1].side).toBe("sell"); // 'no' => 'sell'
|
|
359
|
+
expect(trades[1].price).toBe(0.45); // 45 / 100
|
|
360
|
+
});
|
|
361
|
+
it("should pass outcomeId as ticker (stripping -NO suffix) and date params", async () => {
|
|
362
|
+
const mockResponse = { data: { fills: [] } };
|
|
363
|
+
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
364
|
+
await exchange.fetchMyTrades({
|
|
365
|
+
outcomeId: "TEST-MARKET-NO",
|
|
366
|
+
since: new Date("2026-01-01T00:00:00Z"),
|
|
367
|
+
until: new Date("2026-01-31T00:00:00Z"),
|
|
368
|
+
limit: 50,
|
|
369
|
+
});
|
|
370
|
+
expect(mockAxiosInstance.request).toHaveBeenCalledWith(expect.objectContaining({
|
|
371
|
+
params: expect.objectContaining({
|
|
372
|
+
ticker: "TEST-MARKET", // -NO stripped
|
|
373
|
+
min_ts: Math.floor(new Date("2026-01-01T00:00:00Z").getTime() / 1000),
|
|
374
|
+
max_ts: Math.floor(new Date("2026-01-31T00:00:00Z").getTime() / 1000),
|
|
375
|
+
limit: 50,
|
|
376
|
+
}),
|
|
377
|
+
}));
|
|
378
|
+
});
|
|
379
|
+
it("should return empty array when fills is missing", async () => {
|
|
380
|
+
mockAxiosInstance.request.mockResolvedValue({ data: {} });
|
|
381
|
+
const trades = await exchange.fetchMyTrades();
|
|
382
|
+
expect(trades).toHaveLength(0);
|
|
383
|
+
});
|
|
384
|
+
describe("fetchClosedOrders", () => {
|
|
385
|
+
it("should map GetHistoricalOrders response to Order array", async () => {
|
|
386
|
+
const mockResponse = {
|
|
387
|
+
data: {
|
|
388
|
+
orders: [
|
|
389
|
+
{
|
|
390
|
+
order_id: "hist-order-1",
|
|
391
|
+
ticker: "TEST-MARKET",
|
|
392
|
+
side: "yes",
|
|
393
|
+
type: "limit",
|
|
394
|
+
yes_price: 60,
|
|
395
|
+
count: 8,
|
|
396
|
+
remaining_count: 0,
|
|
397
|
+
status: "executed",
|
|
398
|
+
created_time: "2026-01-10T10:00:00Z",
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
404
|
+
const orders = await exchange.fetchClosedOrders();
|
|
405
|
+
expect(orders).toHaveLength(1);
|
|
406
|
+
expect(orders[0].id).toBe("hist-order-1");
|
|
407
|
+
expect(orders[0].marketId).toBe("TEST-MARKET");
|
|
408
|
+
expect(orders[0].side).toBe("buy"); // 'yes' => 'buy'
|
|
409
|
+
expect(orders[0].price).toBe(0.6); // 60 / 100
|
|
410
|
+
expect(orders[0].amount).toBe(8);
|
|
411
|
+
expect(orders[0].filled).toBe(8); // count - remaining_count (0)
|
|
412
|
+
expect(orders[0].remaining).toBe(0);
|
|
413
|
+
expect(orders[0].status).toBe("filled"); // 'executed' => 'filled'
|
|
414
|
+
});
|
|
415
|
+
it("should pass marketId as ticker and limit", async () => {
|
|
416
|
+
const mockResponse = { data: { orders: [] } };
|
|
417
|
+
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
418
|
+
await exchange.fetchClosedOrders({
|
|
419
|
+
marketId: "TEST-MARKET",
|
|
420
|
+
limit: 25,
|
|
421
|
+
});
|
|
422
|
+
expect(mockAxiosInstance.request).toHaveBeenCalledWith(expect.objectContaining({
|
|
423
|
+
params: expect.objectContaining({
|
|
424
|
+
ticker: "TEST-MARKET",
|
|
425
|
+
limit: 25,
|
|
426
|
+
}),
|
|
427
|
+
}));
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
describe("fetchAllOrders", () => {
|
|
431
|
+
it("should merge live and historical orders, dedup, and sort descending by timestamp", async () => {
|
|
432
|
+
const liveResponse = {
|
|
433
|
+
data: {
|
|
434
|
+
orders: [
|
|
435
|
+
{
|
|
436
|
+
order_id: "order-live-1",
|
|
437
|
+
ticker: "TEST",
|
|
438
|
+
side: "yes",
|
|
439
|
+
type: "limit",
|
|
440
|
+
yes_price: 50,
|
|
441
|
+
count: 5,
|
|
442
|
+
remaining_count: 5,
|
|
443
|
+
status: "resting",
|
|
444
|
+
created_time: "2026-01-15T10:00:00Z",
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
// duplicate that also appears in historical
|
|
448
|
+
order_id: "order-hist-1",
|
|
449
|
+
ticker: "TEST",
|
|
450
|
+
side: "no",
|
|
451
|
+
type: "limit",
|
|
452
|
+
yes_price: 40,
|
|
453
|
+
count: 3,
|
|
454
|
+
remaining_count: 0,
|
|
455
|
+
status: "executed",
|
|
456
|
+
created_time: "2026-01-10T08:00:00Z",
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
const historicalResponse = {
|
|
462
|
+
data: {
|
|
463
|
+
orders: [
|
|
464
|
+
{
|
|
465
|
+
order_id: "order-hist-1", // duplicate
|
|
466
|
+
ticker: "TEST",
|
|
467
|
+
side: "no",
|
|
468
|
+
type: "limit",
|
|
469
|
+
yes_price: 40,
|
|
470
|
+
count: 3,
|
|
471
|
+
remaining_count: 0,
|
|
472
|
+
status: "executed",
|
|
473
|
+
created_time: "2026-01-10T08:00:00Z",
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
order_id: "order-hist-2",
|
|
477
|
+
ticker: "TEST",
|
|
478
|
+
side: "yes",
|
|
479
|
+
type: "limit",
|
|
480
|
+
yes_price: 55,
|
|
481
|
+
count: 10,
|
|
482
|
+
remaining_count: 0,
|
|
483
|
+
status: "executed",
|
|
484
|
+
created_time: "2026-01-05T06:00:00Z",
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
mockAxiosInstance.request
|
|
490
|
+
.mockResolvedValueOnce(liveResponse)
|
|
491
|
+
.mockResolvedValueOnce(historicalResponse);
|
|
492
|
+
const orders = await exchange.fetchAllOrders();
|
|
493
|
+
// 3 unique orders (order-hist-1 deduped)
|
|
494
|
+
expect(orders).toHaveLength(3);
|
|
495
|
+
// sorted descending: order-live-1, order-hist-1, order-hist-2
|
|
496
|
+
expect(orders[0].id).toBe("order-live-1");
|
|
497
|
+
expect(orders[1].id).toBe("order-hist-1");
|
|
498
|
+
expect(orders[2].id).toBe("order-hist-2");
|
|
499
|
+
});
|
|
500
|
+
});
|
|
324
501
|
});
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
502
|
+
describe("buildOrder", () => {
|
|
503
|
+
it("should return correct raw body for a buy limit order without making HTTP call", async () => {
|
|
504
|
+
const params = {
|
|
505
|
+
marketId: "TEST-MARKET",
|
|
506
|
+
outcomeId: "yes",
|
|
507
|
+
side: "buy",
|
|
508
|
+
type: "limit",
|
|
509
|
+
amount: 10,
|
|
510
|
+
price: 0.55,
|
|
511
|
+
};
|
|
512
|
+
const built = await exchange.buildOrder(params);
|
|
513
|
+
expect(mockAxiosInstance.request).not.toHaveBeenCalled();
|
|
514
|
+
expect(built.exchange).toBe("Kalshi");
|
|
515
|
+
expect(built.params).toBe(params);
|
|
516
|
+
expect(built.raw).toEqual(expect.objectContaining({
|
|
517
|
+
ticker: "TEST-MARKET",
|
|
518
|
+
side: "yes",
|
|
519
|
+
action: "buy",
|
|
520
|
+
count: 10,
|
|
521
|
+
type: "limit",
|
|
522
|
+
yes_price: 55,
|
|
523
|
+
}));
|
|
524
|
+
});
|
|
525
|
+
it("should return correct raw body for a sell limit order", async () => {
|
|
526
|
+
const params = {
|
|
527
|
+
marketId: "TEST-MARKET",
|
|
528
|
+
outcomeId: "no",
|
|
529
|
+
side: "sell",
|
|
530
|
+
type: "limit",
|
|
531
|
+
amount: 5,
|
|
532
|
+
price: 0.45,
|
|
533
|
+
};
|
|
534
|
+
const built = await exchange.buildOrder(params);
|
|
535
|
+
expect(mockAxiosInstance.request).not.toHaveBeenCalled();
|
|
536
|
+
expect(built.exchange).toBe("Kalshi");
|
|
537
|
+
expect(built.raw).toEqual(expect.objectContaining({
|
|
538
|
+
ticker: "TEST-MARKET",
|
|
539
|
+
side: "no",
|
|
540
|
+
action: "sell",
|
|
541
|
+
count: 5,
|
|
542
|
+
type: "limit",
|
|
543
|
+
no_price: 45,
|
|
544
|
+
}));
|
|
545
|
+
});
|
|
546
|
+
it("should mirror input params in built.params", async () => {
|
|
547
|
+
const params = {
|
|
548
|
+
marketId: "SOME-MARKET",
|
|
549
|
+
outcomeId: "yes",
|
|
550
|
+
side: "buy",
|
|
551
|
+
type: "market",
|
|
552
|
+
amount: 3,
|
|
553
|
+
};
|
|
554
|
+
const built = await exchange.buildOrder(params);
|
|
555
|
+
expect(built.params).toBe(params);
|
|
556
|
+
});
|
|
340
557
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
558
|
+
describe("submitOrder", () => {
|
|
559
|
+
it("should POST built.raw to CreateOrder and return mapped order", async () => {
|
|
560
|
+
const params = {
|
|
561
|
+
marketId: "TEST-MARKET",
|
|
562
|
+
outcomeId: "yes",
|
|
563
|
+
side: "buy",
|
|
564
|
+
type: "limit",
|
|
565
|
+
amount: 10,
|
|
566
|
+
price: 0.55,
|
|
567
|
+
};
|
|
568
|
+
const built = await exchange.buildOrder(params);
|
|
569
|
+
const mockResponse = {
|
|
570
|
+
data: {
|
|
571
|
+
order: {
|
|
572
|
+
order_id: "order-789",
|
|
573
|
+
ticker: "TEST-MARKET",
|
|
574
|
+
side: "yes",
|
|
575
|
+
type: "limit",
|
|
576
|
+
yes_price: 55,
|
|
577
|
+
count: 10,
|
|
578
|
+
remaining_count: 10,
|
|
579
|
+
status: "resting",
|
|
580
|
+
created_time: "2026-01-13T12:00:00Z",
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
585
|
+
const order = await exchange.submitOrder(built);
|
|
586
|
+
expect(mockAxiosInstance.request).toHaveBeenCalledWith(expect.objectContaining({
|
|
587
|
+
method: "POST",
|
|
588
|
+
url: "https://api.elections.kalshi.com/trade-api/v2/portfolio/orders",
|
|
589
|
+
data: expect.objectContaining({
|
|
590
|
+
ticker: "TEST-MARKET",
|
|
591
|
+
side: "yes",
|
|
592
|
+
action: "buy",
|
|
348
593
|
count: 10,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
594
|
+
type: "limit",
|
|
595
|
+
yes_price: 55,
|
|
596
|
+
}),
|
|
597
|
+
}));
|
|
598
|
+
expect(order.id).toBe("order-789");
|
|
599
|
+
expect(order.status).toBe("open");
|
|
600
|
+
expect(order.price).toBe(0.55);
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
describe("Order Status Mapping", () => {
|
|
604
|
+
beforeEach(() => {
|
|
605
|
+
exchange = new kalshi_1.KalshiExchange(mockCredentials);
|
|
606
|
+
});
|
|
607
|
+
it("should map resting to open", async () => {
|
|
608
|
+
const mockResponse = {
|
|
609
|
+
data: {
|
|
610
|
+
order: {
|
|
611
|
+
order_id: "order-123",
|
|
612
|
+
ticker: "TEST",
|
|
613
|
+
status: "resting",
|
|
614
|
+
count: 10,
|
|
615
|
+
created_time: "2026-01-13T12:00:00Z",
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
620
|
+
const order = await exchange.fetchOrder("order-123");
|
|
621
|
+
expect(order.status).toBe("open");
|
|
622
|
+
});
|
|
623
|
+
it("should map executed to filled", async () => {
|
|
624
|
+
const mockResponse = {
|
|
625
|
+
data: {
|
|
626
|
+
order: {
|
|
627
|
+
order_id: "order-123",
|
|
628
|
+
ticker: "TEST",
|
|
629
|
+
status: "executed",
|
|
630
|
+
count: 10,
|
|
631
|
+
created_time: "2026-01-13T12:00:00Z",
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
mockAxiosInstance.request.mockResolvedValue(mockResponse);
|
|
636
|
+
const order = await exchange.fetchOrder("order-123");
|
|
637
|
+
expect(order.status).toBe("filled");
|
|
638
|
+
});
|
|
356
639
|
});
|
|
357
640
|
});
|
|
358
641
|
});
|