feat: metaculus group questions, description from api
This commit is contained in:
parent
d684d074f5
commit
f5a3c16322
|
@ -1,283 +0,0 @@
|
||||||
/* Imports */
|
|
||||||
import Ajv, { JTDDataType } from "ajv/dist/jtd";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import { average } from "../../utils";
|
|
||||||
import { sleep } from "../utils/sleep";
|
|
||||||
import { FetchedQuestion, Platform } from "./";
|
|
||||||
|
|
||||||
/* Definitions */
|
|
||||||
const platformName = "metaculus";
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
const SLEEP_TIME = 5000;
|
|
||||||
|
|
||||||
const apiQuestionSchema = {
|
|
||||||
properties: {
|
|
||||||
page_url: {
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
publish_time: {
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
close_time: {
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
resolve_time: {
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
number_of_predictions: {
|
|
||||||
type: "uint32",
|
|
||||||
},
|
|
||||||
possibilities: {
|
|
||||||
properties: {
|
|
||||||
type: {
|
|
||||||
type: "string", // TODO - enum?
|
|
||||||
},
|
|
||||||
},
|
|
||||||
additionalProperties: true,
|
|
||||||
},
|
|
||||||
community_prediction: {
|
|
||||||
properties: {
|
|
||||||
full: {
|
|
||||||
properties: {
|
|
||||||
q1: {
|
|
||||||
type: "float64",
|
|
||||||
},
|
|
||||||
q2: {
|
|
||||||
type: "float64",
|
|
||||||
},
|
|
||||||
q3: {
|
|
||||||
type: "float64",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
additionalProperties: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
additionalProperties: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
additionalProperties: true,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const apiMultipleQuestionsSchema = {
|
|
||||||
properties: {
|
|
||||||
results: {
|
|
||||||
elements: apiQuestionSchema,
|
|
||||||
},
|
|
||||||
next: {
|
|
||||||
type: "string",
|
|
||||||
nullable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
additionalProperties: true,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type ApiQuestion = JTDDataType<typeof apiQuestionSchema>;
|
|
||||||
type ApiMultipleQuestions = JTDDataType<typeof apiMultipleQuestionsSchema>;
|
|
||||||
|
|
||||||
const validateApiQuestion = new Ajv().compile<ApiQuestion>(apiQuestionSchema);
|
|
||||||
const validateApiMultipleQuestions = new Ajv().compile<
|
|
||||||
JTDDataType<typeof apiMultipleQuestionsSchema>
|
|
||||||
>(apiMultipleQuestionsSchema);
|
|
||||||
|
|
||||||
async function fetchWithRetries<T = unknown>(url: string): Promise<T> {
|
|
||||||
try {
|
|
||||||
const response = await axios.get<T>(url);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error while fetching ${url}`);
|
|
||||||
console.log(error);
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
if (error.response?.headers["retry-after"]) {
|
|
||||||
const timeout = error.response.headers["retry-after"];
|
|
||||||
console.log(`Timeout: ${timeout}`);
|
|
||||||
await sleep(Number(timeout) * 1000 + SLEEP_TIME);
|
|
||||||
} else {
|
|
||||||
await sleep(SLEEP_TIME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const response = await axios.get<T>(url);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Support functions */
|
|
||||||
async function fetchApiQuestions(next: string): Promise<ApiMultipleQuestions> {
|
|
||||||
const data = await fetchWithRetries<object>(next);
|
|
||||||
if (validateApiMultipleQuestions(data)) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
throw new Error("Response validation failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchSingleApiQuestion(url: string): Promise<ApiQuestion> {
|
|
||||||
const data = await fetchWithRetries<object>(url);
|
|
||||||
if (validateApiQuestion(data)) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
throw new Error("Response validation failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchQuestionHtml(slug: string) {
|
|
||||||
return await fetchWithRetries<string>("https://www.metaculus.com" + slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchQuestionPage(slug: string) {
|
|
||||||
const questionPage = await fetchQuestionHtml(slug);
|
|
||||||
const isPublicFigurePrediction = questionPage.includes(
|
|
||||||
"A public prediction by"
|
|
||||||
);
|
|
||||||
|
|
||||||
let description: string = "";
|
|
||||||
if (!isPublicFigurePrediction) {
|
|
||||||
const match = questionPage.match(
|
|
||||||
/\s*window\.metacData\.question = (.+);\s*/
|
|
||||||
);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error("metacData not found");
|
|
||||||
}
|
|
||||||
description = JSON.parse(match[1]).description;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isPublicFigurePrediction,
|
|
||||||
description,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiQuestionToFetchedQuestion(
|
|
||||||
apiQuestion: ApiQuestion
|
|
||||||
): Promise<FetchedQuestion | null> {
|
|
||||||
if (apiQuestion.publish_time > now || now > apiQuestion.resolve_time) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
await sleep(SLEEP_TIME / 2);
|
|
||||||
|
|
||||||
const questionPage = await fetchQuestionPage(apiQuestion.page_url);
|
|
||||||
|
|
||||||
if (questionPage.isPublicFigurePrediction) {
|
|
||||||
console.log("- [Skipping public prediction]");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isBinary = apiQuestion.possibilities.type === "binary";
|
|
||||||
let options: FetchedQuestion["options"] = [];
|
|
||||||
if (isBinary) {
|
|
||||||
const probability = Number(apiQuestion.community_prediction.full.q2);
|
|
||||||
options = [
|
|
||||||
{
|
|
||||||
name: "Yes",
|
|
||||||
probability: probability,
|
|
||||||
type: "PROBABILITY",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "No",
|
|
||||||
probability: 1 - probability,
|
|
||||||
type: "PROBABILITY",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
const question: FetchedQuestion = {
|
|
||||||
id: `${platformName}-${apiQuestion.id}`,
|
|
||||||
title: apiQuestion.title,
|
|
||||||
url: "https://www.metaculus.com" + apiQuestion.page_url,
|
|
||||||
description: questionPage.description,
|
|
||||||
options,
|
|
||||||
qualityindicators: {
|
|
||||||
numforecasts: apiQuestion.number_of_predictions,
|
|
||||||
},
|
|
||||||
extra: {
|
|
||||||
resolution_data: {
|
|
||||||
publish_time: apiQuestion.publish_time,
|
|
||||||
resolution: apiQuestion.resolution,
|
|
||||||
close_time: apiQuestion.close_time,
|
|
||||||
resolve_time: apiQuestion.resolve_time,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
//"status": result.status,
|
|
||||||
//"publish_time": result.publish_time,
|
|
||||||
//"close_time": result.close_time,
|
|
||||||
//"type": result.possibilities.type, // We want binary ones here.
|
|
||||||
//"last_activity_time": result.last_activity_time,
|
|
||||||
};
|
|
||||||
if (apiQuestion.number_of_predictions < 10) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return question;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const metaculus: Platform<"id" | "debug"> = {
|
|
||||||
name: platformName,
|
|
||||||
label: "Metaculus",
|
|
||||||
color: "#006669",
|
|
||||||
version: "v2",
|
|
||||||
fetcherArgs: ["id", "debug"],
|
|
||||||
async fetcher(opts) {
|
|
||||||
let allQuestions: FetchedQuestion[] = [];
|
|
||||||
|
|
||||||
if (opts.args?.id) {
|
|
||||||
const apiQuestion = await fetchSingleApiQuestion(
|
|
||||||
`https://www.metaculus.com/api2/questions/${opts.args?.id}`
|
|
||||||
);
|
|
||||||
const question = await apiQuestionToFetchedQuestion(apiQuestion);
|
|
||||||
console.log(question);
|
|
||||||
return {
|
|
||||||
questions: question ? [question] : [],
|
|
||||||
partial: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let next: string | null = "https://www.metaculus.com/api2/questions/";
|
|
||||||
let i = 1;
|
|
||||||
while (next) {
|
|
||||||
if (i % 20 === 0) {
|
|
||||||
console.log("Sleeping for 500ms");
|
|
||||||
await sleep(SLEEP_TIME);
|
|
||||||
}
|
|
||||||
console.log(`\nQuery #${i}`);
|
|
||||||
|
|
||||||
const metaculusQuestions: ApiMultipleQuestions = await fetchApiQuestions(
|
|
||||||
next
|
|
||||||
);
|
|
||||||
const results = metaculusQuestions.results;
|
|
||||||
|
|
||||||
let j = false;
|
|
||||||
|
|
||||||
for (const result of results) {
|
|
||||||
const question = await apiQuestionToFetchedQuestion(result);
|
|
||||||
if (!question) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.log(`- ${question.title}`);
|
|
||||||
if ((!j && i % 20 === 0) || opts.args?.debug) {
|
|
||||||
console.log(question);
|
|
||||||
j = true;
|
|
||||||
}
|
|
||||||
allQuestions.push(question);
|
|
||||||
}
|
|
||||||
|
|
||||||
next = metaculusQuestions.next;
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
questions: allQuestions,
|
|
||||||
partial: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
calculateStars(data) {
|
|
||||||
const { numforecasts } = data.qualityindicators;
|
|
||||||
const nuno = () =>
|
|
||||||
(numforecasts || 0) > 300 ? 4 : (numforecasts || 0) > 100 ? 3 : 2;
|
|
||||||
const eli = () => 3;
|
|
||||||
const misha = () => 3;
|
|
||||||
const starsDecimal = average([nuno(), eli(), misha()]);
|
|
||||||
const starsInteger = Math.round(starsDecimal);
|
|
||||||
return starsInteger;
|
|
||||||
},
|
|
||||||
};
|
|
213
src/backend/platforms/metaculus/api.ts
Normal file
213
src/backend/platforms/metaculus/api.ts
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
import Ajv, { JTDDataType, ValidateFunction } from "ajv/dist/jtd";
|
||||||
|
import axios from "axios";
|
||||||
|
import { sleep } from "../../utils/sleep";
|
||||||
|
|
||||||
|
// Type examples:
|
||||||
|
// - group: https://www.metaculus.com/api2/questions/9866/
|
||||||
|
// - claim: https://www.metaculus.com/api2/questions/9668/
|
||||||
|
// - subquestion forecast: https://www.metaculus.com/api2/questions/10069/
|
||||||
|
// - basic forecast: https://www.metaculus.com/api2/questions/11005/
|
||||||
|
|
||||||
|
const RETRY_SLEEP_TIME = 5000;
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
id: {
|
||||||
|
type: "uint32",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const predictableProps = {
|
||||||
|
publish_time: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
close_time: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
resolve_time: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
resolution: {
|
||||||
|
type: "float64",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
possibilities: {
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
// Enum["binary", "continuous"], via https://github.com/quantified-uncertainty/metaforecast/pull/84#discussion_r878240875
|
||||||
|
// but metaculus might add new values in the future and we don't want the fetcher to break
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
number_of_predictions: {
|
||||||
|
type: "uint32",
|
||||||
|
},
|
||||||
|
community_prediction: {
|
||||||
|
properties: {
|
||||||
|
full: {
|
||||||
|
properties: {
|
||||||
|
q1: {
|
||||||
|
type: "float64",
|
||||||
|
},
|
||||||
|
q2: {
|
||||||
|
type: "float64",
|
||||||
|
},
|
||||||
|
q3: {
|
||||||
|
type: "float64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const pageProps = {
|
||||||
|
page_url: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
type: "uint32",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// these are missing in /api2/questions/ requests, and building two schemas is too much pain
|
||||||
|
const optionalPageProps = {
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
description_html: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const apiQuestionSchema = {
|
||||||
|
discriminator: "type",
|
||||||
|
mapping: {
|
||||||
|
forecast: {
|
||||||
|
properties: {
|
||||||
|
...commonProps,
|
||||||
|
...pageProps,
|
||||||
|
...predictableProps,
|
||||||
|
},
|
||||||
|
optionalProperties: {
|
||||||
|
...optionalPageProps,
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
properties: {
|
||||||
|
...commonProps,
|
||||||
|
...pageProps,
|
||||||
|
sub_questions: {
|
||||||
|
elements: {
|
||||||
|
properties: {
|
||||||
|
...commonProps,
|
||||||
|
...predictableProps,
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
optionalProperties: {
|
||||||
|
...optionalPageProps,
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
// we're not interested in claims currently (but we should be?)
|
||||||
|
claim: {
|
||||||
|
properties: {
|
||||||
|
...commonProps,
|
||||||
|
...pageProps,
|
||||||
|
},
|
||||||
|
optionalProperties: {
|
||||||
|
...optionalPageProps,
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const apiMultipleQuestionsSchema = {
|
||||||
|
properties: {
|
||||||
|
results: {
|
||||||
|
elements: apiQuestionSchema,
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ApiCommon = JTDDataType<{
|
||||||
|
properties: typeof commonProps;
|
||||||
|
}>;
|
||||||
|
export type ApiPredictable = JTDDataType<{
|
||||||
|
properties: typeof predictableProps;
|
||||||
|
}>;
|
||||||
|
export type ApiQuestion = JTDDataType<typeof apiQuestionSchema>;
|
||||||
|
export type ApiMultipleQuestions = JTDDataType<
|
||||||
|
typeof apiMultipleQuestionsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
const validateApiQuestion = new Ajv().compile<ApiQuestion>(apiQuestionSchema);
|
||||||
|
const validateApiMultipleQuestions = new Ajv().compile<ApiMultipleQuestions>(
|
||||||
|
apiMultipleQuestionsSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
async function fetchWithRetries<T = unknown>(url: string): Promise<T> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<T>(url);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error while fetching ${url}`);
|
||||||
|
console.log(error);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response?.headers["retry-after"]) {
|
||||||
|
const timeout = error.response.headers["retry-after"];
|
||||||
|
console.log(`Timeout: ${timeout}`);
|
||||||
|
await sleep(Number(timeout) * 1000 + 1000);
|
||||||
|
} else {
|
||||||
|
await sleep(RETRY_SLEEP_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await axios.get<T>(url);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchAndValidate = async <T = unknown>(
|
||||||
|
url: string,
|
||||||
|
validator: ValidateFunction<T>
|
||||||
|
): Promise<T> => {
|
||||||
|
console.log(url);
|
||||||
|
const data = await fetchWithRetries<object>(url);
|
||||||
|
if (validator(data)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Response validation for url ${url} failed: ` +
|
||||||
|
JSON.stringify(validator.errors)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchApiQuestions(
|
||||||
|
next: string
|
||||||
|
): Promise<ApiMultipleQuestions> {
|
||||||
|
return await fetchAndValidate(next, validateApiMultipleQuestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchSingleApiQuestion(id: number): Promise<ApiQuestion> {
|
||||||
|
return await fetchAndValidate(
|
||||||
|
`https://www.metaculus.com/api2/questions/${id}/`,
|
||||||
|
validateApiQuestion
|
||||||
|
);
|
||||||
|
}
|
184
src/backend/platforms/metaculus/index.ts
Normal file
184
src/backend/platforms/metaculus/index.ts
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import { FetchedQuestion, Platform } from "..";
|
||||||
|
import { average } from "../../../utils";
|
||||||
|
import { sleep } from "../../utils/sleep";
|
||||||
|
import {
|
||||||
|
ApiCommon,
|
||||||
|
ApiMultipleQuestions,
|
||||||
|
ApiPredictable,
|
||||||
|
ApiQuestion,
|
||||||
|
fetchApiQuestions,
|
||||||
|
fetchSingleApiQuestion,
|
||||||
|
} from "./api";
|
||||||
|
|
||||||
|
const platformName = "metaculus";
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const SLEEP_TIME = 2500;
|
||||||
|
|
||||||
|
async function apiQuestionToFetchedQuestions(
|
||||||
|
apiQuestion: ApiQuestion
|
||||||
|
): Promise<FetchedQuestion[]> {
|
||||||
|
// one item can expand:
|
||||||
|
// - to 0 questions if we don't want it;
|
||||||
|
// - to 1 question if it's a simple forecast
|
||||||
|
// - to multiple questions if it's a group (see https://github.com/quantified-uncertainty/metaforecast/pull/84 for details)
|
||||||
|
|
||||||
|
await sleep(SLEEP_TIME);
|
||||||
|
|
||||||
|
const skip = (q: ApiPredictable): boolean => {
|
||||||
|
if (q.publish_time > now || now > q.resolve_time) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (q.number_of_predictions < 10) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFetchedQuestion = (
|
||||||
|
q: ApiPredictable & ApiCommon
|
||||||
|
): Omit<FetchedQuestion, "url" | "description" | "title"> => {
|
||||||
|
const isBinary = q.possibilities.type === "binary";
|
||||||
|
let options: FetchedQuestion["options"] = [];
|
||||||
|
if (isBinary) {
|
||||||
|
const probability = Number(q.community_prediction.full.q2);
|
||||||
|
options = [
|
||||||
|
{
|
||||||
|
name: "Yes",
|
||||||
|
probability: probability,
|
||||||
|
type: "PROBABILITY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No",
|
||||||
|
probability: 1 - probability,
|
||||||
|
type: "PROBABILITY",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: `${platformName}-${q.id}`,
|
||||||
|
options,
|
||||||
|
qualityindicators: {
|
||||||
|
numforecasts: q.number_of_predictions,
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
resolution_data: {
|
||||||
|
publish_time: apiQuestion.publish_time,
|
||||||
|
resolution: apiQuestion.resolution,
|
||||||
|
close_time: apiQuestion.close_time,
|
||||||
|
resolve_time: apiQuestion.resolve_time,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiQuestion.type === "group") {
|
||||||
|
const apiQuestionDetails = await fetchSingleApiQuestion(apiQuestion.id);
|
||||||
|
return apiQuestion.sub_questions
|
||||||
|
.filter((q) => !skip(q))
|
||||||
|
.map((sq) => {
|
||||||
|
const tmp = buildFetchedQuestion(sq);
|
||||||
|
return {
|
||||||
|
...tmp,
|
||||||
|
title: `${apiQuestion.title} (${sq.title})`,
|
||||||
|
description: apiQuestionDetails.description || "",
|
||||||
|
url: `https://www.metaculus.com${apiQuestion.page_url}?sub-question=${sq.id}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (apiQuestion.type === "forecast") {
|
||||||
|
const apiQuestionDetails = await fetchSingleApiQuestion(apiQuestion.id);
|
||||||
|
if (apiQuestion.group) {
|
||||||
|
return []; // sub-question, should be handled on the group level
|
||||||
|
}
|
||||||
|
if (skip(apiQuestion)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmp = buildFetchedQuestion(apiQuestion);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...tmp,
|
||||||
|
title: apiQuestion.title,
|
||||||
|
description: apiQuestionDetails.description || "",
|
||||||
|
url: "https://www.metaculus.com" + apiQuestion.page_url,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
if (apiQuestion.type !== "claim") {
|
||||||
|
// should never happen, since `discriminator` in JTD schema causes a strict runtime check
|
||||||
|
console.log(
|
||||||
|
`Unknown metaculus question type: ${
|
||||||
|
(apiQuestion as any).type
|
||||||
|
}, skipping`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metaculus: Platform<"id" | "debug"> = {
|
||||||
|
name: platformName,
|
||||||
|
label: "Metaculus",
|
||||||
|
color: "#006669",
|
||||||
|
version: "v2",
|
||||||
|
fetcherArgs: ["id", "debug"],
|
||||||
|
async fetcher(opts) {
|
||||||
|
let allQuestions: FetchedQuestion[] = [];
|
||||||
|
|
||||||
|
if (opts.args?.id) {
|
||||||
|
const id = Number(opts.args.id);
|
||||||
|
const apiQuestion = await fetchSingleApiQuestion(id);
|
||||||
|
const questions = await apiQuestionToFetchedQuestions(apiQuestion);
|
||||||
|
console.log(questions);
|
||||||
|
return {
|
||||||
|
questions,
|
||||||
|
partial: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let next: string | null = "https://www.metaculus.com/api2/questions/";
|
||||||
|
let i = 1;
|
||||||
|
while (next) {
|
||||||
|
if (i % 20 === 0) {
|
||||||
|
console.log(`Sleeping for ${SLEEP_TIME}ms`);
|
||||||
|
await sleep(SLEEP_TIME);
|
||||||
|
}
|
||||||
|
console.log(`\nQuery #${i} - ${next}`);
|
||||||
|
|
||||||
|
const apiQuestions: ApiMultipleQuestions = await fetchApiQuestions(next);
|
||||||
|
const results = apiQuestions.results;
|
||||||
|
|
||||||
|
let j = false;
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const questions = await apiQuestionToFetchedQuestions(result);
|
||||||
|
for (const question of questions) {
|
||||||
|
console.log(`- ${question.title}`);
|
||||||
|
if ((!j && i % 20 === 0) || opts.args?.debug) {
|
||||||
|
console.log(question);
|
||||||
|
j = true;
|
||||||
|
}
|
||||||
|
allQuestions.push(question);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next = apiQuestions.next;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
questions: allQuestions,
|
||||||
|
partial: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
calculateStars(data) {
|
||||||
|
const { numforecasts } = data.qualityindicators;
|
||||||
|
const nuno = () =>
|
||||||
|
(numforecasts || 0) > 300 ? 4 : (numforecasts || 0) > 100 ? 3 : 2;
|
||||||
|
const eli = () => 3;
|
||||||
|
const misha = () => 3;
|
||||||
|
const starsDecimal = average([nuno(), eli(), misha()]);
|
||||||
|
const starsInteger = Math.round(starsDecimal);
|
||||||
|
return starsInteger;
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user