Merge pull request #79 from quantified-uncertainty/stricter-typescript

Strict typescript & platform/cli arguments
This commit is contained in:
Vyacheslav Matyukhin 2022-05-12 17:32:22 +03:00 committed by GitHub
commit 6fbeea2267
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1196 additions and 2089 deletions

757
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,13 +33,18 @@
"@pothos/plugin-prisma": "^3.4.0", "@pothos/plugin-prisma": "^3.4.0",
"@pothos/plugin-relay": "^3.10.0", "@pothos/plugin-relay": "^3.10.0",
"@prisma/client": "^3.11.1", "@prisma/client": "^3.11.1",
"@quri/squiggle-lang": "^0.2.8",
"@tailwindcss/forms": "^0.4.0", "@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.1", "@tailwindcss/typography": "^0.5.1",
"@types/chroma-js": "^2.1.3",
"@types/dom-to-image": "^2.6.4", "@types/dom-to-image": "^2.6.4",
"@types/google-spreadsheet": "^3.2.1",
"@types/jsdom": "^16.2.14", "@types/jsdom": "^16.2.14",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",
"@types/react-copy-to-clipboard": "^5.0.2", "@types/react-copy-to-clipboard": "^5.0.2",
"@types/textversionjs": "^1.1.1",
"@types/tunnel": "^0.0.3",
"airtable": "^0.11.1", "airtable": "^0.11.1",
"algoliasearch": "^4.10.3", "algoliasearch": "^4.10.3",
"autoprefixer": "^10.1.0", "autoprefixer": "^10.1.0",
@ -86,7 +91,6 @@
"react-safe": "^1.3.0", "react-safe": "^1.3.0",
"react-select": "^5.2.2", "react-select": "^5.2.2",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"squiggle-experimental": "^0.1.9",
"tabletojson": "^2.0.4", "tabletojson": "^2.0.4",
"tailwindcss": "^3.0.22", "tailwindcss": "^3.0.22",
"textversionjs": "^1.1.3", "textversionjs": "^1.1.3",

View File

