Compare commits
2 Commits
master
...
fetchers-v
Author | SHA1 | Date | |
---|---|---|---|
|
1ec6f34908 | ||
|
571d968aab |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -31,7 +31,6 @@ yarn-error.log*
|
|||
|
||||
# yarn vs npm conflict
|
||||
package-lock.json ## use yarn.lock instead
|
||||
# yarn.lock
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
Copyright 2023 Quantified Uncertainty Research Institute.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
38
README.md
38
README.md
|
@ -11,8 +11,8 @@ This repository includes the source code for both the website and the library th
|
|||
### 1. Download this repository
|
||||
|
||||
```
|
||||
$ git clone https://github.com/quantified-uncertainty/metaforecast
|
||||
$ cd metaforecast
|
||||
$ git clone https://github.com/QURIresearch/metaforecast
|
||||
$ cd metaforecasts
|
||||
$ npm install
|
||||
```
|
||||
|
||||
|
@ -26,28 +26,17 @@ See [./docs/configuration.md](./docs/configuration.md) for details.
|
|||
|
||||
### 3. Actually run
|
||||
|
||||
After installing and building (`npm run build`) the application, `npm run cli` starts a local CLI which presents the user with choices. If you would like to skip that step, use the option name instead, e.g., `npm run cli wildeford`.
|
||||
`npm run cli` starts a local CLI which presents the user with choices. If you would like to skip that step, use the option name instead, e.g., `npm run cli wildeford`.
|
||||
|
||||
![](./public/screenshot-cli.png)
|
||||
|
||||
`npm run next-dev` starts a Next.js dev server with the website on `http://localhost:3000`.
|
||||
|
||||
So overall this would look like
|
||||
|
||||
```
|
||||
$ git clone https://github.com/quantified-uncertainty/metaforecast
|
||||
$ cd metaforecast
|
||||
$ npm install
|
||||
$ npm run build
|
||||
$ npm run cli
|
||||
$ npm run next-dev
|
||||
```
|
||||
|
||||
### 4. Example: download the metaforecasts database
|
||||
|
||||
```
|
||||
$ git clone https://github.com/quantified-uncertainty/metaforecast
|
||||
$ cd metaforecast
|
||||
$ git clone https://github.com/QURIresearch/metaforecast
|
||||
$ cd metaforecasts
|
||||
$ npm install
|
||||
$ node src/backend/manual/manualDownload.js
|
||||
```
|
||||
|
@ -89,22 +78,5 @@ Overall, the services which we use are:
|
|||
|
||||
## Various notes
|
||||
|
||||
- This repository is released under the [MIT license](https://opensource.org/licenses/MIT). See `LICENSE.md`
|
||||
- Commits follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
|
||||
- For elicit and metaculus, this library currently filters out questions with <10 predictions.
|
||||
- The database is updated once a day, at 3:00 AM UTC, with the command `ts-node -T src/backend/flow/doEverythingForScheduler.ts`. The frontpage is updated after that, at 6:00 AM UTC with the command `ts-node -T src/backend/index.ts frontpage`. It's possible that either of these two operations makes the webpage briefly go down.
|
||||
|
||||
## To do
|
||||
|
||||
- [x] Update Metaculus and Manifold Markets fetchers
|
||||
- [x] Add markets from [Insight Prediction](https://insightprediction.com/).
|
||||
- [ ] Use <https://news.manifold.markets/p/above-the-fold-midterms-special> to update stars calculation for Manifold.
|
||||
- [ ] Add a few more snippets, with fetching individual questions, questions with histories, questions added within the last 24h to the /contrib folder (good first issue)
|
||||
- [ ] Refactor code so that users can capture and push the question history chart to imgur (good first issue)
|
||||
- [ ] Upgrade to [React 18](https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html). This will require dealing with the workaround we used for [this issue](https://github.com/vercel/next.js/issues/36019#issuecomment-1103266481)
|
||||
- [ ] Add database of resolutions
|
||||
- [ ] Allow users to embed predictions in the EA Forum/LessWrong (in progress)
|
||||
- [ ] Find a long-term mantainer for this project
|
||||
- [ ] Allow users to record their own predictions
|
||||
- [ ] Release snapshots (I think @niplav is working on this)
|
||||
- [ ] ...
|
||||
|
|
69772
package-lock.json
generated
Normal file
69772
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
128
package.json
128
package.json
|
@ -24,101 +24,95 @@
|
|||
"build": "prisma generate && next build",
|
||||
"next-start": "next start",
|
||||
"next-export": "next export",
|
||||
"dbshell": ". .env && psql $DIGITALOCEAN_POSTGRES",
|
||||
"upgrade-interactive": "yarn upgrade-interactive --latest"
|
||||
"dbshell": ". .env && psql $DIGITALOCEAN_POSTGRES"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^0.7.2",
|
||||
"@graphql-yoga/plugin-response-cache": "^1.1.0",
|
||||
"@pothos/core": "^3.22.8",
|
||||
"@pothos/plugin-prisma": "^3.35.6",
|
||||
"@pothos/plugin-relay": "^3.28.6",
|
||||
"@prisma/client": "^3.15.2",
|
||||
"@quri/squiggle-lang": "^0.5.1",
|
||||
"@tailwindcss/forms": "^0.4.1",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"@types/chroma-js": "^2.1.4",
|
||||
"@floating-ui/react-dom": "^0.7.0",
|
||||
"@graphql-yoga/node": "^2.1.0",
|
||||
"@pothos/core": "^3.5.1",
|
||||
"@pothos/plugin-prisma": "^3.4.0",
|
||||
"@pothos/plugin-relay": "^3.10.0",
|
||||
"@prisma/client": "^3.11.1",
|
||||
"@quri/squiggle-lang": "^0.2.8",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
"@tailwindcss/typography": "^0.5.1",
|
||||
"@types/chroma-js": "^2.1.3",
|
||||
"@types/dom-to-image": "^2.6.4",
|
||||
"@types/google-spreadsheet": "^3.3.0",
|
||||
"@types/jsdom": "^16.2.15",
|
||||
"@types/google-spreadsheet": "^3.2.1",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "<18.0.0",
|
||||
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||
"@types/textversionjs": "^1.1.1",
|
||||
"@types/tunnel": "^0.0.3",
|
||||
"airtable": "^0.11.5",
|
||||
"airtable": "^0.11.1",
|
||||
"ajv": "^8.11.0",
|
||||
"algoliasearch": "^4.14.2",
|
||||
"autoprefixer": "10.4.5",
|
||||
"axios": "^1.2.0",
|
||||
"algoliasearch": "^4.10.3",
|
||||
"autoprefixer": "^10.1.0",
|
||||
"axios": "^0.25.0",
|
||||
"chroma-js": "^2.4.2",
|
||||
"critters": "^0.0.16",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns": "^2.28.0",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"dotenv": "^16.0.0",
|
||||
"fetch": "^1.1.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"fuse.js": "^6.6.2",
|
||||
"google-spreadsheet": "^3.3.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^5.0.0",
|
||||
"graphql-yoga": "^3.0.0-next.10",
|
||||
"html-to-image": "^1.10.8",
|
||||
"fuse.js": "^6.4.6",
|
||||
"google-spreadsheet": "^3.1.15",
|
||||
"graphql": "^16.3.0",
|
||||
"graphql-request": "^4.0.0",
|
||||
"html-to-image": "^1.7.0",
|
||||
"https": "^1.0.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jsdom": "^19.0.0",
|
||||
"json2csv": "^5.0.7",
|
||||
"multiselect-react-dropdown": "^2.0.25",
|
||||
"next": "^12.3.1",
|
||||
"next-plausible": "^3.6.3",
|
||||
"next-urql": "^3.3.3",
|
||||
"json2csv": "^5.0.5",
|
||||
"multiselect-react-dropdown": "^2.0.17",
|
||||
"next": "12",
|
||||
"next-plausible": "^3.1.6",
|
||||
"next-urql": "^3.3.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"open": "^7.4.2",
|
||||
"papaparse": "^5.3.2",
|
||||
"pg": "^8.8.0",
|
||||
"postcss": "^8.4.18",
|
||||
"open": "^7.3.1",
|
||||
"papaparse": "^5.3.0",
|
||||
"pg": "^8.7.3",
|
||||
"postcss": "^8.2.1",
|
||||
"postcss-flexbugs-fixes": "^5.0.2",
|
||||
"postcss-preset-env": "^7.8.2",
|
||||
"prisma": "^3.15.2",
|
||||
"postcss-preset-env": "^7.3.2",
|
||||
"prisma": "^3.11.1",
|
||||
"query-string": "^7.1.1",
|
||||
"re-resizable": "^6.9.9",
|
||||
"react": "^17.0.2",
|
||||
"react-component-export-image": "^1.0.6",
|
||||
"react-compound-slider": "^3.4.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-compound-slider": "^3.3.1",
|
||||
"react-copy-to-clipboard": "^5.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropdown": "^1.11.0",
|
||||
"react-hook-form": "^7.38.0",
|
||||
"react-icons": "^4.6.0",
|
||||
"react-is": "^18.2.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-dropdown": "^1.9.2",
|
||||
"react-hook-form": "^7.27.0",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-is": "^18.0.0",
|
||||
"react-markdown": "^8.0.0",
|
||||
"react-safe": "^1.3.0",
|
||||
"react-select": "^5.5.4",
|
||||
"react-select": "^5.2.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"tabletojson": "^2.0.7",
|
||||
"tailwindcss": "^3.2.0",
|
||||
"tabletojson": "^2.0.4",
|
||||
"tailwindcss": "^3.0.22",
|
||||
"textversionjs": "^1.1.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node": "^10.7.0",
|
||||
"tunnel": "^0.0.6",
|
||||
"urql": "^2.2.3",
|
||||
"urql-custom-scalars-exchange": "^0.1.6",
|
||||
"victory": "^36.6.8"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "<18.0.0"
|
||||
"urql": "^2.2.0",
|
||||
"urql-custom-scalars-exchange": "^0.1.5",
|
||||
"victory": "^36.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^2.13.7",
|
||||
"@graphql-codegen/introspection": "^2.2.1",
|
||||
"@graphql-codegen/near-operation-file-preset": "^2.4.3",
|
||||
"@graphql-codegen/schema-ast": "^2.5.1",
|
||||
"@graphql-codegen/typed-document-node": "^2.3.5",
|
||||
"@graphql-codegen/typescript": "^2.7.5",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.5",
|
||||
"@svgr/cli": "^6.5.0",
|
||||
"@graphql-codegen/cli": "^2.6.2",
|
||||
"@graphql-codegen/introspection": "^2.1.1",
|
||||
"@graphql-codegen/near-operation-file-preset": "^2.2.9",
|
||||
"@graphql-codegen/schema-ast": "^2.4.1",
|
||||
"@graphql-codegen/typed-document-node": "^2.2.8",
|
||||
"@graphql-codegen/typescript": "^2.4.8",
|
||||
"@graphql-codegen/typescript-operations": "^2.3.5",
|
||||
"@netlify/plugin-nextjs": "^4.2.4",
|
||||
"@svgr/cli": "^6.2.1",
|
||||
"@types/pg": "^8.6.5",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-config-next": "^12.3.1",
|
||||
"typescript": "4.9.3"
|
||||
"netlify-cli": "^9.13.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,3 +93,14 @@ model FrontpageId {
|
|||
question Question @relation(fields: [id], references: [id], onDelete: Cascade)
|
||||
id String @unique
|
||||
}
|
||||
|
||||
model Robot {
|
||||
id Int @id @default(autoincrement())
|
||||
platform String
|
||||
url String // non-unique, rescheduling always creates a new row
|
||||
context Json
|
||||
created DateTime @db.Timestamp(6)
|
||||
scheduled DateTime @db.Timestamp(6) // can be equal to `created` or can be in the future for rescheduling or other purposes
|
||||
completed DateTime? @db.Timestamp(6) // becomes non-null when the job is done
|
||||
tried Int @default(0) // used to set a limit on max attempts for badly written platforms
|
||||
}
|
||||
|
|
8
src/Global.d.ts
vendored
8
src/Global.d.ts
vendored
|
@ -1,8 +0,0 @@
|
|||
// Workaround related to: https://github.com/vercel/next.js/issues/29788
|
||||
// https://github.com/vercel/next.js/issues/29788#issuecomment-1000595524
|
||||
declare type StaticImageData = {
|
||||
src: string;
|
||||
height: number;
|
||||
width: number;
|
||||
placeholder?: string;
|
||||
};
|
|
@ -1,5 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd /home/loki/Documents/core/software/fresh/js/metaforecast/metaforecast-monorepo
|
||||
|
||||
/home/loki/.nvm/versions/node/v16.15.0/lib/node_modules/npm/bin/npm-cli.js run cli goodjudgment > ../last-superforecast-fetch.txt
|
||||
/home/loki/.nvm/versions/node/v17.5.0/bin/npm run cli 3 > ../last-superforecast-fetch.txt
|
||||
|
|
|
@ -5,7 +5,6 @@ import {Tabletojson} from "tabletojson";
|
|||
import { average } from "../../utils";
|
||||
import { hash } from "../utils/hash";
|
||||
import { FetchedQuestion, Platform } from "./";
|
||||
import {FullQuestionOption} from "../../common/types";
|
||||
|
||||
/* Definitions */
|
||||
const platformName = "goodjudgment";
|
||||
|
@ -42,15 +41,17 @@ export const goodjudgment: Platform = {
|
|||
// },
|
||||
// });
|
||||
|
||||
const content = await axios.request({
|
||||
const content = await axios
|
||||
.request({
|
||||
url: "https://goodjudgment.io/superforecasts/",
|
||||
method: "get",
|
||||
headers: {
|
||||
"User-Agent": "Chrome"
|
||||
"User-Agent": "Chrome",
|
||||
},
|
||||
// agent,
|
||||
// port: 80,
|
||||
}).then((query) => query.data);
|
||||
})
|
||||
.then((query) => query.data);
|
||||
|
||||
// Processing
|
||||
let results: FetchedQuestion[] = [];
|
||||
|
@ -62,21 +63,38 @@ export const goodjudgment: Platform = {
|
|||
let title = table[0]["0"].split("\t\t\t").splice(3)[0];
|
||||
if (title != undefined) {
|
||||
title = title.replaceAll("</a>", "");
|
||||
const id = `${platformName}-${
|
||||
hash(title)
|
||||
}`;
|
||||
const description = table.filter((row : any) => row["0"].includes("BACKGROUND:")).map((row : any) => row["0"]).map((text : any) => text.split("BACKGROUND:")[1].split("Examples of Superforecaster")[0].split("AT A GLANCE")[0].replaceAll("\n\n", "\n").split("\n").slice(3).join(" ").replaceAll(" ", "").replaceAll("<br> ", ""))[0];
|
||||
const options = table.filter((row : any) => "4" in row).map((row : any) => ({
|
||||
name: row["2"].split('<span class="qTitle">')[1].replace("</span>", ""),
|
||||
const id = `${platformName}-${hash(title)}`;
|
||||
const description = table
|
||||
.filter((row: any) => row["0"].includes("BACKGROUND:"))
|
||||
.map((row: any) => row["0"])
|
||||
.map((text: any) =>
|
||||
text
|
||||
.split("BACKGROUND:")[1]
|
||||
.split("Examples of Superforecaster")[0]
|
||||
.split("AT A GLANCE")[0]
|
||||
.replaceAll("\n\n", "\n")
|
||||
.split("\n")
|
||||
.slice(3)
|
||||
.join(" ")
|
||||
.replaceAll(" ", "")
|
||||
.replaceAll("<br> ", "")
|
||||
)[0];
|
||||
const options = table
|
||||
.filter((row: any) => "4" in row)
|
||||
.map((row: any) => ({
|
||||
name: row["2"]
|
||||
.split('<span class="qTitle">')[1]
|
||||
.replace("</span>", ""),
|
||||
probability: Number(row["3"].split("%")[0]) / 100,
|
||||
type: "PROBABILITY"
|
||||
type: "PROBABILITY",
|
||||
}));
|
||||
let analysis = table.filter((row : any) => row[0] ? row[0].toLowerCase().includes("commentary") : false);
|
||||
let analysis = table.filter((row: any) =>
|
||||
row[0] ? row[0].toLowerCase().includes("commentary") : false
|
||||
);
|
||||
// "Examples of Superforecaster Commentary" / Analysis
|
||||
// The following is necessary twice, because we want to check if there is an empty list, and then get the first element of the first element of the list.
|
||||
analysis = analysis ? analysis[0] : "";
|
||||
analysis = analysis ? analysis[0] : "";
|
||||
// not a duplicate
|
||||
analysis = analysis ? analysis[0] : ""; // not a duplicate
|
||||
// console.log(analysis)
|
||||
let standardObj: FetchedQuestion = {
|
||||
id,
|
||||
|
@ -86,14 +104,16 @@ export const goodjudgment: Platform = {
|
|||
options,
|
||||
qualityindicators: {},
|
||||
extra: {
|
||||
superforecastercommentary: analysis || ""
|
||||
}
|
||||
superforecastercommentary: analysis || "",
|
||||
},
|
||||
};
|
||||
results.push(standardObj);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Failing is not unexpected; see utils/pullSuperforecastsManually.sh/js");
|
||||
console.log(
|
||||
"Failing is not unexpected; see utils/pullSuperforecastsManually.sh/js"
|
||||
);
|
||||
|
||||
return results;
|
||||
},
|
||||
|
@ -104,5 +124,5 @@ export const goodjudgment: Platform = {
|
|||
let starsDecimal = average([nuno()]); //, eli(), misha()])
|
||||
let starsInteger = Math.round(starsDecimal);
|
||||
return starsInteger;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -7,7 +7,6 @@ import {applyIfSecretExists} from "../utils/getSecrets";
|
|||
import { sleep } from "../utils/sleep";
|
||||
import toMarkdown from "../utils/toMarkdown";
|
||||
import { FetchedQuestion, Platform } from "./";
|
||||
import {FullQuestionOption} from "../../common/types";
|
||||
|
||||
/* Definitions */
|
||||
const platformName = "goodjudgmentopen";
|
||||
|
@ -18,26 +17,19 @@ const annoyingPromptUrls = [
|
|||
"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/2237-what-forecasting-questions-should-we-ask-what-questions-would-you-like-to-forecast-on-gjopen",
|
||||
"https://www.gjopen.com/questions/2437-what-forecasting-questions-should-we-ask-what-questions-would-you-like-to-forecast-on-gjopen"
|
||||
];
|
||||
const DEBUG_MODE: "on" | "off" = "off"; // "on"
|
||||
const id = () => 0;
|
||||
|
||||
/* Support functions */
|
||||
|
||||
function cleanDescription(text : string) {
|
||||
let md = toMarkdown(text);
|
||||
let result = md.replaceAll("---", "-").replaceAll(" ", " ");
|
||||
return result;
|
||||
}
|
||||
|
||||
async function fetchPage(page: number, cookie: string) {
|
||||
const response: string = await axios({
|
||||
url: htmlEndPoint + page,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Cookie: cookie
|
||||
}
|
||||
Cookie: cookie,
|
||||
},
|
||||
}).then((res) => res.data);
|
||||
//console.log(response)
|
||||
return response;
|
||||
|
@ -48,68 +40,102 @@ async function fetchStats(questionUrl : string, cookie : string) {
|
|||
url: questionUrl + "/stats",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "text/html",
|
||||
Cookie: cookie,
|
||||
Referer: questionUrl
|
||||
}
|
||||
Referer: questionUrl,
|
||||
},
|
||||
}).then((res) => res.data);
|
||||
//console.log(response)
|
||||
|
||||
if (response.includes("Sign up or sign in to forecast")) {
|
||||
throw Error("Not logged in");
|
||||
}
|
||||
// Init
|
||||
let options: FullQuestionOption[] = [];
|
||||
// Is binary?
|
||||
let isbinary = response.includes("binary?":true");
|
||||
|
||||
// Parse the embedded json
|
||||
let options: FetchedQuestion["options"] = [];
|
||||
if (isbinary) {
|
||||
// Crowd percentage
|
||||
let htmlElements = response.split("\n");
|
||||
let jsonLines = htmlElements.filter((element) => element.includes("data-react-props"));
|
||||
let embeddedJsons = jsonLines.map((jsonLine, i) => {
|
||||
let innerJSONasHTML = jsonLine.split('data-react-props="')[1].split('"')[0];
|
||||
let json = JSON.parse(innerJSONasHTML.replaceAll(""", '"'));
|
||||
return json;
|
||||
});
|
||||
let firstEmbeddedJson = embeddedJsons[0];
|
||||
let title = firstEmbeddedJson.question.name;
|
||||
let description = cleanDescription(firstEmbeddedJson.question.description);
|
||||
let comments_count = firstEmbeddedJson.question.comments_count;
|
||||
let numforecasters = firstEmbeddedJson.question.predictors_count;
|
||||
let numforecasts = firstEmbeddedJson.question.prediction_sets_count;
|
||||
let questionType = firstEmbeddedJson.question.type;
|
||||
if (questionType.includes("Binary") || questionType.includes("NonExclusiveOpinionPoolQuestion") || questionType.includes("Forecast::Question") || ! questionType.includes("Forecast::MultiTimePeriodQuestion")) {
|
||||
options = firstEmbeddedJson.question.answers.map((answer : any) => ({name: answer.name, probability: answer.normalized_probability, type: "PROBABILITY"}));
|
||||
if (options.length == 1 && options[0].name == "Yes") {
|
||||
let probabilityNo = options[0].probability > 1 ? 1 - options[0].probability / 100 : 1 - options[0].probability;
|
||||
options.push({name: "No", probability: probabilityNo, type: "PROBABILITY"});
|
||||
let h3Element = htmlElements.filter((str) => str.includes("<h3>"))[0];
|
||||
// console.log(h3Element)
|
||||
let crowdpercentage = h3Element.split(">")[1].split("<")[0];
|
||||
let probability = Number(crowdpercentage.replace("%", "")) / 100;
|
||||
options.push(
|
||||
{
|
||||
name: "Yes",
|
||||
probability: probability,
|
||||
type: "PROBABILITY",
|
||||
},
|
||||
{
|
||||
name: "No",
|
||||
probability: +(1 - probability).toFixed(2), // avoids floating point shenanigans
|
||||
type: "PROBABILITY",
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let optionsHtmlElement = "<table" + response.split("tbody")[1] + "table>";
|
||||
let tablesAsJson = Tabletojson.convert(optionsHtmlElement);
|
||||
let firstTable = tablesAsJson[0];
|
||||
options = firstTable.map((element: any) => ({
|
||||
name: element["0"],
|
||||
probability: Number(element["1"].replace("%", "")) / 100,
|
||||
type: "PROBABILITY",
|
||||
}));
|
||||
//console.log(optionsHtmlElement)
|
||||
//console.log(options)
|
||||
}
|
||||
|
||||
// Description
|
||||
let descriptionraw = response.split(
|
||||
`<div id="question-background" class="collapse smb">`
|
||||
)[1];
|
||||
let descriptionprocessed1 = descriptionraw.split(`</div>`)[0];
|
||||
let descriptionprocessed2 = toMarkdown(descriptionprocessed1);
|
||||
let descriptionprocessed3 = descriptionprocessed2
|
||||
.split("\n")
|
||||
.filter((string) => !string.includes("Confused? Check our"))
|
||||
.join("\n");
|
||||
let description = descriptionprocessed3;
|
||||
|
||||
// Number of forecasts
|
||||
let numforecasts = response
|
||||
.split("prediction_sets_count":")[1]
|
||||
.split(",")[0];
|
||||
//console.log(numforecasts)
|
||||
|
||||
// Number of predictors
|
||||
let numforecasters = response
|
||||
.split("predictors_count":")[1]
|
||||
.split(",")[0];
|
||||
//console.log(numpredictors)
|
||||
|
||||
let result = {
|
||||
description: description,
|
||||
options: options,
|
||||
description,
|
||||
options,
|
||||
qualityindicators: {
|
||||
numforecasts: Number(numforecasts),
|
||||
numforecasters: Number(numforecasters),
|
||||
comments_count: Number(comments_count)
|
||||
}
|
||||
};
|
||||
// console.log(JSON.stringify(result, null, 4));
|
||||
},
|
||||
// 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;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isSignedIn(html: string) {
|
||||
let isSignedInBool = !(html.includes("You need to sign in or sign up before continuing") || html.includes("Sign up"));
|
||||
let isSignedInBool = !(
|
||||
html.includes("You need to sign in or sign up before continuing") ||
|
||||
html.includes("Sign up")
|
||||
);
|
||||
// console.log(html)
|
||||
if (!isSignedInBool) {
|
||||
console.log("Error: Not signed in.");
|
||||
}
|
||||
console.log(`is signed in? ${
|
||||
isSignedInBool ? "yes" : "no"
|
||||
}`);
|
||||
console.log(`is signed in? ${isSignedInBool ? "yes" : "no"}`);
|
||||
return isSignedInBool;
|
||||
}
|
||||
|
||||
function reachedEnd(html: string) {
|
||||
let reachedEndBool = html.includes("No questions match your filter");
|
||||
if (reachedEndBool) { // console.log(html)
|
||||
if (reachedEndBool) {
|
||||
//console.log(html)
|
||||
}
|
||||
console.log(`Reached end? ${reachedEndBool}`);
|
||||
return reachedEndBool;
|
||||
|
@ -124,7 +150,6 @@ async function goodjudgmentopen_inner(cookie : string) {
|
|||
let results = [];
|
||||
let init = Date.now();
|
||||
// console.log("Downloading... This might take a couple of minutes. Results will be shown.")
|
||||
console.log("Page #1")
|
||||
while (!reachedEnd(response) && isSignedIn(response)) {
|
||||
let htmlLines = response.split("\n");
|
||||
DEBUG_MODE == "on" ? htmlLines.forEach((line) => console.log(line)) : id();
|
||||
|
@ -139,11 +164,12 @@ async function goodjudgmentopen_inner(cookie : string) {
|
|||
await sleep(1000 + Math.random() * 1000); // don't be as noticeable
|
||||
try {
|
||||
let moreinfo = await fetchStats(url, cookie);
|
||||
/*if (moreinfo.isbinary) {
|
||||
if (! moreinfo.crowdpercentage) { // then request again.
|
||||
if (moreinfo.isbinary) {
|
||||
if (!moreinfo.crowdpercentage) {
|
||||
// then request again.
|
||||
moreinfo = await fetchStats(url, cookie);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
let questionNumRegex = new RegExp("questions/([0-9]+)");
|
||||
const questionNumMatch = url.match(questionNumRegex);
|
||||
if (!questionNumMatch) {
|
||||
|
@ -156,19 +182,19 @@ async function goodjudgmentopen_inner(cookie : string) {
|
|||
title: title,
|
||||
url: url,
|
||||
platform: platformName,
|
||||
... moreinfo
|
||||
...moreinfo,
|
||||
};
|
||||
if (j % 30 == 0 || DEBUG_MODE == "on") {
|
||||
console.log(`Page #${i}`);
|
||||
console.log(question);
|
||||
} else {
|
||||
console.log(question.title)
|
||||
}
|
||||
// console.log(question)
|
||||
results.push(question);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log(`We encountered some error when fetching the URL: ${url}, so it won't appear on the final json`);
|
||||
console.log(
|
||||
`We encountered some error when fetching the URL: ${url}, so it won't appear on the final json`
|
||||
);
|
||||
}
|
||||
}
|
||||
j = j + 1;
|
||||
|
@ -181,7 +207,9 @@ async function goodjudgmentopen_inner(cookie : string) {
|
|||
response = await fetchPage(i, cookie);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log(`We encountered some error when fetching page #${i}, so it won't appear on the final json`);
|
||||
console.log(
|
||||
`We encountered some error when fetching page #${i}, so it won't appear on the final json`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,11 +220,9 @@ async function goodjudgmentopen_inner(cookie : string) {
|
|||
|
||||
let end = Date.now();
|
||||
let difference = end - init;
|
||||
console.log(`Took ${
|
||||
difference / 1000
|
||||
} seconds, or ${
|
||||
difference / (1000 * 60)
|
||||
} minutes.`);
|
||||
console.log(
|
||||
`Took ${difference / 1000} seconds, or ${difference / (1000 * 60)} minutes.`
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
@ -211,15 +237,20 @@ export const goodjudgmentopen: Platform = {
|
|||
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 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 misha = () =>
|
||||
minProbability > 0.1 || maxProbability < 0.9 ? 3.1 : 2.5;
|
||||
|
||||
let starsDecimal = average([nuno(), eli(), misha()]);
|
||||
let starsInteger = Math.round(starsDecimal);
|
||||
return starsInteger;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Question } from "@prisma/client";
|
||||
|
||||
import { QuestionOption } from "../../common/types";
|
||||
import { prisma } from "../database/prisma";
|
||||
import { getRobot, Robot } from "../robot";
|
||||
|
||||
// This file includes comon types and functions for working with platforms.
|
||||
// The registry of all platforms is in a separate file, ./registry.ts, to avoid circular dependencies.
|
||||
|
@ -40,6 +40,10 @@ export type FetchedQuestion = Omit<
|
|||
qualityindicators: Omit<QualityIndicators, "stars">; // slightly stronger type than Prisma's JsonValue
|
||||
};
|
||||
|
||||
type MFStorage = {
|
||||
upsert: (q: FetchedQuestion) => Promise<void>;
|
||||
};
|
||||
|
||||
// fetcher should return null if platform failed to fetch questions for some reason
|
||||
type PlatformFetcherV1 = () => Promise<FetchedQuestion[] | null>;
|
||||
|
||||
|
@ -53,13 +57,18 @@ type PlatformFetcherV2<ArgNames extends string> = (opts: {
|
|||
args?: { [k in ArgNames]: string };
|
||||
}) => Promise<PlatformFetcherV2Result>;
|
||||
|
||||
export type PlatformFetcher<ArgNames extends string> =
|
||||
| PlatformFetcherV1
|
||||
| PlatformFetcherV2<ArgNames>;
|
||||
type PlatformFetcherV3<
|
||||
ArgNames extends string,
|
||||
RobotContext = unknown
|
||||
> = (opts: {
|
||||
args?: { [k in ArgNames]: string };
|
||||
robot: Robot<RobotContext>;
|
||||
storage: MFStorage;
|
||||
}) => Promise<void>;
|
||||
|
||||
// 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 = ""> = {
|
||||
export type Platform<ArgNames extends string = "", RobotContext = unknown> = {
|
||||
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"
|
||||
color: string; // used on frontend
|
||||
|
@ -74,6 +83,11 @@ export type Platform<ArgNames extends string = ""> = {
|
|||
fetcherArgs?: ArgNames[];
|
||||
fetcher?: PlatformFetcherV2<ArgNames>;
|
||||
}
|
||||
| {
|
||||
version: "v3";
|
||||
fetcherArgs?: ArgNames[];
|
||||
fetcher?: PlatformFetcherV3<ArgNames, RobotContext>;
|
||||
}
|
||||
);
|
||||
|
||||
// Typing notes:
|
||||
|
@ -92,7 +106,7 @@ type PreparedQuestion = Omit<
|
|||
|
||||
export const prepareQuestion = (
|
||||
q: FetchedQuestion,
|
||||
platform: Platform<any>
|
||||
platform: Platform<any, any>
|
||||
): PreparedQuestion => {
|
||||
return {
|
||||
extra: {},
|
||||
|
@ -120,14 +134,29 @@ export const upsertSingleQuestion = async (
|
|||
// TODO - update history?
|
||||
};
|
||||
|
||||
export const processPlatform = async <T extends string = "">(
|
||||
platform: Platform<T>,
|
||||
export const processPlatform = async <T extends string = "", RC = unknown>(
|
||||
platform: Platform<T, RC>,
|
||||
args?: { [k in T]: string }
|
||||
) => {
|
||||
if (!platform.fetcher) {
|
||||
console.log(`Platform ${platform.name} doesn't have a fetcher, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (platform.version === "v3") {
|
||||
const robot = getRobot(platform);
|
||||
const storage: MFStorage = {
|
||||
async upsert(q) {
|
||||
await upsertSingleQuestion(prepareQuestion(q, platform));
|
||||
},
|
||||
};
|
||||
await platform.fetcher({
|
||||
robot,
|
||||
storage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result =
|
||||
platform.version === "v1"
|
||||
? { questions: await platform.fetcher(), partial: false } // this is not exactly PlatformFetcherV2Result, since `questions` can be null
|
||||
|
|
|
@ -1,339 +0,0 @@
|
|||
/* Imports */
|
||||
import {or} from "ajv/dist/compile/codegen";
|
||||
import axios from "axios";
|
||||
|
||||
import {FetchedQuestion, Platform} from ".";
|
||||
import {QuestionOption} from "../../common/types";
|
||||
import toMarkdown from "../utils/toMarkdown";
|
||||
import { average } from "../../utils";
|
||||
|
||||
/* Definitions */
|
||||
const platformName = "insight";
|
||||
const marketsEnpoint = "https://insightprediction.com/api/markets?orderBy=is_resolved&sortedBy=asc";
|
||||
const getMarketEndpoint = (id : number) => `https://insightprediction.com/api/markets/${id}`;
|
||||
const SPORTS_CATEGORIES = [
|
||||
'World Cup',
|
||||
'MLB',
|
||||
'Futures',
|
||||
'Sports',
|
||||
'EPL',
|
||||
'Golf',
|
||||
'NHL',
|
||||
'College Football'
|
||||
]
|
||||
|
||||
/* Support functions */
|
||||
|
||||
// Stubs
|
||||
const excludeMarketFromTitle = (title : any) => {
|
||||
if (!!title) {
|
||||
return title.includes(" vs ") || title.includes(" Over: ") || title.includes("NFL") || title.includes("Will there be a first time winner") || title.includes("Premier League")
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const hasActiveYesNoOrderBook = (orderbook : any) => {
|
||||
if (!!orderbook) {
|
||||
let yes = !!orderbook.yes && !!orderbook.yes.buy && Array.isArray(orderbook.yes.buy) && orderbook.yes.buy.length != 0 && !!orderbook.yes.buy[0].price && !!orderbook.yes.sell && Array.isArray(orderbook.yes.sell) && orderbook.yes.sell.length != 0 && !!orderbook.yes.sell[0].price
|
||||
let no = !!orderbook.no && !!orderbook.no.buy && Array.isArray(orderbook.no.buy) && orderbook.no.buy.length != 0 && !!orderbook.no.buy[0].price && !!orderbook.no.sell && Array.isArray(orderbook.no.sell) && orderbook.no.sell.length != 0 && !!orderbook.no.sell[0].price
|
||||
return yes && no
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isBinaryQuestion = (data : any) => Array.isArray(data) && data.length == 1
|
||||
|
||||
const geomMean = (a : number, b : number) => Math.sqrt(a * b)
|
||||
|
||||
const processRelativeUrls = (a : string) => a.replaceAll("] (/", "](http://insightprediction.com/").replaceAll("](/", "](http://insightprediction.com/")
|
||||
|
||||
const processDescriptionText = (text : any) => {
|
||||
if (typeof text === 'string') {
|
||||
return processRelativeUrls(toMarkdown(text))
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
const getOrderbookPrize = (orderbook : any) => {
|
||||
let yes_min_cents = orderbook.yes.buy[0].price
|
||||
let yes_max_cents = orderbook.yes.sell[0].price
|
||||
let yes_min = Number(yes_min_cents.slice(0, -1))
|
||||
let yes_max = Number(yes_max_cents.slice(0, -1))
|
||||
let yes_price_orderbook = geomMean(yes_min, yes_max)
|
||||
return yes_price_orderbook
|
||||
}
|
||||
|
||||
const getAnswerProbability = (answer : any) => {
|
||||
let orderbook = answer.orderbook
|
||||
let latest_yes_price = answer.latest_yes_price
|
||||
if (!! orderbook && hasActiveYesNoOrderBook(orderbook)) {
|
||||
let yes_price_orderbook = getOrderbookPrize(orderbook)
|
||||
let yes_probability = (latest_yes_price ? geomMean(latest_yes_price, yes_price_orderbook) : yes_price_orderbook) / 100
|
||||
return yes_probability
|
||||
} else if (!! latest_yes_price) {
|
||||
return latest_yes_price / 100
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// Fetching
|
||||
async function fetchPage(bearer: string, pageNum: number) {
|
||||
let pageUrl = `${marketsEnpoint}&page=${pageNum}`
|
||||
const response = await axios({
|
||||
url: pageUrl, // &orderBy=is_resolved&sortedBy=desc`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${bearer}`
|
||||
}
|
||||
}).then((res) => res.data);
|
||||
// console.log(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
async function fetchMarket(bearer: string, marketId: number) {
|
||||
const response = await axios({
|
||||
url: getMarketEndpoint(marketId),
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${bearer}`
|
||||
}
|
||||
}).then((res) => res.data);
|
||||
// console.log(response)
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
const processMarket = (market : any) => {
|
||||
let options: FetchedQuestion["options"] = []
|
||||
|
||||
if (!!market && !!market.answer && !!market.answer.data) {
|
||||
let data = market.answer.data
|
||||
if (isBinaryQuestion(data)) { // Binary questions
|
||||
let answer = data[0]
|
||||
let probability = getAnswerProbability(answer)
|
||||
if (probability != -1) {
|
||||
options = [
|
||||
{
|
||||
name: "Yes",
|
||||
probability: probability,
|
||||
type: "PROBABILITY"
|
||||
}, {
|
||||
name: "No",
|
||||
probability: 1 - probability,
|
||||
type: "PROBABILITY"
|
||||
},
|
||||
];
|
||||
}
|
||||
} else { // non binary question
|
||||
for (let answer of data) {
|
||||
let probability = getAnswerProbability(answer)
|
||||
if (probability != -1) {
|
||||
let newOption: QuestionOption = ({
|
||||
name: String(answer.title),
|
||||
probability: probability,
|
||||
type: "PROBABILITY"
|
||||
});
|
||||
options.push(newOption)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!! options && Array.isArray(options) && options.length > 0) {
|
||||
const id = `${platformName}-${
|
||||
market.id
|
||||
}`
|
||||
const result: FetchedQuestion = {
|
||||
id: id,
|
||||
title: market.title,
|
||||
url: market.url,
|
||||
description: processDescriptionText(market.rules),
|
||||
options,
|
||||
qualityindicators: market.coin_id == "USD" ? (
|
||||
{volume: market.volume}
|
||||
) : ({})
|
||||
};
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function fetchAllMarkets(bearer: string) {
|
||||
let pageNum = 1
|
||||
let markets = []
|
||||
let categories = []
|
||||
let isEnd = false
|
||||
while (! isEnd) {
|
||||
if(pageNum % 20 == 0){
|
||||
console.log(`Fetching page #${pageNum}`) // : ${pageUrl}
|
||||
}
|
||||
let page = await fetchPage(bearer, pageNum)
|
||||
// console.log(JSON.stringify(page, null, 2))
|
||||
let data = page.data
|
||||
if (!! data && Array.isArray(data) && data.length > 0) {
|
||||
let lastMarket = data[data.length - 1]
|
||||
let isLastMarketResolved = lastMarket.is_resolved
|
||||
if (isLastMarketResolved == true) {
|
||||
isEnd = true
|
||||
}
|
||||
let newMarkets = data.filter(market => !market.is_resolved && !market.is_expired && ! excludeMarketFromTitle(market.title))
|
||||
for (let initMarketData of newMarkets) {
|
||||
let fullMarketDataResponse = await fetchMarket(bearer, initMarketData.id)
|
||||
let fullMarketData = fullMarketDataResponse.data
|
||||
let processedMarketData = processMarket(fullMarketData)
|
||||
|
||||
if (processedMarketData != null && ! SPORTS_CATEGORIES.includes(fullMarketData.category)) {
|
||||
console.log(`- Adding: ${
|
||||
fullMarketData.title
|
||||
}`)
|
||||
console.group()
|
||||
console.log(fullMarketData)
|
||||
console.log(JSON.stringify(processedMarketData, null, 2))
|
||||
console.groupEnd()
|
||||
|
||||
markets.push(processedMarketData)
|
||||
}
|
||||
|
||||
let category = fullMarketData.category
|
||||
categories.push(category)
|
||||
|
||||
}
|
||||
} else {
|
||||
isEnd = true
|
||||
} pageNum = pageNum + 1
|
||||
}
|
||||
console.log(markets)
|
||||
console.log(categories)
|
||||
return markets
|
||||
}
|
||||
/*
|
||||
async function fetchQuestionStats(bearer : string, marketId : number) {
|
||||
const response = await axios({
|
||||
url: getMarketEndpoint(marketId),
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${bearer}`
|
||||
}
|
||||
}).then((res) => res.data);
|
||||
// console.log(response)
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function fetchData(bearer : string) {
|
||||
let pageNum = 1;
|
||||
let reachedEnd = false;
|
||||
let results = [];
|
||||
while (! reachedEnd) {
|
||||
let newPage = await fetchPage(bearer, pageNum);
|
||||
let newPageData = newPage.data;
|
||||
let marketsFromPage = []
|
||||
for (let market of newPageData) {
|
||||
let response = await fetchQuestionStats(bearer, market.id);
|
||||
let marketData = response.data
|
||||
let marketAnswer = marketData.answer.data
|
||||
delete marketData.answer
|
||||
// These are the options and their prices.
|
||||
let marketOptions = marketAnswer.map(answer => {
|
||||
return({name: answer.title, probability: answer.latest_yes_price, type: "PROBABILITY"})
|
||||
})
|
||||
marketsFromPage.push({
|
||||
... marketData,
|
||||
options: marketOptions
|
||||
});
|
||||
}
|
||||
|
||||
let finalObject = marketsFromPage
|
||||
|
||||
console.log(`Page = #${pageNum}`);
|
||||
// console.log(newPageData)
|
||||
console.dir(finalObject, {depth: null});
|
||||
results.push(... finalObject);
|
||||
|
||||
let newPagination = newPage.meta.pagination;
|
||||
if (newPagination.total_pages == pageNum) {
|
||||
reachedEnd = true;
|
||||
} else {
|
||||
pageNum = pageNum + 1;
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
async function processPredictions(predictions : any[]) {
|
||||
let results = await predictions.map((prediction) => {
|
||||
const id = `${platformName}-${
|
||||
prediction.id
|
||||
}`;
|
||||
const probability = prediction.probability;
|
||||
const options: FetchedQuestion["options"] = [
|
||||
{
|
||||
name: "Yes",
|
||||
probability: probability,
|
||||
type: "PROBABILITY"
|
||||
}, {
|
||||
name: "No",
|
||||
probability: 1 - probability,
|
||||
type: "PROBABILITY"
|
||||
},
|
||||
];
|
||||
const result: FetchedQuestion = {
|
||||
id,
|
||||
title: prediction.title,
|
||||
url: "https://example.com",
|
||||
description: prediction.description,
|
||||
options,
|
||||
qualityindicators: {
|
||||
// other: prediction.otherx,
|
||||
// indicators: prediction.indicatorx,
|
||||
}
|
||||
};
|
||||
return result;
|
||||
});
|
||||
return results; // resultsProcessed
|
||||
}
|
||||
*/
|
||||
/* Body */
|
||||
export const insight: Platform = {
|
||||
name: platformName,
|
||||
label: "Insight Prediction",
|
||||
color: "#ff0000",
|
||||
version: "v1",
|
||||
async fetcher() {
|
||||
let bearer = process.env.INSIGHT_BEARER;
|
||||
if (!! bearer) {
|
||||
let data = await fetchAllMarkets(bearer);
|
||||
return data
|
||||
} else {
|
||||
throw Error("No INSIGHT_BEARER available in environment")
|
||||
}
|
||||
// let results: FetchedQuestion[] = []; // await processPredictions(data); // somehow needed
|
||||
// return results;
|
||||
},
|
||||
calculateStars(data) {
|
||||
let nuno = () => {
|
||||
if((data.qualityindicators.volume || 0) > 10000){
|
||||
return 4
|
||||
} else if((data.qualityindicators.volume || 0) > 1000){
|
||||
return 3
|
||||
} else{
|
||||
return 2
|
||||
}
|
||||
}
|
||||
let eli = () => null;
|
||||
let misha = () => null;
|
||||
let starsDecimal = average([nuno()]); //, eli(data), misha(data)])
|
||||
let starsInteger = Math.round(starsDecimal);
|
||||
return starsInteger;
|
||||
}
|
||||
};
|
|
@ -6,12 +6,12 @@ import { FetchedQuestion, Platform } from "./";
|
|||
|
||||
/* Definitions */
|
||||
const platformName = "manifold";
|
||||
const ENDPOINT = "https://manifold.markets/api/v0/markets";
|
||||
const endpoint = "https://manifold.markets/api/v0/markets";
|
||||
// See https://manifoldmarkets.notion.site/Manifold-Markets-API-5e7d0aef4dcf452bb04b319e178fabc5
|
||||
|
||||
/* Support functions */
|
||||
|
||||
async function fetchPage(endpoint: string) {
|
||||
async function fetchData() {
|
||||
let response = await axios({
|
||||
url: endpoint,
|
||||
method: "GET",
|
||||
|
@ -23,31 +23,6 @@ async function fetchPage(endpoint: string) {
|
|||
return response;
|
||||
}
|
||||
|
||||
async function fetchAllData(){
|
||||
let endpoint = ENDPOINT
|
||||
let end = false
|
||||
let allData = []
|
||||
let counter = 1
|
||||
while(!end){
|
||||
console.log(`Query #${counter}: ${endpoint}`)
|
||||
let newData = await fetchPage(endpoint)
|
||||
if(Array.isArray(newData)){
|
||||
allData.push(...newData)
|
||||
let hasReachedEnd = (newData.length == 0) || (newData[newData.length -1] == undefined) || (newData[newData.length -1].id == undefined)
|
||||
if(!hasReachedEnd){
|
||||
let lastId = newData[newData.length -1].id
|
||||
endpoint = `${ENDPOINT}?before=${lastId}`
|
||||
}else{
|
||||
end = true
|
||||
}
|
||||
}else{
|
||||
end = true
|
||||
}
|
||||
counter = counter +1
|
||||
}
|
||||
return allData
|
||||
}
|
||||
|
||||
function showStatistics(results: FetchedQuestion[]) {
|
||||
console.log(`Num unresolved markets: ${results.length}`);
|
||||
let sum = (arr: number[]) => arr.reduce((tally, a) => tally + a, 0);
|
||||
|
@ -88,11 +63,11 @@ function processPredictions(predictions: any[]): FetchedQuestion[] {
|
|||
id: id,
|
||||
title: prediction.question,
|
||||
url: prediction.url,
|
||||
description: prediction.description || "",
|
||||
description: prediction.description,
|
||||
options,
|
||||
qualityindicators: {
|
||||
createdTime: prediction.createdTime,
|
||||
// volume7Days: prediction.volume7Days, // deprecated.
|
||||
volume7Days: prediction.volume7Days,
|
||||
volume24Hours: prediction.volume24Hours,
|
||||
pool: prediction.pool, // normally liquidity, but I don't actually want to show it.
|
||||
},
|
||||
|
@ -115,16 +90,16 @@ export const manifold: Platform = {
|
|||
color: "#793466",
|
||||
version: "v1",
|
||||
async fetcher() {
|
||||
let data = await fetchAllData();
|
||||
let data = await fetchData();
|
||||
let results = processPredictions(data); // somehow needed
|
||||
showStatistics(results);
|
||||
return results;
|
||||
},
|
||||
calculateStars(data) {
|
||||
let nuno = () =>
|
||||
(data.qualityindicators.volume24Hours || 0) > 100 ||
|
||||
(data.qualityindicators.volume7Days || 0) > 250 ||
|
||||
((data.qualityindicators.pool || 0) > 500 &&
|
||||
(data.qualityindicators.volume24Hours || 0) > 50)
|
||||
(data.qualityindicators.volume7Days || 0) > 100)
|
||||
? 2
|
||||
: 1;
|
||||
let eli = () => null;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import Ajv, { JTDDataType, ValidateFunction } from "ajv/dist/jtd";
|
||||
import axios from "axios";
|
||||
import { sleep } from "../../utils/sleep";
|
||||
import Ajv, { JTDDataType } from "ajv/dist/jtd";
|
||||
|
||||
// Type examples:
|
||||
// - group: https://www.metaculus.com/api2/questions/9866/
|
||||
|
@ -64,7 +62,6 @@ const predictableProps = {
|
|||
additionalProperties: true,
|
||||
},
|
||||
},
|
||||
nullable: true,
|
||||
additionalProperties: true,
|
||||
},
|
||||
} as const;
|
||||
|
@ -187,48 +184,38 @@ const validateShallowMultipleQuestions =
|
|||
shallowMultipleQuestionsSchema
|
||||
);
|
||||
|
||||
async function fetchWithRetries<T = unknown>(url: string): Promise<T> {
|
||||
try {
|
||||
const response = await axios.get<T>(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(`Error while fetching ${url}`);
|
||||
console.log(error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.headers["retry-after"]) {
|
||||
const timeout = error.response.headers["retry-after"];
|
||||
console.log(`Timeout: ${timeout}`);
|
||||
await sleep(Number(timeout) * 1000 + 1000);
|
||||
} else {
|
||||
await sleep(RETRY_SLEEP_TIME);
|
||||
}
|
||||
}
|
||||
}
|
||||
const response = await axios.get<T>(url);
|
||||
return response.data;
|
||||
}
|
||||
// async function fetchWithRetries<T = unknown>(url: string): Promise<T> {
|
||||
// try {
|
||||
// const response = await axios.get<T>(url);
|
||||
// return response.data;
|
||||
// } catch (error) {
|
||||
// console.log(`Error while fetching ${url}`);
|
||||
// console.log(error);
|
||||
// if (axios.isAxiosError(error)) {
|
||||
// if (error.response?.headers["retry-after"]) {
|
||||
// const timeout = error.response.headers["retry-after"];
|
||||
// console.log(`Timeout: ${timeout}`);
|
||||
// await sleep(Number(timeout) * 1000 + 1000);
|
||||
// } else {
|
||||
// await sleep(RETRY_SLEEP_TIME);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// const response = await axios.get<T>(url);
|
||||
// return response.data;
|
||||
// }
|
||||
|
||||
const fetchAndValidate = async <T = unknown>(
|
||||
url: string,
|
||||
validator: ValidateFunction<T>
|
||||
): Promise<T> => {
|
||||
// console.log(url);
|
||||
const data = await fetchWithRetries<object>(url);
|
||||
if (validator(data)) {
|
||||
return data;
|
||||
}else{
|
||||
console.log(data)
|
||||
export async function prepareApiQuestions(
|
||||
data: unknown
|
||||
): Promise<ApiMultipleQuestions> {
|
||||
if (!validateShallowMultipleQuestions(data)) {
|
||||
throw new Error(
|
||||
`Response validation for url ${url} failed: ` +
|
||||
JSON.stringify(validator.errors, null, 4)
|
||||
`Response validation failed: ` +
|
||||
JSON.stringify(validateShallowMultipleQuestions.errors) +
|
||||
"\n\n" +
|
||||
JSON.stringify(data)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export async function fetchApiQuestions(
|
||||
next: string
|
||||
): Promise<ApiMultipleQuestions> {
|
||||
const data = await fetchAndValidate(next, validateShallowMultipleQuestions);
|
||||
|
||||
const isDefined = <T>(argument: T | undefined): argument is T => {
|
||||
return argument !== undefined;
|
||||
|
@ -254,9 +241,16 @@ export async function fetchApiQuestions(
|
|||
};
|
||||
}
|
||||
|
||||
export async function fetchSingleApiQuestion(id: number): Promise<ApiQuestion> {
|
||||
return await fetchAndValidate(
|
||||
`https://www.metaculus.com/api2/questions/${id}/`,
|
||||
validateQuestion
|
||||
export async function prepareSingleApiQuestion(
|
||||
data: unknown
|
||||
): Promise<ApiQuestion> {
|
||||
if (!validateQuestion(data)) {
|
||||
throw new Error(
|
||||
`Response validation failed: ` +
|
||||
JSON.stringify(validateQuestion.errors) +
|
||||
"\n\n" +
|
||||
JSON.stringify(data)
|
||||
);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import Error from "next/error";
|
||||
import { FetchedQuestion, Platform } from "..";
|
||||
import { average } from "../../../utils";
|
||||
import {sleep} from "../../utils/sleep";
|
||||
import { Robot, RobotJob } from "../../robot";
|
||||
import {
|
||||
ApiCommon,
|
||||
ApiMultipleQuestions,
|
||||
ApiPredictable,
|
||||
ApiQuestion,
|
||||
fetchApiQuestions,
|
||||
fetchSingleApiQuestion
|
||||
prepareApiQuestions,
|
||||
prepareSingleApiQuestion,
|
||||
} from "./api";
|
||||
|
||||
const platformName = "metaculus";
|
||||
const now = new Date().toISOString();
|
||||
const SLEEP_TIME = 1000;
|
||||
|
||||
async function apiQuestionToFetchedQuestions(apiQuestion: ApiQuestion): Promise<FetchedQuestion[]> {
|
||||
// one item can expand:
|
||||
// - to 0 questions if we don't want it;
|
||||
// - to 1 question if it's a simple forecast
|
||||
// - to multiple questions if it's a group (see https://github.com/quantified-uncertainty/metaforecast/pull/84 for details)
|
||||
type Context =
|
||||
| {
|
||||
type: "apiIndex";
|
||||
}
|
||||
| {
|
||||
type: "apiQuestion";
|
||||
};
|
||||
|
||||
const skip = (q: ApiPredictable): boolean => {
|
||||
if (q.publish_time > now || now > q.resolve_time) {
|
||||
|
@ -31,190 +31,167 @@ async function apiQuestionToFetchedQuestions(apiQuestion: ApiQuestion): Promise<
|
|||
return false;
|
||||
};
|
||||
|
||||
const buildFetchedQuestion = (q : ApiPredictable & ApiCommon) : Omit < FetchedQuestion,
|
||||
"url" | "description" | "title" > => {
|
||||
async function processApiQuestion(
|
||||
apiQuestion: ApiQuestion
|
||||
): Promise<FetchedQuestion[]> {
|
||||
// one item can expand:
|
||||
// - to 0 questions if we don't want it;
|
||||
// - to 1 question if it's a simple forecast
|
||||
// - to multiple questions if it's a group (see https://github.com/quantified-uncertainty/metaforecast/pull/84 for details)
|
||||
|
||||
const buildFetchedQuestion = (
|
||||
q: ApiPredictable & ApiCommon
|
||||
): Omit<FetchedQuestion, "url" | "description" | "title"> => {
|
||||
const isBinary = q.possibilities.type === "binary";
|
||||
let options: FetchedQuestion["options"] = [];
|
||||
if (isBinary) {
|
||||
const probability = q.community_prediction?.full.q2;
|
||||
const probability = q.community_prediction.full.q2;
|
||||
if (probability !== undefined) {
|
||||
options = [
|
||||
{
|
||||
name: "Yes",
|
||||
probability: probability,
|
||||
type: "PROBABILITY"
|
||||
}, {
|
||||
type: "PROBABILITY",
|
||||
},
|
||||
{
|
||||
name: "No",
|
||||
probability: 1 - probability,
|
||||
type: "PROBABILITY"
|
||||
type: "PROBABILITY",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: `${platformName}-${
|
||||
q.id
|
||||
}`,
|
||||
id: `${platformName}-${q.id}`,
|
||||
options,
|
||||
qualityindicators: {
|
||||
numforecasts: q.number_of_predictions
|
||||
numforecasts: q.number_of_predictions,
|
||||
},
|
||||
extra: {
|
||||
resolution_data: {
|
||||
publish_time: apiQuestion.publish_time,
|
||||
resolution: apiQuestion.resolution,
|
||||
close_time: apiQuestion.close_time,
|
||||
resolve_time: apiQuestion.resolve_time
|
||||
}
|
||||
}
|
||||
resolve_time: apiQuestion.resolve_time,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
if (apiQuestion.type === "group") {
|
||||
await sleep(SLEEP_TIME);
|
||||
let apiQuestionDetailsTemp
|
||||
try{
|
||||
apiQuestionDetailsTemp = await fetchSingleApiQuestion(apiQuestion.id);
|
||||
}catch(error){
|
||||
console.log(error)
|
||||
return []
|
||||
}
|
||||
const apiQuestionDetails = apiQuestionDetailsTemp
|
||||
if (apiQuestionDetails.type !== "group") {
|
||||
console.log("Error: expected `group` type")
|
||||
return [] //throw new Error("Expected `group` type"); // shouldn't happen, this is mostly for typescript
|
||||
}else{
|
||||
try{
|
||||
let result = (apiQuestionDetails.sub_questions || []).filter((q) => ! skip(q)).map((sq) => {
|
||||
return (apiQuestion.sub_questions || [])
|
||||
.filter((q) => !skip(q))
|
||||
.map((sq) => {
|
||||
const tmp = buildFetchedQuestion(sq);
|
||||
return {
|
||||
...tmp,
|
||||
title: `${
|
||||
apiQuestion.title
|
||||
} (${
|
||||
sq.title
|
||||
})`,
|
||||
description: apiQuestionDetails.description || "",
|
||||
url: `https://www.metaculus.com${
|
||||
apiQuestion.page_url
|
||||
}?sub-question=${
|
||||
sq.id
|
||||
}`
|
||||
title: `${apiQuestion.title} (${sq.title})`,
|
||||
description: apiQuestion.description || "",
|
||||
url: `https://www.metaculus.com${apiQuestion.page_url}?sub-question=${sq.id}`,
|
||||
};
|
||||
});
|
||||
return result
|
||||
}catch(error){
|
||||
console.log(error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
} else if (apiQuestion.type === "forecast") {
|
||||
if (apiQuestion.group) {
|
||||
return []; // sub-question, should be handled on the group level
|
||||
}
|
||||
if (skip(apiQuestion)) {
|
||||
console.log(`- [Skipping]: ${
|
||||
apiQuestion.title
|
||||
}`)
|
||||
/*console.log(`Close time: ${
|
||||
apiQuestion.close_time
|
||||
}, resolve time: ${
|
||||
apiQuestion.resolve_time
|
||||
}`)*/
|
||||
return [];
|
||||
}
|
||||
|
||||
await sleep(SLEEP_TIME);
|
||||
try{
|
||||
const apiQuestionDetails = await fetchSingleApiQuestion(apiQuestion.id);
|
||||
const tmp = buildFetchedQuestion(apiQuestion);
|
||||
return [{
|
||||
return [
|
||||
{
|
||||
...tmp,
|
||||
title: apiQuestion.title,
|
||||
description: apiQuestionDetails.description || "",
|
||||
url: "https://www.metaculus.com" + apiQuestion.page_url
|
||||
},];
|
||||
}catch(error){
|
||||
console.log(error)
|
||||
return []
|
||||
}
|
||||
description: apiQuestion.description || "",
|
||||
url: "https://www.metaculus.com" + apiQuestion.page_url,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
if (apiQuestion.type !== "claim") { // should never happen, since `discriminator` in JTD schema causes a strict runtime check
|
||||
console.log(`Unknown metaculus question type: ${
|
||||
(apiQuestion as any).type
|
||||
}, skipping`);
|
||||
}
|
||||
console.log(
|
||||
`Unknown metaculus question type: ${apiQuestion.type}, skipping`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export const metaculus: Platform<"id" | "debug"> = {
|
||||
async function processApiIndexQuestion(
|
||||
apiQuestion: ApiQuestion,
|
||||
robot: Robot<Context>
|
||||
): Promise<void> {
|
||||
if (apiQuestion.type === "group" || apiQuestion.type === "forecast") {
|
||||
if (apiQuestion.type === "forecast" && skip(apiQuestion)) {
|
||||
return;
|
||||
}
|
||||
await robot.schedule({
|
||||
url: `https://www.metaculus.com/api2/questions/${apiQuestion.id}/`,
|
||||
context: {
|
||||
type: "apiQuestion",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const metaculus: Platform<"id" | "debug", Context> = {
|
||||
name: platformName,
|
||||
label: "Metaculus",
|
||||
color: "#006669",
|
||||
version: "v2",
|
||||
fetcherArgs: [
|
||||
"id", "debug"
|
||||
],
|
||||
async fetcher(opts) {
|
||||
let allQuestions: FetchedQuestion[] = [];
|
||||
version: "v3",
|
||||
fetcherArgs: ["id", "debug"],
|
||||
async fetcher({ robot, storage }) {
|
||||
await robot.schedule({
|
||||
url: "https://www.metaculus.com/api2/questions/",
|
||||
context: {
|
||||
type: "apiIndex",
|
||||
},
|
||||
});
|
||||
|
||||
if (opts.args ?. id) {
|
||||
try{
|
||||
console.log("Using optional id arg.")
|
||||
const id = Number(opts.args.id);
|
||||
const apiQuestion = await fetchSingleApiQuestion(id);
|
||||
const questions = await apiQuestionToFetchedQuestions(apiQuestion);
|
||||
console.log(questions);
|
||||
return {questions, partial: true};
|
||||
for (
|
||||
let job: RobotJob<Context> | undefined;
|
||||
(job = await robot.nextJob());
|
||||
|
||||
}catch(error){
|
||||
console.log(error)
|
||||
return {questions: [], partial: true};
|
||||
}
|
||||
) {
|
||||
const data = await job.fetch();
|
||||
|
||||
if (job.context.type === "apiIndex") {
|
||||
const apiIndex = await prepareApiQuestions(data);
|
||||
if (apiIndex.next) {
|
||||
await robot.schedule({
|
||||
url: apiIndex.next,
|
||||
context: {
|
||||
type: "apiIndex",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let next: string | null = "https://www.metaculus.com/api2/questions/";
|
||||
let i = 1;
|
||||
while (next) {
|
||||
console.log(`\nQuery #${i} - ${next}`);
|
||||
|
||||
await sleep(SLEEP_TIME);
|
||||
const apiQuestions: ApiMultipleQuestions = await fetchApiQuestions(next);
|
||||
const results = apiQuestions.results;
|
||||
// console.log(results)
|
||||
let j = false;
|
||||
|
||||
for (const result of results) {
|
||||
const questions = await apiQuestionToFetchedQuestions(result);
|
||||
// console.log(questions)
|
||||
for (const question of questions) {
|
||||
console.log(`- ${
|
||||
question.title
|
||||
}`);
|
||||
if ((! j && i % 20 === 0) || opts.args ?. debug) {
|
||||
console.log(question);
|
||||
j = true;
|
||||
for (const apiQuestion of apiIndex.results) {
|
||||
await processApiIndexQuestion(apiQuestion, robot);
|
||||
// for (const question of questions) {
|
||||
// console.log(`- ${question.title}`);
|
||||
// allQuestions.push(question);
|
||||
// }
|
||||
}
|
||||
allQuestions.push(question);
|
||||
} else if (job.context.type === "apiQuestion") {
|
||||
const apiQuestion = await prepareSingleApiQuestion(data);
|
||||
const fetchedQuestions = await processApiQuestion(apiQuestion);
|
||||
for (const q of fetchedQuestions) {
|
||||
await storage.upsert(q);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Unknown context type ${(job.context as any).type}`);
|
||||
}
|
||||
|
||||
next = apiQuestions.next;
|
||||
i += 1;
|
||||
await job.done();
|
||||
}
|
||||
|
||||
return {questions: allQuestions, partial: false};
|
||||
},
|
||||
|
||||
calculateStars(data) {
|
||||
const { numforecasts } = data.qualityindicators;
|
||||
const nuno = () => (numforecasts || 0) > 300 ? 4 : (numforecasts || 0) > 100 ? 3 : 2;
|
||||
const nuno = () =>
|
||||
(numforecasts || 0) > 300 ? 4 : (numforecasts || 0) > 100 ? 3 : 2;
|
||||
const eli = () => 3;
|
||||
const misha = () => 3;
|
||||
const starsDecimal = average([nuno(), eli(), misha()]);
|
||||
const starsInteger = Math.round(starsDecimal);
|
||||
return starsInteger;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -7,7 +7,6 @@ import { goodjudgmentopen } from "./goodjudgmentopen";
|
|||
import { guesstimate } from "./guesstimate";
|
||||
import { Platform, PlatformConfig } from "./index";
|
||||
import { infer } from "./infer";
|
||||
import { insight } from "./insight";
|
||||
import { kalshi } from "./kalshi";
|
||||
import { manifold } from "./manifold";
|
||||
import { metaculus } from "./metaculus";
|
||||
|
@ -19,7 +18,7 @@ import { wildeford } from "./wildeford";
|
|||
import { xrisk } from "./xrisk";
|
||||
|
||||
// function instead of const array, this helps to fight circular dependencies
|
||||
export const getPlatforms = (): Platform<string>[] => {
|
||||
export const getPlatforms = (): Platform<string, any>[] => {
|
||||
return [
|
||||
betfair,
|
||||
fantasyscotus,
|
||||
|
@ -29,7 +28,6 @@ export const getPlatforms = (): Platform<string>[] => {
|
|||
goodjudgmentopen,
|
||||
guesstimate,
|
||||
infer,
|
||||
insight,
|
||||
kalshi,
|
||||
manifold,
|
||||
metaculus,
|
||||
|
|
106
src/backend/robot/index.ts
Normal file
106
src/backend/robot/index.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import axios from "axios";
|
||||
import { prisma } from "../database/prisma";
|
||||
import { Platform } from "../platforms";
|
||||
|
||||
// type Context = Prisma.JsonObject; // untyped for now, might become a generic in the future
|
||||
|
||||
export type RobotJob<Context> = {
|
||||
context: Context;
|
||||
fetch: () => Promise<unknown>;
|
||||
done: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type Robot<Context> = {
|
||||
nextJob: () => Promise<RobotJob<Context> | undefined>;
|
||||
schedule: (args: { url: string; context?: Context }) => Promise<void>;
|
||||
};
|
||||
|
||||
export const getRobot = <Context>(
|
||||
platform: Platform<any, Context>
|
||||
): Robot<Context> => {
|
||||
return {
|
||||
async nextJob() {
|
||||
const jobData = await prisma.robot.findFirst({
|
||||
where: {
|
||||
platform: platform.name,
|
||||
completed: {
|
||||
equals: null,
|
||||
},
|
||||
scheduled: {
|
||||
lte: new Date(),
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
created: "asc",
|
||||
},
|
||||
});
|
||||
if (!jobData) {
|
||||
return;
|
||||
}
|
||||
await prisma.robot.update({
|
||||
where: {
|
||||
id: jobData?.id,
|
||||
},
|
||||
data: {
|
||||
tried: jobData.tried + 1,
|
||||
},
|
||||
});
|
||||
|
||||
const job: RobotJob<Context> = {
|
||||
context: jobData.context as Context,
|
||||
async fetch() {
|
||||
const data = await axios.get(jobData.url);
|
||||
return data.data;
|
||||
},
|
||||
async done() {
|
||||
await prisma.robot.update({
|
||||
where: {
|
||||
id: jobData.id,
|
||||
},
|
||||
data: {
|
||||
completed: new Date(),
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
return job;
|
||||
},
|
||||
|
||||
async schedule({ url, context = {} }) {
|
||||
const now = new Date();
|
||||
|
||||
const oldJob = await prisma.robot.findFirst({
|
||||
where: {
|
||||
platform: platform.name,
|
||||
url,
|
||||
completed: {
|
||||
equals: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (oldJob) {
|
||||
await prisma.robot.update({
|
||||
where: {
|
||||
id: oldJob.id,
|
||||
},
|
||||
data: {
|
||||
created: now,
|
||||
scheduled: now,
|
||||
context,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.robot.create({
|
||||
data: {
|
||||
url,
|
||||
platform: platform.name,
|
||||
created: now,
|
||||
scheduled: now,
|
||||
context,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
|
@ -17,14 +17,12 @@ Router.events.on("routeChangeStart", (as, { shallow }) => {
|
|||
Router.events.on("routeChangeComplete", () => NProgress.done());
|
||||
Router.events.on("routeChangeError", () => NProgress.done());
|
||||
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<PlausibleProvider domain="metaforecast.org">
|
||||
<Component {...pageProps} />
|
||||
</PlausibleProvider>
|
||||
);
|
||||
// Workaround in package.json for: https://github.com/vercel/next.js/issues/36019#issuecomment-1103266481
|
||||
}
|
||||
|
||||
export default withUrqlClient((ssr) => getUrqlClientOptions(ssr), {
|
||||
|
|
|
@ -1,24 +1,13 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
// apollo-server-micro is problematic since v3, see https://github.com/apollographql/apollo-server/issues/5547, so we use graphql-yoga instead
|
||||
import {createYoga} from "graphql-yoga";
|
||||
import {useResponseCache} from '@graphql-yoga/plugin-response-cache'
|
||||
import { createServer } from "@graphql-yoga/node";
|
||||
|
||||
import { schema } from "../../graphql/schema";
|
||||
|
||||
const server = createYoga < {
|
||||
const server = createServer<{
|
||||
req: NextApiRequest;
|
||||
res: NextApiResponse;
|
||||
} > ({
|
||||
schema,
|
||||
graphqlEndpoint: '/api/graphql',
|
||||
plugins: [useResponseCache(
|
||||
{ // global cache
|
||||
session: () => null,
|
||||
ttl: 2 * 60 * 60 * 1000,
|
||||
// ^ 2h * 60 mins per hour, 60 seconds per min 1000 miliseconds per second
|
||||
}
|
||||
)]
|
||||
});
|
||||
}>({ schema });
|
||||
|
||||
export default server;
|
||||
|
|
|
@ -11,7 +11,7 @@ export const BoxedLink: React.FC<Props> = ({
|
|||
children,
|
||||
}) => (
|
||||
<a
|
||||
className={`px-2 py-1 border-2 border-gray-400 rounded-lg text-black no-underline hover:bg-gray-100 inline-flex flex-nowrap space-x-1 items-center text-xs md:text-lg ${
|
||||
className={`px-2 py-1 border-2 border-gray-400 rounded-lg text-black no-underline text-normal hover:bg-gray-100 inline-flex flex-nowrap space-x-1 items-center ${
|
||||
size === "small" ? "text-sm" : ""
|
||||
}`}
|
||||
href={url}
|
||||
|
|
|
@ -11,7 +11,7 @@ export const Spinner: React.FC = () => (
|
|||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
|
|
|
@ -13,11 +13,9 @@ import {
|
|||
VictoryVoronoiContainer,
|
||||
} from "victory";
|
||||
|
||||
import { chartColors, ChartData, ChartSeries, goldenRatio } from "./utils";
|
||||
import { chartColors, ChartData, ChartSeries, height, width } from "./utils";
|
||||
|
||||
const height = 200
|
||||
const width = 200 * goldenRatio
|
||||
let dateFormat = "dd/MM/yy"; // "yyyy-MM-dd" // "MMM do yy"
|
||||
let dateFormat = "MMM do y"; // "yyyy-MM-dd"
|
||||
|
||||
// can't be replaced with React component, VictoryChart requires VictoryGroup elements to be immediate children
|
||||
const getVictoryGroup = ({
|
||||
|
@ -39,7 +37,7 @@ const getVictoryGroup = ({
|
|||
data: {
|
||||
// strokeOpacity: highlight ? 1 : 0.5,
|
||||
strokeOpacity: highlight && !isBinary ? 0.8 : 0.6,
|
||||
strokeWidth: highlight && !isBinary ? 2.5 : 1.5,
|
||||
strokeWidth: highlight && !isBinary ? 4 : 3,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -73,9 +71,9 @@ export const InnerChart: React.FC<Props> = ({
|
|||
const domainMax =
|
||||
maxProbability < 0.5 ? Math.round(10 * (maxProbability + 0.05)) / 10 : 1;
|
||||
const padding = {
|
||||
top: 12,
|
||||
bottom: 33,
|
||||
left: 30,
|
||||
top: 20,
|
||||
bottom: 75,
|
||||
left: 70,
|
||||
right: 17,
|
||||
};
|
||||
|
||||
|
@ -101,12 +99,12 @@ export const InnerChart: React.FC<Props> = ({
|
|||
<VictoryLabel
|
||||
style={[
|
||||
{
|
||||
fontSize: 10,
|
||||
fontSize: 16,
|
||||
fill: "black",
|
||||
strokeWidth: 0.05,
|
||||
},
|
||||
{
|
||||
fontSize: 10,
|
||||
fontSize: 16,
|
||||
fill: "#777",
|
||||
strokeWidth: 0.05,
|
||||
},
|
||||
|
@ -120,7 +118,7 @@ export const InnerChart: React.FC<Props> = ({
|
|||
)}`
|
||||
}
|
||||
style={{
|
||||
fontSize: 10, // needs to be set here and not just in labelComponent for text size calculations
|
||||
fontSize: 17, // needs to be set here and not just in labelComponent for text size calculations
|
||||
fontFamily:
|
||||
'"Gill Sans", "Gill Sans MT", "Seravek", "Trebuchet MS", sans-serif',
|
||||
// default font family from Victory, need to be specified explicitly for some reason, otherwise text size gets miscalculated
|
||||
|
@ -130,10 +128,10 @@ export const InnerChart: React.FC<Props> = ({
|
|||
fill: "white",
|
||||
}}
|
||||
cornerRadius={4}
|
||||
flyoutPadding={{ top: 4, bottom: 4, left: 10, right: 10 }}
|
||||
flyoutPadding={{ top: 4, bottom: 4, left: 16, right: 16 }}
|
||||
/>
|
||||
}
|
||||
radius={20}
|
||||
radius={50}
|
||||
voronoiBlacklist={
|
||||
[...Array(seriesList.length).keys()].map((i) => `line-${i}`)
|
||||
// see: https://github.com/FormidableLabs/victory/issues/545
|
||||
|
@ -161,10 +159,10 @@ export const InnerChart: React.FC<Props> = ({
|
|||
}}
|
||||
tickLabelComponent={
|
||||
<VictoryLabel
|
||||
dx={-10}
|
||||
dx={-40}
|
||||
dy={0}
|
||||
angle={-30}
|
||||
style={{ fontSize: 9, fill: "#777" }}
|
||||
style={{ fontSize: 15, fill: "#777" }}
|
||||
/>
|
||||
}
|
||||
scale={{ x: "time" }}
|
||||
|
@ -176,7 +174,7 @@ export const InnerChart: React.FC<Props> = ({
|
|||
grid: { stroke: "#D3D3D3", strokeWidth: 0.5 },
|
||||
}}
|
||||
tickLabelComponent={
|
||||
<VictoryLabel dy={0} dx={5} style={{ fontSize: 9, fill: "#777" }} />
|
||||
<VictoryLabel dy={0} style={{ fontSize: 18, fill: "#777" }} />
|
||||
}
|
||||
// tickFormat specifies how ticks should be displayed
|
||||
tickFormat={(x) => `${x * 100}%`}
|
||||
|
@ -207,7 +205,6 @@ export const InnerChart: React.FC<Props> = ({
|
|||
})
|
||||
*/
|
||||
}
|
||||
|
||||
</VictoryChart>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,7 +23,7 @@ export const HistoryChart: React.FC<Props> = ({ question }) => {
|
|||
const data = useMemo(() => buildChartData(question), [question]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-y-4 sm:flex-row sm:space-y-0 ">
|
||||
<div className="flex items-center flex-col space-y-4 sm:flex-row sm:space-y-0">
|
||||
<InnerChart data={data} highlight={highlight} />
|
||||
<Legend
|
||||
items={data.seriesNames.map((name, i) => ({
|
||||
|
|
|
@ -18,7 +18,7 @@ export const chartColors = [
|
|||
"#F59E0B", // amber-500
|
||||
];
|
||||
|
||||
export const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
||||
const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
||||
// used both for chart and for ssr placeholder
|
||||
export const width = 750;
|
||||
export const height = width / goldenRatio;
|
||||
|
|
|
@ -74,7 +74,7 @@ const getCurrencySymbolIfNeeded = ({
|
|||
"openInterest",
|
||||
"liquidity",
|
||||
];
|
||||
let dollarPlatforms = ["predictit", "kalshi", "polymarket", "insight"];
|
||||
let dollarPlatforms = ["predictit", "kalshi", "polymarket"];
|
||||
if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) {
|
||||
if (dollarPlatforms.includes(platform)) {
|
||||
return "$";
|
||||
|
@ -172,7 +172,6 @@ export const QuestionFooter: React.FC<Props> = ({
|
|||
>
|
||||
{question.platform.label
|
||||
.replace("Good Judgment Open", "GJOpen")
|
||||
.replace("Insight Prediction", "Insight")
|
||||
.replace(/ /g, "\u00a0")}
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -17,8 +17,8 @@ const truncateText = (length: number, text: string): string => {
|
|||
return text;
|
||||
}
|
||||
const breakpoints = " .!?";
|
||||
let lastLetter
|
||||
let lastIndex
|
||||
let lastLetter: string | undefined = undefined;
|
||||
let lastIndex: number | undefined = undefined;
|
||||
for (let index = length; index > 0; index--) {
|
||||
const letter = text[index];
|
||||
if (breakpoints.includes(letter)) {
|
||||
|
|
|
@ -101,7 +101,7 @@ const OptionRow: React.FC<OptionProps> = ({ option, mode, textMode }) => {
|
|||
<div
|
||||
className={`flex-none rounded-md text-center ${
|
||||
mode === "primary"
|
||||
? "text-sm md:text-lg text-normal text-white px-2 py-0.5 font-bold"
|
||||
? "text-normal text-white px-2 py-0.5 font-bold"
|
||||
: "text-sm w-14 py-0.5"
|
||||
} ${
|
||||
mode === "primary"
|
||||
|
@ -113,7 +113,7 @@ const OptionRow: React.FC<OptionProps> = ({ option, mode, textMode }) => {
|
|||
</div>
|
||||
<div
|
||||
className={`leading-snug ${
|
||||
mode === "primary" ? "text-sm md:text-lg text-normal" : "text-sm"
|
||||
mode === "primary" ? "text-normal" : "text-sm"
|
||||
} ${
|
||||
mode === "primary" ? textColor(option.probability) : "text-gray-700"
|
||||
}`}
|
||||
|
|
|
@ -10,7 +10,7 @@ export const QuestionTitle: React.FC<Props> = ({
|
|||
question,
|
||||
linkToMetaforecast,
|
||||
}) => (
|
||||
<h1 className="sm:text-3xl text-lg">
|
||||
<h1 className="sm:text-3xl text-xl">
|
||||
<a
|
||||
className="text-black no-underline hover:text-gray-700"
|
||||
href={
|
||||
|
|
|
@ -54,5 +54,5 @@ function getStarsColor(numstars: number) {
|
|||
}
|
||||
|
||||
export const Stars: React.FC<{ num: number }> = ({ num }) => {
|
||||
return <div className={getStarsColor(num) + " text-xs md:text-lg"}>{getstars(num)}</div>;
|
||||
return <div className={getStarsColor(num)}>{getstars(num)}</div>;
|
||||
};
|
||||
|
|
|
@ -30,25 +30,24 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
|||
props: {
|
||||
urqlState: ssrCache.extractData(),
|
||||
id,
|
||||
question
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const EmbedQuestionPage: NextPage<Props> = ({ id }) => {
|
||||
return (
|
||||
<div className="block bg-white min-h-screen">
|
||||
<div className="bg-white min-h-screen">
|
||||
<Query document={QuestionPageDocument} variables={{ id }}>
|
||||
{({ data: { result: question } }) =>
|
||||
question ? (
|
||||
<div className="flex flex-col p-2 w-full h-12/12">
|
||||
{/*<QuestionTitle question={question} linkToMetaforecast={true} /> */}
|
||||
<div className="p-4">
|
||||
<QuestionTitle question={question} linkToMetaforecast={true} />
|
||||
|
||||
<div className="mb-1 mt-1">
|
||||
<div className="mb-5 mt-5">
|
||||
<QuestionInfoRow question={question} />
|
||||
</div>
|
||||
|
||||
<div className="mb-0">
|
||||
<div className="mb-10">
|
||||
<QuestionChartOrVisualization question={question} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -72,7 +72,7 @@ const Section: React.FC<{ title: string; id?: string }> = ({
|
|||
const EmbedSection: React.FC<{ question: QuestionWithHistoryFragment }> = ({
|
||||
question,
|
||||
}) => {
|
||||
const url = `https://${getBasePath()}/questions/embed/${question.id}`;
|
||||
const url = getBasePath() + `/questions/embed/${question.id}`;
|
||||
return (
|
||||
<Section title="Embed" id="embed">
|
||||
<CopyParagraph
|
||||
|
|
|
@ -2,7 +2,7 @@ import { QuestionFragment } from "./fragments.generated";
|
|||
|
||||
export const getBasePath = () => {
|
||||
if (process.env.NEXT_PUBLIC_VERCEL_URL) {
|
||||
return `https://metaforecast.org`;//`https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
|
||||
return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
|
||||
}
|
||||
|
||||
// can be used for local development if you prefer non-default port
|
||||
|
@ -14,8 +14,8 @@ export const getBasePath = () => {
|
|||
};
|
||||
|
||||
export const cleanText = (text: string): string => {
|
||||
// TODO - move to GraphQL:
|
||||
// { description(clean: true, truncate: 250) }
|
||||
// Note: should no longer be necessary?
|
||||
// Still needed for e.g. /questions/rootclaim-what-caused-the-disappearance-of-malaysia-airlines-flight-370
|
||||
let textString = !!text ? text : "";
|
||||
textString = textString
|
||||
.replaceAll("] (", "](")
|
||||
|
@ -23,13 +23,12 @@ export const cleanText = (text: string): string => {
|
|||
.replaceAll("( [", "([")
|
||||
.replaceAll(") ,", "),")
|
||||
.replaceAll("==", "") // Denotes a title in markdown
|
||||
.replaceAll(/^#+\s+/gm, "")
|
||||
.replaceAll(/^Background\n/gm, "")
|
||||
.replaceAll(/^Context\n/gm, "")
|
||||
.replaceAll("Background\n", "")
|
||||
.replaceAll("Context\n", "")
|
||||
.replaceAll("--- \n", "- ")
|
||||
.replaceAll(/\[(.*?)\]\(.*?\)/g, "$1");
|
||||
textString = textString.slice(0, 1) == "=" ? textString.slice(1) : textString;
|
||||
|
||||
//console.log(textString)
|
||||
return textString;
|
||||
};
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ export default async function searchWithAlgolia({
|
|||
url: "https://metaforecast.org",
|
||||
platform: "metaforecast",
|
||||
platformLabel: "metaforecast",
|
||||
description: "Maybe try a broader query, e.g., reduce the number of 'stars' by clicking in 'Advanced options'?",
|
||||
description: "Maybe try a broader query?",
|
||||
options: [
|
||||
{
|
||||
name: "Yes",
|
||||
|
@ -166,7 +166,7 @@ export default async function searchWithAlgolia({
|
|||
url: "https://metaforecast.org",
|
||||
platform: "metaforecast",
|
||||
platformLabel: "metaforecast",
|
||||
description: "Maybe try a broader query? Maybe try a broader query, e.g., reduce the number of 'stars' by clicking in 'Advanced options'? That said, we could be wrong.",
|
||||
description: "Maybe try a broader query? That said, we could be wrong.",
|
||||
options: [
|
||||
{
|
||||
name: "Yes",
|
||||
|
|
|
@ -5,7 +5,7 @@ export async function uploadToImgur(dataURL: string): Promise<string> {
|
|||
method: "post",
|
||||
url: "https://api.imgur.com/3/image",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.IMGUR_BEARER}`,
|
||||
Authorization: "Bearer 8e9666fb889318515a62208560d4e8393dac26d8",
|
||||
},
|
||||
data: {
|
||||
type: "base64",
|
||||
|
|
Loading…
Reference in New Issue
Block a user