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.
- package/dist/BaseExchange.d.ts +2 -0
- package/dist/exchanges/kalshi/api.d.ts +1 -1
- package/dist/exchanges/kalshi/api.js +1 -1
- package/dist/exchanges/limitless/api.d.ts +1 -1
- package/dist/exchanges/limitless/api.js +1 -1
- package/dist/exchanges/metaculus/api.d.ts +212 -0
- package/dist/exchanges/metaculus/api.js +418 -0
- package/dist/exchanges/metaculus/cancelOrder.d.ts +38 -0
- package/dist/exchanges/metaculus/cancelOrder.js +74 -0
- package/dist/exchanges/metaculus/createOrder.d.ts +107 -0
- package/dist/exchanges/metaculus/createOrder.js +272 -0
- package/dist/exchanges/metaculus/errors.d.ts +21 -0
- package/dist/exchanges/metaculus/errors.js +59 -0
- package/dist/exchanges/metaculus/fetchEvents.d.ts +5 -0
- package/dist/exchanges/metaculus/fetchEvents.js +187 -0
- package/dist/exchanges/metaculus/fetchMarkets.d.ts +6 -0
- package/dist/exchanges/metaculus/fetchMarkets.js +198 -0
- package/dist/exchanges/metaculus/index.d.ts +105 -0
- package/dist/exchanges/metaculus/index.js +166 -0
- package/dist/exchanges/metaculus/utils.d.ts +40 -0
- package/dist/exchanges/metaculus/utils.js +320 -0
- package/dist/exchanges/myriad/api.d.ts +1 -1
- package/dist/exchanges/myriad/api.js +1 -1
- package/dist/exchanges/opinion/api.d.ts +1 -1
- package/dist/exchanges/opinion/api.js +1 -1
- package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
- package/dist/exchanges/polymarket/api-clob.js +1 -1
- package/dist/exchanges/polymarket/api-data.d.ts +1 -1
- package/dist/exchanges/polymarket/api-data.js +1 -1
- package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
- package/dist/exchanges/polymarket/api-gamma.js +1 -1
- package/dist/exchanges/probable/api.d.ts +1 -1
- package/dist/exchanges/probable/api.js +1 -1
- package/dist/exchanges/probable/auth.js +5 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -1
- package/dist/server/app.js +10 -1
- 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;
|