pmxt-core 2.22.2 → 2.23.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 (38) 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/probable/api.d.ts +1 -1
  33. package/dist/exchanges/probable/api.js +1 -1
  34. package/dist/exchanges/probable/auth.js +5 -2
  35. package/dist/index.d.ts +4 -0
  36. package/dist/index.js +5 -1
  37. package/dist/server/app.js +10 -1
  38. package/package.json +3 -3
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchEvents = fetchEvents;
4
+ const utils_1 = require("./utils");
5
+ const errors_1 = require("./errors");
6
+ const BATCH_SIZE = 100;
7
+ const MAX_PAGES = 200;
8
+ /**
9
+ * Map pmxt status values to Metaculus `statuses` array param.
10
+ */
11
+ function toApiStatuses(status) {
12
+ if (!status || status === "all")
13
+ return undefined;
14
+ if (status === "closed" || status === "inactive")
15
+ return ["closed", "resolved"];
16
+ return ["open"];
17
+ }
18
+ /**
19
+ * Fetch pages of posts with pagination.
20
+ */
21
+ async function fetchPostPages(callApi, apiParams, targetCount) {
22
+ let all = [];
23
+ let offset = 0;
24
+ let page = 0;
25
+ do {
26
+ const data = await callApi("GetPosts", {
27
+ ...apiParams,
28
+ limit: BATCH_SIZE,
29
+ offset,
30
+ });
31
+ const results = data.results ?? [];
32
+ if (results.length === 0)
33
+ break;
34
+ all = all.concat(results);
35
+ offset += results.length;
36
+ page++;
37
+ if (targetCount && all.length >= targetCount)
38
+ break;
39
+ if (!data.next)
40
+ break;
41
+ } while (page < MAX_PAGES);
42
+ return all;
43
+ }
44
+ /**
45
+ * Wrap a single Metaculus Post as a UnifiedEvent.
46
+ *
47
+ * For single-question posts, the event contains one market.
48
+ * For group-of-questions posts, the event contains one market per sub-question
49
+ * (expanded via expandPost).
50
+ */
51
+ function postToEvent(post) {
52
+ const markets = (0, utils_1.expandPost)(post);
53
+ if (markets.length === 0)
54
+ return null;
55
+ const id = String(post.id);
56
+ return {
57
+ id,
58
+ title: post.title ?? "",
59
+ description: post.question?.description
60
+ ?? post.group_of_questions?.description
61
+ ?? post.question?.resolution_criteria
62
+ ?? "",
63
+ slug: post.slug ?? post.url_title ?? id,
64
+ markets,
65
+ volume24h: 0,
66
+ volume: 0,
67
+ url: `https://www.metaculus.com/questions/${id}/`,
68
+ image: post.projects?.default_project?.header_image ?? undefined,
69
+ category: post?.projects?.category?.[0] != null
70
+ ? typeof post.projects.category[0] === "string"
71
+ ? post.projects.category[0]
72
+ : post.projects.category[0]?.name
73
+ : undefined,
74
+ tags: markets[0]?.tags ?? [],
75
+ };
76
+ }
77
+ /**
78
+ * Fetch a single post by numeric ID and return it as a UnifiedEvent.
79
+ */
80
+ async function fetchEventByPostId(id, callApi) {
81
+ const numericId = parseInt(id, 10);
82
+ if (isNaN(numericId))
83
+ return [];
84
+ const data = await callApi("GetPost", { postId: numericId });
85
+ if (!data || !data.id)
86
+ return [];
87
+ const event = postToEvent(data);
88
+ return event ? [event] : [];
89
+ }
90
+ /**
91
+ * Look up an event by slug -- try numeric ID first, then tournament slug,
92
+ * then client-side slug match.
93
+ */
94
+ async function fetchEventBySlug(slug, callApi) {
95
+ // Try as numeric post ID
96
+ const byId = await fetchEventByPostId(slug, callApi);
97
+ if (byId.length > 0)
98
+ return byId;
99
+ // Try as tournament-slug filter -- fetch posts belonging to that tournament
100
+ try {
101
+ const posts = await fetchPostPages(callApi, { tournaments: [slug], with_cp: true, order_by: "-forecasts_count" }, 100);
102
+ if (posts.length > 0) {
103
+ // Represent the whole tournament as a single event whose markets
104
+ // are the individual posts (and their sub-questions, expanded)
105
+ const markets = posts.flatMap((p) => (0, utils_1.expandPost)(p, slug));
106
+ return [
107
+ {
108
+ id: slug,
109
+ title: slug,
110
+ description: "",
111
+ slug,
112
+ markets,
113
+ volume24h: 0,
114
+ volume: 0,
115
+ url: `https://www.metaculus.com/tournament/${slug}/`,
116
+ image: undefined,
117
+ category: undefined,
118
+ tags: [],
119
+ },
120
+ ];
121
+ }
122
+ }
123
+ catch {
124
+ // fall through
125
+ }
126
+ // Finally try slug match against post.slug / post.url_title
127
+ const posts = await fetchPostPages(callApi, { with_cp: true, order_by: "-forecasts_count" }, 500);
128
+ const lower = slug.toLowerCase();
129
+ for (const p of posts) {
130
+ if ((p.slug ?? "").toLowerCase() === lower ||
131
+ (p.url_title ?? "").toLowerCase() === lower) {
132
+ const event = postToEvent(p);
133
+ return event ? [event] : [];
134
+ }
135
+ }
136
+ return [];
137
+ }
138
+ async function fetchEvents(params, callApi) {
139
+ try {
140
+ // Direct lookup by slug (post ID, tournament slug, or url_title)
141
+ if (params.slug) {
142
+ return await fetchEventBySlug(params.slug, callApi);
143
+ }
144
+ // Direct lookup by eventId (post ID or tournament slug)
145
+ if (params.eventId) {
146
+ // Try as numeric post ID first
147
+ const byId = await fetchEventByPostId(params.eventId, callApi);
148
+ if (byId.length > 0)
149
+ return byId;
150
+ // Try as tournament slug
151
+ return await fetchEventBySlug(params.eventId, callApi);
152
+ }
153
+ // Default listing -- wrap posts as standalone events
154
+ const limit = params?.limit ?? 50;
155
+ const offset = params?.offset ?? 0;
156
+ const query = (params?.query ?? "").toLowerCase();
157
+ const statuses = toApiStatuses(params?.status);
158
+ const apiParams = {
159
+ with_cp: true,
160
+ };
161
+ if (statuses)
162
+ apiParams.statuses = statuses;
163
+ // Sort mapping
164
+ if (params?.sort === "newest") {
165
+ apiParams.order_by = "-published_at";
166
+ }
167
+ else {
168
+ apiParams.order_by = "-forecasts_count";
169
+ }
170
+ const posts = await fetchPostPages(callApi, apiParams, (offset + limit) * (query ? 5 : 1));
171
+ // Client-side keyword filter
172
+ const filtered = query
173
+ ? posts.filter((p) => (p.title ?? "").toLowerCase().includes(query) ||
174
+ (p.question?.description ?? "").toLowerCase().includes(query))
175
+ : posts;
176
+ const events = [];
177
+ for (const p of filtered.slice(offset, offset + limit)) {
178
+ const e = postToEvent(p);
179
+ if (e)
180
+ events.push(e);
181
+ }
182
+ return events;
183
+ }
184
+ catch (error) {
185
+ throw errors_1.metaculusErrorMapper.mapError(error);
186
+ }
187
+ }
@@ -0,0 +1,6 @@
1
+ import { MarketFetchParams } from "../../BaseExchange";
2
+ import { UnifiedMarket } from "../../types";
3
+ type CallApi = (operationId: string, params?: Record<string, any>) => Promise<any>;
4
+ export declare function resetCache(): void;
5
+ export declare function fetchMarkets(params: MarketFetchParams | undefined, callApi: CallApi): Promise<UnifiedMarket[]>;
6
+ export {};
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resetCache = resetCache;
4
+ exports.fetchMarkets = fetchMarkets;
5
+ const utils_1 = require("./utils");
6
+ const errors_1 = require("./errors");
7
+ const BATCH_SIZE = 100; // max per page
8
+ const MAX_PAGES = 200; // safety cap (~20 000 posts)
9
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
10
+ // Module-level cache for the default active-posts listing
11
+ let cachedPosts = null;
12
+ let lastCacheTime = 0;
13
+ function resetCache() {
14
+ cachedPosts = null;
15
+ lastCacheTime = 0;
16
+ }
17
+ /**
18
+ * Map pmxt status values to Metaculus `statuses` array param.
19
+ */
20
+ function toApiStatuses(status) {
21
+ if (!status || status === "all")
22
+ return undefined;
23
+ if (status === "closed" || status === "inactive")
24
+ return ["closed", "resolved"];
25
+ return ["open"]; // "active" or anything else -> open
26
+ }
27
+ /**
28
+ * Fetch pages of posts from /api/posts/ using offset-based pagination.
29
+ *
30
+ * Note: group-of-questions posts expand into multiple markets, so the
31
+ * actual number of markets may exceed the number of raw posts fetched.
32
+ * The targetCount is a rough guide, not exact.
33
+ */
34
+ async function fetchPostPages(callApi, apiParams, targetCount) {
35
+ let all = [];
36
+ let offset = 0;
37
+ let page = 0;
38
+ do {
39
+ const data = await callApi("GetPosts", {
40
+ ...apiParams,
41
+ limit: BATCH_SIZE,
42
+ offset,
43
+ });
44
+ const results = data.results ?? [];
45
+ if (results.length === 0)
46
+ break;
47
+ all = all.concat(results);
48
+ offset += results.length;
49
+ page++;
50
+ // Early-exit when we have enough results (with buffer for filtering)
51
+ if (targetCount && all.length >= targetCount * 1.5)
52
+ break;
53
+ if (!data.next)
54
+ break;
55
+ } while (page < MAX_PAGES);
56
+ return all;
57
+ }
58
+ /**
59
+ * Expand a list of raw posts into UnifiedMarket[], handling both
60
+ * single-question and group-of-questions posts.
61
+ */
62
+ function expandPosts(posts, eventId) {
63
+ const markets = [];
64
+ for (const p of posts) {
65
+ markets.push(...(0, utils_1.expandPost)(p, eventId));
66
+ }
67
+ return markets;
68
+ }
69
+ /**
70
+ * Fetch a single post by numeric ID and expand it.
71
+ * A group post will return multiple markets (one per sub-question).
72
+ */
73
+ async function fetchMarketById(id, callApi) {
74
+ const numericId = parseInt(id, 10);
75
+ if (isNaN(numericId))
76
+ return [];
77
+ const data = await callApi("GetPost", { postId: numericId });
78
+ if (!data || !data.id)
79
+ return [];
80
+ return (0, utils_1.expandPost)(data);
81
+ }
82
+ /**
83
+ * Search posts by keyword -- the Metaculus /api/posts/ has no server-side
84
+ * `search` param, so we fetch a batch of recent open posts and filter
85
+ * client-side by title/description match.
86
+ */
87
+ async function searchMarkets(query, params, callApi) {
88
+ const limit = params?.limit ?? 200;
89
+ const statuses = toApiStatuses(params?.status);
90
+ const apiParams = {
91
+ order_by: "-forecasts_count",
92
+ with_cp: true,
93
+ };
94
+ if (statuses)
95
+ apiParams.statuses = statuses;
96
+ // Fetch enough posts to give the client-side filter something to work with
97
+ const posts = await fetchPostPages(callApi, apiParams, Math.max(limit * 5, 500));
98
+ const lower = query.toLowerCase();
99
+ const markets = [];
100
+ for (const p of posts) {
101
+ const title = (p.title ?? "").toLowerCase();
102
+ const desc = (p.question?.description ?? "").toLowerCase();
103
+ if (title.includes(lower) || desc.includes(lower)) {
104
+ markets.push(...(0, utils_1.expandPost)(p));
105
+ }
106
+ if (markets.length >= limit)
107
+ break;
108
+ }
109
+ return markets.slice(0, limit);
110
+ }
111
+ async function fetchMarketsDefault(params, callApi) {
112
+ const limit = params?.limit ?? 100;
113
+ const offset = params?.offset ?? 0;
114
+ const now = Date.now();
115
+ const statuses = toApiStatuses(params?.status ?? "active");
116
+ const useCache = (!params?.status || params.status === "active") && !params?.sort;
117
+ let posts;
118
+ if (useCache && cachedPosts && now - lastCacheTime < CACHE_TTL) {
119
+ posts = cachedPosts;
120
+ }
121
+ else {
122
+ const apiParams = {
123
+ with_cp: true,
124
+ };
125
+ if (statuses)
126
+ apiParams.statuses = statuses;
127
+ // Map sort to the new order_by enum values
128
+ if (params?.sort === "newest") {
129
+ apiParams.order_by = "-published_at";
130
+ }
131
+ else {
132
+ apiParams.order_by = "-forecasts_count";
133
+ }
134
+ const fetchLimit = params?.sort === "volume" || params?.sort === "liquidity"
135
+ ? 2000
136
+ : limit + offset;
137
+ posts = await fetchPostPages(callApi, apiParams, fetchLimit);
138
+ if (useCache && posts.length >= 100) {
139
+ cachedPosts = posts;
140
+ lastCacheTime = now;
141
+ }
142
+ }
143
+ const markets = expandPosts(posts);
144
+ if (params?.sort === "liquidity") {
145
+ markets.sort((a, b) => b.liquidity - a.liquidity);
146
+ }
147
+ return markets.slice(offset, offset + limit);
148
+ }
149
+ async function fetchMarkets(params, callApi) {
150
+ try {
151
+ // Direct lookup by numeric post/question ID
152
+ if (params?.marketId) {
153
+ return await fetchMarketById(params.marketId, callApi);
154
+ }
155
+ // outcomeId pattern: "<questionId>-YES" / "<questionId>-NO" / "<questionId>-<idx>"
156
+ if (params?.outcomeId) {
157
+ const id = params.outcomeId.split("-")[0];
158
+ return await fetchMarketById(id, callApi);
159
+ }
160
+ // slug: try as numeric ID first (Metaculus slugs are typically words,
161
+ // but callers may pass the numeric post ID as a slug)
162
+ if (params?.slug) {
163
+ const byId = await fetchMarketById(params.slug, callApi);
164
+ if (byId.length > 0)
165
+ return byId;
166
+ // Fall back to slug-string match against post.slug / post.url_title
167
+ const posts = await fetchPostPages(callApi, { with_cp: true, order_by: "-forecasts_count" }, 500);
168
+ const lower = params.slug.toLowerCase();
169
+ for (const p of posts) {
170
+ if ((p.slug ?? "").toLowerCase() === lower ||
171
+ (p.url_title ?? "").toLowerCase() === lower) {
172
+ return (0, utils_1.expandPost)(p);
173
+ }
174
+ }
175
+ return [];
176
+ }
177
+ // eventId is a tournament slug -- filter posts by that tournament
178
+ if (params?.eventId) {
179
+ const apiParams = {
180
+ tournaments: [params.eventId],
181
+ with_cp: true,
182
+ order_by: "-forecasts_count",
183
+ };
184
+ const posts = await fetchPostPages(callApi, apiParams, params?.limit ?? 1000);
185
+ const markets = expandPosts(posts);
186
+ return markets.slice(0, params?.limit ?? markets.length);
187
+ }
188
+ // Keyword search -- client-side filter (no server-side search param)
189
+ if (params?.query) {
190
+ return await searchMarkets(params.query, params, callApi);
191
+ }
192
+ // Default: recent active posts ordered by forecast count
193
+ return await fetchMarketsDefault(params, callApi);
194
+ }
195
+ catch (error) {
196
+ throw errors_1.metaculusErrorMapper.mapError(error);
197
+ }
198
+ }
@@ -0,0 +1,105 @@
1
+ import { PredictionMarketExchange, MarketFetchParams, EventFetchParams, ExchangeCredentials } from "../../BaseExchange";
2
+ import { UnifiedMarket, UnifiedEvent, CreateOrderParams, Order } from "../../types";
3
+ /**
4
+ * Metaculus exchange integration.
5
+ *
6
+ * Metaculus is a reputation-based forecasting platform. Unlike CLOB exchanges
7
+ * (Polymarket, Kalshi), there are no financial stakes -- users submit
8
+ * probability forecasts and earn reputation points scored on accuracy.
9
+ *
10
+ * ## Supported operations
11
+ *
12
+ * - **fetchMarkets / fetchEvents**: Browse questions, community predictions,
13
+ * and tournament structures. Group-of-questions posts are automatically
14
+ * expanded into individual sub-question markets.
15
+ *
16
+ * - **createOrder**: Submit a probability forecast on a question.
17
+ * Maps `price` (0-1 exclusive) to `probability_yes`. The `side`, `type`,
18
+ * and `amount` params are ignored since Metaculus forecasts are not
19
+ * buy/sell orders. See {@link createOrder} for details.
20
+ *
21
+ * - **cancelOrder**: Withdraw a forecast from a question. Pass the Metaculus
22
+ * question ID as the orderId.
23
+ *
24
+ * ## Authentication
25
+ *
26
+ * Pass `{ apiToken: "..." }` from your Metaculus account settings.
27
+ * All API operations require a token -- Metaculus no longer allows
28
+ * unauthenticated access to any endpoint.
29
+ *
30
+ * ## Question types
31
+ *
32
+ * | Type | fetchMarkets | createOrder |
33
+ * |------|-------------|-------------|
34
+ * | Binary | Yes (YES/NO outcomes) | Yes (`price` = probability_yes) |
35
+ * | Multiple-choice | Yes (one outcome per option) | Yes (redistributes other categories) |
36
+ * | Group-of-questions | Yes (expanded to sub-question markets) | Yes (per sub-question) |
37
+ * | Continuous/numeric/date | Yes (read-only HIGHER/LOWER) | No (requires 201-point CDF) |
38
+ */
39
+ export declare class MetaculusExchange extends PredictionMarketExchange {
40
+ readonly has: {
41
+ fetchMarkets: true;
42
+ fetchEvents: true;
43
+ createOrder: true;
44
+ cancelOrder: true;
45
+ fetchOHLCV: false;
46
+ fetchOrderBook: false;
47
+ fetchTrades: false;
48
+ fetchOrder: false;
49
+ fetchOpenOrders: false;
50
+ fetchPositions: false;
51
+ fetchBalance: false;
52
+ watchAddress: false;
53
+ unwatchAddress: false;
54
+ watchOrderBook: false;
55
+ watchTrades: false;
56
+ fetchMyTrades: false;
57
+ fetchClosedOrders: false;
58
+ fetchAllOrders: false;
59
+ buildOrder: false;
60
+ submitOrder: false;
61
+ };
62
+ private readonly apiToken?;
63
+ constructor(credentials?: ExchangeCredentials);
64
+ get name(): string;
65
+ protected mapImplicitApiError(error: any): any;
66
+ /**
67
+ * Sign requests with an API token when one is provided.
68
+ * Metaculus uses token-based auth: `Authorization: Token <token>`.
69
+ */
70
+ protected sign(_method: string, _path: string, _params: Record<string, any>): Record<string, string>;
71
+ /**
72
+ * Get auth headers, throwing if no token is configured.
73
+ * Used by trading methods that require authentication.
74
+ */
75
+ private getAuthHeaders;
76
+ protected fetchMarketsImpl(params?: MarketFetchParams): Promise<UnifiedMarket[]>;
77
+ protected fetchEventsImpl(params: EventFetchParams): Promise<UnifiedEvent[]>;
78
+ /**
79
+ * Submit a probability forecast on a Metaculus question.
80
+ *
81
+ * Maps from the unified `createOrder` interface:
82
+ * - `price` -> the probability to forecast (0-1 exclusive)
83
+ * - `outcomeId` -> encodes the question ID and type
84
+ * - `side`, `type`, `amount` -> ignored (Metaculus forecasts are not orders)
85
+ *
86
+ * For binary questions, sets `probability_yes` directly.
87
+ * For multiple-choice, redistributes other categories proportionally.
88
+ * Continuous questions are not supported (throws InvalidOrder).
89
+ *
90
+ * @throws {AuthenticationError} If no API token is configured.
91
+ * @throws {InvalidOrder} If the question type is continuous or price is invalid.
92
+ */
93
+ createOrder(params: CreateOrderParams): Promise<Order>;
94
+ /**
95
+ * Withdraw a forecast from a Metaculus question.
96
+ *
97
+ * The `orderId` should be the Metaculus question ID (numeric).
98
+ * If you used createOrder, extract the question ID from the
99
+ * outcomeId (the part before the hyphen).
100
+ *
101
+ * @throws {AuthenticationError} If no API token is configured.
102
+ * @throws {ValidationError} If orderId is not a valid question ID.
103
+ */
104
+ cancelOrder(orderId: string): Promise<Order>;
105
+ }
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetaculusExchange = void 0;
4
+ const BaseExchange_1 = require("../../BaseExchange");
5
+ const errors_1 = require("../../errors");
6
+ const openapi_1 = require("../../utils/openapi");
7
+ const api_1 = require("./api");
8
+ const errors_2 = require("./errors");
9
+ const utils_1 = require("./utils");
10
+ const fetchMarkets_1 = require("./fetchMarkets");
11
+ const fetchEvents_1 = require("./fetchEvents");
12
+ const createOrder_1 = require("./createOrder");
13
+ const cancelOrder_1 = require("./cancelOrder");
14
+ /**
15
+ * Metaculus exchange integration.
16
+ *
17
+ * Metaculus is a reputation-based forecasting platform. Unlike CLOB exchanges
18
+ * (Polymarket, Kalshi), there are no financial stakes -- users submit
19
+ * probability forecasts and earn reputation points scored on accuracy.
20
+ *
21
+ * ## Supported operations
22
+ *
23
+ * - **fetchMarkets / fetchEvents**: Browse questions, community predictions,
24
+ * and tournament structures. Group-of-questions posts are automatically
25
+ * expanded into individual sub-question markets.
26
+ *
27
+ * - **createOrder**: Submit a probability forecast on a question.
28
+ * Maps `price` (0-1 exclusive) to `probability_yes`. The `side`, `type`,
29
+ * and `amount` params are ignored since Metaculus forecasts are not
30
+ * buy/sell orders. See {@link createOrder} for details.
31
+ *
32
+ * - **cancelOrder**: Withdraw a forecast from a question. Pass the Metaculus
33
+ * question ID as the orderId.
34
+ *
35
+ * ## Authentication
36
+ *
37
+ * Pass `{ apiToken: "..." }` from your Metaculus account settings.
38
+ * All API operations require a token -- Metaculus no longer allows
39
+ * unauthenticated access to any endpoint.
40
+ *
41
+ * ## Question types
42
+ *
43
+ * | Type | fetchMarkets | createOrder |
44
+ * |------|-------------|-------------|
45
+ * | Binary | Yes (YES/NO outcomes) | Yes (`price` = probability_yes) |
46
+ * | Multiple-choice | Yes (one outcome per option) | Yes (redistributes other categories) |
47
+ * | Group-of-questions | Yes (expanded to sub-question markets) | Yes (per sub-question) |
48
+ * | Continuous/numeric/date | Yes (read-only HIGHER/LOWER) | No (requires 201-point CDF) |
49
+ */
50
+ class MetaculusExchange extends BaseExchange_1.PredictionMarketExchange {
51
+ has = {
52
+ fetchMarkets: true,
53
+ fetchEvents: true,
54
+ createOrder: true,
55
+ cancelOrder: true,
56
+ // Metaculus is a forecasting platform -- no order book, no trading history
57
+ fetchOHLCV: false,
58
+ fetchOrderBook: false,
59
+ fetchTrades: false,
60
+ fetchOrder: false,
61
+ fetchOpenOrders: false,
62
+ fetchPositions: false,
63
+ fetchBalance: false,
64
+ watchAddress: false,
65
+ unwatchAddress: false,
66
+ watchOrderBook: false,
67
+ watchTrades: false,
68
+ fetchMyTrades: false,
69
+ fetchClosedOrders: false,
70
+ fetchAllOrders: false,
71
+ buildOrder: false,
72
+ submitOrder: false,
73
+ };
74
+ apiToken;
75
+ constructor(credentials) {
76
+ super(credentials);
77
+ this.apiToken = credentials?.apiToken;
78
+ // Rate-limit conservatively; authenticated users get higher Metaculus quotas
79
+ this.rateLimit = 500;
80
+ const descriptor = (0, openapi_1.parseOpenApiSpec)(api_1.metaculusApiSpec, utils_1.BASE_URL);
81
+ this.defineImplicitApi(descriptor);
82
+ }
83
+ get name() {
84
+ return "Metaculus";
85
+ }
86
+ mapImplicitApiError(error) {
87
+ throw errors_2.metaculusErrorMapper.mapError(error);
88
+ }
89
+ /**
90
+ * Sign requests with an API token when one is provided.
91
+ * Metaculus uses token-based auth: `Authorization: Token <token>`.
92
+ */
93
+ sign(_method, _path, _params) {
94
+ if (this.apiToken) {
95
+ return { Authorization: `Token ${this.apiToken}` };
96
+ }
97
+ return {};
98
+ }
99
+ /**
100
+ * Get auth headers, throwing if no token is configured.
101
+ * Used by trading methods that require authentication.
102
+ */
103
+ getAuthHeaders() {
104
+ if (!this.apiToken) {
105
+ throw new errors_1.AuthenticationError('Metaculus API token required for this operation. '
106
+ + 'Pass { apiToken: "..." } when constructing MetaculusExchange.', "Metaculus");
107
+ }
108
+ return { Authorization: `Token ${this.apiToken}` };
109
+ }
110
+ // -------------------------------------------------------------------------
111
+ // Market Data
112
+ // -------------------------------------------------------------------------
113
+ async fetchMarketsImpl(params) {
114
+ return (0, fetchMarkets_1.fetchMarkets)(params, this.callApi.bind(this));
115
+ }
116
+ async fetchEventsImpl(params) {
117
+ return (0, fetchEvents_1.fetchEvents)(params, this.callApi.bind(this));
118
+ }
119
+ // -------------------------------------------------------------------------
120
+ // Trading (Forecasting)
121
+ // -------------------------------------------------------------------------
122
+ /**
123
+ * Submit a probability forecast on a Metaculus question.
124
+ *
125
+ * Maps from the unified `createOrder` interface:
126
+ * - `price` -> the probability to forecast (0-1 exclusive)
127
+ * - `outcomeId` -> encodes the question ID and type
128
+ * - `side`, `type`, `amount` -> ignored (Metaculus forecasts are not orders)
129
+ *
130
+ * For binary questions, sets `probability_yes` directly.
131
+ * For multiple-choice, redistributes other categories proportionally.
132
+ * Continuous questions are not supported (throws InvalidOrder).
133
+ *
134
+ * @throws {AuthenticationError} If no API token is configured.
135
+ * @throws {InvalidOrder} If the question type is continuous or price is invalid.
136
+ */
137
+ async createOrder(params) {
138
+ const ctx = {
139
+ http: this.http,
140
+ getAuthHeaders: () => this.getAuthHeaders(),
141
+ fetchOutcomes: async (marketId) => {
142
+ const markets = await this.fetchMarkets({ marketId });
143
+ return markets.length > 0 ? markets[0].outcomes : [];
144
+ },
145
+ };
146
+ return (0, createOrder_1.createOrder)(params, ctx);
147
+ }
148
+ /**
149
+ * Withdraw a forecast from a Metaculus question.
150
+ *
151
+ * The `orderId` should be the Metaculus question ID (numeric).
152
+ * If you used createOrder, extract the question ID from the
153
+ * outcomeId (the part before the hyphen).
154
+ *
155
+ * @throws {AuthenticationError} If no API token is configured.
156
+ * @throws {ValidationError} If orderId is not a valid question ID.
157
+ */
158
+ async cancelOrder(orderId) {
159
+ const ctx = {
160
+ http: this.http,
161
+ getAuthHeaders: () => this.getAuthHeaders(),
162
+ };
163
+ return (0, cancelOrder_1.cancelOrder)(orderId, ctx);
164
+ }
165
+ }
166
+ exports.MetaculusExchange = MetaculusExchange;