@ -3,14 +3,7 @@ import { executeJobByName } from "./jobs";
/* Do everything */ /* Do everything */
export async function doEverything() { export async function doEverything() {
let jobNames = [ let jobNames = [...platforms.map((platform) => platform.name), "algolia"];
...platforms.map((platform) => platform.name),
"merge",
"algolia",
"history",
"netlify",
];
// Removed Good Judgment from the fetcher, doing it using cron instead because cloudflare blocks the utility on heroku.
console.log(""); console.log("");
console.log(""); console.log("");

View File

@ -1,11 +0,0 @@
import { prisma } from "../../database/prisma";
export async function updateHistory() {
const questions = await prisma.question.findMany({});
await prisma.history.createMany({
data: questions.map((q) => ({
...q,
idref: q.id,
})),
});
}

View File

@ -1,38 +1,29 @@
import { doEverything } from "../flow/doEverything"; import { doEverything } from "../flow/doEverything";
import { updateHistory } from "../flow/history/updateHistory";
import { rebuildNetlifySiteWithNewData } from "../flow/rebuildNetliftySiteWithNewData";
import { rebuildFrontpage } from "../frontpage"; import { rebuildFrontpage } from "../frontpage";
import { platforms, processPlatform } from "../platforms"; import { platforms, processPlatform } from "../platforms";
import { rebuildAlgoliaDatabase } from "../utils/algolia"; import { rebuildAlgoliaDatabase } from "../utils/algolia";
import { sleep } from "../utils/sleep";
interface Job { interface Job<ArgNames extends string = ""> {
name: string; name: string;
message: string; message: string;
run: () => Promise<void>; args?: ArgNames[];
run: (args?: { [k in ArgNames]: string }) => Promise<void>;
separate?: boolean; separate?: boolean;
} }
export const jobs: Job[] = [ export const jobs: Job<string>[] = [
...platforms.map((platform) => ({ ...platforms.map((platform) => ({
name: platform.name, name: platform.name,
message: `Download predictions from ${platform.name}`, message: `Download predictions from ${platform.name}`,
run: () => processPlatform(platform), ...(platform.version === "v2" ? { args: platform.fetcherArgs } : {}),
run: (args: any) => processPlatform(platform, args),
})), })),
{ {
name: "algolia", name: "algolia",
message: 'Rebuild algolia database ("index")', message: 'Rebuild algolia database ("index")',
run: rebuildAlgoliaDatabase, run: rebuildAlgoliaDatabase,
}, },
{
name: "history",
message: "Update history",
run: updateHistory,
},
{
name: "netlify",
message: `Rebuild netlify site with new data`,
run: rebuildNetlifySiteWithNewData,
},
{ {
name: "frontpage", name: "frontpage",
message: "Rebuild frontpage", message: "Rebuild frontpage",
@ -46,31 +37,39 @@ export const jobs: Job[] = [
}, },
]; ];
function sleep(ms: number) { async function tryCatchTryAgain<T extends object = never>(
return new Promise((resolve) => setTimeout(resolve, ms)); fun: (args: T) => Promise<void>,
} args: T
) {
async function tryCatchTryAgain(fun: () => Promise<void>) {
try { try {
console.log("Initial try"); console.log("Initial try");
await fun(); await fun(args);
} catch (error) { } catch (error) {
sleep(10000); sleep(10000);
console.log("Second try"); console.log("Second try");
console.log(error); console.log(error);
try { try {
await fun(); await fun(args);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
} }
} }
export const executeJobByName = async (option: string) => { export const executeJobByName = async (
const job = jobs.find((job) => job.name === option); jobName: string,
jobArgs: { [k: string]: string } = {}
) => {
const job = jobs.find((job) => job.name === jobName);
if (!job) { if (!job) {
console.log(`Error, job ${option} not found`); console.log(`Error, job ${jobName} not found`);
} else { return;
await tryCatchTryAgain(job.run);
} }
for (const key of Object.keys(jobArgs)) {
if (!job.args || job.args.indexOf(key) < 0) {
throw new Error(`Job ${jobName} doesn't accept ${key} argument`);
}
}
await tryCatchTryAgain(job.run, jobArgs);
}; };

View File

@ -1,15 +0,0 @@
import axios from "axios";
import { applyIfSecretExists } from "../utils/getSecrets";
async function rebuildNetlifySiteWithNewData_inner(cookie) {
let payload = {};
let response = await axios.post(cookie, payload);
let data = response.data;
console.log(data);
}
export async function rebuildNetlifySiteWithNewData() {
let cookie = process.env.REBUIDNETLIFYHOOKURL;
await applyIfSecretExists(cookie, rebuildNetlifySiteWithNewData_inner);
}

View File

@ -2,11 +2,10 @@
import "dotenv/config"; import "dotenv/config";
import readline from "readline"; import readline from "readline";
import util from "util";
import { executeJobByName, jobs } from "./flow/jobs"; import { executeJobByName, jobs } from "./flow/jobs";
let generateWhatToDoMessage = () => { const generateWhatToDoMessage = () => {
const color = "\x1b[36m"; const color = "\x1b[36m";
const resetColor = "\x1b[0m"; const resetColor = "\x1b[0m";
let completeMessages = [ let completeMessages = [
@ -23,27 +22,56 @@ let generateWhatToDoMessage = () => {
return completeMessages; return completeMessages;
}; };
let whattodoMessage = generateWhatToDoMessage(); const whattodoMessage = generateWhatToDoMessage();
/* BODY */
let commandLineUtility = async () => {
const pickOption = async () => {
if (process.argv.length === 3) {
return process.argv[2]; // e.g., npm run cli polymarket
}
const askForJobName = async () => {
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout, output: process.stdout,
}); });
const question = util.promisify(rl.question).bind(rl); const question = (query: string) => {
return new Promise((resolve: (s: string) => void) => {
rl.question(query, resolve);
});
};
const answer = await question(whattodoMessage); const answer = await question(whattodoMessage);
rl.close(); rl.close();
return answer; return answer;
}; };
await executeJobByName(await pickOption()); const pickJob = async (): Promise<[string, { [k: string]: string }]> => {
if (process.argv.length < 3) {
const jobName = await askForJobName();
return [jobName, {}]; // e.g., npm run cli polymarket
}
const jobName = process.argv[2];
if ((process.argv.length - 3) % 2) {
throw new Error("Number of extra arguments must be even");
}
const args: { [k: string]: string } = {};
for (let i = 3; i < process.argv.length; i += 2) {
let argName = process.argv[i];
const argValue = process.argv[i + 1];
if (argName.slice(0, 2) !== "--") {
throw new Error(`${argName} should start with --`);
}
argName = argName.slice(2);
args[argName] = argValue;
}
return [jobName, args];
};
/* BODY */
const commandLineUtility = async () => {
const [jobName, jobArgs] = await pickJob();
await executeJobByName(jobName, jobArgs);
process.exit(); process.exit();
}; };

View File

@ -1,7 +1,6 @@
/* Imports */ /* Imports */
import axios from "axios"; import axios from "axios";
import { calculateStars } from "../utils/stars";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
@ -22,11 +21,11 @@ async function fetchData() {
return response; return response;
} }
async function processPredictions(predictions) { async function processPredictions(predictions: any[]) {
let results = await predictions.map((prediction) => { let results = await predictions.map((prediction) => {
const id = `${platformName}-${prediction.id}`; const id = `${platformName}-${prediction.id}`;
const probability = prediction.probability; const probability = prediction.probability;
const options = [ const options: FetchedQuestion["options"] = [
{ {
name: "Yes", name: "Yes",
probability: probability, probability: probability,
@ -41,14 +40,10 @@ async function processPredictions(predictions) {
const result: FetchedQuestion = { const result: FetchedQuestion = {
id, id,
title: prediction.title, title: prediction.title,
url: `https://example.com`, url: "https://example.com",
platform: platformName,
description: prediction.description, description: prediction.description,
options, options,
qualityindicators: { qualityindicators: {
stars: calculateStars(platformName, {
/* some: somex, factors: factors */
}),
// other: prediction.otherx, // other: prediction.otherx,
// indicators: prediction.indicatorx, // indicators: prediction.indicatorx,
}, },
@ -64,9 +59,13 @@ export const example: Platform = {
name: platformName, name: platformName,
label: "Example platform", label: "Example platform",
color: "#ff0000", color: "#ff0000",
version: "v1",
async fetcher() { async fetcher() {
let data = await fetchData(); let data = await fetchData();
let results = await processPredictions(data); // somehow needed let results = await processPredictions(data); // somehow needed
return results; return results;
}, },
calculateStars(data) {
return 2;
},
}; };

View File

@ -2,16 +2,16 @@
import axios from "axios"; import axios from "axios";
import https from "https"; import https from "https";
import { calculateStars } from "../utils/stars"; import { average } from "../../utils";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
const platformName = "betfair"; const platformName = "betfair";
/* Definitions */ /* Definitions */
let endpoint = process.env.SECRET_BETFAIR_ENDPOINT; const endpoint = process.env.SECRET_BETFAIR_ENDPOINT;
/* Utilities */ /* Utilities */
let arraysEqual = (a, b) => { const arraysEqual = (a: string[], b: string[]) => {
if (a === b) return true; if (a === b) return true;
if (a == null || b == null) return false; if (a == null || b == null) return false;
if (a.length !== b.length) return false; if (a.length !== b.length) return false;
@ -26,7 +26,8 @@ let arraysEqual = (a, b) => {
} }
return true; return true;
}; };
let mergeRunners = (runnerCatalog, runnerBook) => {
const mergeRunners = (runnerCatalog: any, runnerBook: any) => {
let keys = Object.keys(runnerCatalog); let keys = Object.keys(runnerCatalog);
let result = []; let result = [];
for (let key of keys) { for (let key of keys) {
@ -41,19 +42,16 @@ async function fetchPredictions() {
const agent = new https.Agent({ const agent = new https.Agent({
rejectUnauthorized: false, rejectUnauthorized: false,
}); });
let response = await axios({ const response = await axios({
url: endpoint, url: endpoint,
method: "GET", method: "GET",
headers: {
"Content-Type": "text/html",
},
httpsAgent: agent, httpsAgent: agent,
}).then((response) => response.data); }).then((response) => response.data);
return response; return response;
} }
async function whipIntoShape(data) { async function whipIntoShape(data: any) {
let catalogues = data.market_catalogues; let catalogues = data.market_catalogues;
let books = data.market_books; let books = data.market_books;
let keys1 = Object.keys(catalogues).sort(); let keys1 = Object.keys(catalogues).sort();
@ -77,7 +75,7 @@ async function whipIntoShape(data) {
return results; return results;
} }
async function processPredictions(data) { async function processPredictions(data: any) {
let predictions = await whipIntoShape(data); let predictions = await whipIntoShape(data);
// console.log(JSON.stringify(predictions, null, 4)) // console.log(JSON.stringify(predictions, null, 4))
let results: FetchedQuestion[] = predictions.map((prediction) => { let results: FetchedQuestion[] = predictions.map((prediction) => {
@ -86,13 +84,17 @@ async function processPredictions(data) {
} */ } */
let id = `${platformName}-${prediction.marketId}`; let id = `${platformName}-${prediction.marketId}`;
let normalizationFactor = prediction.options let normalizationFactor = prediction.options
.filter((option) => option.status == "ACTIVE" && option.totalMatched > 0) .filter(
.map((option) => option.lastPriceTraded) (option: any) => option.status == "ACTIVE" && option.totalMatched > 0
.map((x) => 1 / x) )
.reduce((a, b) => a + b, 0); .map((option: any) => option.lastPriceTraded)
.map((x: any) => 1 / x)
.reduce((a: any, b: any) => a + b, 0);
let options = prediction.options let options = prediction.options
.filter((option) => option.status == "ACTIVE" && option.totalMatched > 0) .filter(
.map((option) => ({ (option: any) => option.status == "ACTIVE" && option.totalMatched > 0
)
.map((option: any) => ({
name: option.runnerName, name: option.runnerName,
probability: probability:
option.lastPriceTraded != 0 option.lastPriceTraded != 0
@ -114,22 +116,19 @@ async function processPredictions(data) {
if (rules == undefined) { if (rules == undefined) {
// console.log(prediction.description) // console.log(prediction.description)
} }
let title = rules.split("? ")[0] + "?"; let title = rules.split("? ")[0] + "?";
let description = rules.split("? ")[1].trim(); let description = rules.split("? ")[1].trim();
if (title.includes("of the named")) { if (title.includes("of the named")) {
title = prediction.marketName + ": " + title; title = prediction.marketName + ": " + title;
} }
let result = { const result: FetchedQuestion = {
id: id, id,
title: title, title,
url: `https://www.betfair.com/exchange/plus/politics/market/${prediction.marketId}`, url: `https://www.betfair.com/exchange/plus/politics/market/${prediction.marketId}`,
platform: platformName, description,
description: description, options,
options: options,
qualityindicators: { qualityindicators: {
stars: calculateStars(platformName, {
volume: prediction.totalMatched,
}),
volume: prediction.totalMatched, volume: prediction.totalMatched,
}, },
}; };
@ -142,9 +141,31 @@ export const betfair: Platform = {
name: platformName, name: platformName,
label: "Betfair", label: "Betfair",
color: "#3d674a", color: "#3d674a",
version: "v1",
async fetcher() { async fetcher() {
const data = await fetchPredictions(); const data = await fetchPredictions();
const results = await processPredictions(data); // somehow needed const results = await processPredictions(data);
return results; return results;
}, },
calculateStars(data) {
const volume = data.qualityindicators.volume || 0;
let nuno = () => (volume > 10000 ? 4 : volume > 1000 ? 3 : 2);
let eli = () => (volume > 10000 ? null : null);
let misha = () => null;
let starsDecimal = average([nuno()]); //, eli(), misha()])
const firstOption = data.options[0];
// Substract 1 star if probability is above 90% or below 10%
if (
firstOption &&
((firstOption.probability || 0) < 0.1 ||
(firstOption.probability || 0) > 0.9)
) {
starsDecimal = starsDecimal - 1;
}
let starsInteger = Math.round(starsDecimal);
return starsInteger;
},
}; };

View File

@ -1,7 +1,6 @@
/* Imports */ /* Imports */
import axios from "axios"; import axios from "axios";
import { calculateStars } from "../utils/stars";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
const platformName = "fantasyscotus"; const platformName = "fantasyscotus";
@ -30,7 +29,7 @@ async function fetchData() {
return response; return response;
} }
async function getPredictionsData(caseUrl) { async function getPredictionsData(caseUrl: string) {
let newCaseUrl = `https://fantasyscotus.net/user-predictions${caseUrl}?filterscount=0&groupscount=0&sortdatafield=username&sortorder=asc&pagenum=0&pagesize=20&recordstartindex=0&recordendindex=20&_=${unixtime}`; let newCaseUrl = `https://fantasyscotus.net/user-predictions${caseUrl}?filterscount=0&groupscount=0&sortdatafield=username&sortorder=asc&pagenum=0&pagesize=20&recordstartindex=0&recordendindex=20&_=${unixtime}`;
//console.log(newCaseUrl) //console.log(newCaseUrl)
let predictions = await axios({ let predictions = await axios({
@ -50,7 +49,7 @@ async function getPredictionsData(caseUrl) {
}).then((res) => res.data); }).then((res) => res.data);
let predictionsAffirm = predictions.filter( let predictionsAffirm = predictions.filter(
(prediction) => prediction.percent_affirm > 50 (prediction: any) => prediction.percent_affirm > 50
); );
//console.log(predictions) //console.log(predictions)
//console.log(predictionsAffirm.length/predictions.length) //console.log(predictionsAffirm.length/predictions.length)
@ -62,7 +61,7 @@ async function getPredictionsData(caseUrl) {
}; };
} }
async function processData(data) { async function processData(data: any) {
let events = data.object_list; let events = data.object_list;
let historicalPercentageCorrect = data.stats.pcnt_correct; let historicalPercentageCorrect = data.stats.pcnt_correct;
let historicalProbabilityCorrect = let historicalProbabilityCorrect =
@ -79,7 +78,6 @@ async function processData(data) {
id: id, id: id,
title: `In ${event.short_name}, the SCOTUS will affirm the lower court's decision`, title: `In ${event.short_name}, the SCOTUS will affirm the lower court's decision`,
url: `https://fantasyscotus.net/user-predictions${event.docket_url}`, url: `https://fantasyscotus.net/user-predictions${event.docket_url}`,
platform: platformName,
description: `${(pAffirm * 100).toFixed(2)}% (${ description: `${(pAffirm * 100).toFixed(2)}% (${
predictionData.numAffirm predictionData.numAffirm
} out of ${ } out of ${
@ -101,7 +99,6 @@ async function processData(data) {
], ],
qualityindicators: { qualityindicators: {
numforecasts: Number(predictionData.numForecasts), numforecasts: Number(predictionData.numForecasts),
stars: calculateStars(platformName, {}),
}, },
}; };
results.push(eventObject); results.push(eventObject);
@ -116,9 +113,13 @@ export const fantasyscotus: Platform = {
name: platformName, name: platformName,
label: "FantasySCOTUS", label: "FantasySCOTUS",
color: "#231149", color: "#231149",
version: "v1",
async fetcher() { async fetcher() {
let rawData = await fetchData(); let rawData = await fetchData();
let results = await processData(rawData); let results = await processData(rawData);
return results; return results;
}, },
calculateStars(data) {
return 2;
},
}; };

View File

@ -1,7 +1,7 @@
/* Imports */ /* Imports */
import axios from "axios"; import axios from "axios";
import { calculateStars } from "../utils/stars"; import { average } from "../../utils";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
@ -18,8 +18,10 @@ let highQualityCommunities = [
]; ];
/* Support functions */ /* Support functions */
async function fetchAllCommunityQuestions(communityId) { async function fetchAllCommunityQuestions(communityId: string) {
let response = await axios({ // TODO - fetch foretold graphql schema to type the result properly?
// (should be doable with graphql-code-generator, why not)
const response = await axios({
url: graphQLendpoint, url: graphQLendpoint,
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -52,14 +54,15 @@ async function fetchAllCommunityQuestions(communityId) {
}) })
.then((res) => res.data) .then((res) => res.data)
.then((res) => res.data.measurables.edges); .then((res) => res.data.measurables.edges);
//console.log(response)
return response; return response as any[];
} }
export const foretold: Platform = { export const foretold: Platform = {
name: platformName, name: platformName,
label: "Foretold", label: "Foretold",
color: "#62520b", color: "#62520b",
version: "v1",
async fetcher() { async fetcher() {
let results: FetchedQuestion[] = []; let results: FetchedQuestion[] = [];
for (let community of highQualityCommunities) { for (let community of highQualityCommunities) {
@ -67,10 +70,11 @@ export const foretold: Platform = {
questions = questions.map((question) => question.node); questions = questions.map((question) => question.node);
questions = questions.filter((question) => question.previousAggregate); // Questions without any predictions questions = questions.filter((question) => question.previousAggregate); // Questions without any predictions
questions.forEach((question) => { questions.forEach((question) => {
let id = `${platformName}-${question.id}`; const id = `${platformName}-${question.id}`;
let options = [];
let options: FetchedQuestion["options"] = [];
if (question.valueType == "PERCENTAGE") { if (question.valueType == "PERCENTAGE") {
let probability = question.previousAggregate.value.percentage; const probability = question.previousAggregate.value.percentage;
options = [ options = [
{ {
name: "Yes", name: "Yes",
@ -84,16 +88,15 @@ export const foretold: Platform = {
}, },
]; ];
} }
let result: FetchedQuestion = {
const result: FetchedQuestion = {
id, id,
title: question.name, title: question.name,
url: `https://www.foretold.io/c/${community}/m/${question.id}`, url: `https://www.foretold.io/c/${community}/m/${question.id}`,
platform: platformName,
description: "", description: "",
options, options,
qualityindicators: { qualityindicators: {
numforecasts: Math.floor(Number(question.measurementCount) / 2), numforecasts: Math.floor(Number(question.measurementCount) / 2),
stars: calculateStars(platformName, {}),
}, },
/*liquidity: liquidity.toFixed(2), /*liquidity: liquidity.toFixed(2),
tradevolume: tradevolume.toFixed(2), tradevolume: tradevolume.toFixed(2),
@ -105,4 +108,12 @@ export const foretold: Platform = {
} }
return results; return results;
}, },
calculateStars(data) {
let nuno = () => 2;
let eli = () => null;
let misha = () => null;
let starsDecimal = average([nuno()]); //, eli(), misha()])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
},
}; };

View File

@ -2,14 +2,14 @@
import axios from "axios"; import axios from "axios";
import fs from "fs"; import fs from "fs";
import { calculateStars } from "../utils/stars"; import { average } from "../../utils";
import { Platform } from "./"; import { Platform } from "./";
const platformName = "givewellopenphil"; const platformName = "givewellopenphil";
/* Support functions */ /* Support functions */
async function fetchPage(url: string) { async function fetchPage(url: string): Promise<string> {
let response = await axios({ const response = await axios({
url: url, url: url,
method: "GET", method: "GET",
headers: { headers: {
@ -53,9 +53,7 @@ async function main1() {
platform: platformName, platform: platformName,
description, description,
options: [], options: [],
qualityindicators: { qualityindicators: {},
stars: calculateStars(platformName, {}),
},
}; // Note: This requires some processing afterwards }; // Note: This requires some processing afterwards
// console.log(result) // console.log(result)
results.push(result); results.push(result);
@ -70,6 +68,7 @@ export const givewellopenphil: Platform = {
name: platformName, name: platformName,
label: "GiveWell/OpenPhilanthropy", label: "GiveWell/OpenPhilanthropy",
color: "#32407e", color: "#32407e",
version: "v1",
async fetcher() { async fetcher() {
// main1() // main1()
return; // not necessary to refill the DB every time return; // not necessary to refill the DB every time
@ -84,4 +83,12 @@ export const givewellopenphil: Platform = {
})); }));
return dataWithDate; return dataWithDate;
}, },
calculateStars(data) {
let nuno = () => 2;
let eli = () => null;
let misha = () => null;
let starsDecimal = average([nuno()]); //, eli(), misha()])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
},
}; };

View File

@ -1,27 +1,24 @@
/* Imports */ /* Imports */
import axios from "axios"; import axios from "axios";
import { Tabletojson } from "tabletojson"; import { Tabletojson } from "tabletojson";
import tunnel from "tunnel";
import { average } from "../../utils";
import { hash } from "../utils/hash"; import { hash } from "../utils/hash";
import { calculateStars } from "../utils/stars";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
const platformName = "goodjudgment"; const platformName = "goodjudgment";
let endpoint = "https://goodjudgment.io/superforecasts/"; const endpoint = "https://goodjudgment.io/superforecasts/";
String.prototype.replaceAll = function replaceAll(search, replace) {
return this.split(search).join(replace);
};
/* Body */ /* Body */
export const goodjudgment: Platform = { export const goodjudgment: Platform = {
name: platformName, name: platformName,
label: "Good Judgment", label: "Good Judgment",
color: "#7d4f1b", color: "#7d4f1b",
version: "v1",
async fetcher() { async fetcher() {
// Proxy fuckery // Proxy fuckery
let proxy; // let proxy;
/* /*
* try { * try {
proxy = await axios proxy = await axios
@ -32,19 +29,19 @@ export const goodjudgment: Platform = {
console.log("Proxy generation failed; using backup proxy instead"); console.log("Proxy generation failed; using backup proxy instead");
// hard-coded backup proxy // hard-coded backup proxy
*/ */
proxy = { // proxy = {
ip: process.env.BACKUP_PROXY_IP, // ip: process.env.BACKUP_PROXY_IP,
port: process.env.BACKUP_PROXY_PORT, // port: process.env.BACKUP_PROXY_PORT,
}; // };
// } // // }
let agent = tunnel.httpsOverHttp({ // let agent = tunnel.httpsOverHttp({
proxy: { // proxy: {
host: proxy.ip, // host: proxy.ip,
port: proxy.port, // port: proxy.port,
}, // },
}); // });
let content = await axios const content = await axios
.request({ .request({
url: "https://goodjudgment.io/superforecasts/", url: "https://goodjudgment.io/superforecasts/",
method: "get", method: "get",
@ -61,17 +58,16 @@ export const goodjudgment: Platform = {
let jsonTable = Tabletojson.convert(content, { stripHtmlFromCells: false }); let jsonTable = Tabletojson.convert(content, { stripHtmlFromCells: false });
jsonTable.shift(); // deletes first element jsonTable.shift(); // deletes first element
jsonTable.pop(); // deletes last element jsonTable.pop(); // deletes last element
// console.log(jsonTable)
for (let table of jsonTable) { for (let table of jsonTable) {
// console.log(table)
let title = table[0]["0"].split("\t\t\t").splice(3)[0]; let title = table[0]["0"].split("\t\t\t").splice(3)[0];
if (title != undefined) { if (title != undefined) {
title = title.replaceAll("</a>", ""); title = title.replaceAll("</a>", "");
let id = `${platformName}-${hash(title)}`; const id = `${platformName}-${hash(title)}`;
let description = table const description = table
.filter((row) => row["0"].includes("BACKGROUND:")) .filter((row: any) => row["0"].includes("BACKGROUND:"))
.map((row) => row["0"]) .map((row: any) => row["0"])
.map((text) => .map((text: any) =>
text text
.split("BACKGROUND:")[1] .split("BACKGROUND:")[1]
.split("Examples of Superforecaster")[0] .split("Examples of Superforecaster")[0]
@ -83,16 +79,16 @@ export const goodjudgment: Platform = {
.replaceAll(" ", "") .replaceAll(" ", "")
.replaceAll("<br> ", "") .replaceAll("<br> ", "")
)[0]; )[0];
let options = table const options = table
.filter((row) => "4" in row) .filter((row: any) => "4" in row)
.map((row) => ({ .map((row: any) => ({
name: row["2"] name: row["2"]
.split('<span class="qTitle">')[1] .split('<span class="qTitle">')[1]
.replace("</span>", ""), .replace("</span>", ""),
probability: Number(row["3"].split("%")[0]) / 100, probability: Number(row["3"].split("%")[0]) / 100,
type: "PROBABILITY", type: "PROBABILITY",
})); }));
let analysis = table.filter((row) => let analysis = table.filter((row: any) =>
row[0] ? row[0].toLowerCase().includes("commentary") : false row[0] ? row[0].toLowerCase().includes("commentary") : false
); );
// "Examples of Superforecaster Commentary" / Analysis // "Examples of Superforecaster Commentary" / Analysis
@ -104,12 +100,9 @@ export const goodjudgment: Platform = {
id, id,
title, title,
url: endpoint, url: endpoint,
platform: platformName,
description, description,
options, options,
qualityindicators: { qualityindicators: {},
stars: calculateStars(platformName, {}),
},
extra: { extra: {
superforecastercommentary: analysis || "", superforecastercommentary: analysis || "",
}, },
@ -124,4 +117,12 @@ export const goodjudgment: Platform = {
return results; return results;
}, },
calculateStars(data) {
let nuno = () => 4;
let eli = () => 4;
let misha = () => 3.5;
let starsDecimal = average([nuno()]); //, eli(), misha()])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
},
}; };

View File

@ -2,16 +2,17 @@
import axios from "axios"; import axios from "axios";
import { Tabletojson } from "tabletojson"; import { Tabletojson } from "tabletojson";
import { average } from "../../utils";
import { applyIfSecretExists } from "../utils/getSecrets"; import { applyIfSecretExists } from "../utils/getSecrets";
import { calculateStars } from "../utils/stars"; import { sleep } from "../utils/sleep";
import toMarkdown from "../utils/toMarkdown"; import toMarkdown from "../utils/toMarkdown";
import { Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
const platformName = "goodjudgmentopen"; const platformName = "goodjudgmentopen";
let htmlEndPoint = "https://www.gjopen.com/questions?page="; const htmlEndPoint = "https://www.gjopen.com/questions?page=";
let annoyingPromptUrls = [ const annoyingPromptUrls = [
"https://www.gjopen.com/questions/1933-what-forecasting-questions-should-we-ask-what-questions-would-you-like-to-forecast-on-gjopen", "https://www.gjopen.com/questions/1933-what-forecasting-questions-should-we-ask-what-questions-would-you-like-to-forecast-on-gjopen",
"https://www.gjopen.com/questions/1779-are-there-any-forecasting-tips-tricks-and-experiences-you-would-like-to-share-and-or-discuss-with-your-fellow-forecasters", "https://www.gjopen.com/questions/1779-are-there-any-forecasting-tips-tricks-and-experiences-you-would-like-to-share-and-or-discuss-with-your-fellow-forecasters",
"https://www.gjopen.com/questions/2246-are-there-any-forecasting-tips-tricks-and-experiences-you-would-like-to-share-and-or-discuss-with-your-fellow-forecasters-2022-thread", "https://www.gjopen.com/questions/2246-are-there-any-forecasting-tips-tricks-and-experiences-you-would-like-to-share-and-or-discuss-with-your-fellow-forecasters-2022-thread",
@ -22,12 +23,11 @@ const id = () => 0;
/* Support functions */ /* Support functions */
async function fetchPage(page, cookie) { async function fetchPage(page: number, cookie: string) {
let response = await axios({ const response: string = await axios({
url: htmlEndPoint + page, url: htmlEndPoint + page,
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "text/html",
Cookie: cookie, Cookie: cookie,
}, },
}).then((res) => res.data); }).then((res) => res.data);
@ -35,12 +35,11 @@ async function fetchPage(page, cookie) {
return response; return response;
} }
async function fetchStats(questionUrl, cookie) { async function fetchStats(questionUrl: string, cookie: string) {
let response = await axios({ let response: string = await axios({
url: questionUrl + "/stats", url: questionUrl + "/stats",
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "text/html",
Cookie: cookie, Cookie: cookie,
Referer: questionUrl, Referer: questionUrl,
}, },
@ -50,7 +49,7 @@ async function fetchStats(questionUrl, cookie) {
// Is binary? // Is binary?
let isbinary = response.includes("binary?&quot;:true"); let isbinary = response.includes("binary?&quot;:true");
let options = []; let options: FetchedQuestion["options"] = [];
if (isbinary) { if (isbinary) {
// Crowd percentage // Crowd percentage
let htmlElements = response.split("\n"); let htmlElements = response.split("\n");
@ -74,7 +73,7 @@ async function fetchStats(questionUrl, cookie) {
let optionsHtmlElement = "<table" + response.split("tbody")[1] + "table>"; let optionsHtmlElement = "<table" + response.split("tbody")[1] + "table>";
let tablesAsJson = Tabletojson.convert(optionsHtmlElement); let tablesAsJson = Tabletojson.convert(optionsHtmlElement);
let firstTable = tablesAsJson[0]; let firstTable = tablesAsJson[0];
options = firstTable.map((element) => ({ options = firstTable.map((element: any) => ({
name: element["0"], name: element["0"],
probability: Number(element["1"].replace("%", "")) / 100, probability: Number(element["1"].replace("%", "")) / 100,
type: "PROBABILITY", type: "PROBABILITY",
@ -107,21 +106,12 @@ async function fetchStats(questionUrl, cookie) {
.split(",")[0]; .split(",")[0];
//console.log(numpredictors) //console.log(numpredictors)
// Calculate the stars
let minProbability = Math.min(...options.map((option) => option.probability));
let maxProbability = Math.max(...options.map((option) => option.probability));
let result = { let result = {
description: description, description,
options: options, options,
qualityindicators: { qualityindicators: {
numforecasts: Number(numforecasts), numforecasts: Number(numforecasts),
numforecasters: Number(numforecasters), numforecasters: Number(numforecasters),
stars: calculateStars("Good Judgment Open", {
numforecasts,
minProbability,
maxProbability,
}),
}, },
// this mismatches the code below, and needs to be fixed, but I'm doing typescript conversion and don't want to touch any logic for now // this mismatches the code below, and needs to be fixed, but I'm doing typescript conversion and don't want to touch any logic for now
} as any; } as any;
@ -129,7 +119,7 @@ async function fetchStats(questionUrl, cookie) {
return result; return result;
} }
function isSignedIn(html) { function isSignedIn(html: string) {
let isSignedInBool = !( let isSignedInBool = !(
html.includes("You need to sign in or sign up before continuing") || html.includes("You need to sign in or sign up before continuing") ||
html.includes("Sign up") html.includes("Sign up")
@ -142,7 +132,7 @@ function isSignedIn(html) {
return isSignedInBool; return isSignedInBool;
} }
function reachedEnd(html) { function reachedEnd(html: string) {
let reachedEndBool = html.includes("No questions match your filter"); let reachedEndBool = html.includes("No questions match your filter");
if (reachedEndBool) { if (reachedEndBool) {
//console.log(html) //console.log(html)
@ -151,13 +141,9 @@ function reachedEnd(html) {
return reachedEndBool; return reachedEndBool;
} }
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/* Body */ /* Body */
async function goodjudgmentopen_inner(cookie) { async function goodjudgmentopen_inner(cookie: string) {
let i = 1; let i = 1;
let response = await fetchPage(i, cookie); let response = await fetchPage(i, cookie);
@ -185,7 +171,11 @@ async function goodjudgmentopen_inner(cookie) {
} }
} }
let questionNumRegex = new RegExp("questions/([0-9]+)"); let questionNumRegex = new RegExp("questions/([0-9]+)");
let questionNum = url.match(questionNumRegex)[1]; //.split("questions/")[1].split("-")[0]; const questionNumMatch = url.match(questionNumRegex);
if (!questionNumMatch) {
throw new Error(`Couldn't find question num in ${url}`);
}
let questionNum = questionNumMatch[1];
let id = `${platformName}-${questionNum}`; let id = `${platformName}-${questionNum}`;
let question = { let question = {
id: id, id: id,
@ -241,8 +231,26 @@ export const goodjudgmentopen: Platform = {
name: platformName, name: platformName,
label: "Good Judgment Open", label: "Good Judgment Open",
color: "#002455", color: "#002455",
version: "v1",
async fetcher() { async fetcher() {
let cookie = process.env.GOODJUDGMENTOPENCOOKIE; let cookie = process.env.GOODJUDGMENTOPENCOOKIE;
return await applyIfSecretExists(cookie, goodjudgmentopen_inner); return (await applyIfSecretExists(cookie, goodjudgmentopen_inner)) || null;
},
calculateStars(data) {
let minProbability = Math.min(
...data.options.map((option) => option.probability || 0)
);
let maxProbability = Math.max(
...data.options.map((option) => option.probability || 0)
);
let nuno = () => ((data.qualityindicators.numforecasts || 0) > 100 ? 3 : 2);
let eli = () => 3;
let misha = () =>
minProbability > 0.1 || maxProbability < 0.9 ? 3.1 : 2.5;
let starsDecimal = average([nuno(), eli(), misha()]);
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}, },
}; };

View File

@ -1,12 +1,10 @@
import axios from "axios"; import axios from "axios";
import { parseISO } from "date-fns";
/* Imports */
import { Question } from "@prisma/client"; import { Question } from "@prisma/client";
import { AlgoliaQuestion } from "../../backend/utils/algolia";
import { prisma } from "../database/prisma"; import { prisma } from "../database/prisma";
import { Platform } from "./"; import { AlgoliaQuestion } from "../utils/algolia";
import { FetchedQuestion, Platform, prepareQuestion } from "./";
/* Definitions */ /* Definitions */
const searchEndpoint = const searchEndpoint =
@ -14,25 +12,20 @@ const searchEndpoint =
const apiEndpoint = "https://guesstimate.herokuapp.com"; const apiEndpoint = "https://guesstimate.herokuapp.com";
/* Body */ const modelToQuestion = (model: any): ReturnType<typeof prepareQuestion> => {
const modelToQuestion = (model: any): Question => {
const { description } = model; const { description } = model;
// const description = model.description // const description = model.description
// ? model.description.replace(/\n/g, " ").replace(/ /g, " ") // ? model.description.replace(/\n/g, " ").replace(/ /g, " ")
// : ""; // : "";
const stars = description.length > 250 ? 2 : 1; // const timestamp = parseISO(model.created_at);
const timestamp = parseISO(model.created_at); const fq: FetchedQuestion = {
const q: Question = {
id: `guesstimate-${model.id}`, id: `guesstimate-${model.id}`,
title: model.name, title: model.name,
url: `https://www.getguesstimate.com/models/${model.id}`, url: `https://www.getguesstimate.com/models/${model.id}`,
timestamp, // timestamp,
platform: "guesstimate",
description, description,
options: [], options: [],
qualityindicators: { qualityindicators: {
stars,
numforecasts: 1, numforecasts: 1,
numforecasters: 1, numforecasters: 1,
}, },
@ -41,6 +34,7 @@ const modelToQuestion = (model: any): Question => {
}, },
// ranking: 10 * (index + 1) - 0.5, //(model._rankingInfo - 1*index)// hack // ranking: 10 * (index + 1) - 0.5, //(model._rankingInfo - 1*index)// hack
}; };
const q = prepareQuestion(fq, guesstimate);
return q; return q;
}; };
@ -68,7 +62,7 @@ async function search(query: string): Promise<AlgoliaQuestion[]> {
}); });
// filter for duplicates. Surprisingly common. // filter for duplicates. Surprisingly common.
let uniqueTitles = []; let uniqueTitles: string[] = [];
let uniqueModels: AlgoliaQuestion[] = []; let uniqueModels: AlgoliaQuestion[] = [];
for (let model of mappedModels) { for (let model of mappedModels) {
if (!uniqueTitles.includes(model.title) && !model.title.includes("copy")) { if (!uniqueTitles.includes(model.title) && !model.title.includes("copy")) {
@ -83,12 +77,11 @@ async function search(query: string): Promise<AlgoliaQuestion[]> {
const fetchQuestion = async (id: number): Promise<Question> => { const fetchQuestion = async (id: number): Promise<Question> => {
const response = await axios({ url: `${apiEndpoint}/spaces/${id}` }); const response = await axios({ url: `${apiEndpoint}/spaces/${id}` });
let q = modelToQuestion(response.data); let q = modelToQuestion(response.data);
q = await prisma.question.upsert({ return await prisma.question.upsert({
where: { id: q.id }, where: { id: q.id },
create: q, create: q,
update: q, update: q,
}); });
return q;
}; };
export const guesstimate: Platform & { export const guesstimate: Platform & {
@ -99,5 +92,7 @@ export const guesstimate: Platform & {
label: "Guesstimate", label: "Guesstimate",
color: "#223900", color: "#223900",
search, search,
version: "v1",
fetchQuestion, fetchQuestion,
calculateStars: (q) => (q.description.length > 250 ? 2 : 1),
}; };

View File

@ -1,5 +1,6 @@
import { Question } from "@prisma/client"; import { Question } from "@prisma/client";
import { QuestionOption } from "../../common/types";
import { prisma } from "../database/prisma"; import { prisma } from "../database/prisma";
import { betfair } from "./betfair"; import { betfair } from "./betfair";
import { fantasyscotus } from "./fantasyscotus"; import { fantasyscotus } from "./fantasyscotus";
@ -41,32 +42,51 @@ export interface QualityIndicators {
export type FetchedQuestion = Omit< export type FetchedQuestion = Omit<
Question, Question,
"extra" | "qualityindicators" | "timestamp" "extra" | "qualityindicators" | "timestamp" | "platform" | "options"
> & { > & {
timestamp?: Date; timestamp?: Date;
extra?: object; // required in DB but annoying to return empty; also this is slightly stricter than Prisma's JsonValue extra?: object; // required in DB but annoying to return empty; also this is slightly stricter than Prisma's JsonValue
qualityindicators: QualityIndicators; // slightly stronger type than Prisma's JsonValue options: QuestionOption[]; // stronger type than Prisma's JsonValue
qualityindicators: Omit<QualityIndicators, "stars">; // slightly stronger type than Prisma's JsonValue
}; };
// fetcher should return null if platform failed to fetch questions for some reason // fetcher should return null if platform failed to fetch questions for some reason
export type PlatformFetcher = () => Promise<FetchedQuestion[] | null>; type PlatformFetcherV1 = () => Promise<FetchedQuestion[] | null>;
export interface Platform { type PlatformFetcherV2Result = {
questions: FetchedQuestion[];
// if partial is true then we won't cleanup old questions from the database; this is useful when manually invoking a fetcher with arguments for updating a single question
partial: boolean;
} | null;
type PlatformFetcherV2<ArgNames extends string> = (opts: {
args?: { [k in ArgNames]: string };
}) => Promise<PlatformFetcherV2Result>;
export type PlatformFetcher<ArgNames extends string> =
| PlatformFetcherV1
| PlatformFetcherV2<ArgNames>;
// using "" as ArgNames default is technically incorrect, but shouldn't cause any real issues
// (I couldn't find a better solution for signifying an empty value, though there probably is one)
export type Platform<ArgNames extends string = ""> = {
name: string; // short name for ids and `platform` db column, e.g. "xrisk" name: string; // short name for ids and `platform` db column, e.g. "xrisk"
label: string; // longer name for displaying on frontend etc., e.g. "X-risk estimates" label: string; // longer name for displaying on frontend etc., e.g. "X-risk estimates"
color: string; // used on frontend color: string; // used on frontend
fetcher?: PlatformFetcher; calculateStars: (question: FetchedQuestion) => number;
} & (
| {
version: "v1";
fetcher?: PlatformFetcherV1;
} }
| {
version: "v2";
fetcherArgs?: ArgNames[];
fetcher?: PlatformFetcherV2<ArgNames>;
}
);
// draft for the future callback-based streaming/chunking API: export const platforms: Platform<string>[] = [
// interface FetchOptions {
// since?: string; // some kind of cursor, Date object or opaque string?
// save: (questions: Question[]) => Promise<void>;
// }
// export type PlatformFetcher = (options: FetchOptions) => Promise<void>;
export const platforms: Platform[] = [
betfair, betfair,
fantasyscotus, fantasyscotus,
foretold, foretold,
@ -86,27 +106,60 @@ export const platforms: Platform[] = [
xrisk, xrisk,
]; ];
export const processPlatform = async (platform: Platform) => { // Typing notes:
if (!platform.fetcher) { // There's a difference between prisma's Question type (type returned from `find` and `findMany`) and its input types due to JsonValue vs InputJsonValue mismatch.
console.log(`Platform ${platform.name} doesn't have a fetcher, skipping`); // On the other hand, we can't use Prisma.QuestionUpdateInput or Prisma.QuestionCreateManyInput either, because we use this question in guesstimate's code for preparing questions from guesstimate models...
return; // So here we build a new type which should be ok to use both in place of prisma's Question type and as an input to its update or create methods.
} type PreparedQuestion = Omit<
const fetchedQuestions = await platform.fetcher(); Question,
if (!fetchedQuestions || !fetchedQuestions.length) { "extra" | "qualityindicators" | "options"
console.log(`Platform ${platform.name} didn't return any results`); > & {
return; extra: NonNullable<Question["extra"]>;
} qualityindicators: NonNullable<Question["qualityindicators"]>;
options: NonNullable<Question["options"]>;
};
const prepareQuestion = (q: FetchedQuestion): Question => { export const prepareQuestion = (
q: FetchedQuestion,
platform: Platform<any>
): PreparedQuestion => {
return { return {
extra: {}, extra: {},
timestamp: new Date(), timestamp: new Date(),
...q, ...q,
platform: platform.name, platform: platform.name,
qualityindicators: q.qualityindicators as object, // fighting typescript qualityindicators: {
...q.qualityindicators,
stars: platform.calculateStars(q),
},
}; };
}; };
export const processPlatform = async <T extends string = "">(
platform: Platform<T>,
args?: { [k in T]: string }
) => {
if (!platform.fetcher) {
console.log(`Platform ${platform.name} doesn't have a fetcher, skipping`);
return;
}
const result =
platform.version === "v1"
? { questions: await platform.fetcher(), partial: false } // this is not exactly PlatformFetcherV2Result, since `questions` can be null
: await platform.fetcher({ args });
if (!result) {
console.log(`Platform ${platform.name} didn't return any results`);
return;
}
const { questions: fetchedQuestions, partial } = result;
if (!fetchedQuestions || !fetchedQuestions.length) {
console.log(`Platform ${platform.name} didn't return any results`);
return;
}
const oldQuestions = await prisma.question.findMany({ const oldQuestions = await prisma.question.findMany({
where: { where: {
platform: platform.name, platform: platform.name,
@ -119,11 +172,11 @@ export const processPlatform = async (platform: Platform) => {
const fetchedIdsSet = new Set(fetchedIds); const fetchedIdsSet = new Set(fetchedIds);
const oldIdsSet = new Set(oldIds); const oldIdsSet = new Set(oldIds);
const createdQuestions: Question[] = []; const createdQuestions: PreparedQuestion[] = [];
const updatedQuestions: Question[] = []; const updatedQuestions: PreparedQuestion[] = [];
const deletedIds = oldIds.filter((id) => !fetchedIdsSet.has(id)); const deletedIds = oldIds.filter((id) => !fetchedIdsSet.has(id));
for (const q of fetchedQuestions.map((q) => prepareQuestion(q))) { for (const q of fetchedQuestions.map((q) => prepareQuestion(q, platform))) {
if (oldIdsSet.has(q.id)) { if (oldIdsSet.has(q.id)) {
updatedQuestions.push(q); updatedQuestions.push(q);
} else { } else {
@ -132,17 +185,23 @@ export const processPlatform = async (platform: Platform) => {
} }
} }
const stats: { created?: number; updated?: number; deleted?: number } = {};
await prisma.question.createMany({ await prisma.question.createMany({
data: createdQuestions, data: createdQuestions,
}); });
stats.created = createdQuestions.length;
for (const q of updatedQuestions) { for (const q of updatedQuestions) {
await prisma.question.update({ await prisma.question.update({
where: { id: q.id }, where: { id: q.id },
data: q, data: q,
}); });
stats.updated ??= 0;
stats.updated++;
} }
if (!partial) {
await prisma.question.deleteMany({ await prisma.question.deleteMany({
where: { where: {
id: { id: {
@ -150,9 +209,21 @@ export const processPlatform = async (platform: Platform) => {
}, },
}, },
}); });
stats.deleted = deletedIds.length;
}
await prisma.history.createMany({
data: [...createdQuestions, ...updatedQuestions].map((q) => ({
...q,
idref: q.id,
})),
});
console.log( console.log(
`Done, ${deletedIds.length} deleted, ${updatedQuestions.length} updated, ${createdQuestions.length} created` "Done, " +
Object.entries(stats)
.map(([k, v]) => `${v} ${k}`)
.join(", ")
); );
}; };

View File

@ -1,38 +1,37 @@
/* Imports */ /* Imports */
import axios from "axios"; import axios from "axios";
import { FullQuestionOption } from "../../common/types";
import { average } from "../../utils";
import { applyIfSecretExists } from "../utils/getSecrets"; import { applyIfSecretExists } from "../utils/getSecrets";
import { measureTime } from "../utils/measureTime"; import { measureTime } from "../utils/measureTime";
import { calculateStars } from "../utils/stars"; import { sleep } from "../utils/sleep";
import toMarkdown from "../utils/toMarkdown"; import toMarkdown from "../utils/toMarkdown";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
const platformName = "infer"; const platformName = "infer";
let htmlEndPoint = "https://www.infer-pub.com/questions"; const htmlEndPoint = "https://www.infer-pub.com/questions";
String.prototype.replaceAll = function replaceAll(search, replace) {
return this.split(search).join(replace);
};
const DEBUG_MODE: "on" | "off" = "off"; // "off" const DEBUG_MODE: "on" | "off" = "off"; // "off"
const SLEEP_TIME_RANDOM = 7000; // miliseconds const SLEEP_TIME_RANDOM = 7000; // miliseconds
const SLEEP_TIME_EXTRA = 2000; const SLEEP_TIME_EXTRA = 2000;
/* Support functions */ /* Support functions */
function cleanDescription(text) { function cleanDescription(text: string) {
let md = toMarkdown(text); let md = toMarkdown(text);
let result = md.replaceAll("---", "-").replaceAll(" ", " "); let result = md.replaceAll("---", "-").replaceAll(" ", " ");
return result; return result;
} }
async function fetchPage(page, cookie) { async function fetchPage(page: number, cookie: string) {
console.log(`Page #${page}`); console.log(`Page #${page}`);
if (page == 1) { if (page == 1) {
cookie = cookie.split(";")[0]; // Interesting that it otherwise doesn't work :( cookie = cookie.split(";")[0]; // Interesting that it otherwise doesn't work :(
} }
let urlEndpoint = `${htmlEndPoint}/?page=${page}`; let urlEndpoint = `${htmlEndPoint}/?page=${page}`;
console.log(urlEndpoint); console.log(urlEndpoint);
let response = await axios({ const response: string = await axios({
url: urlEndpoint, url: urlEndpoint,
method: "GET", method: "GET",
headers: { headers: {
@ -44,8 +43,8 @@ async function fetchPage(page, cookie) {
return response; return response;
} }
async function fetchStats(questionUrl, cookie) { async function fetchStats(questionUrl: string, cookie: string) {
let response = await axios({ let response: string = await axios({
url: questionUrl + "/stats", url: questionUrl + "/stats",
method: "GET", method: "GET",
headers: { headers: {
@ -59,7 +58,7 @@ async function fetchStats(questionUrl, cookie) {
throw Error("Not logged in"); throw Error("Not logged in");
} }
// Init // Init
let options = []; let options: FullQuestionOption[] = [];
// Parse the embedded json // Parse the embedded json
let htmlElements = response.split("\n"); let htmlElements = response.split("\n");
@ -84,7 +83,7 @@ async function fetchStats(questionUrl, cookie) {
questionType.includes("Forecast::Question") || questionType.includes("Forecast::Question") ||
!questionType.includes("Forecast::MultiTimePeriodQuestion") !questionType.includes("Forecast::MultiTimePeriodQuestion")
) { ) {
options = firstEmbeddedJson.question.answers.map((answer) => ({ options = firstEmbeddedJson.question.answers.map((answer: any) => ({
name: answer.name, name: answer.name,
probability: answer.normalized_probability, probability: answer.normalized_probability,
type: "PROBABILITY", type: "PROBABILITY",
@ -94,12 +93,11 @@ async function fetchStats(questionUrl, cookie) {
options[0].probability > 1 options[0].probability > 1
? 1 - options[0].probability / 100 ? 1 - options[0].probability / 100
: 1 - options[0].probability; : 1 - options[0].probability;
let optionNo = { options.push({
name: "No", name: "No",
probability: probabilityNo, probability: probabilityNo,
type: "PROBABILITY", type: "PROBABILITY",
}; });
options.push(optionNo);
} }
} }
let result = { let result = {
@ -109,14 +107,13 @@ async function fetchStats(questionUrl, cookie) {
numforecasts: Number(numforecasts), numforecasts: Number(numforecasts),
numforecasters: Number(numforecasters), numforecasters: Number(numforecasters),
comments_count: Number(comments_count), comments_count: Number(comments_count),
stars: calculateStars(platformName, { numforecasts }),
}, },
}; };
// console.log(JSON.stringify(result, null, 4)); // console.log(JSON.stringify(result, null, 4));
return result; return result;
} }
function isSignedIn(html) { function isSignedIn(html: string) {
let isSignedInBool = !( let isSignedInBool = !(
html.includes("You need to sign in or sign up before continuing") || html.includes("You need to sign in or sign up before continuing") ||
html.includes("Sign up") html.includes("Sign up")
@ -128,7 +125,7 @@ function isSignedIn(html) {
return isSignedInBool; return isSignedInBool;
} }
function reachedEnd(html) { function reachedEnd(html: string) {
let reachedEndBool = html.includes("No questions match your filter"); let reachedEndBool = html.includes("No questions match your filter");
if (reachedEndBool) { if (reachedEndBool) {
//console.log(html) //console.log(html)
@ -137,10 +134,6 @@ function reachedEnd(html) {
return reachedEndBool; return reachedEndBool;
} }
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/* Body */ /* Body */
async function infer_inner(cookie: string) { async function infer_inner(cookie: string) {
@ -173,17 +166,18 @@ async function infer_inner(cookie: string) {
await sleep(Math.random() * SLEEP_TIME_RANDOM + SLEEP_TIME_EXTRA); // don't be as noticeable await sleep(Math.random() * SLEEP_TIME_RANDOM + SLEEP_TIME_EXTRA); // don't be as noticeable
try { try {
let moreinfo = await fetchStats(url, cookie); const moreinfo = await fetchStats(url, cookie);
let questionNumRegex = new RegExp("questions/([0-9]+)"); const questionNumRegex = new RegExp("questions/([0-9]+)");
let questionNum = url.match(questionNumRegex)[1]; //.split("questions/")[1].split("-")[0]; const questionNumMatch = url.match(questionNumRegex);
let id = `${platformName}-${questionNum}`; if (!questionNumMatch) {
throw new Error(`Couldn't find question num in ${url}`);
}
let questionNum = questionNumMatch[1];
const id = `${platformName}-${questionNum}`;
let question: FetchedQuestion = { let question: FetchedQuestion = {
id: id, id,
title: title, title,
description: moreinfo.description, url,
url: url,
platform: platformName,
options: moreinfo.options,
...moreinfo, ...moreinfo,
}; };
console.log(JSON.stringify(question, null, 4)); console.log(JSON.stringify(question, null, 4));
@ -236,8 +230,17 @@ export const infer: Platform = {
name: platformName, name: platformName,
label: "Infer", label: "Infer",
color: "#223900", color: "#223900",
version: "v1",
async fetcher() { async fetcher() {
let cookie = process.env.INFER_COOKIE; let cookie = process.env.INFER_COOKIE;
return await applyIfSecretExists(cookie, infer_inner); return (await applyIfSecretExists(cookie, infer_inner)) || null;
},
calculateStars(data) {
let nuno = () => 2;
let eli = () => null;
let misha = () => null;
let starsDecimal = average([nuno()]); //, eli(), misha()])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}, },
}; };

View File

@ -1,29 +1,28 @@
/* Imports */ /* Imports */
import axios from "axios"; import axios from "axios";
import { calculateStars } from "../utils/stars"; import { average } from "../../utils";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
const platformName = "kalshi"; const platformName = "kalshi";
let jsonEndpoint = "https://trading-api.kalshi.com/v1/cached/markets/"; //"https://subgraph-matic.poly.market/subgraphs/name/TokenUnion/polymarket"//"https://subgraph-backup.poly.market/subgraphs/name/TokenUnion/polymarket"//'https://subgraph-matic.poly.market/subgraphs/name/TokenUnion/polymarket3' let jsonEndpoint = "https://trading-api.kalshi.com/v1/cached/markets/";
async function fetchAllMarkets() { async function fetchAllMarkets() {
// for info which the polymarket graphql API
let response = await axios let response = await axios
.get(jsonEndpoint) .get(jsonEndpoint)
.then((response) => response.data.markets); .then((response) => response.data.markets);
// console.log(response)
return response; return response;
} }
async function processMarkets(markets) { async function processMarkets(markets: any[]) {
let dateNow = new Date().toISOString(); let dateNow = new Date().toISOString();
// console.log(markets) // console.log(markets)
markets = markets.filter((market) => market.close_date > dateNow); markets = markets.filter((market) => market.close_date > dateNow);
let results = await markets.map((market) => { let results = await markets.map((market) => {
const probability = market.last_price / 100; const probability = market.last_price / 100;
const options = [ const options: FetchedQuestion["options"] = [
{ {
name: "Yes", name: "Yes",
probability: probability, probability: probability,
@ -40,41 +39,63 @@ async function processMarkets(markets) {
id, id,
title: market.title.replaceAll("*", ""), title: market.title.replaceAll("*", ""),
url: `https://kalshi.com/markets/${market.ticker_name}`, url: `https://kalshi.com/markets/${market.ticker_name}`,
platform: platformName,
description: `${market.settle_details}. The resolution source is: ${market.ranged_group_name} (${market.settle_source_url})`, description: `${market.settle_details}. The resolution source is: ${market.ranged_group_name} (${market.settle_source_url})`,
options, options,
qualityindicators: { qualityindicators: {
stars: calculateStars(platformName, {
shares_volume: market.volume,
interest: market.open_interest,
}),
yes_bid: market.yes_bid, yes_bid: market.yes_bid,
yes_ask: market.yes_ask, yes_ask: market.yes_ask,
spread: Math.abs(market.yes_bid - market.yes_ask), spread: Math.abs(market.yes_bid - market.yes_ask),
shares_volume: market.volume, // Assuming that half of all buys are for yes and half for no, which is a big if. shares_volume: market.volume, // Assuming that half of all buys are for yes and half for no, which is a big if.
// "open_interest": market.open_interest, also in shares // "open_interest": market.open_interest, also in shares
}, },
extra: {
open_interest: market.open_interest,
},
}; };
return result; return result;
}); });
//console.log(results.length)
// console.log(results.map(result => result.title))
// console.log(results.map(result => result.title).length)
console.log([...new Set(results.map((result) => result.title))]); console.log([...new Set(results.map((result) => result.title))]);
console.log( console.log(
"Number of unique questions: ", "Number of unique questions: ",
[...new Set(results.map((result) => result.title))].length [...new Set(results.map((result) => result.title))].length
); );
// console.log([...new Set(results.map(result => result.title))].length)
return results; //resultsProcessed return results;
} }
export const kalshi: Platform = { export const kalshi: Platform = {
name: platformName, name: platformName,
label: "Kalshi", label: "Kalshi",
color: "#615691", color: "#615691",
version: "v1",
fetcher: async function () { fetcher: async function () {
let markets = await fetchAllMarkets(); let markets = await fetchAllMarkets();
return await processMarkets(markets); return await processMarkets(markets);
}, },
calculateStars(data) {
let nuno = () =>
((data.extra as any)?.open_interest || 0) > 500 &&
data.qualityindicators.shares_volume > 10000
? 4
: data.qualityindicators.shares_volume > 2000
? 3
: 2;
// let eli = (data) => data.interest > 10000 ? 5 : 4
// let misha = (data) => 4
let starsDecimal = average([nuno()]); //, eli(data), misha(data)])
// Substract 1 star if probability is above 90% or below 10%
if (
data.options instanceof Array &&
data.options[0] &&
((data.options[0].probability || 0) < 0.1 ||
(data.options[0].probability || 0) > 0.9)
) {
starsDecimal = starsDecimal - 1;
}
let starsInteger = Math.round(starsDecimal);
return starsInteger;
},
}; };

View File

@ -1,12 +1,12 @@
/* Imports */ /* Imports */
import axios from "axios"; import axios from "axios";
import { calculateStars } from "../utils/stars"; import { average } from "../../utils";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
const platformName = "manifold"; const platformName = "manifold";
let endpoint = "https://manifold.markets/api/v0/markets"; const endpoint = "https://manifold.markets/api/v0/markets";
// See https://manifoldmarkets.notion.site/Manifold-Markets-API-5e7d0aef4dcf452bb04b319e178fabc5 // See https://manifoldmarkets.notion.site/Manifold-Markets-API-5e7d0aef4dcf452bb04b319e178fabc5
/* Support functions */ /* Support functions */
@ -25,16 +25,16 @@ async function fetchData() {
function showStatistics(results: FetchedQuestion[]) { function showStatistics(results: FetchedQuestion[]) {
console.log(`Num unresolved markets: ${results.length}`); console.log(`Num unresolved markets: ${results.length}`);
let sum = (arr) => arr.reduce((tally, a) => tally + a, 0); let sum = (arr: number[]) => arr.reduce((tally, a) => tally + a, 0);
let num2StarsOrMore = results.filter( let num2StarsOrMore = results.filter(
(result) => result.qualityindicators.stars >= 2 (result) => manifold.calculateStars(result) >= 2
); );
console.log( console.log(
`Manifold has ${num2StarsOrMore.length} markets with 2 stars or more` `Manifold has ${num2StarsOrMore.length} markets with 2 stars or more`
); );
console.log( console.log(
`Mean volume: ${ `Mean volume: ${
sum(results.map((result) => result.qualityindicators.volume7Days)) / sum(results.map((result) => result.qualityindicators.volume7Days || 0)) /
results.length results.length
}; mean pool: ${ }; mean pool: ${
sum(results.map((result) => result.qualityindicators.pool)) / sum(results.map((result) => result.qualityindicators.pool)) /
@ -43,11 +43,11 @@ function showStatistics(results: FetchedQuestion[]) {
); );
} }
async function processPredictions(predictions) { function processPredictions(predictions: any[]): FetchedQuestion[] {
let results: FetchedQuestion[] = await predictions.map((prediction) => { let results: FetchedQuestion[] = predictions.map((prediction) => {
let id = `${platformName}-${prediction.id}`; // oops, doesn't match platform name let id = `${platformName}-${prediction.id}`; // oops, doesn't match platform name
let probability = prediction.probability; let probability = prediction.probability;
let options = [ let options: FetchedQuestion["options"] = [
{ {
name: "Yes", name: "Yes",
probability: probability, probability: probability,
@ -63,15 +63,9 @@ async function processPredictions(predictions) {
id: id, id: id,
title: prediction.question, title: prediction.question,
url: prediction.url, url: prediction.url,
platform: platformName,
description: prediction.description, description: prediction.description,
options: options, options,
qualityindicators: { qualityindicators: {
stars: calculateStars(platformName, {
volume7Days: prediction.volume7Days,
volume24Hours: prediction.volume24Hours,
pool: prediction.pool,
}),
createdTime: prediction.createdTime, createdTime: prediction.createdTime,
volume7Days: prediction.volume7Days, volume7Days: prediction.volume7Days,
volume24Hours: prediction.volume24Hours, volume24Hours: prediction.volume24Hours,
@ -94,10 +88,24 @@ export const manifold: Platform = {
name: platformName, name: platformName,
label: "Manifold Markets", label: "Manifold Markets",
color: "#793466", color: "#793466",
version: "v1",
async fetcher() { async fetcher() {
let data = await fetchData(); let data = await fetchData();
let results = await processPredictions(data); // somehow needed let results = processPredictions(data); // somehow needed
showStatistics(results); showStatistics(results);
return results; return results;
}, },
calculateStars(data) {
let nuno = () =>
(data.qualityindicators.volume7Days || 0) > 250 ||
((data.qualityindicators.pool || 0) > 500 &&
(data.qualityindicators.volume7Days || 0) > 100)
? 2
: 1;
let eli = () => null;
let misha = () => null;
let starsDecimal = average([nuno()]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
},
}; };

View File

@ -1,18 +1,19 @@
/* Imports */ /* Imports */
import axios from "axios"; import axios from "axios";
import { calculateStars } from "../utils/stars"; import { average } from "../../utils";
import { sleep } from "../utils/sleep";
import toMarkdown from "../utils/toMarkdown"; import toMarkdown from "../utils/toMarkdown";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
const platformName = "metaculus"; const platformName = "metaculus";
let jsonEndPoint = "https://www.metaculus.com/api2/questions/?page=";
let now = new Date().toISOString(); let now = new Date().toISOString();
let DEBUG_MODE = "off"; let DEBUG_MODE = "off";
let SLEEP_TIME = 5000; let SLEEP_TIME = 5000;
/* Support functions */ /* Support functions */
async function fetchMetaculusQuestions(next) { async function fetchMetaculusQuestions(next: string) {
// Numbers about a given address: how many, how much, at what price, etc. // Numbers about a given address: how many, how much, at what price, etc.
let response; let response;
let data; let data;
@ -25,14 +26,16 @@ async function fetchMetaculusQuestions(next) {
data = response.data; data = response.data;
} catch (error) { } catch (error) {
console.log(`Error in async function fetchMetaculusQuestions(next)`); console.log(`Error in async function fetchMetaculusQuestions(next)`);
if (!!error.response.headers["retry-after"]) { console.log(error);
let timeout = error.response.headers["retry-after"]; if (axios.isAxiosError(error)) {
if (error.response?.headers["retry-after"]) {
const timeout = error.response.headers["retry-after"];
console.log(`Timeout: ${timeout}`); console.log(`Timeout: ${timeout}`);
await sleep(Number(timeout) * 1000 + SLEEP_TIME); await sleep(Number(timeout) * 1000 + SLEEP_TIME);
} else { } else {
await sleep(SLEEP_TIME); await sleep(SLEEP_TIME);
} }
console.log(error); }
} finally { } finally {
try { try {
response = await axios({ response = await axios({
@ -50,11 +53,7 @@ async function fetchMetaculusQuestions(next) {
return data; return data;
} }
function sleep(ms: number) { async function fetchMetaculusQuestionDescription(slug: string) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchMetaculusQuestionDescription(slug) {
try { try {
let response = await axios({ let response = await axios({
method: "get", method: "get",
@ -67,11 +66,12 @@ async function fetchMetaculusQuestionDescription(slug) {
`We encountered some error when attempting to fetch a metaculus page. Trying again` `We encountered some error when attempting to fetch a metaculus page. Trying again`
); );
if ( if (
axios.isAxiosError(error) &&
typeof error.response != "undefined" && typeof error.response != "undefined" &&
typeof error.response.headers != "undefined" && typeof error.response.headers != "undefined" &&
typeof error.response.headers["retry-after"] != "undefined" typeof error.response.headers["retry-after"] != "undefined"
) { ) {
let timeout = error.response.headers["retry-after"]; const timeout = error.response.headers["retry-after"];
console.log(`Timeout: ${timeout}`); console.log(`Timeout: ${timeout}`);
await sleep(Number(timeout) * 1000 + SLEEP_TIME); await sleep(Number(timeout) * 1000 + SLEEP_TIME);
} else { } else {
@ -98,6 +98,7 @@ export const metaculus: Platform = {
name: platformName, name: platformName,
label: "Metaculus", label: "Metaculus",
color: "#006669", color: "#006669",
version: "v1",
async fetcher() { async fetcher() {
// let metaculusQuestionsInit = await fetchMetaculusQuestions(1) // let metaculusQuestionsInit = await fetchMetaculusQuestions(1)
// let numQueries = Math.round(Number(metaculusQuestionsInit.count) / 20) // let numQueries = Math.round(Number(metaculusQuestionsInit.count) / 20)
@ -131,7 +132,7 @@ export const metaculus: Platform = {
let description = descriptionprocessed2; let description = descriptionprocessed2;
let isbinary = result.possibilities.type == "binary"; let isbinary = result.possibilities.type == "binary";
let options = []; let options: FetchedQuestion["options"] = [];
if (isbinary) { if (isbinary) {
let probability = Number(result.community_prediction.full.q2); let probability = Number(result.community_prediction.full.q2);
options = [ options = [
@ -152,14 +153,10 @@ export const metaculus: Platform = {
id, id,
title: result.title, title: result.title,
url: "https://www.metaculus.com" + result.page_url, url: "https://www.metaculus.com" + result.page_url,
platform: platformName,
description, description,
options, options,
qualityindicators: { qualityindicators: {
numforecasts: Number(result.number_of_predictions), numforecasts: Number(result.number_of_predictions),
stars: calculateStars(platformName, {
numforecasts: result.number_of_predictions,
}),
}, },
extra: { extra: {
resolution_data: { resolution_data: {
@ -194,4 +191,15 @@ export const metaculus: Platform = {
return all_questions; return all_questions;
}, },
calculateStars(data) {
const { numforecasts } = data.qualityindicators;
let nuno = () =>
(numforecasts || 0) > 300 ? 4 : (numforecasts || 0) > 100 ? 3 : 2;
let eli = () => 3;
let misha = () => 3;
let starsDecimal = average([nuno(), eli(), misha()]);
let starsInteger = Math.round(starsDecimal);
return starsInteger;
},
}; };

View File

@ -1,13 +1,13 @@
/* Imports */ /* Imports */
import axios from "axios"; import axios from "axios";
import { calculateStars } from "../utils/stars"; import { average } from "../../utils";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
const platformName = "polymarket"; const platformName = "polymarket";
let graphQLendpoint = const graphQLendpoint =
"https://api.thegraph.com/subgraphs/name/polymarket/matic-markets-5"; // "https://api.thegraph.com/subgraphs/name/polymarket/matic-markets-4"// "https://api.thegraph.com/subgraphs/name/tokenunion/polymarket-matic"//"https://subgraph-matic.poly.market/subgraphs/name/TokenUnion/polymarket"//"https://subgraph-backup.poly.market/subgraphs/name/TokenUnion/polymarket"//'https://subgraph-matic.poly.market/subgraphs/name/TokenUnion/polymarket3' "https://api.thegraph.com/subgraphs/name/polymarket/matic-markets-5";
let units = 10 ** 6; let units = 10 ** 6;
async function fetchAllContractInfo() { async function fetchAllContractInfo() {
@ -18,11 +18,11 @@ async function fetchAllContractInfo() {
// "https://strapi-matic.poly.market/markets?active=true&_sort=volume:desc&_limit=-1" to get all markets, including closed ones // "https://strapi-matic.poly.market/markets?active=true&_sort=volume:desc&_limit=-1" to get all markets, including closed ones
) )
.then((query) => query.data); .then((query) => query.data);
response = response.filter((res) => res.closed != true); response = response.filter((res: any) => res.closed != true);
return response; return response;
} }
async function fetchIndividualContractData(marketMakerAddress) { async function fetchIndividualContractData(marketMakerAddress: string) {
let daysSinceEra = Math.round(Date.now() / (1000 * 24 * 60 * 60)) - 7; // last week let daysSinceEra = Math.round(Date.now() / (1000 * 24 * 60 * 60)) - 7; // last week
let response = await axios({ let response = await axios({
url: graphQLendpoint, url: graphQLendpoint,
@ -59,7 +59,7 @@ async function fetchIndividualContractData(marketMakerAddress) {
}) })
.then((res) => res.data) .then((res) => res.data)
.then((res) => res.data.fixedProductMarketMakers); .then((res) => res.data.fixedProductMarketMakers);
// console.log(response)
return response; return response;
} }
@ -67,6 +67,7 @@ export const polymarket: Platform = {
name: platformName, name: platformName,
label: "PolyMarket", label: "PolyMarket",
color: "#00314e", color: "#00314e",
version: "v1",
async fetcher() { async fetcher() {
let results: FetchedQuestion[] = []; let results: FetchedQuestion[] = [];
let webpageEndpointData = await fetchAllContractInfo(); let webpageEndpointData = await fetchAllContractInfo();
@ -93,11 +94,11 @@ export const polymarket: Platform = {
// let isbinary = Number(moreMarketInfo.conditions[0].outcomeSlotCount) == 2 // let isbinary = Number(moreMarketInfo.conditions[0].outcomeSlotCount) == 2
// let percentage = Number(moreMarketInfo.outcomeTokenPrices[0]) * 100 // let percentage = Number(moreMarketInfo.outcomeTokenPrices[0]) * 100
// let percentageFormatted = isbinary ? (percentage.toFixed(0) + "%") : "none" // let percentageFormatted = isbinary ? (percentage.toFixed(0) + "%") : "none"
let options = []; let options: FetchedQuestion["options"] = [];
for (let outcome in moreMarketInfo.outcomeTokenPrices) { for (let outcome in moreMarketInfo.outcomeTokenPrices) {
options.push({ options.push({
name: marketInfo.outcomes[outcome], name: String(marketInfo.outcomes[outcome]),
probability: moreMarketInfo.outcomeTokenPrices[outcome], probability: Number(moreMarketInfo.outcomeTokenPrices[outcome]),
type: "PROBABILITY", type: "PROBABILITY",
}); });
} }
@ -106,18 +107,12 @@ export const polymarket: Platform = {
id: id, id: id,
title: marketInfo.question, title: marketInfo.question,
url: "https://polymarket.com/market/" + marketInfo.slug, url: "https://polymarket.com/market/" + marketInfo.slug,
platform: platformName,
description: marketInfo.description, description: marketInfo.description,
options: options, options,
qualityindicators: { qualityindicators: {
numforecasts: numforecasts.toFixed(0), numforecasts: numforecasts.toFixed(0),
liquidity: liquidity.toFixed(2), liquidity: liquidity.toFixed(2),
tradevolume: tradevolume.toFixed(2), tradevolume: tradevolume.toFixed(2),
stars: calculateStars(platformName, {
liquidity,
option: options[0],
volume: tradevolume,
}),
}, },
extra: { extra: {
address: marketInfo.address, address: marketInfo.address,
@ -133,4 +128,33 @@ export const polymarket: Platform = {
} }
return results; return results;
}, },
calculateStars(data) {
// let nuno = (data) => (data.volume > 10000 ? 4 : data.volume > 1000 ? 3 : 2);
// let eli = (data) => data.liquidity > 10000 ? 5 : 4
// let misha = (data) => 4
const liquidity = data.qualityindicators.liquidity || 0;
const volume = data.qualityindicators.tradevolume || 0;
let nuno = () =>
liquidity > 1000 && volume > 10000
? 4
: liquidity > 500 && volume > 1000
? 3
: 2;
let starsDecimal = average([nuno()]); //, eli(data), misha(data)])
// Substract 1 star if probability is above 90% or below 10%
if (
data.options instanceof Array &&
data.options[0] &&
((data.options[0].probability || 0) < 0.1 ||
(data.options[0].probability || 0) > 0.9)
) {
starsDecimal = starsDecimal - 1;
}
let starsInteger = Math.round(starsDecimal);
return starsInteger;
},
}; };

View File

@ -1,24 +1,25 @@
import axios from "axios"; import axios from "axios";
import { calculateStars } from "../utils/stars"; import { average } from "../../utils";
import { sleep } from "../utils/sleep";
import toMarkdown from "../utils/toMarkdown"; import toMarkdown from "../utils/toMarkdown";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
const platformName = "predictit"; const platformName = "predictit";
/* Support functions */ /* Support functions */
async function fetchmarkets() { async function fetchmarkets(): Promise<any[]> {
let response = await axios({ const response = await axios({
method: "get", method: "get",
url: "https://www.predictit.org/api/marketdata/all/", url: "https://www.predictit.org/api/marketdata/all/",
}); });
let openMarkets = response.data.markets.filter( const openMarkets = response.data.markets.filter(
(market) => market.status == "Open" (market: any) => market.status == "Open"
); );
return openMarkets; return openMarkets;
} }
async function fetchmarketrules(market_id) { async function fetchmarketrules(market_id: string | number) {
let response = await axios({ let response = await axios({
method: "get", method: "get",
url: "https://www.predictit.org/api/Market/" + market_id, url: "https://www.predictit.org/api/Market/" + market_id,
@ -34,15 +35,12 @@ async function fetchmarketvolumes() {
return response.data; return response.data;
} }
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/* Body */ /* Body */
export const predictit: Platform = { export const predictit: Platform = {
name: platformName, name: platformName,
label: "PredictIt", label: "PredictIt",
color: "#460c00", color: "#460c00",
version: "v1",
async fetcher() { async fetcher() {
let markets = await fetchmarkets(); let markets = await fetchmarkets();
let marketVolumes = await fetchmarketvolumes(); let marketVolumes = await fetchmarketvolumes();
@ -65,13 +63,15 @@ export const predictit: Platform = {
let shares_volume = market["TotalSharesTraded"]; let shares_volume = market["TotalSharesTraded"];
// let percentageFormatted = isbinary ? Number(Number(market.contracts[0].lastTradePrice) * 100).toFixed(0) + "%" : "none" // let percentageFormatted = isbinary ? Number(Number(market.contracts[0].lastTradePrice) * 100).toFixed(0) + "%" : "none"
let options = market.contracts.map((contract) => ({ let options: FetchedQuestion["options"] = (market.contracts as any[]).map(
name: contract.name, (contract) => ({
probability: contract.lastTradePrice, name: String(contract.name),
probability: Number(contract.lastTradePrice),
type: "PROBABILITY", type: "PROBABILITY",
})); })
);
let totalValue = options let totalValue = options
.map((element) => Number(element.probability)) .map((element: any) => Number(element.probability))
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
if (options.length != 1 && totalValue > 1) { if (options.length != 1 && totalValue > 1) {
@ -81,7 +81,7 @@ export const predictit: Platform = {
})); }));
} else if (options.length == 1) { } else if (options.length == 1) {
let option = options[0]; let option = options[0];
let probability = option["probability"]; let probability = option.probability;
options = [ options = [
{ {
name: "Yes", name: "Yes",
@ -90,7 +90,7 @@ export const predictit: Platform = {
}, },
{ {
name: "No", name: "No",
probability: 1 - probability, probability: 1 - (probability || 0),
type: "PROBABILITY", type: "PROBABILITY",
}, },
]; ];
@ -100,11 +100,9 @@ export const predictit: Platform = {
id, id,
title: market["name"], title: market["name"],
url: market.url, url: market.url,
platform: platformName,
description, description,
options, options,
qualityindicators: { qualityindicators: {
stars: calculateStars(platformName, {}),
shares_volume, shares_volume,
}, },
}; };
@ -114,4 +112,12 @@ export const predictit: Platform = {
return results; return results;
}, },
calculateStars(data) {
let nuno = () => 3;
let eli = () => 3.5;
let misha = () => 2.5;
let starsDecimal = average([nuno(), eli(), misha()]);
let starsInteger = Math.round(starsDecimal);
return starsInteger;
},
}; };

View File

@ -1,7 +1,7 @@
import axios from "axios"; import axios from "axios";
import { JSDOM } from "jsdom"; import { JSDOM } from "jsdom";
import { calculateStars } from "../utils/stars"; import { average } from "../../utils";
import toMarkdown from "../utils/toMarkdown"; import toMarkdown from "../utils/toMarkdown";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
@ -48,6 +48,7 @@ export const rootclaim: Platform = {
name: platformName, name: platformName,
label: "Rootclaim", label: "Rootclaim",
color: "#0d1624", color: "#0d1624",
version: "v1",
async fetcher() { async fetcher() {
const claims = await fetchAllRootclaims(); const claims = await fetchAllRootclaims();
const results: FetchedQuestion[] = []; const results: FetchedQuestion[] = [];
@ -55,7 +56,7 @@ export const rootclaim: Platform = {
for (const claim of claims) { for (const claim of claims) {
const id = `${platformName}-${claim.slug.toLowerCase()}`; const id = `${platformName}-${claim.slug.toLowerCase()}`;
let options = []; let options: FetchedQuestion["options"] = [];
for (let scenario of claim.scenarios) { for (let scenario of claim.scenarios) {
options.push({ options.push({
name: toMarkdown(scenario.name || scenario.text) name: toMarkdown(scenario.name || scenario.text)
@ -75,16 +76,22 @@ export const rootclaim: Platform = {
id, id,
title: toMarkdown(claim.question).replace("\n", ""), title: toMarkdown(claim.question).replace("\n", ""),
url, url,
platform: platformName,
description: toMarkdown(description).replace("&#39;", "'"), description: toMarkdown(description).replace("&#39;", "'"),
options: options, options,
qualityindicators: { qualityindicators: {
numforecasts: 1, numforecasts: 1,
stars: calculateStars(platformName, {}),
}, },
}; };
results.push(obj); results.push(obj);
} }
return results; return results;
}, },
calculateStars(data) {
let nuno = () => 4;
let eli = () => null;
let misha = () => null;
let starsDecimal = average([nuno() /*, eli(data), misha(data)*/]);
let starsInteger = Math.round(starsDecimal);
return starsInteger;
},
}; };

View File

@ -1,146 +1,152 @@
import axios from "axios"; import axios from "axios";
import { calculateStars } from "../utils/stars"; import { QuestionOption } from "../../common/types";
import { average } from "../../utils";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
const platformName = "smarkets"; const platformName = "smarkets";
let htmlEndPointEntrance = "https://api.smarkets.com/v3/events/"; const apiEndpoint = "https://api.smarkets.com/v3"; // documented at https://docs.smarkets.com/
let VERBOSE = false;
let empty = () => 0; type Context = {
verbose: boolean;
};
/* Support functions */ /* Support functions */
async function fetchEvents(url) { async function fetchEvents(ctx: Context) {
let response = await axios({ let queryString =
url: htmlEndPointEntrance + url, "?state=new&state=upcoming&state=live&type_domain=politics&type_scope=single_event&with_new_type=true&sort=id&limit=50";
let events = [];
while (queryString) {
const data = await axios({
url: `${apiEndpoint}/events/${queryString}`,
method: "GET", method: "GET",
headers: {
"Content-Type": "text/html",
},
}).then((res) => res.data); }).then((res) => res.data);
VERBOSE ? console.log(response) : empty();
return response; events.push(...data.events);
queryString = data.pagination.next_page;
}
ctx.verbose && console.log(events);
return events;
} }
async function fetchMarkets(eventid) { async function fetchSingleEvent(id: string, ctx: Context) {
let response = await axios({ const events = await fetchEvents(ctx);
url: `https://api.smarkets.com/v3/events/${eventid}/markets/`, const event = events.find((event) => event.id === id);
if (!event) {
throw new Error(`Event ${id} not found`);
}
return event;
}
async function fetchMarkets(eventId: string) {
const response = await axios({
url: `${apiEndpoint}/events/${eventId}/markets/`,
method: "GET", method: "GET",
headers: {
"Content-Type": "text/json",
},
}) })
.then((res) => res.data) .then((res) => res.data)
.then((res) => res.markets); .then((res) => res.markets);
return response; return response;
} }
async function fetchContracts(marketid) { async function fetchContracts(marketId: string, ctx: Context) {
let response = await axios({ const response = await axios({
url: `https://api.smarkets.com/v3/markets/${marketid}/contracts/`, url: `${apiEndpoint}/markets/${marketId}/contracts/?include_hidden=true`,
method: "GET", method: "GET",
headers: {
"Content-Type": "text/html",
},
}).then((res) => res.data); }).then((res) => res.data);
VERBOSE ? console.log(response) : empty(); ctx.verbose && console.log(response);
return response;
if (!(response.contracts instanceof Array)) {
throw new Error("Invalid response while fetching contracts");
}
return response.contracts as any[];
} }
async function fetchPrices(marketid) { async function fetchPrices(marketId: string, ctx: Context) {
let response = await axios({ const response = await axios({
url: `https://api.smarkets.com/v3/markets/${marketid}/last_executed_prices/`, url: `https://api.smarkets.com/v3/markets/${marketId}/last_executed_prices/`,
method: "GET", method: "GET",
headers: {
"Content-Type": "text/html",
},
}).then((res) => res.data); }).then((res) => res.data);
VERBOSE ? console.log(response) : empty(); ctx.verbose && console.log(response);
return response; if (!response.last_executed_prices) {
throw new Error("Invalid response while fetching prices");
}
return response.last_executed_prices;
} }
export const smarkets: Platform = { async function processEventMarkets(event: any, ctx: Context) {
name: platformName, ctx.verbose && console.log(Date.now());
label: "Smarkets", ctx.verbose && console.log(event.name);
color: "#6f5b41",
async fetcher() {
let htmlPath =
"?state=new&state=upcoming&state=live&type_domain=politics&type_scope=single_event&with_new_type=true&sort=id&limit=50";
let events = []; let markets = await fetchMarkets(event.id);
while (htmlPath) { markets = markets.map((market: any) => ({
let data = await fetchEvents(htmlPath);
events.push(...data.events);
htmlPath = data.pagination.next_page;
}
VERBOSE ? console.log(events) : empty();
let markets = [];
for (let event of events) {
VERBOSE ? console.log(Date.now()) : empty();
VERBOSE ? console.log(event.name) : empty();
let eventMarkets = await fetchMarkets(event.id);
eventMarkets = eventMarkets.map((market) => ({
...market, ...market,
// smarkets doesn't have separate urls for different markets in a single event
// we could use anchors (e.g. https://smarkets.com/event/886716/politics/uk/uk-party-leaders/next-conservative-leader#contract-collapse-9815728-control), but it's unclear if they aren't going to change
slug: event.full_slug, slug: event.full_slug,
})); }));
VERBOSE ? console.log("Markets fetched") : empty(); ctx.verbose && console.log(`Markets for ${event.id} fetched`);
VERBOSE ? console.log(event.id) : empty(); ctx.verbose && console.log(markets);
VERBOSE ? console.log(eventMarkets) : empty();
markets.push(...eventMarkets); let results: FetchedQuestion[] = [];
//let lastPrices = await fetchPrices(market.id) for (const market of markets) {
ctx.verbose && console.log("================");
ctx.verbose && console.log("Market:", market);
const contracts = await fetchContracts(market.id, ctx);
ctx.verbose && console.log("Contracts:", contracts);
const prices = await fetchPrices(market.id, ctx);
ctx.verbose && console.log("Prices:", prices[market.id]);
let optionsObj: {
[k: string]: QuestionOption;
} = {};
const contractsById = Object.fromEntries(
contracts.map((c) => [c.id as string, c])
);
for (const price of prices[market.id]) {
const contract = contractsById[price.contract_id];
if (!contract) {
console.warn(
`Couldn't find contract ${price.contract_id} in contracts data for ${market.id}, event ${market.event_id}, skipping`
);
continue;
} }
VERBOSE ? console.log(markets) : empty();
let results = [];
for (let market of markets) {
VERBOSE ? console.log("================") : empty();
VERBOSE ? console.log("Market: ", market) : empty();
let id = `${platformName}-${market.id}`;
let name = market.name;
let contracts = await fetchContracts(market.id);
VERBOSE ? console.log("Contracts: ", contracts) : empty();
let prices = await fetchPrices(market.id);
VERBOSE
? console.log("Prices: ", prices["last_executed_prices"][market.id])
: empty();
let optionsObj = {};
for (let contract of contracts["contracts"]) {
optionsObj[contract.id] = { name: contract.name };
}
for (let price of prices["last_executed_prices"][market.id]) {
optionsObj[price.contract_id] = { optionsObj[price.contract_id] = {
...optionsObj[price.contract_id], name: contract.name,
probability: price.last_executed_price probability: contract.hidden ? 0 : Number(price.last_executed_price),
? Number(price.last_executed_price)
: null,
type: "PROBABILITY", type: "PROBABILITY",
}; };
} }
let options: any[] = Object.values(optionsObj); let options: QuestionOption[] = Object.values(optionsObj);
ctx.verbose && console.log("Options before patching:", options);
// monkey patch the case where there are only two options and only one has traded. // monkey patch the case where there are only two options and only one has traded.
if ( if (
options.length == 2 && options.length === 2 &&
options.map((option) => option.probability).includes(null) options.map((option) => option.probability).includes(0)
) { ) {
let nonNullPrice = const nonNullPrice = options[0].probability || options[1].probability;
options[0].probability == null
? options[1].probability if (nonNullPrice) {
: options[0].probability;
options = options.map((option) => { options = options.map((option) => {
let probability = option.probability;
return { return {
...option, ...option,
probability: probability == null ? 100 - nonNullPrice : probability, probability: option.probability || 100 - nonNullPrice,
// yes, 100, because prices are not yet normalized. // yes, 100, because prices are not yet normalized.
}; };
}); });
} }
}
ctx.verbose && console.log("Options after patching:", options);
// Normalize normally // Normalize normally
let totalValue = options const totalValue = options
.map((element) => Number(element.probability)) .map((element) => Number(element.probability))
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
@ -148,33 +154,59 @@ export const smarkets: Platform = {
...element, ...element,
probability: Number(element.probability) / totalValue, probability: Number(element.probability) / totalValue,
})); }));
VERBOSE ? console.log(options) : empty(); ctx.verbose && console.log("Normalized options:", options);
/* const result: FetchedQuestion = {
if(contracts["contracts"].length == 2){ id: `${platformName}-${market.id}`,
isBinary = true title: market.name,
percentage = ( Number(prices["last_executed_prices"][market.id][0].last_executed_price) + (100 - Number(prices["last_executed_prices"][market.id][1].last_executed_price)) ) / 2
percentage = Math.round(percentage)+"%"
let contractName = contracts["contracts"][0].name
name = name+ (contractName=="Yes"?'':` (${contracts["contracts"][0].name})`)
}
*/
let result: FetchedQuestion = {
id: id,
title: name,
url: "https://smarkets.com/event/" + market.event_id + market.slug, url: "https://smarkets.com/event/" + market.event_id + market.slug,
platform: platformName,
description: market.description, description: market.description,
options: options, options,
timestamp: new Date(), timestamp: new Date(),
qualityindicators: { qualityindicators: {},
stars: calculateStars(platformName, {}),
},
}; };
VERBOSE ? console.log(result) : empty(); ctx.verbose && console.log(result);
results.push(result); results.push(result);
} }
VERBOSE ? console.log(results) : empty();
return results; return results;
}
export const smarkets: Platform<"eventId" | "verbose"> = {
name: platformName,
label: "Smarkets",
color: "#6f5b41",
version: "v2",
fetcherArgs: ["eventId", "verbose"],
async fetcher(opts) {
const ctx = {
verbose: Boolean(opts.args?.verbose) || false,
};
let events: any[] = [];
let partial = true;
if (opts.args?.eventId) {
events = [await fetchSingleEvent(opts.args.eventId, ctx)];
} else {
events = await fetchEvents(ctx);
partial = false;
}
let results: FetchedQuestion[] = [];
for (const event of events) {
const eventResults = await processEventMarkets(event, ctx);
results.push(...eventResults);
}
return {
questions: results,
partial,
};
},
calculateStars(data) {
const nuno = () => 2;
const eli = () => null;
const misha = () => null;
const starsDecimal = average([nuno()]); //, eli(), misha()])
const starsInteger = Math.round(starsDecimal);
return starsInteger;
}, },
}; };

View File

@ -1,9 +1,9 @@
/* Imports */ /* Imports */
import { GoogleSpreadsheet } from "google-spreadsheet"; import { GoogleSpreadsheet } from "google-spreadsheet";
import { average } from "../../utils";
import { applyIfSecretExists } from "../utils/getSecrets"; import { applyIfSecretExists } from "../utils/getSecrets";
import { hash } from "../utils/hash"; import { hash } from "../utils/hash";
import { calculateStars } from "../utils/stars";
import { FetchedQuestion, Platform } from "./"; import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
@ -13,7 +13,7 @@ const endpoint = `https://docs.google.com/spreadsheets/d/${SHEET_ID}/edit#gid=0`
// https://docs.google.com/spreadsheets/d/1xcgYF7Q0D95TPHLLSgwhWBHFrWZUGJn7yTyAhDR4vi0/edit#gid=0 // https://docs.google.com/spreadsheets/d/1xcgYF7Q0D95TPHLLSgwhWBHFrWZUGJn7yTyAhDR4vi0/edit#gid=0
/* Support functions */ /* Support functions */
const formatRow = (row) => { const formatRow = (row: string[]) => {
let colNames = [ let colNames = [
"Prediction Date", "Prediction Date",
"Prediction", "Prediction",
@ -23,15 +23,15 @@ const formatRow = (row) => {
"Prediction Right?", "Prediction Right?",
"Brier Score", "Brier Score",
"Notes", "Notes",
]; ] as const;
let result = {}; let result: Partial<{ [k in typeof colNames[number]]: string }> = {};
row.forEach((col, i) => { row.forEach((col: string, i) => {
result[colNames[i]] = col; result[colNames[i]] = col;
}); });
return result; return result as Required<typeof result>;
}; };
async function fetchGoogleDoc(google_api_key) { async function fetchGoogleDoc(google_api_key: string) {
// https://gist.github.com/micalevisk/9bc831bd4b3e5a3f62b9810330129c59 // https://gist.github.com/micalevisk/9bc831bd4b3e5a3f62b9810330129c59
let results = []; let results = [];
const doc = new GoogleSpreadsheet(SHEET_ID); const doc = new GoogleSpreadsheet(SHEET_ID);
@ -41,7 +41,7 @@ async function fetchGoogleDoc(google_api_key) {
console.log(">>", doc.title); console.log(">>", doc.title);
const sheet = doc.sheetsByIndex[0]; const sheet = doc.sheetsByIndex[0];
const rows = await sheet.getRows({ offset: 0 }); const rows = await sheet.getRows();
console.log("# " + rows[0]._sheet.headerValues.join(",")); console.log("# " + rows[0]._sheet.headerValues.join(","));
let isEnd = false; let isEnd = false;
@ -68,7 +68,9 @@ async function fetchGoogleDoc(google_api_key) {
return results; return results;
} }
async function processPredictions(predictions) { async function processPredictions(
predictions: Awaited<ReturnType<typeof fetchGoogleDoc>>
) {
let currentPredictions = predictions.filter( let currentPredictions = predictions.filter(
(prediction) => prediction["Actual"] == "Unknown" (prediction) => prediction["Actual"] == "Unknown"
); );
@ -76,7 +78,7 @@ async function processPredictions(predictions) {
let title = prediction["Prediction"].replace(" [update]", ""); let title = prediction["Prediction"].replace(" [update]", "");
let id = `${platformName}-${hash(title)}`; let id = `${platformName}-${hash(title)}`;
let probability = Number(prediction["Odds"].replace("%", "")) / 100; let probability = Number(prediction["Odds"].replace("%", "")) / 100;
let options = [ let options: FetchedQuestion["options"] = [
{ {
name: "Yes", name: "Yes",
probability: probability, probability: probability,
@ -92,20 +94,17 @@ async function processPredictions(predictions) {
id, id,
title, title,
url: prediction["url"], url: prediction["url"],
platform: platformName,
description: prediction["Notes"] || "", description: prediction["Notes"] || "",
options, options,
timestamp: new Date(Date.parse(prediction["Prediction Date"] + "Z")), timestamp: new Date(Date.parse(prediction["Prediction Date"] + "Z")),
qualityindicators: { qualityindicators: {},
stars: calculateStars(platformName, null),
},
}; };
return result; return result;
}); });
results = results.reverse(); results = results.reverse();
let uniqueTitles = []; let uniqueTitles: string[] = [];
let uniqueResults = []; let uniqueResults: FetchedQuestion[] = [];
results.forEach((result) => { results.forEach((result) => {
if (!uniqueTitles.includes(result.title)) uniqueResults.push(result); if (!uniqueTitles.includes(result.title)) uniqueResults.push(result);
uniqueTitles.push(result.title); uniqueTitles.push(result.title);
@ -113,7 +112,7 @@ async function processPredictions(predictions) {
return uniqueResults; return uniqueResults;
} }
export async function wildeford_inner(google_api_key) { export async function wildeford_inner(google_api_key: string) {
let predictions = await fetchGoogleDoc(google_api_key); let predictions = await fetchGoogleDoc(google_api_key);
return await processPredictions(predictions); return await processPredictions(predictions);
} }
@ -122,8 +121,17 @@ export const wildeford: Platform = {
name: platformName, name: platformName,
label: "Peter Wildeford", label: "Peter Wildeford",
color: "#984158", color: "#984158",
version: "v1",
async fetcher() { async fetcher() {
const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY; // See: https://developers.google.com/sheets/api/guides/authorizing#APIKey const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY; // See: https://developers.google.com/sheets/api/guides/authorizing#APIKey
return await applyIfSecretExists(GOOGLE_API_KEY, wildeford_inner); return (await applyIfSecretExists(GOOGLE_API_KEY, wildeford_inner)) || null;
},
calculateStars(data) {
let nuno = () => 3;
let eli = () => null;
let misha = () => null;
let starsDecimal = average([nuno()]); //, eli(), misha()])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}, },
}; };

View File

@ -9,21 +9,22 @@ export const xrisk: Platform = {
name: "xrisk", name: "xrisk",
label: "X-risk estimates", label: "X-risk estimates",
color: "#272600", color: "#272600",
version: "v1",
async fetcher() { async fetcher() {
// return; // not necessary to refill the DB every time // return; // not necessary to refill the DB every time
let fileRaw = fs.readFileSync("./input/xrisk-questions.json", { let fileRaw = fs.readFileSync("./input/xrisk-questions.json", {
encoding: "utf-8", encoding: "utf-8",
}); });
let results = JSON.parse(fileRaw); let parsedData = JSON.parse(fileRaw);
results = results.map((item) => { const results = parsedData.map((item: any) => {
item.extra = item.moreoriginsdata; item.extra = item.moreoriginsdata;
delete item.moreoriginsdata; delete item.moreoriginsdata;
return { return {
...item, ...item,
id: `${platformName}-${hash(item.title + " | " + item.url)}`, // some titles are non-unique, but title+url pair is always unique id: `${platformName}-${hash(item.title + " | " + item.url)}`, // some titles are non-unique, but title+url pair is always unique
platform: platformName,
}; };
}); });
return results; return results;
}, },
calculateStars: () => 2,
}; };

View File

@ -5,13 +5,14 @@ import { Question } from "@prisma/client";
import { prisma } from "../database/prisma"; import { prisma } from "../database/prisma";
import { platforms } from "../platforms"; import { platforms } from "../platforms";
let cookie = process.env.ALGOLIA_MASTER_API_KEY; let cookie = process.env.ALGOLIA_MASTER_API_KEY || "";
const algoliaAppId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID; const algoliaAppId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "";
const client = algoliasearch(algoliaAppId, cookie); const client = algoliasearch(algoliaAppId, cookie);
const index = client.initIndex("metaforecast"); const index = client.initIndex("metaforecast");
export type AlgoliaQuestion = Omit<Question, "timestamp"> & { export type AlgoliaQuestion = Omit<Question, "timestamp"> & {
timestamp: string; timestamp: string;
optionsstringforsearch?: string;
}; };
const getoptionsstringforsearch = (record: Question): string => { const getoptionsstringforsearch = (record: Question): string => {
@ -43,7 +44,7 @@ export async function rebuildAlgoliaDatabase() {
}) })
); );
if (index.exists()) { if (await index.exists()) {
console.log("Index exists"); console.log("Index exists");
await index.replaceAllObjects(records, { safe: true }); await index.replaceAllObjects(records, { safe: true });
console.log( console.log(

View File

@ -1,51 +0,0 @@
/* Imports */
import fs from "fs";
import { prisma } from "../../database/prisma";
/* Definitions */
/* Utilities */
/* Support functions */
const getQualityIndicators = (question) =>
Object.entries(question.qualityindicators)
.map((entry) => `${entry[0]}: ${entry[1]}`)
.join("; ");
/* Body */
const main = async () => {
let highQualityPlatforms = [
"CSET-foretell",
"Foretold",
"Good Judgment Open",
"Metaculus",
"PredictIt",
"Rootclaim",
];
const json = await prisma.question.findMany({});
console.log(json.length);
//let uniquePlatforms = [...new Set(json.map(forecast => forecast.platform))]
//console.log(uniquePlatforms)
const questionsFromGoodPlatforms = json.filter((question) =>
highQualityPlatforms.includes(question.platform)
);
const tsv =
"index\ttitle\turl\tqualityindicators\n" +
questionsFromGoodPlatforms
.map((question, index) => {
let row = `${index}\t${question.title}\t${
question.url
}\t${getQualityIndicators(question)}`;
console.log(row);
return row;
})
.join("\n");
//console.log(tsv)
// let string = JSON.stringify(json, null, 2)
fs.writeFileSync("metaforecasts.tsv", tsv);
};
main();

View File

@ -1,48 +0,0 @@
/* Imports */
import fs from "fs";
import { shuffleArray } from "../../../utils";
import { prisma } from "../../database/prisma";
/* Definitions */
/* Utilities */
/* Support functions */
let getQualityIndicators = (question) =>
Object.entries(question.qualityindicators)
.map((entry) => `${entry[0]}: ${entry[1]}`)
.join("; ");
/* Body */
let main = async () => {
let highQualityPlatforms = ["Metaculus"]; // ['CSET-foretell', 'Foretold', 'Good Judgment Open', 'Metaculus', 'PredictIt', 'Rootclaim']
let json = await prisma.question.findMany({});
console.log(json.length);
//let uniquePlatforms = [...new Set(json.map(question => question.platform))]
//console.log(uniquePlatforms)
let questionsFromGoodPlatforms = json.filter((question) =>
highQualityPlatforms.includes(question.platform)
);
let questionsFromGoodPlatformsShuffled = shuffleArray(
questionsFromGoodPlatforms
);
let tsv =
"index\ttitle\turl\tqualityindicators\n" +
questionsFromGoodPlatforms
.map((question, index) => {
let row = `${index}\t${question.title}\t${
question.url
}\t${getQualityIndicators(question)}`;
console.log(row);
return row;
})
.join("\n");
//console.log(tsv)
// let string = JSON.stringify(json, null, 2)
fs.writeFileSync("metaforecasts_metaculus_v2.tsv", tsv);
};
main();

View File

@ -1,5 +1,5 @@
export async function applyIfSecretExists<T>( export async function applyIfSecretExists<T>(
cookie: string, cookie: string | undefined,
fun: (cookie: string) => T fun: (cookie: string) => T
) { ) {
if (cookie) { if (cookie) {

View File

@ -11,7 +11,7 @@ let locationData = "./data/";
// let rawdata = fs.readFileSync("./data/merged-questions.json") // run from topmost folder, not from src // let rawdata = fs.readFileSync("./data/merged-questions.json") // run from topmost folder, not from src
async function main() { async function main() {
const data = await prisma.question.findMany({}); const data = await prisma.question.findMany({});
const processDescription = (description) => { const processDescription = (description: string | null | undefined) => {
if (description == null || description == undefined || description == "") { if (description == null || description == undefined || description == "") {
return ""; return "";
} else { } else {

View File

@ -10,7 +10,7 @@ let rawdata = fs.readFileSync("../data/merged-questions.json", {
}); });
let data = JSON.parse(rawdata); let data = JSON.parse(rawdata);
let results = []; let results: any[] = [];
for (let datum of data) { for (let datum of data) {
// do something // do something
} }

View File

@ -1,25 +0,0 @@
export function roughSizeOfObject(object) {
var objectList = [];
var stack = [object];
var bytes = 0;
while (stack.length) {
var value = stack.pop();
if (typeof value === "boolean") {
bytes += 4;
} else if (typeof value === "string") {
bytes += value.length * 2;
} else if (typeof value === "number") {
bytes += 8;
} else if (typeof value === "object" && objectList.indexOf(value) === -1) {
objectList.push(value);
for (var i in value) {
stack.push(value[i]);
}
}
}
let megaBytes = bytes / 1024 ** 2;
let megaBytesRounded = Math.round(megaBytes * 10) / 10;
return megaBytesRounded;
}

View File

@ -0,0 +1,3 @@
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@ -1,356 +0,0 @@
export function getStarSymbols(numstars) {
let stars = "★★☆☆☆";
switch (numstars) {
case 0:
stars = "☆☆☆☆☆";
break;
case 1:
stars = "★☆☆☆☆";
break;
case 2:
stars = "★★☆☆☆";
break;
case 3:
stars = "★★★☆☆";
break;
case 4:
stars = "★★★★☆";
break;
case 5:
stars = "★★★★★";
break;
default:
stars = "★★☆☆☆";
}
return stars;
}
let average = (array) => array.reduce((a, b) => a + b, 0) / array.length;
function calculateStarsAstralCodexTen(data) {
let nuno = (data) => 3;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsBetfair(data) {
let nuno = (data) => (data.volume > 10000 ? 4 : data.volume > 1000 ? 3 : 2);
let eli = (data) => (data.volume > 10000 ? null : null);
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
// Substract 1 star if probability is above 90% or below 10%
if (
data.option &&
(data.option.probability < 0.1 || data.option.probability > 0.9)
) {
starsDecimal = starsDecimal - 1;
}
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsCoupCast(data) {
let nuno = (data) => 3;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsCSETForetell(data) {
let nuno = (data) => (data.numforecasts > 100 ? 3 : 2);
let eli = (data) => 3;
let misha = (data) => 2;
let starsDecimal = average([nuno(data), eli(data), misha(data)]);
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsElicit(data) {
let nuno = (data) => 1;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsEstimize(data) {
let nuno = (data) => 2;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsForetold(data) {
let nuno = (data) => 2;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsGiveWellOpenPhil(data) {
let nuno = (data) => 2;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsGoodJudgment(data) {
let nuno = (data) => 4;
let eli = (data) => 4;
let misha = (data) => 3.5;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsGoodJudgmentOpen(data) {
let nuno = (data) => (data.numforecasts > 100 ? 3 : 2);
let eli = (data) => 3;
let misha = (data) =>
data.minProbability > 0.1 || data.maxProbability < 0.9 ? 3.1 : 2.5;
let starsDecimal = average([nuno(data), eli(data), misha(data)]);
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsHypermind(data) {
let nuno = (data) => 3;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsInfer(data) {
let nuno = (data) => 2;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsKalshi(data) {
let nuno = (data) =>
data.interest > 500 && data.shares_volume > 10000
? 4
: data.shares_volume > 2000
? 3
: 2;
// let eli = (data) => data.interest > 10000 ? 5 : 4
// let misha = (data) => 4
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
// Substract 1 star if probability is above 90% or below 10%
if (
data.option &&
(data.option.probability < 0.1 || data.option.probability > 0.9)
) {
starsDecimal = starsDecimal - 1;
}
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsLadbrokes(data) {
let nuno = (data) => 2;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsManifold(data) {
let nuno = (data) =>
data.volume7Days > 250 || (data.pool > 500 && data.volume7Days > 100)
? 2
: 1;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
// console.log(data);
// console.log(starsInteger);
return starsInteger;
}
function calculateStarsMetaculus(data) {
let nuno = (data) =>
data.numforecasts > 300 ? 4 : data.numforecasts > 100 ? 3 : 2;
let eli = (data) => 3;
let misha = (data) => 3;
let starsDecimal = average([nuno(data), eli(data), misha(data)]);
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsOmen(data) {
let nuno = (data) => 1;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsPolymarket(data) {
// let nuno = (data) => (data.volume > 10000 ? 4 : data.volume > 1000 ? 3 : 2);
// let eli = (data) => data.liquidity > 10000 ? 5 : 4
// let misha = (data) => 4
let nuno = (data) =>
data.liquidity > 1000 && data.volume > 10000
? 4
: data.liquidity > 500 && data.volume > 1000
? 3
: 2;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
// Substract 1 star if probability is above 90% or below 10%
if (
data.option &&
(data.option.probability < 0.1 || data.option.probability > 0.9)
) {
starsDecimal = starsDecimal - 1;
}
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsPredictIt(data) {
let nuno = (data) => 3;
let eli = (data) => 3.5;
let misha = (data) => 2.5;
let starsDecimal = average([nuno(data), eli(data), misha(data)]);
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsRootclaim(data) {
let nuno = (data) => 4;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data) /*, eli(data), misha(data)*/]);
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsSmarkets(data) {
let nuno = (data) => 2;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsWildeford(data) {
let nuno = (data) => 3;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
function calculateStarsWilliamHill(data) {
let nuno = (data) => 2;
let eli = (data) => null;
let misha = (data) => null;
let starsDecimal = average([nuno(data)]); //, eli(data), misha(data)])
let starsInteger = Math.round(starsDecimal);
return starsInteger;
}
export function calculateStars(platform: string, data) {
let stars = 2;
switch (platform) {
case "betfair":
stars = calculateStarsBetfair(data);
break;
case "infer":
stars = calculateStarsInfer(data);
break;
case "foretold":
stars = calculateStarsForetold(data);
break;
case "givewellopenphil":
stars = calculateStarsGiveWellOpenPhil(data);
break;
case "goodjudgment":
stars = calculateStarsGoodJudgment(data);
break;
case "goodjudgmentopen":
stars = calculateStarsGoodJudgmentOpen(data);
break;
case "kalshi":
stars = calculateStarsKalshi(data);
break;
case "manifold":
stars = calculateStarsManifold(data);
break;
case "metaculus":
stars = calculateStarsMetaculus(data);
break;
case "polymarket":
stars = calculateStarsPolymarket(data);
break;
case "predictit":
stars = calculateStarsPredictIt(data);
break;
case "rootclaim":
stars = calculateStarsRootclaim(data);
break;
case "smarkets":
stars = calculateStarsSmarkets(data);
break;
case "wildeford":
stars = calculateStarsWildeford(data);
break;
// deprecated
case "AstralCodexTen":
stars = calculateStarsAstralCodexTen(data);
break;
case "CoupCast":
stars = calculateStarsCoupCast(data);
break;
case "CSET-foretell":
stars = calculateStarsCSETForetell(data);
break;
case "Elicit":
stars = calculateStarsElicit(data);
break;
case "Estimize":
stars = calculateStarsEstimize(data);
break;
case "Hypermind":
stars = calculateStarsHypermind(data);
break;
case "Ladbrokes":
stars = calculateStarsLadbrokes(data);
break;
case "Omen":
stars = calculateStarsOmen(data);
break;
case "WilliamHill":
stars = calculateStarsWilliamHill(data);
break;
default:
stars = 2;
}
return stars;
}

View File

@ -1,26 +1,16 @@
/* Imports */
import textVersion from "textversionjs"; import textVersion from "textversionjs";
/* Definitions */ export default function toMarkdown(htmlText: string) {
String.prototype.replaceAll = function replaceAll(search, replace) { let html2 = htmlText.replaceAll(`='`, `="`).replaceAll(`'>`, `">`);
return this.split(search).join(replace); return textVersion(html2, {
}; linkProcess: (href, linkText) => {
let newHref = href
var styleConfig = { ? href.replace(/\(/g, "%28").replace(/\)/g, "%29")
linkProcess: function (href, linkText) { : "";
let newHref = href ? href.replace(/\(/g, "%28").replace(/\)/g, "%29") : ""; // Deal correctly in markdown with links that contain parenthesis
// Deal corretly in markdown with links that contain parenthesis
return `[${linkText}](${newHref})`; return `[${linkText}](${newHref})`;
}, },
}; });
/* Support functions */
/* Body */
export default function toMarkdown(htmlText) {
let html2 = htmlText.replaceAll(`='`, `="`).replaceAll(`'>`, `">`);
return textVersion(html2, styleConfig);
} }
// toMarkdown() // toMarkdown()

22
src/common/types.ts Normal file
View File

@ -0,0 +1,22 @@
import { QuestionFragment } from "../web/fragments.generated";
// this type is good both for backend (e.g. FetchedQuestion["options"]) and for graphql shapes
export type QuestionOption = {
name?: string;
probability?: number;
type: "PROBABILITY";
};
export type FullQuestionOption = Exclude<
QuestionOption,
"name" | "probability"
> & {
name: NonNullable<QuestionOption["name"]>;
probability: NonNullable<QuestionOption["probability"]>;
};
export const isFullQuestionOption = (
option: QuestionOption | QuestionFragment["options"][0]
): option is FullQuestionOption => {
return option.name != null && option.probability != null;
};

View File

@ -36,6 +36,7 @@ const DashboardObj = builder.objectRef<Dashboard>("Dashboard").implement({
builder.queryField("dashboard", (t) => builder.queryField("dashboard", (t) =>
t.field({ t.field({
type: DashboardObj, type: DashboardObj,
nullable: true,
description: "Look up a single dashboard by its id", description: "Look up a single dashboard by its id",
args: { args: {
id: t.arg({ type: "ID", required: true }), id: t.arg({ type: "ID", required: true }),

View File

@ -140,6 +140,7 @@ builder.queryField("questions", (t) =>
builder.queryField("question", (t) => builder.queryField("question", (t) =>
t.field({ t.field({
type: QuestionObj, type: QuestionObj,
nullable: true,
description: "Look up a single question by its id", description: "Look up a single question by its id",
args: { args: {
id: t.arg({ type: "ID", required: true }), id: t.arg({ type: "ID", required: true }),
@ -149,7 +150,6 @@ builder.queryField("question", (t) =>
const [platform, id] = [parts[0], parts.slice(1).join("-")]; const [platform, id] = [parts[0], parts.slice(1).join("-")];
if (platform === "guesstimate") { if (platform === "guesstimate") {
const q = await guesstimate.fetchQuestion(Number(id)); const q = await guesstimate.fetchQuestion(Number(id));
console.log(q);
return q; return q;
} }
return await prisma.question.findUnique({ return await prisma.question.findUnique({

View File

@ -32,19 +32,19 @@ builder.queryField("searchQuestions", (t) =>
// defs // defs
const query = input.query === undefined ? "" : input.query; const query = input.query === undefined ? "" : input.query;
if (query === "") return []; if (query === "") return [];
const forecastsThreshold = input.forecastsThreshold; const { forecastsThreshold, starsThreshold } = input;
const starsThreshold = input.starsThreshold;
const platformsIncludeGuesstimate = const platformsIncludeGuesstimate =
input.forecastingPlatforms?.includes("guesstimate") && input.forecastingPlatforms?.includes("guesstimate") &&
starsThreshold <= 1; (!starsThreshold || starsThreshold <= 1);
// preparation // preparation
const unawaitedAlgoliaResponse = searchWithAlgolia({ const unawaitedAlgoliaResponse = searchWithAlgolia({
queryString: query, queryString: query,
hitsPerPage: input.limit + 50, hitsPerPage: input.limit ?? 50,
starsThreshold, starsThreshold: starsThreshold ?? undefined,
filterByPlatforms: input.forecastingPlatforms, filterByPlatforms: input.forecastingPlatforms ?? undefined,
forecastsThreshold, forecastsThreshold: forecastsThreshold ?? undefined,
}); });
let results: AlgoliaQuestion[] = []; let results: AlgoliaQuestion[] = [];

View File

@ -1,5 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next/types"; import { NextApiRequest, NextApiResponse } from "next/types";
import { runMePlease } from "squiggle-experimental/dist/index.js";
import { run } from "@quri/squiggle-lang";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
@ -24,6 +25,6 @@ $ curl -X POST -H "Content-Type: application/json" -d '{"model": "1 to 4"}'
}); });
} else { } else {
console.log(body.model); console.log(body.model);
res.status(200).send(runMePlease(body.model)); res.status(200).send(run(body.model));
} }
} }

View File

@ -1,5 +1,5 @@
import { GetServerSideProps, NextPage } from "next"; import { GetServerSideProps, NextPage } from "next";
import Error from "next/error"; import NextError from "next/error";
import { import {
DashboardByIdDocument, DashboardFragment DashboardByIdDocument, DashboardFragment
@ -19,9 +19,13 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
const dashboardId = context.query.id as string; const dashboardId = context.query.id as string;
const numCols = Number(context.query.numCols); const numCols = Number(context.query.numCols);
const dashboard = ( const response = await client
await client.query(DashboardByIdDocument, { id: dashboardId }).toPromise() .query(DashboardByIdDocument, { id: dashboardId })
).data?.result; .toPromise();
if (!response.data) {
throw new Error(`GraphQL query failed: ${response.error}`);
}
const dashboard = response.data.result;
if (!dashboard) { if (!dashboard) {
context.res.statusCode = 404; context.res.statusCode = 404;
@ -32,14 +36,14 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
// reduntant: page component doesn't do graphql requests, but it's still nice/more consistent to have data in cache // reduntant: page component doesn't do graphql requests, but it's still nice/more consistent to have data in cache
urqlState: ssrCache.extractData(), urqlState: ssrCache.extractData(),
dashboard, dashboard,
numCols: !numCols ? null : numCols < 5 ? numCols : 4, numCols: !numCols ? undefined : numCols < 5 ? numCols : 4,
}, },
}; };
}; };
const EmbedDashboardPage: NextPage<Props> = ({ dashboard, numCols }) => { const EmbedDashboardPage: NextPage<Props> = ({ dashboard, numCols }) => {
if (!dashboard) { if (!dashboard) {
return <Error statusCode={404} />; return <NextError statusCode={404} />;
} }
return ( return (

View File

@ -45,8 +45,11 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
const defaultNumDisplay = 21; const defaultNumDisplay = 21;
const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay; const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay;
const defaultResults = (await client.query(FrontpageDocument).toPromise()) const response = await client.query(FrontpageDocument).toPromise();
.data.result; if (!response.data) {
throw new Error(`GraphQL query failed: ${response.error}`);
}
const defaultResults = response.data.result;
if ( if (
!!initialQueryParameters && !!initialQueryParameters &&
@ -58,7 +61,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
.query(SearchDocument, { .query(SearchDocument, {
input: { input: {
...initialQueryParameters, ...initialQueryParameters,
limit: initialNumDisplay, limit: initialNumDisplay + 50,
}, },
}) })
.toPromise(); .toPromise();

View File

@ -29,16 +29,19 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
let results: QuestionFragment[] = []; let results: QuestionFragment[] = [];
if (initialQueryParameters.query !== "") { if (initialQueryParameters.query !== "") {
results = ( const response = await client
await client
.query(SearchDocument, { .query(SearchDocument, {
input: { input: {
...initialQueryParameters, ...initialQueryParameters,
limit: 1, limit: 1,
}, },
}) })
.toPromise() .toPromise();
).data.result; if (response.data?.result) {
results = response.data.result;
} else {
throw new Error("GraphQL request failed");
}
} }
return { return {

View File

@ -6,3 +6,6 @@ export const shuffleArray = <T>(array: T[]): T[] => {
} }
return array; return array;
}; };
export const average = (array: number[]) =>
array.reduce((a, b) => a + b, 0) / array.length;

View File

@ -2,7 +2,7 @@ import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import React, { ErrorInfo } from "react"; import React, { ErrorInfo } from "react";
import { Logo2 } from "../icons/index"; import { Logo2 } from "../icons";
interface MenuItem { interface MenuItem {
page: string; page: string;

View File

@ -87,7 +87,7 @@ export const MultiSelectPlatform: React.FC<Props> = ({
const selectValue = value.map((v) => id2option[v]).filter((v) => v); const selectValue = value.map((v) => id2option[v]).filter((v) => v);
const onSelectChange = (newValue: Option[]) => { const onSelectChange = (newValue: readonly Option[]) => {
onChange(newValue.map((o) => o.value)); onChange(newValue.map((o) => o.value));
}; };

View File

@ -1,4 +1,4 @@
import React, { EventHandler, SyntheticEvent, useState } from "react"; import React, { ChangeEvent, EventHandler, SyntheticEvent, useState } from "react";
import { Button } from "../common/Button"; import { Button } from "../common/Button";
import { InfoBox } from "../common/InfoBox"; import { InfoBox } from "../common/InfoBox";
@ -18,7 +18,7 @@ export const DashboardCreator: React.FC<Props> = ({ handleSubmit }) => {
const [value, setValue] = useState(exampleInput); const [value, setValue] = useState(exampleInput);
const [acting, setActing] = useState(false); const [acting, setActing] = useState(false);
const handleChange = (event) => { const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setValue(event.target.value); setValue(event.target.value);
}; };
@ -37,7 +37,9 @@ export const DashboardCreator: React.FC<Props> = ({ handleSubmit }) => {
} }
} catch (error) { } catch (error) {
setActing(false); setActing(false);
const substituteText = `Error: ${error.message} const substituteText = `Error: ${
error instanceof Error ? error.message : "Unknown"
}
Try something like: Try something like:
${exampleInput} ${exampleInput}

View File

@ -1,6 +1,7 @@
/* Imports */
import React from "react"; import React from "react";
import { Handles, Rail, Slider, Tracks } from "react-compound-slider"; import {
GetHandleProps, GetTrackProps, Handles, Rail, Slider, SliderItem, Tracks
} from "react-compound-slider";
// https://sghall.github.io/react-compound-slider/#/getting-started/tutorial // https://sghall.github.io/react-compound-slider/#/getting-started/tutorial
@ -24,12 +25,11 @@ const railStyle = {
}; };
/* Support functions */ /* Support functions */
function Handle({ const Handle: React.FC<{
handle: { id, value, percent }, handle: SliderItem;
getHandleProps, getHandleProps: GetHandleProps;
displayFunction, displayFunction: (value: number) => string;
handleWidth, }> = ({ handle: { id, value, percent }, getHandleProps, displayFunction }) => {
}) {
return ( return (
<> <>
<div className="justify-center text-center text-gray-600 text-xs"> <div className="justify-center text-center text-gray-600 text-xs">
@ -53,9 +53,13 @@ function Handle({
></div> ></div>
</> </>
); );
} };
function Track({ source, target, getTrackProps }) { const Track: React.FC<{
source: SliderItem;
target: SliderItem;
getTrackProps: GetTrackProps;
}> = ({ source, target, getTrackProps }) => {
return ( return (
<div <div
style={{ style={{
@ -74,16 +78,15 @@ function Track({ source, target, getTrackProps }) {
} }
/> />
); );
} };
interface Props { interface Props {
value: number; value: number;
onChange: (event: any) => void; onChange: (value: number) => void;
displayFunction: (value: number) => string; displayFunction: (value: number) => string;
} }
/* Body */ /* Body */
// Two functions, essentially identical.
export const SliderElement: React.FC<Props> = ({ export const SliderElement: React.FC<Props> = ({
onChange, onChange,
value, value,
@ -96,21 +99,20 @@ export const SliderElement: React.FC<Props> = ({
} }
domain={[0, 200]} domain={[0, 200]}
values={[value]} values={[value]}
onChange={onChange} onChange={(values) => onChange(values[0])}
> >
<Rail> <Rail>
{({ getRailProps }) => <div style={railStyle} {...getRailProps()} />} {({ getRailProps }) => <div style={railStyle} {...getRailProps()} />}
</Rail> </Rail>
<Handles> <Handles>
{({ handles, getHandleProps }) => ( {({ handles, getHandleProps }) => (
<div className="slider-handles"> <div>
{handles.map((handle) => ( {handles.map((handle) => (
<Handle <Handle
key={handle.id} key={handle.id}
handle={handle} handle={handle}
getHandleProps={getHandleProps} getHandleProps={getHandleProps}
displayFunction={displayFunction} displayFunction={displayFunction}
handleWidth={"15em"}
/> />
))} ))}
</div> </div>
@ -118,7 +120,7 @@ export const SliderElement: React.FC<Props> = ({
</Handles> </Handles>
<Tracks right={false}> <Tracks right={false}>
{({ tracks, getTrackProps }) => ( {({ tracks, getTrackProps }) => (
<div className="slider-tracks"> <div>
{tracks.map(({ id, source, target }) => ( {tracks.map(({ id, source, target }) => (
<Track <Track
key={id} key={id}

View File

@ -1,6 +1,4 @@
import * as React from "react"; export const Favicon: React.FC<React.SVGAttributes<SVGElement>> = (props) => {
function SvgFavicon(props) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -11,6 +9,4 @@ function SvgFavicon(props) {
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm198.4-588.1a32 32 0 00-24.5.5L414.9 415 296.4 686c-3.6 8.2-3.6 17.5 0 25.7 3.4 7.8 9.7 13.9 17.7 17 3.8 1.5 7.7 2.2 11.7 2.2 4.4 0 8.7-.9 12.8-2.7l271-118.6 118.5-271a32.06 32.06 0 00-17.7-42.7zM576.8 534.4l26.2 26.2-42.4 42.4-26.2-26.2L380 644.4 447.5 490 422 464.4l42.4-42.4 25.5 25.5L644.4 380l-67.6 154.4zM464.4 422L422 464.4l25.5 25.6 86.9 86.8 26.2 26.2 42.4-42.4-26.2-26.2-86.8-86.9z" /> <path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm198.4-588.1a32 32 0 00-24.5.5L414.9 415 296.4 686c-3.6 8.2-3.6 17.5 0 25.7 3.4 7.8 9.7 13.9 17.7 17 3.8 1.5 7.7 2.2 11.7 2.2 4.4 0 8.7-.9 12.8-2.7l271-118.6 118.5-271a32.06 32.06 0 00-17.7-42.7zM576.8 534.4l26.2 26.2-42.4 42.4-26.2-26.2L380 644.4 447.5 490 422 464.4l42.4-42.4 25.5 25.5L644.4 380l-67.6 154.4zM464.4 422L422 464.4l25.5 25.6 86.9 86.8 26.2 26.2 42.4-42.4-26.2-26.2-86.8-86.9z" />
</svg> </svg>
); );
} };
export default SvgFavicon;

View File

@ -1,6 +1,4 @@
import * as React from "react"; export const Logo: React.FC<React.SVGAttributes<SVGElement>> = (props) => {
function SvgLogo(props) {
return ( return (
<svg <svg
width={1333.333} width={1333.333}
@ -76,6 +74,4 @@ function SvgLogo(props) {
/> />
</svg> </svg>
); );
} };
export default SvgLogo;

View File

@ -1,6 +1,4 @@
import * as React from "react"; export const Logo2: React.FC<React.SVGAttributes<SVGElement>> = (props) => {
function SvgLogo2(props) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -69,6 +67,4 @@ function SvgLogo2(props) {
/> />
</svg> </svg>
); );
} };
export default SvgLogo2;

View File

@ -1,3 +1,3 @@
export { default as Favicon } from "./Favicon"; export { Favicon } from "./Favicon";
export { default as Logo } from "./Logo"; export { Logo } from "./Logo";
export { default as Logo2 } from "./Logo2"; export { Logo2 } from "./Logo2";

View File

@ -34,10 +34,12 @@ const getVictoryGroup = ({
); );
}; };
export const InnerChart: React.FC<{ export type Props = {
data: ChartData; data: ChartData;
highlight: number | undefined; highlight: number | undefined;
}> = ({ };
export const InnerChart: React.FC<Props> = ({
data: { maxProbability, seriesList, minDate, maxDate }, data: { maxProbability, seriesList, minDate, maxDate },
highlight, highlight,
}) => { }) => {
@ -120,7 +122,7 @@ export const InnerChart: React.FC<{
<VictoryAxis <VictoryAxis
tickCount={Math.min(7, differenceInDays(maxDate, minDate) + 1)} tickCount={Math.min(7, differenceInDays(maxDate, minDate) + 1)}
style={{ style={{
grid: { stroke: null, strokeWidth: 0.5 }, grid: { strokeWidth: 0.5 },
}} }}
tickLabelComponent={ tickLabelComponent={
<VictoryLabel <VictoryLabel

View File

@ -12,15 +12,17 @@ const LegendItem: React.FC<{ item: Item; onHighlight: () => void }> = ({
onHighlight, onHighlight,
}) => { }) => {
const { x, y, reference, floating, strategy } = useFloating({ const { x, y, reference, floating, strategy } = useFloating({
// placement: "right",
middleware: [shift()], middleware: [shift()],
}); });
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
const textRef = useRef<HTMLDivElement>(); const textRef = useRef<HTMLDivElement>(null);
const onHover = () => { const onHover = () => {
if (textRef.current.scrollWidth > textRef.current.clientWidth) { if (
textRef.current &&
textRef.current.scrollWidth > textRef.current.clientWidth
) {
setShowTooltip(true); setShowTooltip(true);
} }
onHighlight(); onHighlight();

View File

@ -2,11 +2,12 @@ import dynamic from "next/dynamic";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { QuestionWithHistoryFragment } from "../../../fragments.generated"; import { QuestionWithHistoryFragment } from "../../../fragments.generated";
import { Props as InnerChartProps } from "./InnerChart"; // hopefully doesn't import code, just types - need to check
import { InnerChartPlaceholder } from "./InnerChartPlaceholder"; import { InnerChartPlaceholder } from "./InnerChartPlaceholder";
import { Legend } from "./Legend"; import { Legend } from "./Legend";
import { buildChartData, chartColors } from "./utils"; import { buildChartData, chartColors } from "./utils";
const InnerChart = dynamic( const InnerChart = dynamic<InnerChartProps>(
() => import("./InnerChart").then((mod) => mod.InnerChart), () => import("./InnerChart").then((mod) => mod.InnerChart),
{ ssr: false, loading: () => <InnerChartPlaceholder /> } { ssr: false, loading: () => <InnerChartPlaceholder /> }
); );

View File

@ -1,6 +1,8 @@
import { addDays, startOfDay, startOfToday, startOfTomorrow } from "date-fns"; import { addDays, startOfDay, startOfToday, startOfTomorrow } from "date-fns";
import { isFullQuestionOption } from "../../../../common/types";
import { QuestionWithHistoryFragment } from "../../../fragments.generated"; import { QuestionWithHistoryFragment } from "../../../fragments.generated";
import { isQuestionBinary } from "../../../utils";
export type ChartSeries = { x: Date; y: number; name: string }[]; export type ChartSeries = { x: Date; y: number; name: string }[];
@ -33,6 +35,7 @@ export const buildChartData = (
question: QuestionWithHistoryFragment question: QuestionWithHistoryFragment
): ChartData => { ): ChartData => {
let seriesNames = question.options let seriesNames = question.options
.filter(isFullQuestionOption)
.sort((a, b) => { .sort((a, b) => {
if (a.probability > b.probability) { if (a.probability > b.probability) {
return -1; return -1;
@ -44,9 +47,7 @@ export const buildChartData = (
.map((o) => o.name) .map((o) => o.name)
.slice(0, MAX_LINES); .slice(0, MAX_LINES);
const isBinary = const isBinary = isQuestionBinary(question);
(seriesNames[0] === "Yes" && seriesNames[1] === "No") ||
(seriesNames[0] === "No" && seriesNames[1] === "Yes");
if (isBinary) { if (isBinary) {
seriesNames = ["Yes"]; seriesNames = ["Yes"];
} }
@ -69,6 +70,9 @@ export const buildChartData = (
const date = new Date(item.timestamp * 1000); const date = new Date(item.timestamp * 1000);
for (const option of item.options) { for (const option of item.options) {
if (option.name == null || option.probability == null) {
continue;
}
const idx = nameToIndex[option.name]; const idx = nameToIndex[option.name];
if (idx === undefined) { if (idx === undefined) {
continue; continue;

View File

@ -45,18 +45,18 @@ export const IndicatorsTable: React.FC<Props> = ({ question }) => (
) : null} ) : null}
{Object.keys(question.qualityIndicators) {Object.keys(question.qualityIndicators)
.filter( .filter(
(indicator) => (indicator): indicator is UsedIndicatorName =>
question.qualityIndicators[indicator] != null && (question.qualityIndicators as any)[indicator] != null &&
!!qualityIndicatorLabels[indicator] indicator in qualityIndicatorLabels
) )
.map((indicator: UsedIndicatorName) => { .map((indicator) => {
return ( return (
<TableRow <TableRow
title={qualityIndicatorLabels[indicator]} title={qualityIndicatorLabels[indicator]}
key={indicator} key={indicator}
> >
{formatIndicatorValue( {formatIndicatorValue(
question.qualityIndicators[indicator], Number(question.qualityIndicators[indicator]), // must be non-null due to former check
indicator, indicator,
question.platform.id question.platform.id
)} )}

View File

@ -30,13 +30,17 @@ export const qualityIndicatorLabels: { [k in UsedIndicatorName]: string } = {
openInterest: "Interest", openInterest: "Interest",
}; };
const formatNumber = (num) => { const isUsedIndicatorName = (name: string): name is UsedIndicatorName => {
if (Number(num) < 1000) { return name in qualityIndicatorLabels;
return Number(num).toFixed(0); };
const formatNumber = (num: number) => {
if (num < 1000) {
return num.toFixed(0);
} else if (num < 10000) { } else if (num < 10000) {
return (Number(num) / 1000).toFixed(1) + "k"; return (num / 1000).toFixed(1) + "k";
} else { } else {
return (Number(num) / 1000).toFixed(0) + "k"; return (num / 1000).toFixed(0) + "k";
} }
}; };
@ -100,7 +104,7 @@ const FirstQualityIndicator: React.FC<{
}; };
export const formatIndicatorValue = ( export const formatIndicatorValue = (
value: any, value: number,
indicator: UsedIndicatorName, indicator: UsedIndicatorName,
platform: string platform: string
): string => { ): string => {
@ -119,21 +123,26 @@ const QualityIndicatorsList: React.FC<{
return ( return (
<div className="text-sm"> <div className="text-sm">
<FirstQualityIndicator question={question} /> <FirstQualityIndicator question={question} />
{Object.entries(question.qualityIndicators).map((entry, i) => { {Object.entries(question.qualityIndicators).map(
const indicatorLabel = qualityIndicatorLabels[entry[0]]; ([indicator, value], i) => {
if (!indicatorLabel || entry[1] === null) return; if (!isUsedIndicatorName(indicator)) return;
const indicator = entry[0] as UsedIndicatorName; // guaranteed by the previous line const indicatorLabel = qualityIndicatorLabels[indicator];
const value = entry[1]; if (!indicatorLabel || value === null) return;
return ( return (
<div key={indicator}> <div key={indicator}>
<span>{indicatorLabel}:</span>&nbsp; <span>{indicatorLabel}:</span>&nbsp;
<span className="font-bold"> <span className="font-bold">
{formatIndicatorValue(value, indicator, question.platform.id)} {formatIndicatorValue(
Number(value),
indicator,
question.platform.id
)}
</span> </span>
</div> </div>
); );
})} }
)}
</div> </div>
); );
}; };

View File

@ -13,62 +13,25 @@ const truncateText = (length: number, text: string): string => {
if (!text) { if (!text) {
return ""; return "";
} }
if (!!text && text.length <= length) { if (text.length <= length) {
return text; return text;
} }
const breakpoints = " .!?"; const breakpoints = " .!?";
let lastLetter = null; let lastLetter: string | undefined = undefined;
let lastIndex = null; let lastIndex: number | undefined = undefined;
for (let index = length; index > 0; index--) { for (let index = length; index > 0; index--) {
let letter = text[index]; const letter = text[index];
if (breakpoints.includes(letter)) { if (breakpoints.includes(letter)) {
lastLetter = letter; lastLetter = letter;
lastIndex = index; lastIndex = index;
break; break;
} }
} }
let truncatedText = !!text.slice let truncatedText =
? text.slice(0, lastIndex) + (lastLetter != "." ? "..." : "..") text.slice(0, lastIndex) + (lastLetter != "." ? "..." : "..");
: "";
return truncatedText; return truncatedText;
}; };
// replaceAll polyfill
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
function replaceAll(
originalString: string,
pattern: string | RegExp,
substitute
) {
return originalString.replace(
new RegExp(escapeRegExp(pattern), "g"),
substitute
);
}
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (
pattern: string | RegExp,
substitute
) {
let originalString = this;
// If a regex pattern
if (
Object.prototype.toString.call(pattern).toLowerCase() ===
"[object regexp]"
) {
return originalString.replace(pattern, substitute);
}
// If a string
return replaceAll(originalString, pattern, substitute);
};
}
// Auxiliary components // Auxiliary components
const DisplayMarkdown: React.FC<{ description: string }> = ({ const DisplayMarkdown: React.FC<{ description: string }> = ({
@ -153,14 +116,14 @@ export const QuestionCard: React.FC<Props> = ({
</div> </div>
{isBinary ? ( {isBinary ? (
<div className="flex justify-between"> <div className="flex justify-between">
<QuestionOptions options={options} /> <QuestionOptions question={question} />
<div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}> <div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}>
<LastUpdated timestamp={lastUpdated} /> <LastUpdated timestamp={lastUpdated} />
</div> </div>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
<QuestionOptions options={options} /> <QuestionOptions question={question} />
<div className={`hidden ${showTimeStamp ? "sm:block" : ""} ml-6`}> <div className={`hidden ${showTimeStamp ? "sm:block" : ""} ml-6`}>
<LastUpdated timestamp={lastUpdated} /> <LastUpdated timestamp={lastUpdated} />
</div> </div>
@ -173,7 +136,7 @@ export const QuestionCard: React.FC<Props> = ({
</div> </div>
)} )}
{question.platform.id === "guesstimate" && ( {question.platform.id === "guesstimate" && question.visualization && (
<img <img
className="rounded-sm" className="rounded-sm"
src={question.visualization} src={question.visualization}

View File

@ -1,227 +0,0 @@
import { QuestionFragment } from "../../fragments.generated";
type QualityIndicator = QuestionFragment["qualityIndicators"];
type IndicatorName = keyof QualityIndicator;
// this duplication can probably be simplified with typescript magic, but this is good enough for now
type UsedIndicatorName =
| "volume"
| "numForecasters"
| "spread"
| "sharesVolume"
| "liquidity"
| "tradeVolume"
| "openInterest";
const qualityIndicatorLabels: { [k in UsedIndicatorName]: string } = {
// numForecasts: null,
// stars: null,
// yesBid: "Yes bid",
// yesAsk: "Yes ask",
volume: "Volume",
numForecasters: "Forecasters",
spread: "Spread",
sharesVolume: "Shares vol.",
liquidity: "Liquidity",
tradeVolume: "Volume",
openInterest: "Interest",
};
const formatNumber = (num) => {
if (Number(num) < 1000) {
return Number(num).toFixed(0);
} else if (num < 10000) {
return (Number(num) / 1000).toFixed(1) + "k";
} else {
return (Number(num) / 1000).toFixed(0) + "k";
}
};
/* Display functions*/
const getPercentageSymbolIfNeeded = ({
indicator,
platform,
}: {
indicator: UsedIndicatorName;
platform: string;
}) => {
let indicatorsWhichNeedPercentageSymbol: IndicatorName[] = ["spread"];
if (indicatorsWhichNeedPercentageSymbol.includes(indicator)) {
return "%";
} else {
return "";
}
};
const getCurrencySymbolIfNeeded = ({
indicator,
platform,
}: {
indicator: UsedIndicatorName;
platform: string;
}) => {
const indicatorsWhichNeedCurrencySymbol: IndicatorName[] = [
"volume",
"tradeVolume",
"openInterest",
"liquidity",
];
let dollarPlatforms = ["predictit", "kalshi", "polymarket"];
if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) {
if (dollarPlatforms.includes(platform)) {
return "$";
} else {
return "£";
}
} else {
return "";
}
};
const FirstQualityIndicator: React.FC<{
question: QuestionFragment;
}> = ({ question }) => {
if (question.qualityIndicators.numForecasts) {
return (
<div className="flex">
<span>Forecasts:</span>&nbsp;
<span className="font-bold">
{Number(question.qualityIndicators.numForecasts).toFixed(0)}
</span>
</div>
);
} else {
return null;
}
};
const QualityIndicatorsList: React.FC<{
question: QuestionFragment;
}> = ({ question }) => {
return (
<div className="text-sm">
<FirstQualityIndicator question={question} />
{Object.entries(question.qualityIndicators).map((entry, i) => {
const indicatorLabel = qualityIndicatorLabels[entry[0]];
if (!indicatorLabel || entry[1] === null) return;
const indicator = entry[0] as UsedIndicatorName; // guaranteed by the previous line
const value = entry[1];
return (
<div key={indicator}>
<span>{indicatorLabel}:</span>&nbsp;
<span className="font-bold">
{`${getCurrencySymbolIfNeeded({
indicator,
platform: question.platform.id,
})}${formatNumber(value)}${getPercentageSymbolIfNeeded({
indicator,
platform: question.platform.id,
})}`}
</span>
</div>
);
})}
</div>
);
};
// Database-like functions
export function getstars(numstars: number) {
let stars = "★★☆☆☆";
switch (numstars) {
case 0:
stars = "☆☆☆☆☆";
break;
case 1:
stars = "★☆☆☆☆";
break;
case 2:
stars = "★★☆☆☆";
break;
case 3:
stars = "★★★☆☆";
break;
case 4:
stars = "★★★★☆";
break;
case 5:
stars = "★★★★★";
break;
default:
stars = "★★☆☆☆";
}
return stars;
}
function getStarsColor(numstars: number) {
let color = "text-yellow-400";
switch (numstars) {
case 0:
color = "text-red-400";
break;
case 1:
color = "text-red-400";
break;
case 2:
color = "text-orange-400";
break;
case 3:
color = "text-yellow-400";
break;
case 4:
color = "text-green-400";
break;
case 5:
color = "text-blue-400";
break;
default:
color = "text-yellow-400";
}
return color;
}
interface Props {
question: QuestionFragment;
expandFooterToFullWidth: boolean;
}
export const QuestionFooter: React.FC<Props> = ({
question,
expandFooterToFullWidth,
}) => {
return (
<div
className={`grid grid-cols-3 ${
expandFooterToFullWidth ? "justify-between" : ""
} text-gray-500 mb-2 mt-1`}
>
<div
className={`self-center col-span-1 ${getStarsColor(
question.qualityIndicators.stars
)}`}
>
{getstars(question.qualityIndicators.stars)}
</div>
<div
className={`${
expandFooterToFullWidth ? "place-self-center" : "self-center"
} col-span-1 font-bold`}
>
{question.platform.label
.replace("Good Judgment Open", "GJOpen")
.replace(/ /g, "\u00a0")}
</div>
<div
className={`${
expandFooterToFullWidth
? "justify-self-end mr-4"
: "justify-self-center"
} col-span-1`}
>
<QualityIndicatorsList question={question} />
</div>
</div>
);
};

View File

@ -1,8 +1,8 @@
import { FullQuestionOption, isFullQuestionOption } from "../../../common/types";
import { QuestionFragment } from "../../fragments.generated"; import { QuestionFragment } from "../../fragments.generated";
import { isQuestionBinary } from "../../utils";
import { formatProbability } from "../utils"; import { formatProbability } from "../utils";
type Option = QuestionFragment["options"][0];
const textColor = (probability: number) => { const textColor = (probability: number) => {
if (probability < 0.03) { if (probability < 0.03) {
return "text-red-600"; return "text-red-600";
@ -89,7 +89,7 @@ const chooseColor = (probability: number) => {
} }
}; };
const OptionRow: React.FC<{ option: Option }> = ({ option }) => { const OptionRow: React.FC<{ option: FullQuestionOption }> = ({ option }) => {
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<div <div
@ -106,15 +106,19 @@ const OptionRow: React.FC<{ option: Option }> = ({ option }) => {
); );
}; };
export const QuestionOptions: React.FC<{ options: Option[] }> = ({ export const QuestionOptions: React.FC<{ question: QuestionFragment }> = ({
options, question,
}) => { }) => {
const isBinary = const isBinary = isQuestionBinary(question);
options.length === 2 &&
(options[0].name === "Yes" || options[0].name === "No");
if (isBinary) { if (isBinary) {
const yesOption = options.find((o) => o.name === "Yes"); const yesOption = question.options.find((o) => o.name === "Yes");
if (!yesOption) {
return null; // shouldn't happen
}
if (!isFullQuestionOption(yesOption)) {
return null; // missing data
}
return ( return (
<div className="space-x-2"> <div className="space-x-2">
<span <span
@ -134,8 +138,11 @@ export const QuestionOptions: React.FC<{ options: Option[] }> = ({
</div> </div>
); );
} else { } else {
const optionsSorted = options.sort((a, b) => b.probability - a.probability); const optionsSorted = question.options
const optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options. .filter(isFullQuestionOption)
.sort((a, b) => b.probability - a.probability);
const optionsMax5 = optionsSorted.slice(0, 5); // display max 5 options.
return ( return (
<div className="space-y-2"> <div className="space-y-2">

View File

@ -74,7 +74,7 @@ const LargeQuestionCard: React.FC<{
</div> </div>
<div className="mb-8"> <div className="mb-8">
{question.platform.id === "guesstimate" ? ( {question.platform.id === "guesstimate" && question.visualization ? (
<a className="no-underline" href={question.url} target="_blank"> <a className="no-underline" href={question.url} target="_blank">
<img <img
className="rounded-sm" className="rounded-sm"

View File

@ -1,3 +1,5 @@
import { ChangeEvent } from "react";
interface Props { interface Props {
value: string; value: string;
onChange: (v: string) => void; onChange: (v: string) => void;
@ -9,7 +11,7 @@ export const QueryForm: React.FC<Props> = ({
onChange, onChange,
placeholder, placeholder,
}) => { }) => {
const handleInputChange = (event) => { const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
event.preventDefault(); event.preventDefault();
onChange(event.target.value); // In this case, the query, e.g. "COVID.19" onChange(event.target.value); // In this case, the query, e.g. "COVID.19"
}; };

View File

@ -1,5 +1,5 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { Fragment, useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { useQuery } from "urql"; import { useQuery } from "urql";
import { PlatformConfig } from "../../../backend/platforms"; import { PlatformConfig } from "../../../backend/platforms";
@ -61,7 +61,7 @@ export const SearchScreen: React.FC<Props> = ({
variables: { variables: {
input: { input: {
...queryParameters, ...queryParameters,
limit: numDisplay, limit: numDisplay + 50,
}, },
}, },
pause: !isFirstRender, pause: !isFirstRender,
@ -126,7 +126,8 @@ export const SearchScreen: React.FC<Props> = ({
}; };
const updateRoute = () => { const updateRoute = () => {
const stringify = (key: string, value: any) => { const stringify = (key: string, obj: { [k: string]: any }) => {
const value = obj[key];
if (key === "forecastingPlatforms") { if (key === "forecastingPlatforms") {
return value.join("|"); return value.join("|");
} else { } else {
@ -134,15 +135,16 @@ export const SearchScreen: React.FC<Props> = ({
} }
}; };
const query = {}; const query: { [k: string]: string } = {};
for (const key of Object.keys(defaultQueryParameters)) { for (const key of Object.keys(defaultQueryParameters)) {
const value = stringify(key, queryParameters[key]); const value = stringify(key, queryParameters);
const defaultValue = stringify(key, defaultQueryParameters[key]); const defaultValue = stringify(key, defaultQueryParameters);
if (value === defaultValue) continue; if (value === defaultValue) continue;
query[key] = value; query[key] = value;
} }
if (numDisplay !== defaultNumDisplay) query["numDisplay"] = numDisplay; if (numDisplay !== defaultNumDisplay)
query["numDisplay"] = String(numDisplay);
router.replace( router.replace(
{ {
@ -191,8 +193,8 @@ export const SearchScreen: React.FC<Props> = ({
(Math.round(value) === 1 ? "" : "s") (Math.round(value) === 1 ? "" : "s")
); );
}; };
const onChangeSliderForNumDisplay = (event) => { const onChangeSliderForNumDisplay = (value: number) => {
setNumDisplay(Math.round(event[0])); setNumDisplay(Math.round(value));
setForceSearch(forceSearch + 1); // FIXME - force new search iff numDisplay is greater than last search limit setForceSearch(forceSearch + 1); // FIXME - force new search iff numDisplay is greater than last search limit
}; };
@ -200,10 +202,10 @@ export const SearchScreen: React.FC<Props> = ({
const displayFunctionNumForecasts = (value: number) => { const displayFunctionNumForecasts = (value: number) => {
return "# Forecasts > " + Math.round(value); return "# Forecasts > " + Math.round(value);
}; };
const onChangeSliderForNumForecasts = (event) => { const onChangeSliderForNumForecasts = (value: number) => {
setQueryParameters({ setQueryParameters({
...queryParameters, ...queryParameters,
forecastsThreshold: Math.round(event[0]), forecastsThreshold: Math.round(value),
}); });
}; };
@ -230,7 +232,7 @@ export const SearchScreen: React.FC<Props> = ({
/* Final return */ /* Final return */
return ( return (
<Fragment> <>
<label className="mb-4 mt-4 flex flex-row justify-center items-center"> <label className="mb-4 mt-4 flex flex-row justify-center items-center">
<div className="w-10/12 mb-2"> <div className="w-10/12 mb-2">
<QueryForm <QueryForm
@ -317,6 +319,6 @@ export const SearchScreen: React.FC<Props> = ({
</p> </p>
</div> </div>
) : null} ) : null}
</Fragment> </>
); );
}; };

View File

@ -36,5 +36,8 @@ export const getUrqlClientOptions = (ssr: SSRExchange) => ({
export const ssrUrql = () => { export const ssrUrql = () => {
const ssrCache = ssrExchange({ isClient: false }); const ssrCache = ssrExchange({ isClient: false });
const client = initUrqlClient(getUrqlClientOptions(ssrCache), false); const client = initUrqlClient(getUrqlClientOptions(ssrCache), false);
if (!client) {
throw new Error("Expected non-null client instance from initUrqlClient");
}
return [ssrCache, client] as const; return [ssrCache, client] as const;
}; };

View File

@ -1,3 +1,5 @@
import { QuestionFragment } from "./fragments.generated";
export const getBasePath = () => { export const getBasePath = () => {
if (process.env.NEXT_PUBLIC_VERCEL_URL) { if (process.env.NEXT_PUBLIC_VERCEL_URL) {
return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`; return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
@ -29,3 +31,12 @@ export const cleanText = (text: string): string => {
//console.log(textString) //console.log(textString)
return textString; return textString;
}; };
export const isQuestionBinary = (question: QuestionFragment): boolean => {
const { options } = question;
return (
options.length === 2 &&
((options[0].name === "Yes" && options[1].name === "No") ||
(options[0].name === "No" && options[1].name === "Yes"))
);
};

View File

@ -1,18 +1,31 @@
import algoliasearch from "algoliasearch"; import algoliasearch from "algoliasearch";
import { Hit } from "@algolia/client-search";
import { AlgoliaQuestion } from "../../backend/utils/algolia"; import { AlgoliaQuestion } from "../../backend/utils/algolia";
const client = algoliasearch( const client = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID, process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "",
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY || ""
); );
const index = client.initIndex("metaforecast"); const index = client.initIndex("metaforecast");
let buildFilter = ({ interface SearchOpts {
queryString: string;
hitsPerPage?: number;
starsThreshold?: number;
filterByPlatforms?: string[];
forecastsThreshold?: number;
}
const buildFilter = ({
starsThreshold, starsThreshold,
filterByPlatforms, filterByPlatforms,
forecastsThreshold, forecastsThreshold,
}) => { }: Pick<
SearchOpts,
"starsThreshold" | "filterByPlatforms" | "forecastsThreshold"
>) => {
const starsFilter = starsThreshold const starsFilter = starsThreshold
? `qualityindicators.stars >= ${starsThreshold}` ? `qualityindicators.stars >= ${starsThreshold}`
: null; : null;
@ -20,7 +33,7 @@ let buildFilter = ({
? filterByPlatforms.map((platform) => `platform:"${platform}"`).join(" OR ") ? filterByPlatforms.map((platform) => `platform:"${platform}"`).join(" OR ")
: null; : null;
const numForecastsFilter = const numForecastsFilter =
forecastsThreshold > 0 forecastsThreshold && forecastsThreshold > 0
? `qualityindicators.numforecasts >= ${forecastsThreshold}` ? `qualityindicators.numforecasts >= ${forecastsThreshold}`
: null; : null;
const finalFilter = [starsFilter, platformsFilter, numForecastsFilter] const finalFilter = [starsFilter, platformsFilter, numForecastsFilter]
@ -35,26 +48,15 @@ let buildFilter = ({
return finalFilter; return finalFilter;
}; };
let buildFacetFilter = ({ filterByPlatforms }) => { const noExactMatch = (queryString: string, result: Hit<AlgoliaQuestion>) => {
let platformsFilter = [];
if (filterByPlatforms.length > 0) {
platformsFilter = [
[filterByPlatforms.map((platform) => `platform:${platform}`)],
];
}
console.log(platformsFilter);
console.log(
"searchWithAlgolia.js/searchWithAlgolia/buildFacetFilter",
platformsFilter
);
return platformsFilter;
};
let noExactMatch = (queryString, result) => {
queryString = queryString.toLowerCase(); queryString = queryString.toLowerCase();
let title = result.title.toLowerCase();
let description = result.description.toLowerCase(); const title = result.title.toLowerCase();
let optionsstringforsearch = result.optionsstringforsearch.toLowerCase(); const description = result.description.toLowerCase();
const optionsstringforsearch = (
result.optionsstringforsearch || ""
).toLowerCase();
return !( return !(
title.includes(queryString) || title.includes(queryString) ||
description.includes(queryString) || description.includes(queryString) ||
@ -62,14 +64,6 @@ let noExactMatch = (queryString, result) => {
); );
}; };
interface SearchOpts {
queryString: string;
hitsPerPage?: number;
starsThreshold: number;
filterByPlatforms: string[];
forecastsThreshold: number;
}
// only query string // only query string
export default async function searchWithAlgolia({ export default async function searchWithAlgolia({
queryString, queryString,

View File

@ -12,7 +12,7 @@
"dom.iterable", "dom.iterable",
"esnext" "esnext"
], ],
"strict": false, "strict": true,
"noEmit": true, "noEmit": true,
"incremental": true, "incremental": true,
"moduleResolution": "node", "moduleResolution": "node",