pmxt-core 2.22.2 → 2.24.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.
Files changed (58) hide show
  1. package/dist/BaseExchange.d.ts +2 -0
  2. package/dist/exchanges/kalshi/api.d.ts +1 -1
  3. package/dist/exchanges/kalshi/api.js +1 -1
  4. package/dist/exchanges/limitless/api.d.ts +1 -1
  5. package/dist/exchanges/limitless/api.js +1 -1
  6. package/dist/exchanges/metaculus/api.d.ts +212 -0
  7. package/dist/exchanges/metaculus/api.js +418 -0
  8. package/dist/exchanges/metaculus/cancelOrder.d.ts +38 -0
  9. package/dist/exchanges/metaculus/cancelOrder.js +74 -0
  10. package/dist/exchanges/metaculus/createOrder.d.ts +107 -0
  11. package/dist/exchanges/metaculus/createOrder.js +272 -0
  12. package/dist/exchanges/metaculus/errors.d.ts +21 -0
  13. package/dist/exchanges/metaculus/errors.js +59 -0
  14. package/dist/exchanges/metaculus/fetchEvents.d.ts +5 -0
  15. package/dist/exchanges/metaculus/fetchEvents.js +187 -0
  16. package/dist/exchanges/metaculus/fetchMarkets.d.ts +6 -0
  17. package/dist/exchanges/metaculus/fetchMarkets.js +198 -0
  18. package/dist/exchanges/metaculus/index.d.ts +105 -0
  19. package/dist/exchanges/metaculus/index.js +166 -0
  20. package/dist/exchanges/metaculus/utils.d.ts +40 -0
  21. package/dist/exchanges/metaculus/utils.js +320 -0
  22. package/dist/exchanges/myriad/api.d.ts +1 -1
  23. package/dist/exchanges/myriad/api.js +1 -1
  24. package/dist/exchanges/opinion/api.d.ts +1 -1
  25. package/dist/exchanges/opinion/api.js +1 -1
  26. package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
  27. package/dist/exchanges/polymarket/api-clob.js +1 -1
  28. package/dist/exchanges/polymarket/api-data.d.ts +1 -1
  29. package/dist/exchanges/polymarket/api-data.js +1 -1
  30. package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
  31. package/dist/exchanges/polymarket/api-gamma.js +1 -1
  32. package/dist/exchanges/polymarket/auth.js +42 -8
  33. package/dist/exchanges/polymarket/index.js +56 -19
  34. package/dist/exchanges/probable/api.d.ts +1 -1
  35. package/dist/exchanges/probable/api.js +1 -1
  36. package/dist/exchanges/probable/auth.js +5 -2
  37. package/dist/exchanges/smarkets/api.d.ts +8067 -0
  38. package/dist/exchanges/smarkets/api.js +10698 -0
  39. package/dist/exchanges/smarkets/auth.d.ts +56 -0
  40. package/dist/exchanges/smarkets/auth.js +105 -0
  41. package/dist/exchanges/smarkets/config.d.ts +41 -0
  42. package/dist/exchanges/smarkets/config.js +47 -0
  43. package/dist/exchanges/smarkets/errors.d.ts +31 -0
  44. package/dist/exchanges/smarkets/errors.js +186 -0
  45. package/dist/exchanges/smarkets/fetcher.d.ts +177 -0
  46. package/dist/exchanges/smarkets/fetcher.js +342 -0
  47. package/dist/exchanges/smarkets/index.d.ts +54 -0
  48. package/dist/exchanges/smarkets/index.js +285 -0
  49. package/dist/exchanges/smarkets/normalizer.d.ts +18 -0
  50. package/dist/exchanges/smarkets/normalizer.js +267 -0
  51. package/dist/exchanges/smarkets/price.d.ts +26 -0
  52. package/dist/exchanges/smarkets/price.js +44 -0
  53. package/dist/exchanges/smarkets/price.test.d.ts +1 -0
  54. package/dist/exchanges/smarkets/price.test.js +50 -0
  55. package/dist/index.d.ts +8 -0
  56. package/dist/index.js +9 -1
  57. package/dist/server/app.js +22 -3
  58. package/package.json +3 -3
