pmxt-core 2.22.1 → 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,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 {};
|