@@ -0,0 +1,38 @@
1
+ import { AxiosInstance } from "axios";
2
+ import { Order } from "../../types";
3
+ /**
4
+ * Parameters for the internal cancelOrder function.
5
+ */
6
+ export interface CancelOrderContext {
7
+ /** The exchange's axios instance (with rate limiting and logging). */
8
+ http: AxiosInstance;
9
+ /** Returns auth headers. Throws if no token is configured. */
10
+ getAuthHeaders: () => Record<string, string>;
11
+ }
12
+ /**
13
+ * Withdraw a forecast on Metaculus, mapped from the unified cancelOrder interface.
14
+ *
15
+ * ## How the mapping works
16
+ *
17
+ * "Cancelling an order" on Metaculus means withdrawing your forecast from a
18
+ * question. After withdrawal, your prediction no longer affects the community
19
+ * aggregate and is not scored.
20
+ *
21
+ * The `orderId` parameter should be the **Metaculus question ID** (the numeric
22
+ * ID used in the forecast API, not the post ID). If you created the forecast
23
+ * via `createOrder`, the question ID is encoded in the returned order's
24
+ * `outcomeId` (the part before the hyphen).
25
+ *
26
+ * ## Authentication
27
+ *
28
+ * Requires a Metaculus API token. Pass `{ apiToken: "..." }` when constructing
29
+ * the MetaculusExchange.
30
+ *
31
+ * @param orderId The Metaculus question ID to withdraw the forecast from.
32
+ * @param ctx HTTP client and auth context.
33
+ * @returns A synthetic Order with status "cancelled".
34
+ *
35
+ * @throws {AuthenticationError} If no API token is configured.
36
+ * @throws {ValidationError} If the orderId is not a valid numeric question ID.
37
+ */
38
+ export declare function cancelOrder(orderId: string, ctx: CancelOrderContext): Promise<Order>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cancelOrder = cancelOrder;
4
+ const errors_1 = require("../../errors");
5
+ const errors_2 = require("./errors");
6
+ const utils_1 = require("./utils");
7
+ /**
8
+ * Withdraw a forecast on Metaculus, mapped from the unified cancelOrder interface.
9
+ *
10
+ * ## How the mapping works
11
+ *
12
+ * "Cancelling an order" on Metaculus means withdrawing your forecast from a
13
+ * question. After withdrawal, your prediction no longer affects the community
14
+ * aggregate and is not scored.
15
+ *
16
+ * The `orderId` parameter should be the **Metaculus question ID** (the numeric
17
+ * ID used in the forecast API, not the post ID). If you created the forecast
18
+ * via `createOrder`, the question ID is encoded in the returned order's
19
+ * `outcomeId` (the part before the hyphen).
20
+ *
21
+ * ## Authentication
22
+ *
23
+ * Requires a Metaculus API token. Pass `{ apiToken: "..." }` when constructing
24
+ * the MetaculusExchange.
25
+ *
26
+ * @param orderId The Metaculus question ID to withdraw the forecast from.
27
+ * @param ctx HTTP client and auth context.
28
+ * @returns A synthetic Order with status "cancelled".
29
+ *
30
+ * @throws {AuthenticationError} If no API token is configured.
31
+ * @throws {ValidationError} If the orderId is not a valid numeric question ID.
32
+ */
33
+ async function cancelOrder(orderId, ctx) {
34
+ try {
35
+ // 1. Validate auth
36
+ const headers = ctx.getAuthHeaders();
37
+ if (!headers.Authorization) {
38
+ throw new errors_1.AuthenticationError('Metaculus forecast withdrawal requires authentication. '
39
+ + 'Pass { apiToken: "..." } when constructing MetaculusExchange.', "Metaculus");
40
+ }
41
+ // 2. Parse question ID
42
+ const questionId = parseInt(orderId, 10);
43
+ if (isNaN(questionId)) {
44
+ throw new errors_1.ValidationError(`Invalid orderId "${orderId}": expected a numeric Metaculus question ID. `
45
+ + "Use the question ID from the outcomeId (the part before the hyphen).", "Metaculus");
46
+ }
47
+ // 3. POST directly to the withdraw endpoint.
48
+ // Bypasses callApi because the API expects an array body.
49
+ await ctx.http.request({
50
+ method: "POST",
51
+ url: `${utils_1.BASE_URL}/questions/withdraw/`,
52
+ data: [{ question: questionId }],
53
+ headers: { "Content-Type": "application/json", ...headers },
54
+ });
55
+ // 4. Return synthetic cancelled order
56
+ return {
57
+ id: `mc-withdraw-${questionId}-${Date.now()}`,
58
+ marketId: orderId,
59
+ outcomeId: orderId,
60
+ side: "buy",
61
+ type: "market",
62
+ amount: 1,
63
+ status: "cancelled",
64
+ filled: 0,
65
+ remaining: 0,
66
+ timestamp: Date.now(),
67
+ };
68
+ }
69
+ catch (error) {
70
+ if (error.statusCode)
71
+ throw error;
72
+ throw errors_2.metaculusErrorMapper.mapError(error);
73
+ }
74
+ }
@@ -0,0 +1,107 @@
1
+ import { AxiosInstance } from "axios";
2
+ import { CreateOrderParams, Order, MarketOutcome } from "../../types";
3
+ /**
4
+ * Parsed result from a Metaculus outcomeId string.
5
+ *
6
+ * OutcomeId format:
7
+ * - Binary: `<questionId>-YES` or `<questionId>-NO`
8
+ * - Multiple-choice: `<questionId>-<categoryIndex>` (numeric index)
9
+ * - Continuous: `<questionId>-HIGHER` or `<questionId>-LOWER` (not tradeable)
10
+ */
11
+ export interface ParsedOutcomeId {
12
+ /** The Metaculus question ID (used in the forecast API). */
13
+ questionId: number;
14
+ /** The question type inferred from the suffix. */
15
+ type: "binary" | "multiple_choice" | "continuous";
16
+ /** The raw suffix after the first hyphen (YES, NO, HIGHER, LOWER, or index). */
17
+ suffix: string;
18
+ /** For multiple-choice outcomes, the 0-based category index. */
19
+ categoryIndex?: number;
20
+ }
21
+ /**
22
+ * Parse a Metaculus outcomeId into its components.
23
+ *
24
+ * @throws {ValidationError} If the outcomeId format is unrecognizable.
25
+ */
26
+ export declare function parseOutcomeId(outcomeId: string): ParsedOutcomeId;
27
+ /**
28
+ * Validate that a probability value is in the open interval (0, 1).
29
+ *
30
+ * Metaculus requires probability_yes to be strictly between 0 and 1.
31
+ * The exact boundaries (0.0 and 1.0) are rejected by the API.
32
+ *
33
+ * @throws {InvalidOrder} If the value is missing or out of range.
34
+ */
35
+ export declare function validateProbability(price: number | undefined): number;
36
+ /**
37
+ * Redistribute multiple-choice probabilities when setting one category.
38
+ *
39
+ * When a user sets category X to probability P, the remaining categories
40
+ * must be adjusted so all probabilities sum to 1.0. This function scales
41
+ * the non-target categories proportionally.
42
+ *
43
+ * @param currentProbabilities Map of category label -> current probability.
44
+ * @param targetLabel The category label being set.
45
+ * @param targetProbability The new probability for the target category.
46
+ * @returns A new map with all probabilities summing to 1.0.
47
+ *
48
+ * @throws {InvalidOrder} If the target category doesn't exist or redistribution
49
+ * is impossible (e.g., target = 1.0 with other categories).
50
+ */
51
+ export declare function redistributeProbabilities(currentProbabilities: Record<string, number>, targetLabel: string, targetProbability: number): Record<string, number>;
52
+ /**
53
+ * Parameters for the internal createOrder function.
54
+ *
55
+ * Accepts the exchange's HTTP client and sign function so the trading
56
+ * module doesn't need access to the full exchange instance.
57
+ */
58
+ export interface CreateOrderContext {
59
+ /** The exchange's axios instance (with rate limiting and logging). */
60
+ http: AxiosInstance;
61
+ /** Returns auth headers. Throws if no token is configured. */
62
+ getAuthHeaders: () => Record<string, string>;
63
+ /**
64
+ * Fetch current market outcomes to read multiple-choice probabilities.
65
+ * Only needed for multiple-choice questions.
66
+ */
67
+ fetchOutcomes?: (marketId: string) => Promise<MarketOutcome[]>;
68
+ }
69
+ /**
70
+ * Submit a forecast on Metaculus, mapped from the unified createOrder interface.
71
+ *
72
+ * ## How the mapping works
73
+ *
74
+ * Metaculus is a reputation-based forecasting platform, not a financial exchange.
75
+ * "Creating an order" means submitting a probability forecast on a question.
76
+ *
77
+ * | CreateOrderParams field | Metaculus meaning |
78
+ * |------------------------|-------------------|
79
+ * | `marketId` | Post ID (for reference only) |
80
+ * | `outcomeId` | Encodes the question ID + type (see {@link parseOutcomeId}) |
81
+ * | `price` | The probability to forecast (0-1 exclusive) |
82
+ * | `side` | Ignored -- forecasts are always "buy" (you submit a belief) |
83
+ * | `type` | Ignored -- forecasts execute instantly (always "market") |
84
+ * | `amount` | Ignored -- Metaculus has no stake size |
85
+ *
86
+ * The returned {@link Order} is synthetic: Metaculus doesn't return order IDs
87
+ * or track fill state. The order is always immediately "filled".
88
+ *
89
+ * ## Supported question types
90
+ *
91
+ * - **Binary**: Sets `probability_yes` directly from `price`.
92
+ * - **Multiple-choice**: Sets the target category's probability and
93
+ * redistributes others proportionally to sum to 1.0.
94
+ * - **Continuous/numeric/date**: NOT supported -- throws {@link InvalidOrder}
95
+ * because a 201-point CDF cannot be expressed as a single price value.
96
+ *
97
+ * ## Authentication
98
+ *
99
+ * Requires a Metaculus API token. Pass `{ apiToken: "..." }` when constructing
100
+ * the MetaculusExchange. Without a token, this method throws
101
+ * {@link AuthenticationError}.
102
+ *
103
+ * @throws {AuthenticationError} If no API token is configured.
104
+ * @throws {InvalidOrder} If the question type is continuous or the price is invalid.
105
+ * @throws {ValidationError} If the outcomeId format is unrecognizable.
106
+ */
107
+ export declare function createOrder(params: CreateOrderParams, ctx: CreateOrderContext): Promise<Order>;
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseOutcomeId = parseOutcomeId;
4
+ exports.validateProbability = validateProbability;
5
+ exports.redistributeProbabilities = redistributeProbabilities;
6
+ exports.createOrder = createOrder;
7
+ const errors_1 = require("../../errors");
8
+ const errors_2 = require("./errors");
9
+ const utils_1 = require("./utils");
10
+ /**
11
+ * Parse a Metaculus outcomeId into its components.
12
+ *
13
+ * @throws {ValidationError} If the outcomeId format is unrecognizable.
14
+ */
15
+ function parseOutcomeId(outcomeId) {
16
+ const dashIdx = outcomeId.indexOf("-");
17
+ if (dashIdx === -1) {
18
+ throw new errors_1.ValidationError(`Invalid Metaculus outcomeId "${outcomeId}". `
19
+ + 'Expected format: "<questionId>-YES", "<questionId>-NO", or "<questionId>-<index>".', "Metaculus");
20
+ }
21
+ const idPart = outcomeId.slice(0, dashIdx);
22
+ const suffix = outcomeId.slice(dashIdx + 1);
23
+ const questionId = parseInt(idPart, 10);
24
+ if (isNaN(questionId)) {
25
+ throw new errors_1.ValidationError(`Invalid question ID in outcomeId "${outcomeId}". The part before the hyphen must be a numeric question ID.`, "Metaculus");
26
+ }
27
+ const upperSuffix = suffix.toUpperCase();
28
+ if (upperSuffix === "HIGHER" || upperSuffix === "LOWER") {
29
+ return { questionId, type: "continuous", suffix };
30
+ }
31
+ if (upperSuffix === "YES" || upperSuffix === "NO") {
32
+ return { questionId, type: "binary", suffix };
33
+ }
34
+ // Numeric suffix -> multiple-choice category index
35
+ const idx = parseInt(suffix, 10);
36
+ if (!isNaN(idx) && idx >= 0) {
37
+ return { questionId, type: "multiple_choice", suffix, categoryIndex: idx };
38
+ }
39
+ throw new errors_1.ValidationError(`Unrecognized outcomeId suffix "${suffix}" in "${outcomeId}". `
40
+ + 'Expected YES, NO, HIGHER, LOWER, or a numeric category index.', "Metaculus");
41
+ }
42
+ // ---------------------------------------------------------------------------
43
+ // Probability Validation
44
+ // ---------------------------------------------------------------------------
45
+ /**
46
+ * Validate that a probability value is in the open interval (0, 1).
47
+ *
48
+ * Metaculus requires probability_yes to be strictly between 0 and 1.
49
+ * The exact boundaries (0.0 and 1.0) are rejected by the API.
50
+ *
51
+ * @throws {InvalidOrder} If the value is missing or out of range.
52
+ */
53
+ function validateProbability(price) {
54
+ if (price === undefined || price === null) {
55
+ throw new errors_1.InvalidOrder("Metaculus createOrder requires `price` (the probability to forecast, between 0 and 1 exclusive).", "Metaculus");
56
+ }
57
+ if (typeof price !== "number" || isNaN(price)) {
58
+ throw new errors_1.InvalidOrder(`Invalid price "${price}": must be a number between 0 and 1 exclusive.`, "Metaculus");
59
+ }
60
+ if (price <= 0 || price >= 1) {
61
+ throw new errors_1.InvalidOrder(`Probability ${price} is out of range. Metaculus requires a value strictly between 0 and 1 (e.g., 0.01 to 0.99).`, "Metaculus");
62
+ }
63
+ return price;
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Multiple-Choice Redistribution
67
+ // ---------------------------------------------------------------------------
68
+ /**
69
+ * Redistribute multiple-choice probabilities when setting one category.
70
+ *
71
+ * When a user sets category X to probability P, the remaining categories
72
+ * must be adjusted so all probabilities sum to 1.0. This function scales
73
+ * the non-target categories proportionally.
74
+ *
75
+ * @param currentProbabilities Map of category label -> current probability.
76
+ * @param targetLabel The category label being set.
77
+ * @param targetProbability The new probability for the target category.
78
+ * @returns A new map with all probabilities summing to 1.0.
79
+ *
80
+ * @throws {InvalidOrder} If the target category doesn't exist or redistribution
81
+ * is impossible (e.g., target = 1.0 with other categories).
82
+ */
83
+ function redistributeProbabilities(currentProbabilities, targetLabel, targetProbability) {
84
+ const labels = Object.keys(currentProbabilities);
85
+ if (!labels.includes(targetLabel)) {
86
+ throw new errors_1.InvalidOrder(`Category "${targetLabel}" not found. Available categories: ${labels.join(", ")}`, "Metaculus");
87
+ }
88
+ if (labels.length < 2) {
89
+ return { [targetLabel]: 1.0 };
90
+ }
91
+ const remaining = 1.0 - targetProbability;
92
+ if (remaining <= 0) {
93
+ throw new errors_1.InvalidOrder(`Cannot set probability to ${targetProbability}: other categories would have zero or negative probability. `
94
+ + "Use a value less than 1.0.", "Metaculus");
95
+ }
96
+ // Sum of current probabilities for non-target categories
97
+ const otherSum = labels.reduce((sum, label) => {
98
+ return label === targetLabel ? sum : sum + (currentProbabilities[label] ?? 0);
99
+ }, 0);
100
+ const result = {};
101
+ if (otherSum <= 0) {
102
+ // All other categories are at zero -- distribute evenly
103
+ const otherCount = labels.length - 1;
104
+ const each = remaining / otherCount;
105
+ for (const label of labels) {
106
+ result[label] = label === targetLabel ? targetProbability : each;
107
+ }
108
+ }
109
+ else {
110
+ // Proportional redistribution
111
+ const scale = remaining / otherSum;
112
+ for (const label of labels) {
113
+ result[label] = label === targetLabel
114
+ ? targetProbability
115
+ : (currentProbabilities[label] ?? 0) * scale;
116
+ }
117
+ }
118
+ // Normalize to fix floating-point drift: adjust the largest non-target category
119
+ const sum = Object.values(result).reduce((a, b) => a + b, 0);
120
+ const drift = sum - 1.0;
121
+ if (Math.abs(drift) > 1e-12) {
122
+ const largest = labels
123
+ .filter((l) => l !== targetLabel)
124
+ .sort((a, b) => result[b] - result[a])[0];
125
+ if (largest) {
126
+ result[largest] -= drift;
127
+ }
128
+ }
129
+ return result;
130
+ }
131
+ // ---------------------------------------------------------------------------
132
+ // Synthetic Order Builder
133
+ // ---------------------------------------------------------------------------
134
+ /**
135
+ * Build a synthetic Order from forecast parameters.
136
+ *
137
+ * Metaculus forecasts are instant (no pending/open state), so the returned
138
+ * order always has status "filled". The order ID is a generated string
139
+ * since Metaculus doesn't return order IDs.
140
+ */
141
+ function buildSyntheticOrder(params, status) {
142
+ return {
143
+ id: `mc-${params.marketId}-${Date.now()}`,
144
+ marketId: params.marketId,
145
+ outcomeId: params.outcomeId,
146
+ side: "buy",
147
+ type: "market",
148
+ price: params.price,
149
+ amount: 1,
150
+ status,
151
+ filled: status === "filled" ? 1 : 0,
152
+ remaining: 0,
153
+ timestamp: Date.now(),
154
+ };
155
+ }
156
+ /**
157
+ * Submit a forecast on Metaculus, mapped from the unified createOrder interface.
158
+ *
159
+ * ## How the mapping works
160
+ *
161
+ * Metaculus is a reputation-based forecasting platform, not a financial exchange.
162
+ * "Creating an order" means submitting a probability forecast on a question.
163
+ *
164
+ * | CreateOrderParams field | Metaculus meaning |
165
+ * |------------------------|-------------------|
166
+ * | `marketId` | Post ID (for reference only) |
167
+ * | `outcomeId` | Encodes the question ID + type (see {@link parseOutcomeId}) |
168
+ * | `price` | The probability to forecast (0-1 exclusive) |
169
+ * | `side` | Ignored -- forecasts are always "buy" (you submit a belief) |
170
+ * | `type` | Ignored -- forecasts execute instantly (always "market") |
171
+ * | `amount` | Ignored -- Metaculus has no stake size |
172
+ *
173
+ * The returned {@link Order} is synthetic: Metaculus doesn't return order IDs
174
+ * or track fill state. The order is always immediately "filled".
175
+ *
176
+ * ## Supported question types
177
+ *
178
+ * - **Binary**: Sets `probability_yes` directly from `price`.
179
+ * - **Multiple-choice**: Sets the target category's probability and
180
+ * redistributes others proportionally to sum to 1.0.
181
+ * - **Continuous/numeric/date**: NOT supported -- throws {@link InvalidOrder}
182
+ * because a 201-point CDF cannot be expressed as a single price value.
183
+ *
184
+ * ## Authentication
185
+ *
186
+ * Requires a Metaculus API token. Pass `{ apiToken: "..." }` when constructing
187
+ * the MetaculusExchange. Without a token, this method throws
188
+ * {@link AuthenticationError}.
189
+ *
190
+ * @throws {AuthenticationError} If no API token is configured.
191
+ * @throws {InvalidOrder} If the question type is continuous or the price is invalid.
192
+ * @throws {ValidationError} If the outcomeId format is unrecognizable.
193
+ */
194
+ async function createOrder(params, ctx) {
195
+ try {
196
+ // 1. Validate auth
197
+ const headers = ctx.getAuthHeaders();
198
+ if (!headers.Authorization) {
199
+ throw new errors_1.AuthenticationError('Metaculus forecast submission requires authentication. '
200
+ + 'Pass { apiToken: "..." } when constructing MetaculusExchange.', "Metaculus");
201
+ }
202
+ // 2. Parse outcomeId to determine question type
203
+ const parsed = parseOutcomeId(params.outcomeId);
204
+ // 3. Continuous questions can't be traded via createOrder
205
+ if (parsed.type === "continuous") {
206
+ throw new errors_1.InvalidOrder("Continuous/numeric/date questions cannot be traded via createOrder. "
207
+ + "These require a 201-point CDF which cannot be expressed as a single price. "
208
+ + "Use the Metaculus API directly for continuous forecasts.", "Metaculus");
209
+ }
210
+ // 4. Validate price
211
+ const probability = validateProbability(params.price);
212
+ // 5. Log warnings for params that don't apply to Metaculus
213
+ if (params.side && params.side !== "buy") {
214
+ console.warn(`[pmxt/Metaculus] Ignoring side="${params.side}" -- Metaculus forecasts are probability submissions, not buy/sell. `
215
+ + "Set the probability via the `price` parameter instead.");
216
+ }
217
+ if (params.type && params.type !== "market") {
218
+ console.warn(`[pmxt/Metaculus] Ignoring type="${params.type}" -- Metaculus forecasts execute instantly (no limit orders).`);
219
+ }
220
+ // 6. Build the forecast payload
221
+ let payload;
222
+ if (parsed.type === "binary") {
223
+ payload = [{ question: parsed.questionId, probability_yes: probability }];
224
+ }
225
+ else {
226
+ // Multiple-choice: need current probabilities to redistribute
227
+ if (!ctx.fetchOutcomes) {
228
+ throw new errors_1.InvalidOrder("Multiple-choice forecast requires market outcome data but fetchOutcomes is not available.", "Metaculus");
229
+ }
230
+ const outcomes = await ctx.fetchOutcomes(params.marketId);
231
+ const mcOutcomes = outcomes.filter((o) => o.metadata?.question_type === "multiple_choice"
232
+ && o.metadata?.question_id === parsed.questionId);
233
+ if (mcOutcomes.length === 0) {
234
+ throw new errors_1.InvalidOrder(`No multiple-choice outcomes found for question ${parsed.questionId}. `
235
+ + "Ensure the market has been fetched and the outcomeId is correct.", "Metaculus");
236
+ }
237
+ // Build current probability map from outcome labels
238
+ const currentProbs = {};
239
+ for (const o of mcOutcomes) {
240
+ currentProbs[o.label] = o.price;
241
+ }
242
+ // Find the target category by index
243
+ const targetOutcome = mcOutcomes.find((o) => o.metadata?.choice_index === parsed.categoryIndex);
244
+ if (!targetOutcome) {
245
+ throw new errors_1.InvalidOrder(`Category index ${parsed.categoryIndex} not found for question ${parsed.questionId}. `
246
+ + `Available indices: 0-${mcOutcomes.length - 1}.`, "Metaculus");
247
+ }
248
+ const redistributed = redistributeProbabilities(currentProbs, targetOutcome.label, probability);
249
+ payload = [{
250
+ question: parsed.questionId,
251
+ probability_yes_per_category: redistributed,
252
+ }];
253
+ }
254
+ // 7. POST directly to the forecast endpoint.
255
+ // We bypass callApi because the Metaculus forecast API expects an
256
+ // array body, but the implicit API infrastructure always sends objects.
257
+ await ctx.http.request({
258
+ method: "POST",
259
+ url: `${utils_1.BASE_URL}/questions/forecast/`,
260
+ data: payload,
261
+ headers: { "Content-Type": "application/json", ...headers },
262
+ });
263
+ // 8. Return synthetic order
264
+ return buildSyntheticOrder(params, "filled");
265
+ }
266
+ catch (error) {
267
+ // Re-throw pmxt errors directly; map everything else
268
+ if (error.statusCode)
269
+ throw error;
270
+ throw errors_2.metaculusErrorMapper.mapError(error);
271
+ }
272
+ }
@@ -0,0 +1,21 @@
1
+ import { ErrorMapper } from '../../utils/error-mapper';
2
+ import { NotFound, BadRequest, BaseError } from '../../errors';
3
+ /**
4
+ * Metaculus-specific error mapper.
5
+ *
6
+ * Extends the base error mapper with:
7
+ * - 404: question/market not-found detection
8
+ * - 401: actionable message pointing users to pass { apiToken }
9
+ * - 403: distinguishes missing auth (-> AuthenticationError) from insufficient permissions
10
+ * - 400: probability validation errors from the forecast API
11
+ */
12
+ export declare class MetaculusErrorMapper extends ErrorMapper {
13
+ constructor();
14
+ protected mapNotFoundError(message: string, _data: any): NotFound;
15
+ protected mapBadRequestError(message: string, data: any): BadRequest;
16
+ /**
17
+ * Override the top-level mapByStatusCode for Metaculus-specific auth messages.
18
+ */
19
+ protected mapByStatusCode(status: number, message: string, data: any, response?: any): BaseError;
20
+ }
21
+ export declare const metaculusErrorMapper: MetaculusErrorMapper;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.metaculusErrorMapper = exports.MetaculusErrorMapper = void 0;
4
+ const error_mapper_1 = require("../../utils/error-mapper");
5
+ const errors_1 = require("../../errors");
6
+ /**
7
+ * Metaculus-specific error mapper.
8
+ *
9
+ * Extends the base error mapper with:
10
+ * - 404: question/market not-found detection
11
+ * - 401: actionable message pointing users to pass { apiToken }
12
+ * - 403: distinguishes missing auth (-> AuthenticationError) from insufficient permissions
13
+ * - 400: probability validation errors from the forecast API
14
+ */
15
+ class MetaculusErrorMapper extends error_mapper_1.ErrorMapper {
16
+ constructor() {
17
+ super('Metaculus');
18
+ }
19
+ mapNotFoundError(message, _data) {
20
+ const lower = message.toLowerCase();
21
+ if (lower.includes('question') || lower.includes('market')) {
22
+ const match = message.match(/[\d]+/);
23
+ const id = match ? match[0] : 'unknown';
24
+ return new errors_1.MarketNotFound(id, this.exchangeName);
25
+ }
26
+ return new errors_1.NotFound(message, this.exchangeName);
27
+ }
28
+ mapBadRequestError(message, data) {
29
+ const lower = message.toLowerCase();
30
+ // Probability validation errors from the forecast API
31
+ if (lower.includes('probability') ||
32
+ lower.includes('continuous_cdf') ||
33
+ lower.includes('forecast')) {
34
+ return new errors_1.InvalidOrder(`Metaculus forecast rejected: ${message}`, this.exchangeName);
35
+ }
36
+ return super.mapBadRequestError(message, data);
37
+ }
38
+ /**
39
+ * Override the top-level mapByStatusCode for Metaculus-specific auth messages.
40
+ */
41
+ mapByStatusCode(status, message, data, response) {
42
+ if (status === 401) {
43
+ return new errors_1.AuthenticationError('Metaculus API token required. Pass { apiToken: "..." } when constructing MetaculusExchange.', this.exchangeName);
44
+ }
45
+ if (status === 403) {
46
+ const lower = message.toLowerCase();
47
+ // Metaculus returns 403 both for missing auth and insufficient permissions.
48
+ // Distinguish by checking if the message mentions authentication.
49
+ if (lower.includes('authenticated') || lower.includes('api token') || lower.includes('log in')) {
50
+ return new errors_1.AuthenticationError('Metaculus API token required. Pass { apiToken: "..." } when constructing MetaculusExchange.', this.exchangeName);
51
+ }
52
+ return new errors_1.PermissionDenied('You do not have permission for this operation. '
53
+ + 'Check your Metaculus account permissions and API token scope.', this.exchangeName);
54
+ }
55
+ return super.mapByStatusCode(status, message, data, response);
56
+ }
57
+ }
58
+ exports.MetaculusErrorMapper = MetaculusErrorMapper;
59
+ exports.metaculusErrorMapper = new MetaculusErrorMapper();
@@ -0,0 +1,5 @@
1
+ import { EventFetchParams } from "../../BaseExchange";
2
+ import { UnifiedEvent } from "../../types";
3
+ type CallApi = (operationId: string, params?: Record<string, any>) => Promise<any>;
4
+ export declare function fetchEvents(params: EventFetchParams, callApi: CallApi): Promise<UnifiedEvent[]>;
5
+ export {};