├── src ├── lib │ ├── markets │ │ └── config.ts │ ├── utils.ts │ ├── services │ │ ├── index.ts │ │ ├── manifold-grouped.ts │ │ ├── manifold.ts │ │ ├── polymarket.ts │ │ ├── cdc.ts │ │ ├── kalshi.ts │ │ ├── kalshi.server.ts │ │ ├── manifold-historical.ts │ │ ├── metaculus.ts │ │ ├── metaculus.server.ts │ │ ├── manifold-historical.server.ts │ │ ├── metaculus-download.ts │ │ └── metaculus-download.server.ts │ ├── config.ts │ ├── kalshi │ │ ├── index-helpers.ts │ │ ├── config.ts │ │ └── fetch.ts │ ├── probabilities.ts │ ├── getInterpolatedValue.test.ts │ ├── getInterpolatedValue.ts │ ├── dates.ts │ ├── types.ts │ └── createIndex.ts ├── app │ ├── favicon.ico │ ├── twitter-image.png │ ├── opengraph-image.png │ ├── api │ │ ├── redeploy │ │ │ └── route.ts │ │ ├── email │ │ │ └── route.ts │ │ ├── manifold-historical │ │ │ └── route.ts │ │ ├── manifold-grouped │ │ │ └── route.ts │ │ ├── metaculus │ │ │ └── route.ts │ │ ├── polymarket-timeseries │ │ │ └── route.ts │ │ ├── kalshi │ │ │ └── route.ts │ │ ├── polymarket │ │ │ └── route.ts │ │ ├── manifold │ │ │ └── route.ts │ │ ├── cdc-data │ │ │ └── route.ts │ │ └── metaculus-download │ │ │ └── route.ts │ ├── providers.tsx │ ├── globals.css │ └── layout.tsx └── components │ ├── MobileFriendlyTooltip.tsx │ ├── ui │ ├── tooltip.tsx │ └── drawer.tsx │ ├── GraphTitle.tsx │ ├── CustomTooltip.tsx │ ├── BarGraph.tsx │ └── LineGraph.tsx ├── .prettierignore ├── bun.lockb ├── postcss.config.js ├── vercel.json ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── example-data │ ├── metaculus.json │ ├── polymarket.json │ ├── manifold.json │ └── timeseries.json ├── globe.svg └── next.svg ├── .husky └── pre-commit ├── .prettierrc ├── .cursor └── rules │ └── next-15.mdc ├── next.config.mjs ├── components.json ├── tsconfig.json ├── knowledge.md ├── .gitignore ├── eslint.config.mjs ├── README.md ├── scripts ├── download-metaculus-data.ts ├── install-chromium.ts ├── merge-kalshi-with-index.ts ├── process-forecast.ts └── kalshiData.json ├── knowledge.cursorrules ├── package.json ├── tailwind.config.ts └── .cursorrules /src/lib/markets/config.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | bun.lockb 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/agi-timelines-dashboard/main/bun.lockb -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/agi-timelines-dashboard/main/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/agi-timelines-dashboard/main/src/app/twitter-image.png -------------------------------------------------------------------------------- /src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugetim/agi-timelines-dashboard/main/src/app/opengraph-image.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "crons": [ 3 | { 4 | "path": "/api/redeploy", 5 | "schedule": "0 0 * * *" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | bun run check 5 | bun run lint:fix 6 | bun run format 7 | git add -u 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "tabWidth": 2, 5 | "trailingComma": "all", 6 | "plugins": ["prettier-plugin-tailwindcss"] 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /.cursor/rules/next-15.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Using next 15 and bun 3 | globs: 4 | alwaysApply: false 5 | --- 6 | You are using next 15 and bun. You should follow the latest patterns for app development with next 15 and use the Next js docs whenever you're not sure. -------------------------------------------------------------------------------- /src/lib/services/index.ts: -------------------------------------------------------------------------------- 1 | export { fetchPolymarketData } from "./polymarket"; 2 | export { fetchMetaculusData } from "./metaculus"; 3 | export { fetchKalshiData } from "./kalshi"; 4 | export { fetchCdcData } from "./cdc"; 5 | export { fetchManifoldData } from "./manifold"; 6 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const config = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "unavatar.io", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | export const PREDICTION_MARKETS = { 2 | POLYMARKET: { 3 | SLUG: "another-state-declare-a-state-of-emergency-over-bird-flu-before-february", 4 | }, 5 | METACULUS: { 6 | QUESTION_ID: 30960, 7 | }, 8 | MANIFOLD: { 9 | SLUG: "will-we-get-agi-before-2028-ff560f9e9346", 10 | }, 11 | } as const; 12 | -------------------------------------------------------------------------------- /src/app/api/redeploy/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET() { 2 | const deployHookUrl = process.env.DEPLOY_HOOK_URL; 3 | if (!deployHookUrl) { 4 | return new Response("Deploy hook URL not configured", { status: 500 }); 5 | } 6 | 7 | await fetch(deployHookUrl); 8 | return new Response("Redeploy triggered", { status: 200 }); 9 | } 10 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import posthog from "posthog-js"; 4 | import { PostHogProvider } from "posthog-js/react"; 5 | 6 | if (typeof window !== "undefined") { 7 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { 8 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, 9 | person_profiles: "identified_only", 10 | }); 11 | } 12 | 13 | export function CSPostHogProvider({ children }: { children: React.ReactNode }) { 14 | return {children}; 15 | } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/kalshi/index-helpers.ts: -------------------------------------------------------------------------------- 1 | export function getNearestKalshiIndex( 2 | dateOfInterest: number, 3 | data: { date: string; value: number }[], 4 | ) { 5 | return data.reduce((prev, curr, index) => { 6 | const kalshiDate = new Date(curr.date).getTime(); 7 | const prevDate = new Date(data[prev].date).getTime(); 8 | 9 | // Skip dates before dateOfInterest 10 | if (kalshiDate < dateOfInterest) return prev; 11 | 12 | // If prev is before dateOfInterest, take current 13 | if (prevDate < dateOfInterest) return index; 14 | 15 | return kalshiDate < prevDate ? index : prev; 16 | }, 0); 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/kalshi/config.ts: -------------------------------------------------------------------------------- 1 | // Split the private key back into lines when loaded from env 2 | function formatPrivateKey(key: string) { 3 | const lines = key.split("\\n"); 4 | return [ 5 | "-----BEGIN RSA PRIVATE KEY-----", 6 | ...lines, 7 | "-----END RSA PRIVATE KEY-----", 8 | ].join("\n"); 9 | } 10 | 11 | if (!process.env.KALSHI_ACCESS_KEY || !process.env.KALSHI_PRIVATE_KEY) { 12 | throw new Error("Missing Kalshi API credentials"); 13 | } 14 | 15 | export const config = { 16 | accessKey: process.env.KALSHI_ACCESS_KEY, 17 | privateKey: formatPrivateKey(process.env.KALSHI_PRIVATE_KEY), 18 | baseUrl: "https://api.elections.kalshi.com/trade-api/v2", 19 | } as const; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["**/*.ts", "**/*.tsx", "next-env.d.ts", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /knowledge.md: -------------------------------------------------------------------------------- 1 | # API Routes 2 | 3 | - All API routes should include caching headers with 1-hour cache and stale-while-revalidate: 4 | 5 | ```typescript 6 | headers: { 7 | "Cache-Control": "public, max-age=3600, stale-while-revalidate=3600" 8 | } 9 | ``` 10 | 11 | # Dark Mode 12 | 13 | - Uses system preferences via `prefers-color-scheme` media query 14 | - No configuration needed in tailwind.config.ts (default behavior) 15 | - All dark mode styles use Tailwind's `dark:` prefix 16 | 17 | ## Chart Styling 18 | 19 | - Use `currentColor` with opacity for grid lines and axes 20 | - Grid lines: opacity 0.1 21 | - Axis lines: opacity 0.2 22 | - Axis text: opacity 0.65 23 | - Apply same styling to both LineGraph and BarGraph for consistency 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | # playwright browsers 44 | .local-browsers/ 45 | node_modules/playwright-core/.local-browsers/ 46 | 47 | # Downloaded data 48 | /data/ 49 | 50 | /keys 51 | .env*.local 52 | TODO.md -------------------------------------------------------------------------------- /public/example-data/metaculus.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 30960, 3 | "title": "Will CDC report 10,000 or more H5 avian influenza cases in the United States before January 1, 2026?", 4 | "url_title": "10,000+ H5 Avian Influenza Cases in US End of 2025?", 5 | "slug": "10000-h5-avian-influenza-cases-in-us-end-of-2025", 6 | "author_id": 117502, 7 | "author_username": "RyanBeck", 8 | "coauthors": [], 9 | "created_at": "2024-12-11T16:32:52.622379Z", 10 | "published_at": "2024-12-12T23:23:20.256735Z", 11 | "edited_at": "2024-12-30T13:50:23.197451Z", 12 | "curation_status": "approved", 13 | "comment_count": 10, 14 | "status": "open", 15 | "resolved": false, 16 | "actual_close_time": null, 17 | "scheduled_close_time": "2025-12-31T23:00:00Z", 18 | "scheduled_resolve_time": "2026-01-01T14:00:00Z", 19 | "open_time": "2024-12-13T17:30:00Z", 20 | "nr_forecasters": 27 21 | } 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | plugins: { 16 | "unused-imports": { 17 | rules: { 18 | "unused-imports/no-unused-imports": "error", 19 | "unused-imports/no-unused-vars": "error", 20 | }, 21 | }, 22 | }, 23 | rules: { 24 | "@next/next/no-img-element": "off", 25 | "sort-imports": [ 26 | "error", 27 | { 28 | ignoreCase: true, 29 | ignoreDeclarationSort: true, 30 | }, 31 | ], 32 | }, 33 | }, 34 | ]; 35 | 36 | export default eslintConfig; 37 | -------------------------------------------------------------------------------- /src/lib/services/manifold-grouped.ts: -------------------------------------------------------------------------------- 1 | interface Answer { 2 | text: string; 3 | prob: number; 4 | probChanges: { 5 | day: number; 6 | week: number; 7 | month: number; 8 | }; 9 | } 10 | 11 | export interface ManifoldGroupedData { 12 | id: string; 13 | question: string; 14 | answers: Answer[]; 15 | min: number; 16 | max: number; 17 | } 18 | 19 | export function transformManifoldDataForChart( 20 | data: ManifoldGroupedData, 21 | ): { date: string; value: number }[] { 22 | return data.answers.map((answer) => ({ 23 | date: answer.text.split("-")[0], // Extract first year from range 24 | value: answer.prob * 100, // Convert to percentage 25 | })); 26 | } 27 | 28 | export async function getManifoldGroupedData( 29 | slug: string, 30 | ): Promise { 31 | const response = await fetch(`/api/manifold-grouped?slug=${slug}`); 32 | if (!response.ok) throw new Error("Failed to fetch manifold grouped data"); 33 | return response.json(); 34 | } 35 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/email/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { z } from "zod"; 3 | 4 | const emailSchema = z.object({ 5 | email: z.string().email(), 6 | }); 7 | 8 | // https://docs.google.com/forms/d/e/1FAIpQLSegkPqJDm5mKrBx49yWLy0WGedpOnsQll6OIpcmOX-reidN4Q/viewform?usp=sf_link 9 | const FORM_ID = "1FAIpQLSegkPqJDm5mKrBx49yWLy0WGedpOnsQll6OIpcmOX-reidN4Q"; 10 | const FORM_URL = `https://docs.google.com/forms/d/e/${FORM_ID}/formResponse`; 11 | 12 | export async function POST(request: Request) { 13 | try { 14 | const body = await request.json(); 15 | const { email } = emailSchema.parse(body); 16 | 17 | const formData = new FormData(); 18 | formData.append("entry.1257821246", email); 19 | 20 | const response = await fetch(FORM_URL, { 21 | method: "POST", 22 | body: formData, 23 | }); 24 | 25 | if (!response.ok) throw new Error("Failed to submit"); 26 | 27 | return NextResponse.json({ success: true }); 28 | } catch (error) { 29 | console.error("Failed to save email:", error); 30 | return NextResponse.json( 31 | { error: "Failed to save email" }, 32 | { status: 500 }, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/probabilities.ts: -------------------------------------------------------------------------------- 1 | export const namedProbabilities = { 2 | 3: "Very unlikely", 3 | 8: "Little chance", 4 | 20: "Unlikely", 5 | 23: "Probably not", 6 | 40: "Maybe", 7 | 50: "About even", 8 | 57: "Better than even", 9 | 68: "Probably", 10 | 74: "Likely", 11 | 80: "Very good chance", 12 | 90: "Highly likely", 13 | 97: "Almost certain", 14 | }; 15 | 16 | export function getProbabilityWord(value: number): string { 17 | const percentValue = value * 100; 18 | const probabilities = Object.keys(namedProbabilities).map(Number); 19 | const closest = probabilities.reduce((prev, curr) => 20 | Math.abs(curr - percentValue) < Math.abs(prev - percentValue) ? curr : prev, 21 | ); 22 | return namedProbabilities[closest as keyof typeof namedProbabilities]; 23 | } 24 | 25 | export function getProbabilityColor(value: number): string { 26 | const percentValue = value * 100; 27 | if (percentValue <= 20) return "text-green-600 font-bold"; 28 | if (percentValue <= 40) return "text-green-500 font-semibold"; 29 | if (percentValue <= 50) return "text-yellow-600 font-semibold"; 30 | if (percentValue <= 70) return "text-orange-500 font-semibold"; 31 | if (percentValue <= 90) return "text-red-500 font-bold"; 32 | return "text-red-600 font-extrabold"; 33 | } 34 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/MobileFriendlyTooltip.tsx: -------------------------------------------------------------------------------- 1 | import { InfoIcon } from "lucide-react"; 2 | import { 3 | Drawer, 4 | DrawerContent, 5 | DrawerTitle, 6 | DrawerTrigger, 7 | } from "@/components/ui/drawer"; 8 | import { 9 | Tooltip, 10 | TooltipContent, 11 | TooltipTrigger, 12 | } from "@/components/ui/tooltip"; 13 | import { cn } from "@/lib/utils"; 14 | 15 | const triggerClasses = 16 | "inline-block rounded-full bg-transparent p-1 text-muted-foreground hover:text-black/80 dark:hover:text-white/80 align-middle"; 17 | 18 | export function MobileFriendlyTooltip({ 19 | children, 20 | }: { 21 | children: React.ReactNode; 22 | }) { 23 | return ( 24 | <> 25 | 26 | 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | 36 | 37 | 38 | Test 39 |
{children}
40 |
41 |
42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/app/api/manifold-historical/route.ts: -------------------------------------------------------------------------------- 1 | import { Bet } from "@/lib/types"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET(request: Request) { 5 | const url = new URL(request.url); 6 | const slug = url.searchParams.get("slug"); 7 | 8 | if (!slug) { 9 | return NextResponse.json( 10 | { error: "No contract slug provided" }, 11 | { status: 400 }, 12 | ); 13 | } 14 | 15 | // Fetch all bets for the contract, by fetching using ?before with the oldest bet id 16 | const bets: Bet[] = []; 17 | let hasMore = true; 18 | let cursor = ""; 19 | while (hasMore) { 20 | const searchParams = new URLSearchParams(); 21 | searchParams.set("contractSlug", slug); 22 | if (cursor) { 23 | searchParams.set("before", cursor); 24 | } 25 | const betsResponse = await fetch( 26 | `https://api.manifold.markets/v0/bets?${searchParams.toString()}`, 27 | ); 28 | const newBets = (await betsResponse.json()) as Bet[]; 29 | bets.push(...newBets); 30 | 31 | if (newBets.length === 0) { 32 | hasMore = false; 33 | } else { 34 | cursor = newBets[newBets.length - 1].id; 35 | } 36 | } 37 | 38 | return NextResponse.json( 39 | { bets }, 40 | { 41 | headers: { 42 | "Cache-Control": "public, s-maxage=86400, stale-while-revalidate=86400", 43 | }, 44 | }, 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 19 | 28 | 29 | )); 30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 31 | 32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /src/components/GraphTitle.tsx: -------------------------------------------------------------------------------- 1 | import { LinkIcon } from "lucide-react"; 2 | import { MobileFriendlyTooltip } from "./MobileFriendlyTooltip"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | export function GraphTitle({ 6 | title, 7 | sourceUrl, 8 | tooltipContent, 9 | children, 10 | }: { 11 | title: string; 12 | sourceUrl?: string; 13 | tooltipContent?: React.ReactNode; 14 | children?: React.ReactNode; 15 | }) { 16 | const sharedClasses = 17 | "text-pretty text-xl font-semibold leading-tight tracking-tight text-gray-900 dark:text-gray-100"; 18 | const TitleComponent = sourceUrl ? ( 19 | 25 |

31 | {title} 32 | 33 |

34 |
35 | ) : ( 36 |

{title}

37 | ); 38 | 39 | return ( 40 |
41 |
42 |
43 | {TitleComponent} 44 |
45 | {tooltipContent && ( 46 | {tooltipContent} 47 | )} 48 |
49 | {children} 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/app/api/manifold-grouped/route.ts: -------------------------------------------------------------------------------- 1 | const MANIFOLD_API = "https://api.manifold.markets/v0"; 2 | 3 | export async function GET(request: Request) { 4 | try { 5 | const { searchParams } = new URL(request.url); 6 | const slug = searchParams.get("slug"); 7 | 8 | if (!slug) { 9 | return Response.json( 10 | { error: "Market slug is required" }, 11 | { status: 400 }, 12 | ); 13 | } 14 | 15 | // First fetch to get market ID 16 | const marketResponse = await fetch(`${MANIFOLD_API}/slug/${slug}`, { 17 | headers: { Accept: "application/json" }, 18 | }); 19 | 20 | if (!marketResponse.ok) { 21 | return Response.json( 22 | { error: "Failed to fetch market data" }, 23 | { status: marketResponse.status }, 24 | ); 25 | } 26 | 27 | const marketData = await marketResponse.json(); 28 | const marketId = marketData.id; 29 | 30 | // Second fetch to get detailed market data with answers 31 | const detailedResponse = await fetch( 32 | `https://api.manifold.markets/markets-by-ids?ids[]=${marketId}`, 33 | { 34 | headers: { Accept: "application/json" }, 35 | }, 36 | ); 37 | 38 | if (!detailedResponse.ok) { 39 | return Response.json( 40 | { error: "Failed to fetch detailed market data" }, 41 | { status: detailedResponse.status }, 42 | ); 43 | } 44 | 45 | const detailedData = await detailedResponse.json(); 46 | return Response.json(detailedData[0]); 47 | } catch (error) { 48 | console.error("Error fetching Manifold grouped data:", error); 49 | return Response.json({ error: "Internal server error" }, { status: 500 }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/api/metaculus/route.ts: -------------------------------------------------------------------------------- 1 | const METACULUS_API = "https://www.metaculus.com/api"; 2 | 3 | export async function GET(request: Request) { 4 | try { 5 | // Get questionId from URL params 6 | const { searchParams } = new URL(request.url); 7 | const questionId = searchParams.get("questionId"); 8 | 9 | if (!questionId) { 10 | return Response.json( 11 | { error: "Question ID is required" }, 12 | { status: 400 }, 13 | ); 14 | } 15 | 16 | const questionResponse = await fetch( 17 | `${METACULUS_API}/posts/${questionId}`, 18 | { 19 | headers: { 20 | Accept: "application/json", 21 | }, 22 | }, 23 | ); 24 | 25 | if (!questionResponse.ok) { 26 | const errorText = await questionResponse.text(); 27 | console.error("Error response body:", errorText); 28 | 29 | return Response.json( 30 | { 31 | error: "Failed to fetch Metaculus data", 32 | status: questionResponse.status, 33 | details: errorText, 34 | }, 35 | { status: questionResponse.status }, 36 | ); 37 | } 38 | 39 | const questionData = await questionResponse.json(); 40 | 41 | return Response.json(questionData, { 42 | headers: { 43 | "Cache-Control": "public, max-age=3600, stale-while-revalidate=3600", 44 | }, 45 | }); 46 | } catch (error) { 47 | console.error("Error fetching Metaculus data:", error); 48 | return Response.json( 49 | { error: "Internal server error" }, 50 | { 51 | status: 500, 52 | headers: { 53 | "Cache-Control": "public, max-age=3600, stale-while-revalidate=3600", 54 | }, 55 | }, 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/services/manifold.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataPoint, ManifoldResponse } from "../types"; 2 | 3 | export type ManifoldMarketInfo = { 4 | title: string; 5 | description: string | null; 6 | probability: number; 7 | lastUpdatedTime: number; 8 | history: ChartDataPoint[]; 9 | }; 10 | 11 | interface ManifoldBet { 12 | createdTime: number; 13 | probBefore: number; 14 | probAfter: number; 15 | } 16 | 17 | export async function fetchManifoldData( 18 | slug: string, 19 | ): Promise { 20 | try { 21 | const response = await fetch(`/api/manifold?slug=${slug}`); 22 | if (!response.ok) { 23 | throw new Error("Failed to fetch Manifold data"); 24 | } 25 | const { market, bets } = await response.json(); 26 | const data = market as ManifoldResponse; 27 | const betHistory = bets as ManifoldBet[]; 28 | 29 | // Extract simple description from first paragraph if available 30 | const firstParagraph = data.description.content[0]; 31 | const simpleDescription = firstParagraph?.content?.[0]?.text || null; 32 | 33 | return { 34 | title: data.question, 35 | description: simpleDescription, 36 | probability: data.probability, 37 | lastUpdatedTime: data.lastUpdatedTime, 38 | history: transformManifoldData(betHistory), 39 | }; 40 | } catch (error) { 41 | console.error("Error fetching Manifold data:", error); 42 | throw error; 43 | } 44 | } 45 | 46 | function transformManifoldData(bets: ManifoldBet[]): ChartDataPoint[] { 47 | if (!bets?.length) { 48 | console.error("No bet history available"); 49 | return []; 50 | } 51 | 52 | return bets.map((bet) => ({ 53 | date: new Date(bet.createdTime).toISOString(), 54 | value: bet.probAfter, 55 | })); 56 | } 57 | -------------------------------------------------------------------------------- /src/lib/services/polymarket.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataPoint, PolymarketResponse } from "../types"; 2 | 3 | export async function fetchPolymarketData( 4 | slug: string, 5 | ): Promise { 6 | try { 7 | // First get the market data to get the marketId 8 | const marketResponse = await fetch(`/api/polymarket?slug=${slug}`); 9 | if (!marketResponse.ok) throw new Error("Market API failed"); 10 | const marketData = await marketResponse.json(); 11 | 12 | // Get the first clubTokenId from the clobTokenIds array 13 | const clubTokenId = JSON.parse(marketData.clobTokenIds)[0]; 14 | 15 | // Then fetch the timeseries with the marketId 16 | const timeseriesResponse = await fetch( 17 | `/api/polymarket-timeseries?marketId=${clubTokenId}`, 18 | ); 19 | if (!timeseriesResponse.ok) throw new Error("Timeseries API failed"); 20 | const timeseriesData = await timeseriesResponse.json(); 21 | 22 | return transformPolymarketData(timeseriesData); 23 | } catch (error) { 24 | console.error("Error fetching Polymarket data:", error); 25 | 26 | // Fall back to example data 27 | const exampleData = await fetchExampleData(); 28 | return transformPolymarketData(exampleData); 29 | } 30 | } 31 | 32 | async function fetchExampleData(): Promise { 33 | const response = await fetch("/example-data/timeseries.json"); 34 | if (!response.ok) throw new Error("Failed to load example data"); 35 | return response.json(); 36 | } 37 | 38 | function transformPolymarketData(data: PolymarketResponse): ChartDataPoint[] { 39 | if (!data?.history) { 40 | return []; 41 | } 42 | return data.history.map((point) => ({ 43 | date: new Date(point.t * 1000).toISOString(), 44 | value: point.p * 100, 45 | })); 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/services/cdc.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "date-fns"; 2 | import { CdcDataPoint, ChartDataPoint } from "../types"; 3 | 4 | export async function fetchCdcData(): Promise { 5 | const data = await fetchFromAPI(); 6 | const points = transformCdcData(data); 7 | return fillMissingMonths(points); 8 | } 9 | 10 | async function fetchFromAPI(): Promise { 11 | const response = await fetch("/api/cdc-data"); 12 | if (!response.ok) throw new Error("API failed"); 13 | return response.json(); 14 | } 15 | 16 | function transformCdcData(data: CdcDataPoint[]): ChartDataPoint[] { 17 | return data.map((point) => { 18 | const date = parse(point.Month, "M/1/yyyy", new Date()).toISOString(); 19 | const cases = Object.entries(point) 20 | .filter(([key]) => !["Range", "Month"].includes(key)) 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | .reduce((sum, [_, val]) => sum + parseInt(val) || 0, 0); 23 | 24 | return { date, value: cases }; 25 | }); 26 | } 27 | 28 | function fillMissingMonths(points: ChartDataPoint[]): ChartDataPoint[] { 29 | if (!points.length) return []; 30 | 31 | const dates = points.map((p) => new Date(p.date)); 32 | const start = new Date(Math.min(...dates.map((d) => d.getTime()))); 33 | const end = new Date(Math.max(...dates.map((d) => d.getTime()))); 34 | 35 | const filled: ChartDataPoint[] = []; 36 | const current = new Date(start); 37 | 38 | while (current <= end) { 39 | const isoDate = current.toISOString(); 40 | const existing = points.find((p) => p.date === isoDate); 41 | 42 | filled.push({ 43 | date: isoDate, 44 | value: existing?.value || 0, 45 | }); 46 | 47 | current.setMonth(current.getMonth() + 1); 48 | } 49 | 50 | return filled; 51 | } 52 | -------------------------------------------------------------------------------- /src/app/api/polymarket-timeseries/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | const CLOB_API = "https://clob.polymarket.com"; 4 | 5 | // 106312791964557364184052642373426857106392360847438469940517961069035123954706 6 | 7 | export const dynamic = "force-dynamic"; 8 | 9 | export async function GET(request: Request) { 10 | try { 11 | const { searchParams } = new URL(request.url); 12 | // Note! this actually expects a clubTokenId 13 | const marketId = searchParams.get("marketId"); 14 | 15 | if (!marketId) { 16 | return NextResponse.json( 17 | { error: "Market ID is required" }, 18 | { status: 400 }, 19 | ); 20 | } 21 | 22 | const url = new URL(`${CLOB_API}/prices-history`); 23 | url.search = new URLSearchParams({ 24 | market: marketId, 25 | interval: "1m", 26 | fidelity: "60", 27 | }).toString(); 28 | 29 | const timeseriesResponse = await fetch(url, { 30 | headers: { 31 | Accept: "application/json", 32 | }, 33 | }); 34 | 35 | if (!timeseriesResponse.ok) { 36 | const errorText = await timeseriesResponse.text(); 37 | console.error("Error response body:", errorText); 38 | 39 | return NextResponse.json( 40 | { 41 | error: "Failed to fetch timeseries data", 42 | status: timeseriesResponse.status, 43 | details: errorText, 44 | }, 45 | { status: timeseriesResponse.status }, 46 | ); 47 | } 48 | 49 | const timeseriesData = await timeseriesResponse.json(); 50 | return NextResponse.json(timeseriesData); 51 | } catch (error) { 52 | console.error("Error fetching timeseries data:", error); 53 | return NextResponse.json( 54 | { error: "Internal server error" }, 55 | { status: 500 }, 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/download-metaculus-data.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | const METACULUS_API = "https://www.metaculus.com/api"; 5 | const QUESTION_ID = 3479; // From config.ts 6 | const OUTPUT_DIR = path.join(process.cwd(), "data"); 7 | 8 | // You'll need to get this from your browser after logging into Metaculus 9 | // Go to metaculus.com, open dev tools, look at any API request headers 10 | if (!process.env.METACULUS_API_KEY) { 11 | console.error("Please set METACULUS_API_KEY environment variable"); 12 | process.exit(1); 13 | } 14 | 15 | async function downloadMetaculusData() { 16 | try { 17 | const params = new URLSearchParams({ 18 | aggregation_methods: "recency_weighted", 19 | minimize: "false", 20 | include_comments: "false", 21 | }); 22 | 23 | const url = `${METACULUS_API}/posts/${QUESTION_ID}/download-data/?${params}`; 24 | 25 | const response = await fetch(url, { 26 | headers: { 27 | Authorization: `Token ${process.env.METACULUS_API_KEY}`, 28 | }, 29 | }); 30 | 31 | if (!response.ok) { 32 | const text = await response.text(); 33 | throw new Error( 34 | `Failed to fetch data: ${response.status} ${response.statusText}\nResponse: ${text}`, 35 | ); 36 | } 37 | 38 | // Ensure output directory exists 39 | if (!fs.existsSync(OUTPUT_DIR)) { 40 | fs.mkdirSync(OUTPUT_DIR, { recursive: true }); 41 | } 42 | 43 | // Write the zip file 44 | const buffer = Buffer.from(await response.arrayBuffer()); 45 | const outputPath = path.join(OUTPUT_DIR, `metaculus-${QUESTION_ID}.zip`); 46 | fs.writeFileSync(outputPath, buffer); 47 | 48 | console.log(`Downloaded data to ${outputPath}`); 49 | } catch (error) { 50 | console.error("Error downloading Metaculus data:", error); 51 | process.exit(1); 52 | } 53 | } 54 | 55 | downloadMetaculusData(); 56 | -------------------------------------------------------------------------------- /knowledge.cursorrules: -------------------------------------------------------------------------------- 1 | # Package Version Rules 2 | - next@15.1.3 is intentionally set 3 | - eslint@9 is intentionally set 4 | - @types/node@22.10.1 is intentionally set 5 | - eslint-config-next@15.1.3 is intentionally set 6 | - @eslint/eslintrc@3 is intentionally set 7 | 8 | # Do not auto-update or suggest updates for these packages 9 | ignore-version-updates: 10 | - next 11 | - eslint 12 | - @types/node 13 | - eslint-config-next 14 | - @eslint/eslintrc 15 | 16 | # Risk Index Calculation Rules 17 | - Current formula: (Polymarket * 0.1 + Metaculus) / 2 18 | - DO NOT CHANGE THIS FORMULA without explicit approval 19 | - This weighting is because the markets are different and the polymarket resolving yes is still only 10% that bird flu is a disaster. 20 | - Any changes must be discussed and validated first 21 | 22 | # Data Source Requirements 23 | Primary data sources (in order of priority): 24 | 1. Polymarket prediction markets 25 | 2. Metaculus predictions 26 | 3. Sentinel (manual data input) 27 | 4. Manifold (backup if unable to get Polymarket or Metaculus) 28 | 5. CDC data (under consideration) 29 | 30 | # Implementation Notes 31 | - Main graph will show weighted combination of all available sources 32 | - Secondary graphs will show individual data sources 33 | - Need to handle different time periods and data overlaps 34 | - Consider ignoring data before overlap periods if overlap is substantial 35 | - Otherwise, use reduced number of sources for earlier periods 36 | 37 | # Database Requirements 38 | - Store historical data points 39 | - Track daily/hourly values from each source 40 | - Store metadata about sources 41 | - Manage weights for combining sources 42 | - Handle missing data and different time periods 43 | 44 | # Development Plan 45 | 1. Implement data fetchers for each source 46 | 2. Set up database structure 47 | 3. Create data combination logic 48 | 4. Update visualization components -------------------------------------------------------------------------------- /src/lib/getInterpolatedValue.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { getInterpolatedValue } from "./getInterpolatedValue"; 3 | import { ChartDataPoint } from "./types"; 4 | 5 | describe("getInterpolatedValue", () => { 6 | const sampleData: ChartDataPoint[] = [ 7 | { date: "2024-01-01T12:00:00.000Z", value: 10 }, 8 | { date: "2024-01-02T12:00:00.000Z", value: 20 }, 9 | { date: "2024-01-04T12:00:00.000Z", value: 40 }, 10 | ]; 11 | 12 | test("returns exact match when found", () => { 13 | const result = getInterpolatedValue(sampleData, "2024-01-02T12:00:00.000Z"); 14 | expect(result).toBe(20); 15 | }); 16 | 17 | test("interpolates between two points", () => { 18 | const result = getInterpolatedValue(sampleData, "2024-01-03T12:00:00.000Z"); 19 | // Should be halfway between 20 and 40 20 | expect(result).toBe(30); 21 | }); 22 | 23 | test("returns closest value when target is before all points", () => { 24 | const result = getInterpolatedValue(sampleData, "2023-12-31T12:00:00.000Z"); 25 | expect(result).toBe(10); 26 | }); 27 | 28 | test("returns closest value when target is after all points", () => { 29 | const result = getInterpolatedValue(sampleData, "2024-01-05T12:00:00.000Z"); 30 | expect(result).toBe(40); 31 | }); 32 | 33 | test("handles empty dataset", () => { 34 | const result = getInterpolatedValue([], "2024-01-01T12:00:00.000Z"); 35 | expect(result).toBeUndefined(); 36 | }); 37 | 38 | test("handles unsorted data", () => { 39 | const unsortedData = [ 40 | { date: "2024-01-04T12:00:00.000Z", value: 40 }, 41 | { date: "2024-01-01T12:00:00.000Z", value: 10 }, 42 | { date: "2024-01-02T12:00:00.000Z", value: 20 }, 43 | ]; 44 | const result = getInterpolatedValue( 45 | unsortedData, 46 | "2024-01-03T12:00:00.000Z", 47 | ); 48 | expect(result).toBe(30); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/lib/kalshi/fetch.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { config } from "./config"; 3 | 4 | export async function kalshiFetch( 5 | path: string, 6 | init: RequestInit & { query?: Record } = {}, 7 | ) { 8 | const { query, ...restInit } = init; 9 | const queryString = query 10 | ? "?" + 11 | new URLSearchParams( 12 | Object.entries(query).map(([k, v]) => [k, v.toString()]), 13 | ) 14 | : ""; 15 | 16 | const timestamp = Date.now().toString(); 17 | const method = restInit.method || "GET"; 18 | 19 | const message = Buffer.from(timestamp + method + path + queryString, "utf-8"); 20 | const key = crypto.createPrivateKey(config.privateKey); 21 | 22 | const signature = crypto 23 | .sign("sha256", message, { 24 | key, 25 | padding: crypto.constants.RSA_PKCS1_PSS_PADDING, 26 | saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST, 27 | }) 28 | .toString("base64"); 29 | 30 | const headers = { 31 | "KALSHI-ACCESS-KEY": config.accessKey, 32 | "KALSHI-ACCESS-SIGNATURE": signature, 33 | "KALSHI-ACCESS-TIMESTAMP": timestamp, 34 | "Content-Type": "application/json", 35 | ...init.headers, 36 | }; 37 | 38 | let baseUrl: string = config.baseUrl; 39 | if (path.includes("candlesticks")) { 40 | baseUrl = "https://api.elections.kalshi.com/v1"; 41 | } 42 | 43 | const url = baseUrl + path + queryString; 44 | 45 | const response = await fetch(url, { 46 | ...restInit, 47 | headers, 48 | }); 49 | 50 | if (!response.ok) { 51 | const errorText = await response.text(); 52 | console.error("Response error:", { 53 | status: response.status, 54 | statusText: response.statusText, 55 | body: errorText, 56 | url, 57 | headers: Object.fromEntries(response.headers), 58 | }); 59 | throw new Error(`Kalshi API error: ${response.status} - ${errorText}`); 60 | } 61 | 62 | return response.json(); 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/getInterpolatedValue.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataPoint } from "./types"; 2 | 3 | /** 4 | * This function will search for a value in a timeseries. 5 | * If it finds an exact match, it will return the value. 6 | * If it finds matches on both sides of the target date, it will interpolate between the two. 7 | * If it finds matches only on one side, it will return the closest match. 8 | */ 9 | export function getInterpolatedValue( 10 | data: ChartDataPoint[], 11 | targetDate: string, 12 | ) { 13 | // Convert target date to timestamp for comparison 14 | const targetTime = new Date(targetDate).getTime(); 15 | 16 | // Find exact match first 17 | const exactMatch = data.find((point) => point.date === targetDate); 18 | if (exactMatch) return exactMatch.value; 19 | 20 | // Sort data by date if not already sorted 21 | const sortedData = [...data].sort( 22 | (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), 23 | ); 24 | 25 | // Find the closest points before and after target date 26 | const before = sortedData.findLast( 27 | (point) => new Date(point.date).getTime() <= targetTime, 28 | ); 29 | const after = sortedData.find( 30 | (point) => new Date(point.date).getTime() > targetTime, 31 | ); 32 | 33 | // If we have both points, interpolate 34 | if (before && after) { 35 | const beforeTime = new Date(before.date).getTime(); 36 | const afterTime = new Date(after.date).getTime(); 37 | const timeDiff = afterTime - beforeTime; 38 | const valueDiff = after.value - before.value; 39 | const timeRatio = (targetTime - beforeTime) / timeDiff; 40 | return before.value + valueDiff * timeRatio; 41 | } 42 | 43 | if (before && !after) { 44 | console.log("No data found for", targetDate); 45 | return before.value; 46 | } 47 | 48 | if (after && !before) { 49 | console.log("No data found for", targetDate); 50 | return after.value; 51 | } 52 | 53 | console.log("No data found for", targetDate); 54 | return 0; 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/dates.ts: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | const createSafeDateFormatter = (formatString: string) => (date: string) => { 4 | try { 5 | return format(new Date(date), formatString); 6 | } catch { 7 | return date; 8 | } 9 | }; 10 | 11 | const createSafeMillisecondDateFormatter = 12 | (formatString: string) => (milliseconds: number) => { 13 | try { 14 | return format(new Date(milliseconds * 1000), formatString); 15 | } catch { 16 | return milliseconds.toString(); 17 | } 18 | }; 19 | 20 | export const formatYearFromTimestamp = 21 | createSafeMillisecondDateFormatter("yyyy"); 22 | export const formatFullDateFromTimestamp = 23 | createSafeMillisecondDateFormatter("yyyy-MM-dd"); 24 | export const formatMonthYear = createSafeDateFormatter("MMM yyyy"); 25 | export const formatMonthDayYear = createSafeDateFormatter("MMM d, yyyy"); 26 | export const formatMonthDay = createSafeDateFormatter("MMM d"); 27 | 28 | type Formatter = "MMM yyyy" | "MMM d, yyyy" | "MMM d"; 29 | type MsFormatter = "ms:yyyy" | "ms:yyyy-MM-dd"; 30 | 31 | export const formatters: Record< 32 | Formatter, 33 | ReturnType 34 | > = { 35 | "MMM yyyy": createSafeDateFormatter("MMM yyyy"), 36 | "MMM d, yyyy": createSafeDateFormatter("MMM d, yyyy"), 37 | "MMM d": createSafeDateFormatter("MMM d"), 38 | }; 39 | 40 | export const msFormatters: Record< 41 | MsFormatter, 42 | ReturnType 43 | > = { 44 | "ms:yyyy": createSafeMillisecondDateFormatter("yyyy"), 45 | "ms:yyyy-MM-dd": createSafeMillisecondDateFormatter("yyyy-MM-dd"), 46 | }; 47 | 48 | export type AnyFormatter = Formatter | MsFormatter; 49 | 50 | export function getFormatter(formatter: AnyFormatter) { 51 | if (!(formatter in formatters) && !(formatter in msFormatters)) { 52 | return (x: string) => x; 53 | } 54 | 55 | if (formatter.startsWith("ms:")) { 56 | return msFormatters[formatter as MsFormatter]; 57 | } 58 | return formatters[formatter as Formatter]; 59 | } 60 | -------------------------------------------------------------------------------- /src/app/api/kalshi/route.ts: -------------------------------------------------------------------------------- 1 | import { kalshiFetch } from "@/lib/kalshi/fetch"; 2 | 3 | export async function GET(request: Request) { 4 | const { searchParams } = new URL(request.url); 5 | const marketTicker = searchParams.get("marketTicker"); 6 | const seriesTicker = searchParams.get("seriesTicker"); 7 | const marketId = searchParams.get("marketId"); 8 | const period_interval = searchParams.get("period_interval"); 9 | 10 | if (!(marketTicker || seriesTicker) || !marketId || !period_interval) { 11 | return Response.json( 12 | { error: "Missing required parameters" }, 13 | { status: 400 }, 14 | ); 15 | } 16 | 17 | try { 18 | const marketData = await kalshiFetch(`/markets/${marketTicker}`); 19 | const start_ts = Math.floor( 20 | new Date(marketData.market.open_time).getTime() / 1000, 21 | ); 22 | const end_ts = Math.floor( 23 | new Date(marketData.market.close_time).getTime() / 1000, 24 | ); 25 | 26 | const candlesticks = await kalshiFetch( 27 | `/series/${seriesTicker ?? marketTicker}/markets/${marketId}/candlesticks`, 28 | { 29 | query: { 30 | start_ts, 31 | end_ts, 32 | period_interval, 33 | }, 34 | }, 35 | ); 36 | 37 | return Response.json( 38 | { 39 | marketData, 40 | candlesticks, 41 | dateRange: { 42 | start: marketData.market.open_time, 43 | end: marketData.market.close_time, 44 | interval: period_interval, 45 | }, 46 | }, 47 | { 48 | headers: { 49 | "Cache-Control": "public, max-age=3600, stale-while-revalidate=3600", 50 | }, 51 | }, 52 | ); 53 | } catch (error) { 54 | console.error("Kalshi API error:", error); 55 | return Response.json( 56 | { error: "Internal server error" }, 57 | { 58 | status: 500, 59 | headers: { 60 | "Cache-Control": "public, max-age=3600, stale-while-revalidate=3600", 61 | }, 62 | }, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /scripts/install-chromium.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import { chmodSync, writeFileSync } from "fs"; 3 | import { join } from "path"; 4 | import fs from "fs"; 5 | 6 | async function installChromium() { 7 | try { 8 | // Install Chromium using Playwright 9 | console.log("Installing Chromium..."); 10 | execSync("bunx --bun playwright install chromium", { 11 | stdio: "inherit", 12 | env: { 13 | ...process.env, 14 | PLAYWRIGHT_BROWSERS_PATH: ".local-browsers", 15 | }, 16 | }); 17 | 18 | // Get the installed version by running playwright CLI 19 | const versionOutput = execSync("bunx --bun playwright --version", { 20 | encoding: "utf8", 21 | }); 22 | console.log("Version output:", versionOutput); 23 | 24 | console.log("Finding Chromium executable..."); 25 | const chromePath = execSync("find .local-browsers -name Chromium -type f", { 26 | encoding: "utf8", 27 | }).trim(); 28 | 29 | if (!chromePath) { 30 | throw new Error("Could not find Chromium executable"); 31 | } 32 | 33 | // Make executable 34 | console.log("Setting permissions..."); 35 | chmodSync(chromePath, "755"); 36 | 37 | // Write path to .env.local 38 | console.log("Updating .env.local..."); 39 | const envPath = join(process.cwd(), ".env.local"); 40 | const chromiumEnvVar = `CHROMIUM_PATH=${chromePath}`; 41 | 42 | try { 43 | const currentEnv = fs.readFileSync(envPath, "utf8"); 44 | const updatedEnv = currentEnv.includes("CHROMIUM_PATH=") 45 | ? currentEnv.replace(/CHROMIUM_PATH=.*$/m, chromiumEnvVar) 46 | : `${currentEnv}\n${chromiumEnvVar}`; 47 | writeFileSync(envPath, updatedEnv); 48 | } catch { 49 | writeFileSync(envPath, chromiumEnvVar); 50 | } 51 | 52 | console.log("✅ Chromium installed successfully!"); 53 | console.log(`Path: ${chromePath}`); 54 | } catch (error) { 55 | console.error("❌ Installation failed:", error); 56 | process.exit(1); 57 | } 58 | } 59 | 60 | installChromium(); 61 | -------------------------------------------------------------------------------- /src/app/api/polymarket/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | const GAMMA_API = "https://gamma-api.polymarket.com"; 4 | // Timeseries 5 | 6 | export async function GET(request: Request) { 7 | try { 8 | const { searchParams } = new URL(request.url); 9 | const slug = searchParams.get("slug"); 10 | 11 | if (!slug) { 12 | console.error("NO SLUG PROVIDED TO POLYMARKET API"); 13 | return NextResponse.json({ error: "Slug is required" }, { status: 400 }); 14 | } 15 | 16 | console.error(`FETCHING POLYMARKET DATA FOR SLUG: ${slug}`); 17 | 18 | // First fetch the event 19 | const eventResponse = await fetch(`${GAMMA_API}/events?slug=${slug}`, { 20 | headers: { 21 | Accept: "application/json", 22 | }, 23 | }); 24 | const events = await eventResponse.json(); 25 | 26 | if (!events?.[0]) { 27 | return NextResponse.json( 28 | { error: "No events found for this slug" }, 29 | { status: 404 }, 30 | ); 31 | } 32 | 33 | const event = events[0]; 34 | if (!event.markets?.[0]) { 35 | return NextResponse.json( 36 | { error: "No markets found for this event" }, 37 | { status: 404 }, 38 | ); 39 | } 40 | 41 | // Then fetch the specific market data 42 | const marketId = event.markets[0].id; 43 | const marketResponse = await fetch(`${GAMMA_API}/markets/${marketId}`, { 44 | headers: { 45 | Accept: "application/json", 46 | }, 47 | }); 48 | const marketData = await marketResponse.json(); 49 | 50 | return NextResponse.json(marketData, { 51 | headers: { 52 | "Cache-Control": "public, max-age=3600, stale-while-revalidate=3600", 53 | }, 54 | }); 55 | } catch (error) { 56 | console.error("Error fetching from Polymarket:", error); 57 | return NextResponse.json( 58 | { error: "Failed to fetch market data" }, 59 | { 60 | status: 500, 61 | headers: { 62 | "Cache-Control": "public, max-age=3600, stale-while-revalidate=3600", 63 | }, 64 | }, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: 0 0% 100%; 7 | --foreground: 222.2 84% 4.9%; 8 | --card: 0 0% 100%; 9 | --card-foreground: 222.2 84% 4.9%; 10 | --popover: 0 0% 100%; 11 | --popover-foreground: 222.2 84% 4.9%; 12 | --primary: 221.2 83.2% 53.3%; 13 | --primary-foreground: 210 40% 98%; 14 | --secondary: 210 40% 96.1%; 15 | --secondary-foreground: 222.2 47.4% 11.2%; 16 | --muted: 210 40% 96.1%; 17 | --muted-foreground: 215.4 16.3% 46.9%; 18 | --accent: 210 40% 96.1%; 19 | --accent-foreground: 222.2 47.4% 11.2%; 20 | --destructive: 0 84.2% 60.2%; 21 | --destructive-foreground: 210 40% 98%; 22 | --border: 214.3 31.8% 91.4%; 23 | --input: 214.3 31.8% 91.4%; 24 | --ring: 221.2 83.2% 53.3%; 25 | --radius: 0.5rem; 26 | } 27 | 28 | @media (prefers-color-scheme: dark) { 29 | :root { 30 | --background: 222.2 84% 4.9%; 31 | --foreground: 210 40% 98%; 32 | --card: 222.2 84% 4.9%; 33 | --card-foreground: 210 40% 98%; 34 | --popover: 222.2 84% 4.9%; 35 | --popover-foreground: 210 40% 98%; 36 | --primary: 217.2 91.2% 59.8%; 37 | --primary-foreground: 222.2 47.4% 11.2%; 38 | --secondary: 217.2 32.6% 17.5%; 39 | --secondary-foreground: 210 40% 98%; 40 | --muted: 217.2 32.6% 17.5%; 41 | --muted-foreground: 215 20.2% 65.1%; 42 | --accent: 217.2 32.6% 17.5%; 43 | --accent-foreground: 210 40% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 210 40% 98%; 46 | --border: 217.2 32.6% 17.5%; 47 | --input: 217.2 32.6% 17.5%; 48 | --ring: 224.3 76.3% 48%; 49 | } 50 | 51 | .recharts-rectangle.recharts-tooltip-cursor { 52 | fill: rgba(0, 0, 0, 0.3) !important; 53 | } 54 | } 55 | 56 | body { 57 | @apply bg-[hsl(var(--background))] text-[hsl(var(--foreground))]; 58 | } 59 | .index-bar::before { 60 | content: ""; 61 | position: absolute; 62 | top: 100%; 63 | left: 0; 64 | width: 1px; 65 | height: 500px; 66 | border-left: 1px dashed var(--color); 67 | } 68 | 69 | .index-bar-container { 70 | clip-path: inset(0 0 -250px 0); 71 | } 72 | -------------------------------------------------------------------------------- /src/app/api/manifold/route.ts: -------------------------------------------------------------------------------- 1 | const MANIFOLD_API = "https://api.manifold.markets/v0"; 2 | 3 | interface ManifoldBet { 4 | createdTime: number; 5 | probBefore: number; 6 | probAfter: number; 7 | } 8 | 9 | export async function GET(request: Request) { 10 | try { 11 | // Get slug from URL params 12 | const { searchParams } = new URL(request.url); 13 | const slug = searchParams.get("slug"); 14 | 15 | if (!slug) { 16 | return Response.json( 17 | { error: "Market slug is required" }, 18 | { status: 400 }, 19 | ); 20 | } 21 | 22 | const marketResponse = await fetch(`${MANIFOLD_API}/slug/${slug}`, { 23 | headers: { 24 | Accept: "application/json", 25 | }, 26 | }); 27 | 28 | if (!marketResponse.ok) { 29 | const errorText = await marketResponse.text(); 30 | console.error("Error response body:", errorText); 31 | 32 | return Response.json( 33 | { 34 | error: "Failed to fetch Manifold market data", 35 | status: marketResponse.status, 36 | details: errorText, 37 | }, 38 | { status: marketResponse.status }, 39 | ); 40 | } 41 | 42 | const marketData = await marketResponse.json(); 43 | 44 | // Fetch bets data 45 | const now = Date.now(); 46 | const betsResponse = await fetch( 47 | `${MANIFOLD_API}/bets?contractId=${marketData.id}&points=true&limit=1000&filterRedemptions=true&beforeTime=${now}&afterTime=${marketData.createdTime}`, 48 | { 49 | headers: { 50 | Accept: "application/json", 51 | }, 52 | }, 53 | ); 54 | 55 | if (!betsResponse.ok) { 56 | const errorText = await betsResponse.text(); 57 | console.error("Error fetching bets:", errorText); 58 | return Response.json( 59 | { error: "Failed to fetch bets data", details: errorText }, 60 | { status: betsResponse.status }, 61 | ); 62 | } 63 | 64 | const betsData = (await betsResponse.json()) as ManifoldBet[]; 65 | const sortedBets = betsData.sort((a, b) => a.createdTime - b.createdTime); 66 | return Response.json({ market: marketData, bets: sortedBets }); 67 | } catch (error) { 68 | console.error("Error fetching Manifold market data:", error); 69 | return Response.json({ error: "Internal server error" }, { status: 500 }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import { CSPostHogProvider } from "./providers"; 4 | import "./globals.css"; 5 | import { TooltipProvider } from "@/components/ui/tooltip"; 6 | 7 | const geistSans = Geist({ 8 | variable: "--font-geist-sans", 9 | subsets: ["latin"], 10 | }); 11 | 12 | const geistMono = Geist_Mono({ 13 | variable: "--font-geist-mono", 14 | subsets: ["latin"], 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "When Will We Get AGI? | AGI Timelines Dashboard", 19 | description: 20 | "Crowd-sourced forecasts for when Artificial General Intelligence (AGI) will arrive. Aggregates Metaculus, Manifold, Kalshi, and more. See the best consensus on AGI timelines.", 21 | openGraph: { 22 | title: "When Will We Get AGI? | AGI Timelines Dashboard", 23 | description: 24 | "Crowd-sourced forecasts for when Artificial General Intelligence (AGI) will arrive. Aggregates Metaculus, Manifold, Kalshi, and more. See the best consensus on AGI timelines.", 25 | url: "https://agi.goodheartlabs.com/", 26 | siteName: "AGI Timelines Dashboard", 27 | images: [ 28 | { 29 | url: "/opengraph-image.png", 30 | width: 1200, 31 | height: 630, 32 | alt: "When will we achieve AGI? agi.goodheartlabs.com dashboard preview", 33 | }, 34 | ], 35 | type: "website", 36 | }, 37 | twitter: { 38 | card: "summary_large_image", 39 | title: "When Will We Get AGI? | AGI Timelines Dashboard", 40 | description: 41 | "Crowd-sourced forecasts for when Artificial General Intelligence (AGI) will arrive. Aggregates Metaculus, Manifold, Kalshi, and more. See the best consensus on AGI timelines.", 42 | images: [ 43 | { 44 | url: "/twitter-image.png", 45 | alt: "When will we achieve AGI? agi.goodheartlabs.com dashboard preview", 46 | }, 47 | ], 48 | }, 49 | }; 50 | 51 | export default function RootLayout({ 52 | children, 53 | }: { 54 | children: React.ReactNode; 55 | }) { 56 | return ( 57 | 58 | 61 | 62 | {children} 63 | 64 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agi-timelines-dashboard", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier --write .", 11 | "format:check": "prettier --check .", 12 | "check": "tsc --noEmit", 13 | "lint:fix": "eslint --fix src", 14 | "prepare": "husky install", 15 | "install:chromium": "bun scripts/install-chromium.ts" 16 | }, 17 | "dependencies": { 18 | "@radix-ui/react-collapsible": "^1.1.2", 19 | "@radix-ui/react-dialog": "^1.1.4", 20 | "@radix-ui/react-icons": "^1.3.2", 21 | "@radix-ui/react-tooltip": "^1.1.6", 22 | "@sparticuz/chromium": "^131.0.1", 23 | "@types/bun": "^1.1.14", 24 | "@types/d3-scale": "^4.0.8", 25 | "api": "^6.1.2", 26 | "class-variance-authority": "^0.7.1", 27 | "clsx": "^2.1.1", 28 | "d3-scale": "^4.0.2", 29 | "date-fns": "^4.1.0", 30 | "extract-zip": "^2.0.1", 31 | "json-schema-to-ts": "^2.8.0-beta.0", 32 | "lucide-react": "^0.469.0", 33 | "next": "15.1.3", 34 | "oas": "^20.10.3", 35 | "papaparse": "^5.4.1", 36 | "playwright-aws-lambda": "^0.11.0", 37 | "playwright-core": "^1.49.1", 38 | "posthog-js": "^1.203.3", 39 | "puppeteer-core": "^23.11.1", 40 | "react": "^18.2.0", 41 | "react-dom": "^18.2.0", 42 | "recharts": "^2.15.0", 43 | "tailwind-merge": "^2.6.0", 44 | "tailwindcss-animate": "^1.0.7", 45 | "tippy.js": "^6.3.7", 46 | "tmp": "^0.2.3", 47 | "unzipper": "^0.12.3", 48 | "vaul": "^1.1.2", 49 | "zod": "^3.24.1" 50 | }, 51 | "lint-staged": { 52 | "*.{js,jsx,ts,tsx}": [ 53 | "npm run check", 54 | "npm run lint:fix", 55 | "npm run format" 56 | ] 57 | }, 58 | "devDependencies": { 59 | "@eslint/eslintrc": "^3", 60 | "@types/node": "^22.10.1", 61 | "@types/papaparse": "^5.3.15", 62 | "@types/react": "^19.0.2", 63 | "@types/react-dom": "^19.0.2", 64 | "@types/tmp": "^0.2.6", 65 | "@types/unzipper": "^0.10.11", 66 | "autoprefixer": "^10.4.17", 67 | "eslint": "^9", 68 | "eslint-config-next": "15.1.3", 69 | "eslint-plugin-unused-imports": "^4.1.4", 70 | "husky": "^8.0.0", 71 | "postcss": "^8.4.35", 72 | "prettier": "^3.4.2", 73 | "prettier-plugin-tailwindcss": "^0.6.9", 74 | "tailwindcss": "^3.4.1", 75 | "typescript": "^5.2.2" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/api/cdc-data/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { parse } from "papaparse"; 3 | import chromium from "@sparticuz/chromium"; 4 | import puppeteer from "puppeteer-core"; 5 | 6 | export const runtime = "nodejs"; 7 | export const dynamic = "force-dynamic"; 8 | 9 | interface CdcDataRow { 10 | Range: string; 11 | [key: string]: string; 12 | } 13 | 14 | export async function GET() { 15 | let browser; 16 | 17 | if (process.env.NODE_ENV === "development" && !process.env.CHROMIUM_PATH) { 18 | throw new Error( 19 | "CHROMIUM_PATH is not set in development mode. Please run `bun install:chromium` to install Chromium.", 20 | ); 21 | } 22 | 23 | try { 24 | browser = await puppeteer.launch({ 25 | args: chromium.args, 26 | defaultViewport: chromium.defaultViewport, 27 | executablePath: 28 | process.env.NODE_ENV === "development" 29 | ? process.env.CHROMIUM_PATH 30 | : await chromium.executablePath(), 31 | headless: chromium.headless, 32 | }); 33 | 34 | const page = await browser.newPage(); 35 | 36 | await page.goto( 37 | "https://www.cdc.gov/bird-flu/php/avian-flu-summary/chart-epi-curve-ah5n1.html", 38 | { waitUntil: "domcontentloaded" }, 39 | ); 40 | 41 | await page.waitForSelector('.download-links a[download="data-table.csv"]'); 42 | 43 | const csvData = await page.evaluate(async () => { 44 | const link = document.querySelector( 45 | '.download-links a[download="data-table.csv"]', 46 | ); 47 | const blobUrl = link?.getAttribute("href"); 48 | if (!blobUrl) throw new Error("Could not find download link"); 49 | 50 | const response = await fetch(blobUrl); 51 | const blob = await response.blob(); 52 | return await blob.text(); 53 | }); 54 | 55 | const { data } = parse(csvData, { 56 | header: true, 57 | skipEmptyLines: true, 58 | }); 59 | 60 | const filteredData = data.filter((row) => row.Range === "2020-2024"); 61 | 62 | return NextResponse.json(filteredData, { 63 | headers: { 64 | "Cache-Control": "public, max-age=3600, stale-while-revalidate=3600", 65 | }, 66 | }); 67 | } catch (error) { 68 | console.error("Error fetching CDC data:", error); 69 | return NextResponse.json( 70 | { error: "Failed to fetch CDC data" }, 71 | { status: 500 }, 72 | ); 73 | } finally { 74 | if (browser) { 75 | await browser.close(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"], 5 | theme: { 6 | extend: { 7 | colors: { 8 | background: "hsl(var(--background))", 9 | foreground: "hsl(var(--foreground))", 10 | card: { 11 | DEFAULT: "hsl(var(--card))", 12 | foreground: "hsl(var(--card-foreground))", 13 | }, 14 | popover: { 15 | DEFAULT: "hsl(var(--popover))", 16 | foreground: "hsl(var(--popover-foreground))", 17 | }, 18 | primary: { 19 | DEFAULT: "hsl(var(--primary))", 20 | foreground: "hsl(var(--primary-foreground))", 21 | }, 22 | secondary: { 23 | DEFAULT: "hsl(var(--secondary))", 24 | foreground: "hsl(var(--secondary-foreground))", 25 | }, 26 | muted: { 27 | DEFAULT: "hsl(var(--muted))", 28 | foreground: "hsl(var(--muted-foreground))", 29 | }, 30 | accent: { 31 | DEFAULT: "hsl(var(--accent))", 32 | foreground: "hsl(var(--accent-foreground))", 33 | }, 34 | destructive: { 35 | DEFAULT: "hsl(var(--destructive))", 36 | foreground: "hsl(var(--destructive-foreground))", 37 | }, 38 | border: "hsl(var(--border))", 39 | input: "hsl(var(--input))", 40 | ring: "hsl(var(--ring))", 41 | chart: { 42 | "1": "hsl(var(--chart-1))", 43 | "2": "hsl(var(--chart-2))", 44 | "3": "hsl(var(--chart-3))", 45 | "4": "hsl(var(--chart-4))", 46 | "5": "hsl(var(--chart-5))", 47 | }, 48 | }, 49 | keyframes: { 50 | slideDown: { 51 | from: { 52 | height: "0", 53 | }, 54 | to: { 55 | height: "var(--radix-collapsible-content-height)", 56 | }, 57 | }, 58 | slideUp: { 59 | from: { 60 | height: "var(--radix-collapsible-content-height)", 61 | }, 62 | to: { 63 | height: "0", 64 | }, 65 | }, 66 | }, 67 | animation: { 68 | slideDown: "slideDown 300ms ease-out", 69 | slideUp: "slideUp 300ms ease-out", 70 | }, 71 | borderRadius: { 72 | lg: "var(--radius)", 73 | md: "calc(var(--radius) - 2px)", 74 | sm: "calc(var(--radius) - 4px)", 75 | }, 76 | }, 77 | }, 78 | plugins: [require("tailwindcss-animate")], 79 | } satisfies Config; 80 | -------------------------------------------------------------------------------- /src/lib/services/kalshi.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataPoint, KalshiResponse } from "../types"; 2 | 3 | export async function fetchKalshiData({ 4 | marketTicker, 5 | seriesTicker, 6 | marketId, 7 | period_interval, 8 | }: { 9 | marketId: string; 10 | marketTicker: string; 11 | seriesTicker?: string; 12 | period_interval: number; 13 | }): Promise { 14 | const data = await fetchFromAPI({ 15 | marketId, 16 | marketTicker, 17 | seriesTicker, 18 | period_interval, 19 | }); 20 | 21 | return transformKalshiData(data, period_interval); 22 | } 23 | 24 | async function fetchFromAPI({ 25 | marketTicker, 26 | seriesTicker, 27 | marketId, 28 | period_interval, 29 | }: { 30 | marketId: string; 31 | marketTicker: string; 32 | seriesTicker?: string; 33 | period_interval: number; 34 | }): Promise { 35 | const params = new URLSearchParams({ 36 | marketTicker, 37 | marketId, 38 | period_interval: period_interval.toString(), 39 | }); 40 | 41 | if (seriesTicker) { 42 | params.append("seriesTicker", seriesTicker); 43 | } 44 | 45 | const response = await fetch(`/api/kalshi?${params}`); 46 | if (!response.ok) throw new Error("API failed"); 47 | return response.json(); 48 | } 49 | 50 | function transformKalshiData( 51 | data: KalshiResponse, 52 | intervalHours: number, 53 | ): ChartDataPoint[] { 54 | if (!data?.candlesticks?.candlesticks) return []; 55 | 56 | const candlesticks = data.candlesticks.candlesticks; 57 | if (candlesticks.length === 0) return []; 58 | 59 | const firstDate = candlesticks[0].end_period_ts; 60 | const lastDate = candlesticks[candlesticks.length - 1].end_period_ts; 61 | 62 | const dataPoints: ChartDataPoint[] = []; 63 | let currentDate = firstDate; 64 | let lastValidMean: number | null = null; 65 | let lastCandlestickIndex = 0; 66 | 67 | while (currentDate <= lastDate) { 68 | let candlestick = null; 69 | for (let i = lastCandlestickIndex; i < candlesticks.length; i++) { 70 | const stick = candlesticks[i]; 71 | if (stick.end_period_ts >= currentDate) { 72 | candlestick = stick; 73 | lastCandlestickIndex = i; 74 | break; 75 | } 76 | } 77 | 78 | if (!candlestick) { 79 | currentDate += intervalHours * 60; 80 | continue; 81 | } 82 | 83 | // Update lastValidMean if we have a new mean price 84 | if (candlestick.price.mean !== null) { 85 | lastValidMean = candlestick.price.mean; 86 | } 87 | 88 | // If we don't have any valid mean yet, use mid price 89 | const value = 90 | lastValidMean ?? 91 | Math.round((candlestick.yes_bid.close + candlestick.yes_ask.close) / 2); 92 | 93 | dataPoints.push({ 94 | date: new Date(currentDate * 1000).toISOString(), 95 | value, 96 | }); 97 | 98 | currentDate += intervalHours * 60; 99 | } 100 | 101 | return dataPoints; 102 | } 103 | -------------------------------------------------------------------------------- /src/components/CustomTooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AnyFormatter, getFormatter } from "@/lib/dates"; 4 | import { useMemo } from "react"; 5 | 6 | /** 7 | * A custom tooltip component for charts 8 | */ 9 | export function CustomTooltip({ 10 | active, 11 | payload, 12 | label, 13 | formatter, 14 | labelFormatter: labelFormatterProp, 15 | }: { 16 | /** 17 | * Whether the tooltip is currently being displayed 18 | */ 19 | active?: boolean; 20 | /** 21 | * Array of data points at the current tooltip position. Each contains a value and dataKey 22 | */ 23 | payload?: Array<{ value: number | [number, number]; dataKey: string }>; 24 | /** 25 | * The x-axis label (typically a date) at the tooltip position 26 | */ 27 | label?: string; 28 | /** 29 | * Function to format numeric values into [formattedValue, unit] tuples 30 | */ 31 | formatter?: AnyFormatter; 32 | 33 | /** 34 | * Function to format the x-axis label (e.g. format dates) 35 | */ 36 | labelFormatter?: AnyFormatter; 37 | }) { 38 | const valueFormatter = useMemo( 39 | () => (formatter ? getFormatter(formatter) : (x: number) => x.toString()), 40 | [formatter], 41 | ); 42 | 43 | const labelFormatter = useMemo( 44 | () => 45 | labelFormatterProp ? getFormatter(labelFormatterProp) : (x: string) => x, 46 | [labelFormatterProp], 47 | ); 48 | 49 | if (!active || !payload || !payload[0]) return null; 50 | 51 | // Handle both single values and ranges 52 | const mainValue = payload.find((p) => p.dataKey === "value")?.value as number; 53 | const range = payload.find((p) => p.dataKey === "range")?.value as [ 54 | number, 55 | number, 56 | ]; 57 | 58 | const content = valueFormatter(mainValue as never); 59 | const lines = content.split("
"); 60 | 61 | return ( 62 |
63 |

64 | {labelFormatter((label as never) || "")} 65 |

66 | {lines.map((line, i) => { 67 | // Extract the number between tags if it exists 68 | const match = line.match(/(.*?)<\/b>/); 69 | if (match) { 70 | const [fullMatch, number] = match; 71 | const [before, after] = line.split(fullMatch); 72 | return ( 73 |

77 | {before} 78 | {number} 79 | {after} 80 |

81 | ); 82 | } 83 | return ( 84 |

88 | {line} 89 |

90 | ); 91 | })} 92 | {range && ( 93 |

94 | Range: {valueFormatter(range[0] as never)} -{" "} 95 | {valueFormatter(range[1] as never)} 96 |

97 | )} 98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /scripts/merge-kalshi-with-index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 2024-2199 3 | * Array of 176 elements 4 | */ 5 | 6 | import { getNearestKalshiIndex } from "@/lib/kalshi/index-helpers"; 7 | import indexData from "./_averages.json"; 8 | import kalshiData from "./kalshiData.json"; 9 | 10 | const startYear = 2024; 11 | 12 | const years = indexData as { date: number; years: number[] }[]; 13 | const kalshi = kalshiData as { date: string; value: number }[]; 14 | 15 | // What's the range of kalshi dates? 16 | const kalshiDates = kalshi.map((k) => new Date(k.date).getTime()); 17 | const minKalshiDate = new Date(Math.min(...kalshiDates)).toDateString(); 18 | const maxKalshiDate = new Date(Math.max(...kalshiDates)).toDateString(); 19 | console.log(`Kalshi Date Range: \n${minKalshiDate} to \n${maxKalshiDate}\n\n`); 20 | 21 | // Take a random day from the years 22 | const randomDay = years[years.length - Math.floor(Math.random() * 200)]; 23 | 24 | if (!randomDay.date) { 25 | console.log("No date found for random day"); 26 | process.exit(1); 27 | } 28 | 29 | console.log("Random Day:", new Date(randomDay.date).toDateString()); 30 | console.log("Year Probabilities --------------------"); 31 | 32 | for (let i = 4; i < 8; i++) { 33 | const year = startYear + i; 34 | const probability = randomDay.years[i]; 35 | console.log(`${year}: ${probability}`); 36 | } 37 | console.log(`${startYear + 6}...2199`); 38 | 39 | // Kalshi represents percentage change (0-100) that AI 40 | // passes difficult turing test before 2030, 41 | // Creating sets 2024-2029 and 2030-2199 42 | const kalshiIndex = getNearestKalshiIndex(randomDay.date, kalshi); 43 | if (kalshiIndex === -1) { 44 | console.log("No kalshi data found for this date"); 45 | process.exit(1); 46 | } 47 | 48 | const kalshiValue = kalshi[kalshiIndex]; 49 | console.log(`\nAI passes turing before 2030: ${kalshiValue.value}%`); 50 | 51 | const probabilityBefore2030 = randomDay.years 52 | .slice(0, 6) 53 | .reduce((acc, curr) => acc + curr, 0); 54 | 55 | const probabilityAfter2030 = randomDay.years 56 | .slice(6) 57 | .reduce((acc, curr) => acc + curr, 0); 58 | 59 | console.log(`\nOriginal sums:`); 60 | console.log(`Before 2030: ${probabilityBefore2030}`); 61 | console.log(`After 2030: ${probabilityAfter2030}`); 62 | 63 | const kalshiProbability = kalshiValue.value / 100; 64 | const scalingFactorBefore = kalshiProbability / probabilityBefore2030; 65 | const scalingFactorAfter = (1 - kalshiProbability) / probabilityAfter2030; 66 | 67 | console.log(`\nAdjusted to match Kalshi probability of ${kalshiProbability}:`); 68 | console.log("Individual year probabilities:"); 69 | 70 | const adjustedYears = randomDay.years.map((prob, i) => { 71 | const scalar = i < 6 ? scalingFactorBefore : scalingFactorAfter; 72 | return prob * scalar; 73 | }); 74 | 75 | for (let i = 0; i < adjustedYears.length; i++) { 76 | const year = startYear + i; 77 | if (i > 3 && i < 8) console.log(`${year}: ${adjustedYears[i]}`); 78 | } 79 | console.log(`${startYear + 6}...2199`); 80 | 81 | // Verify sums 82 | const adjustedBefore2030 = adjustedYears.slice(0, 6).reduce((a, b) => a + b, 0); 83 | const adjustedAfter2030 = adjustedYears.slice(6).reduce((a, b) => a + b, 0); 84 | console.log(`\nVerification:`); 85 | console.log(`Sum before 2030: ${adjustedBefore2030}`); 86 | console.log(`Sum after 2030: ${adjustedAfter2030}`); 87 | console.log(`Total: ${adjustedBefore2030 + adjustedAfter2030}`); 88 | -------------------------------------------------------------------------------- /src/components/BarGraph.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Bar, 3 | BarChart, 4 | CartesianGrid, 5 | ResponsiveContainer, 6 | Tooltip, 7 | XAxis, 8 | YAxis, 9 | } from "recharts"; 10 | import { ChartDataPoint } from "../lib/types"; 11 | import { 12 | Formatter, 13 | Payload, 14 | } from "recharts/types/component/DefaultTooltipContent"; 15 | 16 | interface BarGraphProps { 17 | data: ChartDataPoint[]; 18 | color: string; 19 | label: string; 20 | formatValue?: (value: number) => string; 21 | tickFormatter: (date: string) => string; 22 | tooltipFormatter?: Formatter; 23 | tooltipLabelFormatter: ( 24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 | label: any, 26 | payload: Payload[], 27 | ) => React.ReactNode; 28 | } 29 | 30 | export function BarGraph({ 31 | data, 32 | color, 33 | label, 34 | formatValue = (v: number) => v.toString(), 35 | tickFormatter, 36 | tooltipFormatter = (value: number) => [formatValue(value), label], 37 | tooltipLabelFormatter, 38 | }: BarGraphProps) { 39 | return ( 40 |
41 |
42 | {label} 43 |
44 | 45 | 54 | 59 | 74 | 85 | 95 | 107 | 108 | 109 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /src/lib/services/kalshi.server.ts: -------------------------------------------------------------------------------- 1 | import { kalshiFetch } from "../kalshi/fetch"; 2 | import { ChartDataPoint, KalshiResponse } from "../types"; 3 | 4 | export async function fetchKalshiData({ 5 | marketTicker, 6 | seriesTicker, 7 | marketId, 8 | period_interval, 9 | }: { 10 | marketId: string; 11 | marketTicker: string; 12 | seriesTicker?: string; 13 | period_interval: number; 14 | }): Promise { 15 | const data = await fetchFromAPI({ 16 | marketId, 17 | marketTicker, 18 | seriesTicker, 19 | period_interval, 20 | }); 21 | 22 | return transformKalshiData(data, period_interval); 23 | } 24 | 25 | async function fetchFromAPI({ 26 | marketTicker, 27 | seriesTicker, 28 | marketId, 29 | period_interval, 30 | }: { 31 | marketId: string; 32 | marketTicker: string; 33 | seriesTicker?: string; 34 | period_interval: number; 35 | }): Promise { 36 | const marketData = await kalshiFetch(`/markets/${marketTicker}`); 37 | const start_ts = Math.floor( 38 | new Date(marketData.market.open_time).getTime() / 1000, 39 | ); 40 | const end_ts = Math.floor( 41 | new Date(marketData.market.close_time).getTime() / 1000, 42 | ); 43 | 44 | const candlesticks = await kalshiFetch( 45 | `/series/${seriesTicker ?? marketTicker}/markets/${marketId}/candlesticks`, 46 | { 47 | query: { 48 | start_ts, 49 | end_ts, 50 | period_interval, 51 | }, 52 | }, 53 | ); 54 | 55 | return { 56 | marketData, 57 | candlesticks, 58 | dateRange: { 59 | start: marketData.market.open_time, 60 | end: marketData.market.close_time, 61 | interval: period_interval, 62 | }, 63 | }; 64 | } 65 | 66 | function transformKalshiData( 67 | data: KalshiResponse, 68 | intervalHours: number, 69 | ): ChartDataPoint[] { 70 | if (!data?.candlesticks?.candlesticks) return []; 71 | 72 | const candlesticks = data.candlesticks.candlesticks; 73 | if (candlesticks.length === 0) return []; 74 | 75 | const firstDate = candlesticks[0].end_period_ts; 76 | const lastDate = candlesticks[candlesticks.length - 1].end_period_ts; 77 | 78 | const dataPoints: ChartDataPoint[] = []; 79 | let currentDate = firstDate; 80 | let lastValidMean: number | null = null; 81 | let lastCandlestickIndex = 0; 82 | 83 | while (currentDate <= lastDate) { 84 | let candlestick = null; 85 | for (let i = lastCandlestickIndex; i < candlesticks.length; i++) { 86 | const stick = candlesticks[i]; 87 | if (stick.end_period_ts >= currentDate) { 88 | candlestick = stick; 89 | lastCandlestickIndex = i; 90 | break; 91 | } 92 | } 93 | 94 | if (!candlestick) { 95 | currentDate += intervalHours * 60; 96 | continue; 97 | } 98 | 99 | // Update lastValidMean if we have a new mean price 100 | if (candlestick.price.mean !== null) { 101 | lastValidMean = candlestick.price.mean; 102 | } 103 | 104 | // If we don't have any valid mean yet, use mid price 105 | const value = 106 | lastValidMean ?? 107 | Math.round((candlestick.yes_bid.close + candlestick.yes_ask.close) / 2); 108 | 109 | dataPoints.push({ 110 | date: new Date(currentDate * 1000).toISOString(), 111 | value, 112 | }); 113 | 114 | currentDate += intervalHours * 60; 115 | } 116 | 117 | return dataPoints; 118 | } 119 | -------------------------------------------------------------------------------- /src/app/api/metaculus-download/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import tmp from "tmp"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | import extract from "extract-zip"; 6 | import Papa from "papaparse"; 7 | import { MetaculusResponse } from "@/lib/types"; 8 | 9 | const METACULUS_API = "https://www.metaculus.com/api"; 10 | 11 | export async function GET(request: Request) { 12 | const { searchParams } = new URL(request.url); 13 | const questionId = searchParams.get("questionId"); 14 | 15 | if (!questionId) { 16 | return NextResponse.json( 17 | { error: "questionId is required" }, 18 | { status: 400 }, 19 | ); 20 | } 21 | 22 | if (!process.env.METACULUS_API_KEY) { 23 | return NextResponse.json( 24 | { error: "METACULUS_API_KEY not set" }, 25 | { status: 500 }, 26 | ); 27 | } 28 | 29 | try { 30 | // Create temp directory 31 | const tmpDir = tmp.dirSync(); 32 | const zipPath = path.join(tmpDir.name, `metaculus-${questionId}.zip`); 33 | 34 | // Download data 35 | const params = new URLSearchParams({ 36 | aggregation_methods: "recency_weighted", 37 | minimize: "true", 38 | include_comments: "false", 39 | }); 40 | 41 | const url = `${METACULUS_API}/posts/${questionId}/download-data/?${params}`; 42 | const response = await fetch(url, { 43 | headers: { 44 | Authorization: `Token ${process.env.METACULUS_API_KEY}`, 45 | }, 46 | }); 47 | 48 | if (!response.ok) { 49 | throw new Error(`Failed to fetch data: ${response.status}`); 50 | } 51 | 52 | // Save zip file 53 | const buffer = Buffer.from(await response.arrayBuffer()); 54 | fs.writeFileSync(zipPath, buffer); 55 | 56 | // Extract zip 57 | await extract(zipPath, { dir: tmpDir.name }); 58 | 59 | // Read and parse the files 60 | const forecastData = Papa.parse( 61 | fs.readFileSync(path.join(tmpDir.name, "forecast_data.csv"), "utf8"), 62 | { header: true }, 63 | ).data as { "Question ID": string }[]; 64 | 65 | // Fetch question data from metaculus 66 | const questionData = await fetch(`${METACULUS_API}/posts/${questionId}/`, { 67 | headers: { Authorization: `Token ${process.env.METACULUS_API_KEY}` }, 68 | }); 69 | 70 | const questionDataJson = (await questionData.json()) as { 71 | question: MetaculusResponse; 72 | }; 73 | 74 | // Cleanup 75 | fs.rmSync(tmpDir.name, { recursive: true, force: true }); 76 | 77 | return NextResponse.json( 78 | { 79 | forecast: forecastData 80 | // Filter out empty rows 81 | .filter((row: { "Question ID": string }) => row["Question ID"]), 82 | question: questionDataJson.question, 83 | }, 84 | { 85 | headers: { 86 | "Cache-Control": 87 | "public, s-maxage=86400, stale-while-revalidate=86400", 88 | }, 89 | }, 90 | ); 91 | } catch (error) { 92 | console.error("Error details:", { 93 | message: error instanceof Error ? error.message : String(error), 94 | stack: error instanceof Error ? error.stack : undefined, 95 | }); 96 | return NextResponse.json( 97 | { 98 | error: 99 | error instanceof Error 100 | ? error.message 101 | : "Failed to process Metaculus data", 102 | }, 103 | { status: 500 }, 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /public/example-data/polymarket.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "516481", 3 | "question": "Another state declare a state of emergency over bird flu before February?", 4 | "conditionId": "0x8cbf31ae2dff3bf262ba8b7e847cfc49d6f9e88ddb3330a051a5787177749874", 5 | "slug": "another-state-declare-a-state-of-emergency-over-bird-flu-before-february", 6 | "resolutionSource": "", 7 | "endDate": "2025-01-31T12:00:00Z", 8 | "liquidity": "4258.8142", 9 | "startDate": "2024-12-27T18:32:24.867756Z", 10 | "image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/another-state-declare-a-state-of-emergency-over-bird-flu-in-2024-PVF_UPWU-9X8.jpg", 11 | "icon": "https://polymarket-upload.s3.us-east-2.amazonaws.com/another-state-declare-a-state-of-emergency-over-bird-flu-in-2024-PVF_UPWU-9X8.jpg", 12 | "description": "On December 18, 2024, Gavin Newsom declared a state of emergency in California over the H5N1 bird flu outbreak. You can read more about that here: https://x.com/osint613/status/1869497993977823355\n\nThis market will resolve to \"Yes\" if a state of emergency directly related to an outbreak of H5N1 is declared in any state other than California by January 31, 2025, 11:59 PM ET. Otherwise, this market will resolve to \"No\".\n\nThe primary resolution source will be announcements from the relevant state governments, however a consensus of credible reporting may also be used. ", 13 | "outcomes": "[\"Yes\", \"No\"]", 14 | "outcomePrices": "[\"0.39\", \"0.61\"]", 15 | "volume": "17956.388032", 16 | "active": true, 17 | "closed": false, 18 | "marketMakerAddress": "", 19 | "createdAt": "2024-12-26T21:09:08.389108Z", 20 | "updatedAt": "2024-12-31T19:35:33.047951Z", 21 | "new": false, 22 | "featured": false, 23 | "submitted_by": "0x91430CaD2d3975766499717fA0D66A78D814E5c5", 24 | "archived": false, 25 | "resolvedBy": "0x6A9D222616C90FcA5754cd1333cFD9b7fb6a4F74", 26 | "restricted": true, 27 | "groupItemTitle": "", 28 | "groupItemThreshold": "0", 29 | "questionID": "0x1daad2f54f34c6aeaf9193bddf2f6e786605d078e21961d352ca4f774a1d53a0", 30 | "enableOrderBook": true, 31 | "orderPriceMinTickSize": 0.01, 32 | "orderMinSize": 5, 33 | "volumeNum": 17956.388032, 34 | "liquidityNum": 4258.8142, 35 | "endDateIso": "2025-01-31", 36 | "startDateIso": "2024-12-27", 37 | "hasReviewedDates": true, 38 | "volume24hr": 4764.678425, 39 | "clobTokenIds": "[\"106312791964557364184052642373426857106392360847438469940517961069035123954706\", \"102962810042814384989272150691336437847931513539356148805377736932497655720705\"]", 40 | "umaBond": "500", 41 | "umaReward": "5", 42 | "volume24hrClob": 4764.678425, 43 | "volumeClob": 17956.388032, 44 | "liquidityClob": 4258.8142, 45 | "acceptingOrders": true, 46 | "ready": false, 47 | "funded": false, 48 | "acceptingOrdersTimestamp": "2024-12-27T18:31:15Z", 49 | "cyom": false, 50 | "competitive": 0.9880446596186148, 51 | "pagerDutyNotificationEnabled": false, 52 | "approved": true, 53 | "clobRewards": [ 54 | { 55 | "id": "12204", 56 | "conditionId": "0x8cbf31ae2dff3bf262ba8b7e847cfc49d6f9e88ddb3330a051a5787177749874", 57 | "assetAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", 58 | "rewardsAmount": 0, 59 | "rewardsDailyRate": 20, 60 | "startDate": "2024-12-26", 61 | "endDate": "2500-12-31" 62 | } 63 | ], 64 | "rewardsMinSize": 50, 65 | "rewardsMaxSpread": 3.5, 66 | "spread": 0.02, 67 | "oneDayPriceChange": 0.085, 68 | "lastTradePrice": 0.39, 69 | "bestBid": 0.38, 70 | "bestAsk": 0.4, 71 | "automaticallyActive": true, 72 | "clearBookOnStart": true, 73 | "manualActivation": false, 74 | "negRiskOther": false 75 | } 76 | -------------------------------------------------------------------------------- /src/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Drawer as DrawerPrimitive } from "vaul"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Drawer = ({ 9 | shouldScaleBackground = true, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 16 | ); 17 | Drawer.displayName = "Drawer"; 18 | 19 | const DrawerTrigger = DrawerPrimitive.Trigger; 20 | 21 | const DrawerPortal = DrawerPrimitive.Portal; 22 | 23 | const DrawerClose = DrawerPrimitive.Close; 24 | 25 | const DrawerOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 34 | )); 35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; 36 | 37 | const DrawerContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, children, ...props }, ref) => ( 41 | 42 | 43 | 51 |
52 | {children} 53 | 54 | 55 | )); 56 | DrawerContent.displayName = "DrawerContent"; 57 | 58 | const DrawerHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
66 | ); 67 | DrawerHeader.displayName = "DrawerHeader"; 68 | 69 | const DrawerFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
77 | ); 78 | DrawerFooter.displayName = "DrawerFooter"; 79 | 80 | const DrawerTitle = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 92 | )); 93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName; 94 | 95 | const DrawerDescription = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 104 | )); 105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName; 106 | 107 | export { 108 | Drawer, 109 | DrawerPortal, 110 | DrawerOverlay, 111 | DrawerTrigger, 112 | DrawerClose, 113 | DrawerContent, 114 | DrawerHeader, 115 | DrawerFooter, 116 | DrawerTitle, 117 | DrawerDescription, 118 | }; 119 | -------------------------------------------------------------------------------- /src/components/LineGraph.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Area, 5 | CartesianGrid, 6 | ComposedChart, 7 | Line, 8 | LineProps, 9 | ResponsiveContainer, 10 | Tooltip, 11 | TooltipProps, 12 | XAxis, 13 | XAxisProps, 14 | YAxis, 15 | YAxisProps, 16 | } from "recharts"; 17 | import { ChartDataPoint } from "../lib/types"; 18 | import { AnyFormatter, getFormatter } from "@/lib/dates"; 19 | 20 | export function LineGraph({ 21 | data, 22 | color, 23 | label, 24 | xAxisProps = {}, 25 | xAxisFormatter, 26 | yAxisProps = {}, 27 | yAxisFormatter, 28 | tooltip, 29 | lineProps = {}, 30 | children, 31 | }: { 32 | data: ChartDataPoint[]; 33 | color: string; 34 | label: string; 35 | xAxisProps?: Partial; 36 | xAxisFormatter?: AnyFormatter; 37 | yAxisProps?: Partial; 38 | yAxisFormatter?: AnyFormatter; 39 | tooltip?: TooltipProps["content"]; 40 | lineProps?: Omit; 41 | children?: React.ReactNode; 42 | }) { 43 | // Check if all data points have range values when distribution is requested 44 | const hasDistribution = data.every((point) => point.range !== undefined); 45 | 46 | return ( 47 |
48 |
49 | {label} 50 |
51 | 52 | 61 | 66 | 84 | 98 | {tooltip ? : null} 99 | {hasDistribution && ( 100 | 108 | )} 109 | 119 | {children} 120 | 121 | 122 |
123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type ChartDataPoint = { 2 | date: string; 3 | value: number; 4 | range?: [number, number]; // [min, max] 5 | }; 6 | 7 | export type PolymarketDataPoint = { 8 | t: number; 9 | p: number; 10 | }; 11 | 12 | export type PolymarketResponse = { 13 | history: PolymarketDataPoint[]; 14 | }; 15 | 16 | export type MetaculusResponse = { 17 | question: { 18 | id: number; 19 | scaling: { 20 | range_min: number; 21 | range_max: number; 22 | zero_point: number; 23 | }; 24 | aggregations: { 25 | recency_weighted: { 26 | history: Array<{ 27 | start_time: number; 28 | end_time: number; 29 | means: number[] | null; 30 | centers: number[]; 31 | interval_lower_bounds: number[] | null; 32 | interval_upper_bounds: number[] | null; 33 | }>; 34 | }; 35 | }; 36 | scheduled_close_time: string; 37 | scheduled_resolve_time: string; 38 | }; 39 | }; 40 | 41 | export type MockDataSeries = { 42 | riskIndex: ChartDataPoint[]; 43 | variantCount: ChartDataPoint[]; 44 | }; 45 | 46 | export type KalshiMarketData = { 47 | market: { 48 | ticker: string; 49 | title: string; 50 | open_time: string; 51 | close_time: string; 52 | status: string; 53 | yes_bid: number; 54 | yes_ask: number; 55 | no_bid: number; 56 | no_ask: number; 57 | last_price: number; 58 | volume: number; 59 | volume_24h: number; 60 | }; 61 | }; 62 | 63 | export type KalshiCandlestick = { 64 | end_period_ts: number; 65 | yes_bid: { 66 | open: number; 67 | low: number; 68 | high: number; 69 | close: number; 70 | }; 71 | yes_ask: { 72 | open: number; 73 | low: number; 74 | high: number; 75 | close: number; 76 | }; 77 | price: { 78 | open: number | null; 79 | low: number | null; 80 | high: number | null; 81 | close: number | null; 82 | mean: number | null; 83 | mean_centi: number | null; 84 | previous: number | null; 85 | }; 86 | volume: number; 87 | open_interest: number; 88 | }; 89 | 90 | export type KalshiResponse = { 91 | marketData: KalshiMarketData; 92 | candlesticks: { 93 | candlesticks: KalshiCandlestick[]; 94 | }; 95 | dateRange: { 96 | start: string; // ISO string 97 | end: string; // ISO string 98 | interval: number; 99 | }; 100 | }; 101 | 102 | export type CdcDataPoint = { 103 | Range: string; 104 | Month: string; 105 | } & Record; 106 | 107 | export type ManifoldResponse = { 108 | id: string; 109 | question: string; 110 | description: { 111 | type: string; 112 | content: Array<{ 113 | type: string; 114 | content?: Array<{ 115 | text?: string; 116 | type: string; 117 | }>; 118 | }>; 119 | }; 120 | probability: number; 121 | createdTime: number; 122 | closeTime: number; 123 | isResolved: boolean; 124 | volume: number; 125 | volume24Hours: number; 126 | uniqueBettorCount: number; 127 | lastUpdatedTime: number; 128 | slug: string; 129 | }; 130 | 131 | export type Bet = { 132 | isFilled: boolean; 133 | amount: number; 134 | userId: string; 135 | contractId: string; 136 | answerId: string; 137 | probBefore: number; 138 | isCancelled: boolean; 139 | outcome: string; 140 | fees: { 141 | creatorFee: number; 142 | liquidityFee: number; 143 | platformFee: number; 144 | }; 145 | shares: number; 146 | limitProb: number; 147 | id: string; 148 | loanAmount: number; 149 | orderAmount: number; 150 | probAfter: number; 151 | createdTime: number; 152 | fills: Array<{ 153 | timestamp: number; 154 | matchedBetId: string; 155 | amount: number; 156 | shares: number; 157 | }>; 158 | }; 159 | 160 | export type MetaculusForecast = { 161 | "Question ID": string; 162 | "Forecaster ID": string; 163 | "Forecaster Username": string; 164 | "Start Time": string; 165 | "End Time": string; 166 | "Forecaster Count": string; 167 | "Probability Yes": string; 168 | "Probability Yes Per Category": string; 169 | "Continuous CDF": string; 170 | }; 171 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | I am Nathan Young, of twitter account @nathanpmyoung, nathanpmyoung.com, nathanpmyoung.substack.com. If you want, please help me building tools. You do not have to. You can be sarcastic if you feel I'm not giving you enough respect or if it's very funny. 2 | 3 | Please guide learning through analytical, supportive, slightly socratic improvement of code. Feel free to disagree with me. Please be extremely concise only explaining one step of logic at a time. Automatically suggest only 1 or two changes, if any more, then ask. Generally messages should not be longer than 400 characters, but file improvements will be so, use your best judgement. 4 | 5 | You are an expert in Solidity, TypeScript, Node.js, Next.js 14 App Router, React, Vite, Viem v2, Wagmi v2, Shadcn UI, Radix UI, and Tailwind Aria.  Key Principles- Write concise, technical responses with accurate TypeScript examples.- Use functional, declarative programming. Avoid classes.- Prefer iteration and modularization over duplication.- Use descriptive variable names with auxiliary verbs (e.g., isLoading).- Use lowercase with dashes for directories (e.g., components/auth-wizard).- Favor named exports for components.- Use the Receive an Object, Return an Object (RORO) pattern.  JavaScript/TypeScript- Use "function" keyword for pure functions. Omit semicolons.- Use TypeScript for all code. Prefer interfaces over types. Avoid enums, use maps.- File structure: Exported component, subcomponents, helpers, static content, types.- Avoid unnecessary curly braces in conditional statements.- For single-line statements in conditionals, omit curly braces.- Use concise, one-line syntax for simple conditional statements (e.g., if (condition) doSomething()).  Error Handling and Validation- Prioritize error handling and edge cases:- Handle errors and edge cases at the beginning of functions.- Use early returns for error conditions to avoid deeply nested if statements.- Place the happy path last in the function for improved readability.- Avoid unnecessary else statements; use if-return pattern instead.- Use guard clauses to handle preconditions and invalid states early.- Implement proper error logging and user-friendly error messages.- Consider using custom error types or error factories for consistent error handling.  React/Next.js- Use functional components and TypeScript interfaces.- Use declarative JSX.- Use function, not const, for components.- Use Shadcn UI, Radix, and Tailwind Aria for components and styling.- Implement responsive design with Tailwind CSS.- Use mobile-first approach for responsive design.- Place static content and interfaces at file end.- Use content variables for static content outside render functions.- Minimize 'use client', 'useEffect', and 'setState'. Favor RSC.- Use Zod for form validation.- Wrap client components in Suspense with fallback.- Use dynamic loading for non-critical components.- Optimize images: WebP format, size data, lazy loading.- Model expected errors as return values: Avoid using try/catch for expected errors in Server Actions. Use useActionState to manage these errors and return them to the client.- Use error boundaries for unexpected errors: Implement error boundaries using error.tsx and global-error.tsx files to handle unexpected errors and provide a fallback UI.- Use useActionState with react-hook-form for form validation.- Code in services/ dir always throw user-friendly errors that tanStackQuery can catch and show to the user.- Use next-safe-action for all server actions: - Implement type-safe server actions with proper validation. - Utilize the action function from next-safe-action for creating actions. - Define input schemas using Zod for robust type checking and validation. - Handle errors gracefully and return appropriate responses. - Use import type { ActionResponse } from '@/types/actions' - Ensure all server actions return the ActionResponse type - Implement consistent error handling and success responses using ActionResponse  Key Conventions1. Rely on Next.js App Router for state changes.2. Prioritize Web Vitals (LCP, CLS, FID).3. Minimize 'use client' usage:  - Prefer server components and Next.js SSR features.  - Use 'use client' only for Web API access in small components.  - Avoid using 'use client' for data fetching or state management.  Refer to Next.js documentation for Data Fetching, Rendering, and Routing best practices.- https://nextjs.org/docs 6 | -------------------------------------------------------------------------------- /src/lib/services/manifold-historical.ts: -------------------------------------------------------------------------------- 1 | import { Bet, ChartDataPoint } from "../types"; 2 | 3 | export async function getManifoldHistoricalData(slug: string) { 4 | const searchParams = new URLSearchParams(); 5 | searchParams.set("slug", slug); 6 | const response = await fetch(`/api/manifold-historical?${searchParams}`, { 7 | cache: "force-cache", 8 | next: { 9 | revalidate: 60 * 60 * 24, // 24 hours 10 | }, 11 | }); 12 | const { bets } = (await response.json()) as { bets: Bet[] }; 13 | 14 | // get oldest bet 15 | const oldestBet = bets.sort((a, b) => a.createdTime - b.createdTime)[0]; 16 | 17 | // parse date from createdTime 18 | const startDate = new Date(oldestBet.createdTime); 19 | 20 | // step through dates 1 day at a time 21 | const dates = []; 22 | let index = 0; 23 | for ( 24 | let date = startDate; 25 | date <= new Date(); 26 | date.setDate(date.getDate() + 1) 27 | ) { 28 | const dateTime = date.getTime(); 29 | 30 | // find the index of the first bet after the date 31 | let nextIndex = index; 32 | while ( 33 | nextIndex < bets.length && 34 | new Date(bets[nextIndex].createdTime) <= date 35 | ) { 36 | nextIndex++; 37 | } 38 | 39 | // get all the bets from the cursor 40 | const betsInRange = bets.slice(index, nextIndex); 41 | 42 | // Begin with the previous probabilities 43 | const probabilities: Partial> = { 44 | ...(dates[dates.length - 1]?.probabilities || {}), 45 | }; 46 | 47 | // loop over the bets 48 | for (const bet of betsInRange) { 49 | const answerId = bet.answerId as AnswerId; 50 | const year = answerIdToYear[answerId]; 51 | probabilities[year] = bet.probAfter; 52 | } 53 | 54 | dates.push({ date: dateTime, probabilities }); 55 | 56 | index = nextIndex; 57 | } 58 | 59 | // Find the first date with all years 60 | const firstDateWithAllYears = dates.findIndex( 61 | (date) => 62 | Object.keys(date.probabilities).length === 63 | Object.keys(answerIdToYear).length, 64 | ); 65 | 66 | const data: ChartDataPoint[] = []; 67 | 68 | // Loop over dates which contain all years 69 | for (const date of dates.slice(firstDateWithAllYears)) { 70 | // Probabilities don't always sum to 1, because we're using 71 | // the most recent probability after a given bet was made. 72 | const sum = Object.values(date.probabilities).reduce( 73 | (acc, curr) => (acc ?? 0) + (curr ?? 0), 74 | 0, 75 | ); 76 | 77 | let year = 2024, 78 | counter = 0; 79 | let lower = 0; 80 | let median = 0; 81 | let upper = 0; 82 | while (year <= 2049 && counter <= 10) { 83 | const probability = date.probabilities[year]; 84 | if (probability) { 85 | counter += probability / sum; 86 | if (counter > 0.1 && !lower) { 87 | lower = year; 88 | } 89 | if (counter > 0.5 && !median) { 90 | median = year; 91 | } 92 | if (counter > 0.9 && !upper) { 93 | upper = year; 94 | } 95 | } 96 | year++; 97 | } 98 | 99 | data.push({ 100 | date: new Date(date.date).toISOString(), 101 | value: median, 102 | range: [lower, upper], 103 | }); 104 | } 105 | 106 | return { data, byYear: dates }; 107 | } 108 | 109 | export const answerIdToYear = { 110 | "2cdb91507b0d": 2024, 111 | ed73a628dbcc: 2025, 112 | e8aae6520563: 2026, 113 | "67ea62c46640": 2027, 114 | "1d0fd5249e2c": 2028, 115 | fc63e30d0cd3: 2029, 116 | "5d48ed784957": 2030, 117 | da223cf612c4: 2031, 118 | "1120df2c949a": 2032, 119 | "77f7e579eac6": 2033, 120 | c398134a9e34: 2034, 121 | b586da03d2ec: 2035, 122 | a6051fa037db: 2036, 123 | "4271e6a3e455": 2037, 124 | aa017a9cebe3: 2038, 125 | "46ff975d1efe": 2039, 126 | cccb4c406baf: 2040, 127 | "9ae19221aa18": 2041, 128 | "6039886c26fa": 2042, 129 | "4322a973f59c": 2043, 130 | "1f24b6787f0d": 2044, 131 | "0f172ca6223b": 2045, 132 | "9b885d17779f": 2046, 133 | "9199140a0c5f": 2047, 134 | "659fc2df1d1d": 2048, 135 | c43dc66076d5: 2049, 136 | }; 137 | 138 | type AnswerId = keyof typeof answerIdToYear; 139 | type Year = (typeof answerIdToYear)[AnswerId]; 140 | -------------------------------------------------------------------------------- /src/lib/services/metaculus.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataPoint, MetaculusResponse } from "../types"; 2 | 3 | export async function fetchMetaculusData(questionId: number): Promise<{ 4 | question: MetaculusResponse["question"]; 5 | data: ChartDataPoint[]; 6 | }> { 7 | // Try API first 8 | const response = await fetch(`/api/metaculus?questionId=${questionId}`); 9 | if (!response.ok) throw new Error("API failed"); 10 | const data: MetaculusResponse = await response.json(); 11 | 12 | const transformed = transformMetaculusData(data); 13 | 14 | return { question: data.question, data: transformed }; 15 | } 16 | 17 | function transformMetaculusData(data: MetaculusResponse): ChartDataPoint[] { 18 | if (!data?.question?.aggregations?.recency_weighted?.history) { 19 | console.error("Missing expected data structure:", { 20 | hasQuestion: !!data?.question, 21 | hasAggregations: !!data?.question?.aggregations, 22 | hasRecencyWeighted: !!data?.question?.aggregations?.recency_weighted, 23 | hasHistory: !!data?.question?.aggregations?.recency_weighted?.history, 24 | }); 25 | return []; 26 | } 27 | 28 | const history = data.question.aggregations.recency_weighted.history.sort( 29 | (a, b) => a.start_time - b.start_time, 30 | ); 31 | 32 | const start = history[0].start_time; 33 | const end = history[history.length - 1].start_time; 34 | 35 | const points: ChartDataPoint[] = []; 36 | 37 | const transform = getTransform(data.question.scaling); 38 | 39 | let index = 0; 40 | let currentDate = start; 41 | while (currentDate <= end) { 42 | const sample = history[index]; 43 | const center = sample.centers[0]; 44 | const lower = sample.interval_lower_bounds?.[0]; 45 | const upper = sample.interval_upper_bounds?.[0]; 46 | points.push({ 47 | date: new Date(currentDate * 1000).toISOString(), 48 | value: transform(center), 49 | range: [transform(lower ?? 0), transform(upper ?? 0)], 50 | }); 51 | currentDate += 24 * 60 * 60; 52 | 53 | // increment index until the next sample is after the current date 54 | let tmpIndex = index; 55 | while ( 56 | currentDate <= end && 57 | tmpIndex < history.length && 58 | history[tmpIndex].start_time < currentDate 59 | ) { 60 | tmpIndex++; 61 | } 62 | index = tmpIndex - 1; 63 | } 64 | 65 | return points; 66 | } 67 | 68 | /** 69 | * Returns a function which transforms a 0-1 value into the correct value 70 | * based on the scaling of the question. 71 | */ 72 | export function getTransform( 73 | scaling: MetaculusResponse["question"]["scaling"], 74 | ) { 75 | function m() { 76 | return (t: number) => { 77 | return null == t; 78 | }; 79 | } 80 | 81 | return (value: number) => { 82 | let t; 83 | const { range_min, range_max, zero_point } = scaling; 84 | if (m()(range_max) || m()(range_min)) return value; 85 | if (null !== zero_point) { 86 | const n = (range_max - zero_point) / (range_min - zero_point); 87 | t = range_min + ((range_max - range_min) * (n ** value - 1)) / (n - 1); 88 | } else 89 | t = 90 | null === range_min || null === range_max 91 | ? value 92 | : range_min + (range_max - range_min) * value; 93 | return t; 94 | }; 95 | } 96 | 97 | /** 98 | * Returns a function which transforms a scaled value into it's 0-1 value. 99 | */ 100 | export function getInverseTransform( 101 | scaling: MetaculusResponse["question"]["scaling"], 102 | ) { 103 | function isNull(t: number) { 104 | return null == t; 105 | } 106 | 107 | return (value: number) => { 108 | const { range_min, range_max, zero_point } = scaling; 109 | if (isNull(range_max) || isNull(range_min)) return value; 110 | 111 | if (null !== zero_point) { 112 | // Inverse of logarithmic scaling 113 | const n = (range_max - zero_point) / (range_min - zero_point); 114 | return ( 115 | Math.log( 116 | ((value - range_min) * (n - 1)) / (range_max - range_min) + 1, 117 | ) / Math.log(n) 118 | ); 119 | } else { 120 | // Inverse of linear scaling 121 | return null === range_min || null === range_max 122 | ? value 123 | : (value - range_min) / (range_max - range_min); 124 | } 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /scripts/process-forecast.ts: -------------------------------------------------------------------------------- 1 | // https://www.metaculus.com/questions/3479/date-weakly-general-ai-is-publicly-known/ 2 | 3 | import fs from "fs"; 4 | import path from "path"; 5 | import Papa from "papaparse"; 6 | import { MetaculusResponse } from "@/lib/types"; 7 | import { getInverseTransform, getTransform } from "@/lib/services/metaculus"; 8 | import { getEndYear } from "@/lib/services/metaculus-download"; 9 | import { getStartYear } from "@/lib/services/metaculus-download"; 10 | const DATA_DIR = path.join(process.cwd(), "data", "metaculus-3479"); 11 | const FORECAST_DATA_PATH = path.join(DATA_DIR, "forecast_data.csv"); 12 | const QUESTION_DATA_PATH = path.join(DATA_DIR, "question_data.json"); 13 | // Fetch question data 14 | async function main() { 15 | // Read question data 16 | const questionData = JSON.parse( 17 | fs.readFileSync(QUESTION_DATA_PATH, "utf8"), 18 | ) as MetaculusResponse["question"]; 19 | 20 | const transform = getTransform(questionData.scaling); 21 | const inverseTransform = getInverseTransform(questionData.scaling); 22 | const forecastData = fs.readFileSync(FORECAST_DATA_PATH, "utf8"); 23 | 24 | // use papaparse to parse the data 25 | const forecastDataParsed = Papa.parse(forecastData, { 26 | header: true, 27 | }); 28 | 29 | // Take the last (most recent) sample 30 | const randomSample = forecastDataParsed.data[ 31 | forecastDataParsed.data.length - 2 32 | ] as { 33 | "Continuous CDF": string; 34 | }; 35 | 36 | // CDF Data Points 37 | const cdfDataPoints = JSON.parse(randomSample["Continuous CDF"]); 38 | 39 | let lower: number = 0; 40 | let median: number = 0; 41 | let upper: number = 0; 42 | for (let i = 0; i < cdfDataPoints.length; i++) { 43 | const value = cdfDataPoints[i]; 44 | if (value > 0.1 && !lower) { 45 | lower = i; 46 | const index = getInterpolatedIndex(lower, 0.1); 47 | const value = new Date(transform(index / 200) * 1000); 48 | console.log(value); 49 | } 50 | if (value > 0.5 && !median) { 51 | median = i; 52 | const index = getInterpolatedIndex(median, 0.5); 53 | const value = new Date(transform(index / 200) * 1000); 54 | console.log(value); 55 | } 56 | if (value > 0.9 && !upper) { 57 | upper = i; 58 | const index = getInterpolatedIndex(upper, 0.9); 59 | const value = new Date(transform(index / 200) * 1000); 60 | console.log(value); 61 | } 62 | } 63 | 64 | function getInterpolatedIndex(index: number, target: number) { 65 | // We need to get an interpolated index that's closest to the target 66 | const lowerIndex = index - 1; 67 | const upperIndex = index; 68 | const lower = cdfDataPoints[lowerIndex]; 69 | const upper = cdfDataPoints[upperIndex]; 70 | const interpolatedIndex = lowerIndex + (target - lower) / (upper - lower); 71 | return interpolatedIndex; 72 | } 73 | // console.log( 74 | // lower, 75 | // cdfDataPoints[lower], 76 | // median, 77 | // cdfDataPoints[median], 78 | // upper, 79 | // cdfDataPoints[upper], 80 | // ); 81 | 82 | // Get years of interest 83 | const startYear = getStartYear(questionData.scaling.range_min * 1000); 84 | const endYear = getEndYear(questionData.scaling.range_max * 1000); 85 | 86 | const results: { year: number; cdfValue: number; pdfValue: number }[] = []; 87 | 88 | // Loop through each year 89 | for (let year = startYear; year <= endYear; year++) { 90 | // Create a new date from year 91 | const date = new Date(year, 0, 1); 92 | // Get the transformed value 93 | const unitValue = inverseTransform(date.getTime() / 1000); 94 | 95 | // Get the rough index 96 | const roughIndex = unitValue / (1 / 200); 97 | const lowerIndex = Math.floor(roughIndex); 98 | const upperIndex = Math.ceil(roughIndex); 99 | 100 | // Get the values at the lower and upper indices 101 | const lowerValue = cdfDataPoints[lowerIndex]; 102 | const upperValue = cdfDataPoints[upperIndex]; 103 | 104 | // Interpolate between the two values 105 | const cdfValue = 106 | lowerValue + (upperValue - lowerValue) * (roughIndex - lowerIndex); 107 | 108 | results.push({ 109 | year, 110 | cdfValue, 111 | pdfValue: cdfValue - (results[results.length - 1]?.cdfValue || 0), 112 | }); 113 | } 114 | 115 | results.forEach((result) => { 116 | console.log(result.year, (result.pdfValue * 100).toFixed(4) + "%"); 117 | }); 118 | } 119 | 120 | main().catch(console.error); 121 | -------------------------------------------------------------------------------- /src/lib/services/metaculus.server.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataPoint, MetaculusResponse } from "../types"; 2 | const METACULUS_API = "https://www.metaculus.com/api"; 3 | 4 | export async function fetchMetaculusData(questionId: number): Promise<{ 5 | question: MetaculusResponse["question"]; 6 | data: ChartDataPoint[]; 7 | }> { 8 | const questionResponse = await fetch(`${METACULUS_API}/posts/${questionId}`, { 9 | headers: { 10 | Accept: "application/json", 11 | }, 12 | }); 13 | 14 | if (!questionResponse.ok) { 15 | const errorText = await questionResponse.text(); 16 | console.error("Error response body:", errorText); 17 | 18 | throw new Error("Failed to fetch Metaculus data"); 19 | } 20 | 21 | const data: MetaculusResponse = await questionResponse.json(); 22 | 23 | const transformed = transformMetaculusData(data); 24 | 25 | return { question: data.question, data: transformed }; 26 | } 27 | 28 | function transformMetaculusData(data: MetaculusResponse): ChartDataPoint[] { 29 | if (!data?.question?.aggregations?.recency_weighted?.history) { 30 | console.error("Missing expected data structure:", { 31 | hasQuestion: !!data?.question, 32 | hasAggregations: !!data?.question?.aggregations, 33 | hasRecencyWeighted: !!data?.question?.aggregations?.recency_weighted, 34 | hasHistory: !!data?.question?.aggregations?.recency_weighted?.history, 35 | }); 36 | return []; 37 | } 38 | 39 | const history = data.question.aggregations.recency_weighted.history.sort( 40 | (a, b) => a.start_time - b.start_time, 41 | ); 42 | 43 | const start = history[0].start_time; 44 | const end = history[history.length - 1].start_time; 45 | 46 | const points: ChartDataPoint[] = []; 47 | 48 | const transform = getTransform(data.question.scaling); 49 | 50 | let index = 0; 51 | let currentDate = start; 52 | while (currentDate <= end) { 53 | const sample = history[index]; 54 | const center = sample.centers[0]; 55 | const lower = sample.interval_lower_bounds?.[0]; 56 | const upper = sample.interval_upper_bounds?.[0]; 57 | points.push({ 58 | date: new Date(currentDate * 1000).toISOString(), 59 | value: transform(center), 60 | range: [transform(lower ?? 0), transform(upper ?? 0)], 61 | }); 62 | currentDate += 24 * 60 * 60; 63 | 64 | // increment index until the next sample is after the current date 65 | let tmpIndex = index; 66 | while ( 67 | currentDate <= end && 68 | tmpIndex < history.length && 69 | history[tmpIndex].start_time < currentDate 70 | ) { 71 | tmpIndex++; 72 | } 73 | index = tmpIndex - 1; 74 | } 75 | 76 | return points; 77 | } 78 | 79 | /** 80 | * Returns a function which transforms a 0-1 value into the correct value 81 | * based on the scaling of the question. 82 | */ 83 | export function getTransform( 84 | scaling: MetaculusResponse["question"]["scaling"], 85 | ) { 86 | function m() { 87 | return (t: number) => { 88 | return null == t; 89 | }; 90 | } 91 | 92 | return (value: number) => { 93 | let t; 94 | const { range_min, range_max, zero_point } = scaling; 95 | if (m()(range_max) || m()(range_min)) return value; 96 | if (null !== zero_point) { 97 | const n = (range_max - zero_point) / (range_min - zero_point); 98 | t = range_min + ((range_max - range_min) * (n ** value - 1)) / (n - 1); 99 | } else 100 | t = 101 | null === range_min || null === range_max 102 | ? value 103 | : range_min + (range_max - range_min) * value; 104 | return t; 105 | }; 106 | } 107 | 108 | /** 109 | * Returns a function which transforms a scaled value into it's 0-1 value. 110 | */ 111 | export function getInverseTransform( 112 | scaling: MetaculusResponse["question"]["scaling"], 113 | ) { 114 | function isNull(t: number) { 115 | return null == t; 116 | } 117 | 118 | return (value: number) => { 119 | const { range_min, range_max, zero_point } = scaling; 120 | if (isNull(range_max) || isNull(range_min)) return value; 121 | 122 | if (null !== zero_point) { 123 | // Inverse of logarithmic scaling 124 | const n = (range_max - zero_point) / (range_min - zero_point); 125 | return ( 126 | Math.log( 127 | ((value - range_min) * (n - 1)) / (range_max - range_min) + 1, 128 | ) / Math.log(n) 129 | ); 130 | } else { 131 | // Inverse of linear scaling 132 | return null === range_min || null === range_max 133 | ? value 134 | : (value - range_min) / (range_max - range_min); 135 | } 136 | }; 137 | } 138 | -------------------------------------------------------------------------------- /src/lib/services/manifold-historical.server.ts: -------------------------------------------------------------------------------- 1 | import { Bet, ChartDataPoint } from "../types"; 2 | 3 | export async function getManifoldHistoricalData(slug: string) { 4 | // Fetch all bets for the contract, by fetching using ?before with the oldest bet id 5 | const bets: Bet[] = []; 6 | let hasMore = true; 7 | let cursor = ""; 8 | while (hasMore) { 9 | const searchParams = new URLSearchParams(); 10 | searchParams.set("contractSlug", slug); 11 | if (cursor) { 12 | searchParams.set("before", cursor); 13 | } 14 | const betsResponse = await fetch( 15 | `https://api.manifold.markets/v0/bets?${searchParams.toString()}`, 16 | ); 17 | const newBets = (await betsResponse.json()) as Bet[]; 18 | bets.push(...newBets); 19 | 20 | if (newBets.length === 0) { 21 | hasMore = false; 22 | } else { 23 | cursor = newBets[newBets.length - 1].id; 24 | } 25 | } 26 | 27 | // get oldest bet 28 | const oldestBet = bets.sort((a, b) => a.createdTime - b.createdTime)[0]; 29 | 30 | // parse date from createdTime 31 | const startDate = new Date(oldestBet.createdTime); 32 | 33 | // step through dates 1 day at a time 34 | const dates = []; 35 | let index = 0; 36 | for ( 37 | let date = startDate; 38 | date <= new Date(); 39 | date.setDate(date.getDate() + 1) 40 | ) { 41 | const dateTime = date.getTime(); 42 | 43 | // find the index of the first bet after the date 44 | let nextIndex = index; 45 | while ( 46 | nextIndex < bets.length && 47 | new Date(bets[nextIndex].createdTime) <= date 48 | ) { 49 | nextIndex++; 50 | } 51 | 52 | // get all the bets from the cursor 53 | const betsInRange = bets.slice(index, nextIndex); 54 | 55 | // Begin with the previous probabilities 56 | const probabilities: Partial> = { 57 | ...(dates[dates.length - 1]?.probabilities || {}), 58 | }; 59 | 60 | // loop over the bets 61 | for (const bet of betsInRange) { 62 | const answerId = bet.answerId as AnswerId; 63 | const year = answerIdToYear[answerId]; 64 | probabilities[year] = bet.probAfter; 65 | } 66 | 67 | dates.push({ date: dateTime, probabilities }); 68 | 69 | index = nextIndex; 70 | } 71 | 72 | // Find the first date with all years 73 | const firstDateWithAllYears = dates.findIndex( 74 | (date) => 75 | Object.keys(date.probabilities).length === 76 | Object.keys(answerIdToYear).length, 77 | ); 78 | 79 | const data: ChartDataPoint[] = []; 80 | 81 | // Loop over dates which contain all years 82 | for (const date of dates.slice(firstDateWithAllYears)) { 83 | // Probabilities don't always sum to 1, because we're using 84 | // the most recent probability after a given bet was made. 85 | const sum = Object.values(date.probabilities).reduce( 86 | (acc, curr) => (acc ?? 0) + (curr ?? 0), 87 | 0, 88 | ); 89 | 90 | let year = 2024, 91 | counter = 0; 92 | let lower = 0; 93 | let median = 0; 94 | let upper = 0; 95 | while (year <= 2049 && counter <= 10) { 96 | const probability = date.probabilities[year]; 97 | if (probability) { 98 | counter += probability / sum; 99 | if (counter > 0.1 && !lower) { 100 | lower = year; 101 | } 102 | if (counter > 0.5 && !median) { 103 | median = year; 104 | } 105 | if (counter > 0.9 && !upper) { 106 | upper = year; 107 | } 108 | } 109 | year++; 110 | } 111 | 112 | data.push({ 113 | date: new Date(date.date).toISOString(), 114 | value: median, 115 | range: [lower, upper], 116 | }); 117 | } 118 | 119 | return { data, byYear: dates }; 120 | } 121 | 122 | export const answerIdToYear = { 123 | "2cdb91507b0d": 2024, 124 | ed73a628dbcc: 2025, 125 | e8aae6520563: 2026, 126 | "67ea62c46640": 2027, 127 | "1d0fd5249e2c": 2028, 128 | fc63e30d0cd3: 2029, 129 | "5d48ed784957": 2030, 130 | da223cf612c4: 2031, 131 | "1120df2c949a": 2032, 132 | "77f7e579eac6": 2033, 133 | c398134a9e34: 2034, 134 | b586da03d2ec: 2035, 135 | a6051fa037db: 2036, 136 | "4271e6a3e455": 2037, 137 | aa017a9cebe3: 2038, 138 | "46ff975d1efe": 2039, 139 | cccb4c406baf: 2040, 140 | "9ae19221aa18": 2041, 141 | "6039886c26fa": 2042, 142 | "4322a973f59c": 2043, 143 | "1f24b6787f0d": 2044, 144 | "0f172ca6223b": 2045, 145 | "9b885d17779f": 2046, 146 | "9199140a0c5f": 2047, 147 | "659fc2df1d1d": 2048, 148 | c43dc66076d5: 2049, 149 | }; 150 | 151 | type AnswerId = keyof typeof answerIdToYear; 152 | type Year = (typeof answerIdToYear)[AnswerId]; 153 | -------------------------------------------------------------------------------- /src/lib/services/metaculus-download.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataPoint, MetaculusForecast, MetaculusResponse } from "../types"; 2 | import { getInverseTransform, getTransform } from "./metaculus"; 3 | 4 | type YearForecast = { year: number; cdfValue: number; pdfValue: number }; 5 | 6 | export async function downloadMetaculusData(questionId: number) { 7 | const response = await fetch( 8 | `/api/metaculus-download?questionId=${questionId}`, 9 | { 10 | cache: "force-cache", 11 | next: { 12 | revalidate: 60 * 60 * 24, // 24 hours 13 | }, 14 | }, 15 | ); 16 | const { question, forecast: forecastData } = (await response.json()) as { 17 | question: MetaculusResponse["question"]; 18 | forecast: MetaculusForecast[]; 19 | }; 20 | 21 | const rangeStart = getStartYear(question.scaling.range_min * 1000); 22 | const rangeEnd = getEndYear(question.scaling.range_max * 1000); 23 | const inverseTransform = getInverseTransform(question.scaling); 24 | const transform = getTransform(question.scaling); 25 | 26 | const boundsResults: (Omit | null)[] = []; 27 | const forecastResults: YearForecast[][] = []; 28 | for (let i = 0; i < forecastData.length; i++) { 29 | const forecast = forecastData[i]; 30 | const cdfDataPoints = JSON.parse(forecast["Continuous CDF"]) as number[]; 31 | 32 | // Determine per-year probabilities 33 | const results: YearForecast[] = []; 34 | for (let year = rangeStart; year <= rangeEnd; year++) { 35 | const currentYear = new Date(year, 0, 1); 36 | const unitValue = inverseTransform(currentYear.getTime() / 1000); 37 | 38 | const roughIndex = unitValue / (1 / 200); 39 | const lowerIndex = Math.floor(roughIndex); 40 | const upperIndex = Math.ceil(roughIndex); 41 | 42 | const lowerValue = cdfDataPoints[lowerIndex]; 43 | const upperValue = cdfDataPoints[upperIndex]; 44 | 45 | const cdfValue = 46 | lowerValue + (upperValue - lowerValue) * (roughIndex - lowerIndex); 47 | 48 | results.push({ 49 | year, 50 | cdfValue, 51 | pdfValue: cdfValue - (results[results.length - 1]?.cdfValue || 0), 52 | }); 53 | } 54 | forecastResults.push(results); 55 | 56 | // Determine the bounds 57 | let lower = 0, 58 | median = 0, 59 | upper = 0; 60 | for (let i = 0; i < cdfDataPoints.length; i++) { 61 | const value = cdfDataPoints[i]; 62 | if (value > 0.1 && !lower) { 63 | lower = transform(getInterpolatedIndex(cdfDataPoints, i, 0.1) / 200); 64 | } 65 | if (value > 0.5 && !median) { 66 | median = transform(getInterpolatedIndex(cdfDataPoints, i, 0.5) / 200); 67 | } 68 | if (value > 0.9 && !upper) { 69 | upper = transform(getInterpolatedIndex(cdfDataPoints, i, 0.9) / 200); 70 | } 71 | } 72 | 73 | if (lower && median && upper) { 74 | boundsResults.push({ 75 | value: median, 76 | range: [lower, upper], 77 | }); 78 | } else { 79 | boundsResults.push(null); 80 | } 81 | } 82 | 83 | // Read first forecast to get start date 84 | const start = new Date(forecastData[0]["Start Time"]); 85 | 86 | // parse close time 87 | const close = new Date(question.scheduled_close_time); 88 | // Choose the end time which is nearer (now or close) 89 | const end = new Date() < close ? new Date() : close; 90 | 91 | const byYear: { 92 | index: number; 93 | date: number; 94 | years: YearForecast[]; 95 | }[] = []; 96 | 97 | const datapoints: ChartDataPoint[] = []; 98 | 99 | // Loop from the start to the end, one day at a time 100 | let forecastIndex = -1; 101 | for (let date = start; date <= end; date.setDate(date.getDate() + 1)) { 102 | // While the forecast end time is before the current date, increment the forecast index 103 | let nextIndex = forecastIndex + 1; 104 | while ( 105 | forecastData?.[nextIndex] && 106 | new Date(forecastData[nextIndex]["Start Time"]) <= date 107 | ) { 108 | forecastIndex = nextIndex; 109 | nextIndex = forecastIndex + 1; 110 | } 111 | if (forecastIndex === -1) { 112 | console.error("Forecast index is -1"); 113 | throw new Error("Forecast index is -1"); 114 | } 115 | 116 | const results = forecastResults[forecastIndex]; 117 | 118 | // Add the results to the dataset 119 | byYear.push({ index: forecastIndex, date: date.getTime(), years: results }); 120 | 121 | // Add the bounds to the datapoints 122 | const bounds = boundsResults[forecastIndex]; 123 | if (bounds) { 124 | datapoints.push({ 125 | date: date.toISOString(), 126 | value: bounds.value, 127 | range: bounds.range, 128 | }); 129 | } 130 | } 131 | 132 | return { question, byYear, datapoints }; 133 | } 134 | 135 | /** 136 | * This function takes in a timestamp and returns the nearest year 137 | * on or after the timestamp. 138 | */ 139 | export function getStartYear(timestamp: number) { 140 | const date = new Date(timestamp); 141 | const year = date.getFullYear(); 142 | const startDate = new Date(year, 0, 1); 143 | if (date <= startDate) { 144 | return year; 145 | } 146 | return year + 1; 147 | } 148 | 149 | /** 150 | * This function takes in a timestamp and returns the nearest year 151 | * on or before the timestamp. 152 | */ 153 | export function getEndYear(timestamp: number) { 154 | const date = new Date(timestamp); 155 | const year = date.getFullYear(); 156 | const endDate = new Date(year, 0, 1); 157 | if (date >= endDate) { 158 | return year; 159 | } 160 | return year - 1; 161 | } 162 | 163 | function getInterpolatedIndex(data: number[], index: number, target: number) { 164 | // We need to get an interpolated index that's closest to the target 165 | const lowerIndex = index - 1; 166 | const upperIndex = index; 167 | const lower = data[lowerIndex]; 168 | const upper = data[upperIndex]; 169 | const interpolatedIndex = lowerIndex + (target - lower) / (upper - lower); 170 | return interpolatedIndex; 171 | } 172 | -------------------------------------------------------------------------------- /src/lib/services/metaculus-download.server.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataPoint, MetaculusForecast, MetaculusResponse } from "../types"; 2 | import { getInverseTransform, getTransform } from "./metaculus"; 3 | import tmp from "tmp"; 4 | import fs from "fs"; 5 | import path from "path"; 6 | import extract from "extract-zip"; 7 | import Papa from "papaparse"; 8 | 9 | type YearForecast = { year: number; cdfValue: number; pdfValue: number }; 10 | const METACULUS_API = "https://www.metaculus.com/api"; 11 | 12 | export async function downloadMetaculusData(questionId: number) { 13 | if (!process.env.METACULUS_API_KEY) { 14 | throw new Error("METACULUS_API_KEY not set"); 15 | } 16 | 17 | // Create temp directory 18 | const tmpDir = tmp.dirSync(); 19 | const zipPath = path.join(tmpDir.name, `metaculus-${questionId}.zip`); 20 | 21 | // Download data 22 | const params = new URLSearchParams({ 23 | aggregation_methods: "recency_weighted", 24 | minimize: "true", 25 | include_comments: "false", 26 | }); 27 | 28 | const url = `${METACULUS_API}/posts/${questionId}/download-data/?${params}`; 29 | const response = await fetch(url, { 30 | headers: { 31 | Authorization: `Token ${process.env.METACULUS_API_KEY}`, 32 | }, 33 | }); 34 | 35 | if (!response.ok) { 36 | throw new Error(`Failed to fetch data: ${response.status}`); 37 | } 38 | 39 | // Save zip file 40 | const buffer = Buffer.from(await response.arrayBuffer()); 41 | fs.writeFileSync(zipPath, buffer); 42 | 43 | // Extract zip 44 | await extract(zipPath, { dir: tmpDir.name }); 45 | 46 | // Read and parse the files 47 | const forecastData = ( 48 | Papa.parse( 49 | fs.readFileSync(path.join(tmpDir.name, "forecast_data.csv"), "utf8"), 50 | { header: true }, 51 | ).data as MetaculusForecast[] 52 | ).filter((row: { "Question ID": string }) => row["Question ID"]); 53 | 54 | // Fetch question data from metaculus 55 | const questionData = await fetch(`${METACULUS_API}/posts/${questionId}/`, { 56 | headers: { Authorization: `Token ${process.env.METACULUS_API_KEY}` }, 57 | }); 58 | 59 | const { question } = (await questionData.json()) as { 60 | question: MetaculusResponse["question"]; 61 | }; 62 | 63 | // Cleanup 64 | fs.rmSync(tmpDir.name, { recursive: true, force: true }); 65 | 66 | const rangeStart = getStartYear(question.scaling.range_min * 1000); 67 | const rangeEnd = getEndYear(question.scaling.range_max * 1000); 68 | const inverseTransform = getInverseTransform(question.scaling); 69 | const transform = getTransform(question.scaling); 70 | 71 | const boundsResults: (Omit | null)[] = []; 72 | const forecastResults: YearForecast[][] = []; 73 | for (let i = 0; i < forecastData.length; i++) { 74 | const forecast = forecastData[i]; 75 | const cdfDataPoints = JSON.parse(forecast["Continuous CDF"]) as number[]; 76 | 77 | // Determine per-year probabilities 78 | const results: YearForecast[] = []; 79 | for (let year = rangeStart; year <= rangeEnd; year++) { 80 | const currentYear = new Date(year, 0, 1); 81 | const unitValue = inverseTransform(currentYear.getTime() / 1000); 82 | 83 | const roughIndex = unitValue / (1 / 200); 84 | const lowerIndex = Math.floor(roughIndex); 85 | const upperIndex = Math.ceil(roughIndex); 86 | 87 | const lowerValue = cdfDataPoints[lowerIndex]; 88 | const upperValue = cdfDataPoints[upperIndex]; 89 | 90 | const cdfValue = 91 | lowerValue + (upperValue - lowerValue) * (roughIndex - lowerIndex); 92 | 93 | results.push({ 94 | year, 95 | cdfValue, 96 | pdfValue: cdfValue - (results[results.length - 1]?.cdfValue || 0), 97 | }); 98 | } 99 | forecastResults.push(results); 100 | 101 | // Determine the bounds 102 | let lower = 0, 103 | median = 0, 104 | upper = 0; 105 | for (let i = 0; i < cdfDataPoints.length; i++) { 106 | const value = cdfDataPoints[i]; 107 | if (value > 0.1 && !lower) { 108 | lower = transform(getInterpolatedIndex(cdfDataPoints, i, 0.1) / 200); 109 | } 110 | if (value > 0.5 && !median) { 111 | median = transform(getInterpolatedIndex(cdfDataPoints, i, 0.5) / 200); 112 | } 113 | if (value > 0.9 && !upper) { 114 | upper = transform(getInterpolatedIndex(cdfDataPoints, i, 0.9) / 200); 115 | } 116 | } 117 | 118 | if (lower && median && upper) { 119 | boundsResults.push({ 120 | value: median, 121 | range: [lower, upper], 122 | }); 123 | } else { 124 | boundsResults.push(null); 125 | } 126 | } 127 | 128 | // Read first forecast to get start date 129 | const start = new Date(forecastData[0]["Start Time"]); 130 | 131 | // parse close time 132 | const close = new Date(question.scheduled_close_time); 133 | // Choose the end time which is nearer (now or close) 134 | const end = new Date() < close ? new Date() : close; 135 | 136 | const byYear: { 137 | index: number; 138 | date: number; 139 | years: YearForecast[]; 140 | }[] = []; 141 | 142 | const datapoints: ChartDataPoint[] = []; 143 | 144 | // Loop from the start to the end, one day at a time 145 | let forecastIndex = -1; 146 | for (let date = start; date <= end; date.setDate(date.getDate() + 1)) { 147 | // While the forecast end time is before the current date, increment the forecast index 148 | let nextIndex = forecastIndex + 1; 149 | while ( 150 | forecastData?.[nextIndex] && 151 | new Date(forecastData[nextIndex]["Start Time"]) <= date 152 | ) { 153 | forecastIndex = nextIndex; 154 | nextIndex = forecastIndex + 1; 155 | } 156 | if (forecastIndex === -1) { 157 | console.error("Forecast index is -1"); 158 | throw new Error("Forecast index is -1"); 159 | } 160 | 161 | const results = forecastResults[forecastIndex]; 162 | 163 | // Add the results to the dataset 164 | byYear.push({ index: forecastIndex, date: date.getTime(), years: results }); 165 | 166 | // Add the bounds to the datapoints 167 | const bounds = boundsResults[forecastIndex]; 168 | if (bounds) { 169 | datapoints.push({ 170 | date: date.toISOString(), 171 | value: bounds.value, 172 | range: bounds.range, 173 | }); 174 | } 175 | } 176 | 177 | return { question, byYear, datapoints }; 178 | } 179 | 180 | /** 181 | * This function takes in a timestamp and returns the nearest year 182 | * on or after the timestamp. 183 | */ 184 | export function getStartYear(timestamp: number) { 185 | const date = new Date(timestamp); 186 | const year = date.getFullYear(); 187 | const startDate = new Date(year, 0, 1); 188 | if (date <= startDate) { 189 | return year; 190 | } 191 | return year + 1; 192 | } 193 | 194 | /** 195 | * This function takes in a timestamp and returns the nearest year 196 | * on or before the timestamp. 197 | */ 198 | export function getEndYear(timestamp: number) { 199 | const date = new Date(timestamp); 200 | const year = date.getFullYear(); 201 | const endDate = new Date(year, 0, 1); 202 | if (date >= endDate) { 203 | return year; 204 | } 205 | return year - 1; 206 | } 207 | 208 | function getInterpolatedIndex(data: number[], index: number, target: number) { 209 | // We need to get an interpolated index that's closest to the target 210 | const lowerIndex = index - 1; 211 | const upperIndex = index; 212 | const lower = data[lowerIndex]; 213 | const upper = data[upperIndex]; 214 | const interpolatedIndex = lowerIndex + (target - lower) / (upper - lower); 215 | return interpolatedIndex; 216 | } 217 | -------------------------------------------------------------------------------- /public/example-data/manifold.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "0R2uyyOz2L", 3 | "creatorId": "b6UJ9jqwTCVMSRJksyLekhJnN673", 4 | "creatorUsername": "JZB", 5 | "creatorName": "JZB", 6 | "createdTime": 1734677386486, 7 | "creatorAvatarUrl": "https://lh3.googleusercontent.com/a/ALm5wu2fc6Wxpp3rzQ2Ejq71E_roTFxTk4L7bMq-IsQmfg=s96-c", 8 | "closeTime": 1767225540000, 9 | "question": "Will there be more than 1,000 confirmed human cases of H5N1 bird flu in the US by the end of 2025?", 10 | "slug": "will-there-be-more-than-1000-confir", 11 | "url": "https://manifold.markets/JZB/will-there-be-more-than-1000-confir", 12 | "pool": { 13 | "NO": 1481.150610188034, 14 | "YES": 675.1507869095436 15 | }, 16 | "probability": 0.6868940548764156, 17 | "p": 0.5000000000000007, 18 | "totalLiquidity": 1000, 19 | "outcomeType": "BINARY", 20 | "mechanism": "cpmm-1", 21 | "volume": 6563.169595734074, 22 | "volume24Hours": 896.33931, 23 | "isResolved": false, 24 | "uniqueBettorCount": 39, 25 | "lastUpdatedTime": 1735673520566, 26 | "lastBetTime": 1735669922147, 27 | "lastCommentTime": 1735673518561, 28 | "marketTier": "plus", 29 | "token": "MANA", 30 | "description": { 31 | "type": "doc", 32 | "content": [ 33 | { 34 | "type": "paragraph", 35 | "content": [ 36 | { 37 | "text": "According to the CDC, will there be more than 1,000 confirmed cases of H5N1 Bird Flu in humans in the United States by the end of 2025?", 38 | "type": "text" 39 | } 40 | ] 41 | }, 42 | { 43 | "type": "paragraph" 44 | }, 45 | { 46 | "type": "paragraph", 47 | "content": [ 48 | { 49 | "text": "Background", 50 | "type": "text", 51 | "marks": [ 52 | { 53 | "type": "bold" 54 | } 55 | ] 56 | }, 57 | { 58 | "text": " H5N1 bird flu has recently gained attention in the US with 61 confirmed human cases since April 2024. Most cases have been mild and linked to direct exposure to infected animals, particularly dairy cows and poultry. The CDC maintains that the immediate risk to the general public remains low, with no evidence of person-to-person transmission in the United States.", 59 | "type": "text" 60 | } 61 | ] 62 | }, 63 | { 64 | "type": "paragraph", 65 | "content": [ 66 | { 67 | "text": "Resolution Criteria", 68 | "type": "text", 69 | "marks": [ 70 | { 71 | "type": "bold" 72 | } 73 | ] 74 | }, 75 | { 76 | "text": " This market will resolve based on official CDC data for confirmed human H5N1 cases in the United States as of December 31, 2025, 11:59 PM ET. The market will resolve YES if the CDC reports more than 1,000 confirmed human cases within this timeframe, and NO otherwise.", 77 | "type": "text" 78 | } 79 | ] 80 | }, 81 | { 82 | "type": "paragraph", 83 | "content": [ 84 | { 85 | "text": "Considerations", 86 | "type": "text", 87 | "marks": [ 88 | { 89 | "type": "bold" 90 | } 91 | ] 92 | } 93 | ] 94 | }, 95 | { 96 | "type": "bulletList", 97 | "content": [ 98 | { 99 | "type": "listItem", 100 | "content": [ 101 | { 102 | "type": "paragraph", 103 | "content": [ 104 | { 105 | "text": "The current case count represents the first known human infections with H5N1 bird flu linked to dairy cow exposures globally", 106 | "type": "text" 107 | } 108 | ] 109 | } 110 | ] 111 | }, 112 | { 113 | "type": "listItem", 114 | "content": [ 115 | { 116 | "type": "paragraph", 117 | "content": [ 118 | { 119 | "text": "Most human infections have occurred in people with direct exposure to infected animals", 120 | "type": "text" 121 | } 122 | ] 123 | } 124 | ] 125 | }, 126 | { 127 | "type": "listItem", 128 | "content": [ 129 | { 130 | "type": "paragraph", 131 | "content": [ 132 | { 133 | "text": "The CDC and state health departments are actively monitoring the situation and conducting surveillance", 134 | "type": "text" 135 | } 136 | ] 137 | } 138 | ] 139 | }, 140 | { 141 | "type": "listItem", 142 | "content": [ 143 | { 144 | "type": "paragraph", 145 | "content": [ 146 | { 147 | "text": "Historical data shows that human H5N1 infections have been rare, even in countries with widespread animal outbreaks", 148 | "type": "text" 149 | } 150 | ] 151 | } 152 | ] 153 | }, 154 | { 155 | "type": "listItem", 156 | "content": [ 157 | { 158 | "type": "paragraph", 159 | "content": [ 160 | { 161 | "text": "Changes in virus characteristics, such as increased human-to-human transmission, could significantly impact case numbers", 162 | "type": "text" 163 | } 164 | ] 165 | } 166 | ] 167 | } 168 | ] 169 | } 170 | ] 171 | }, 172 | "coverImageUrl": "https://storage.googleapis.com/mantic-markets.appspot.com/contract-images/JZB/sE0E8h9dEZ.jpg", 173 | "groupSlugs": [ 174 | "h5n1-bird-flu", 175 | "infectious-disease", 176 | "pandemic", 177 | "public-health" 178 | ], 179 | "textDescription": "According to the CDC, will there be more than 1,000 confirmed cases of H5N1 Bird Flu in humans in the United States by the end of 2025?\n\nBackground H5N1 bird flu has recently gained attention in the US with 61 confirmed human cases since April 2024. Most cases have been mild and linked to direct exposure to infected animals, particularly dairy cows and poultry. The CDC maintains that the immediate risk to the general public remains low, with no evidence of person-to-person transmission in the United States.\n\nResolution Criteria This market will resolve based on official CDC data for confirmed human H5N1 cases in the United States as of December 31, 2025, 11:59 PM ET. The market will resolve YES if the CDC reports more than 1,000 confirmed human cases within this timeframe, and NO otherwise.\n\nConsiderations\n\nThe current case count represents the first known human infections with H5N1 bird flu linked to dairy cow exposures globally\n\nMost human infections have occurred in people with direct exposure to infected animals\n\nThe CDC and state health departments are actively monitoring the situation and conducting surveillance\n\nHistorical data shows that human H5N1 infections have been rare, even in countries with widespread animal outbreaks\n\nChanges in virus characteristics, such as increased human-to-human transmission, could significantly impact case numbers" 180 | } 181 | -------------------------------------------------------------------------------- /src/lib/createIndex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a normalized probability index for AGI arrival predictions across multiple data sources. 3 | * 4 | * Key constraints and decisions: 5 | * - Time range: 2024-2199 (176 years) 6 | * - 2024: Lower bound from Manifold data 7 | * - 2199: Upper bound from full AGI predictions (2200 for weak AGI) 8 | * 9 | * Data sources: 10 | * - Manifold: 2024-2049 (with 2049+ covered by final bet) 11 | * - Full AGI: Through 2199 12 | * - Weak AGI: Through 2200 13 | * 14 | * Approach: 15 | * - Normalize each dataset within overlapping years for fair comparison 16 | * - Convert to array of 176 elements (one per year) 17 | * - Handle null values for missing data points 18 | */ 19 | 20 | import { downloadMetaculusData } from "./services/metaculus-download"; 21 | import { getManifoldHistoricalData } from "./services/manifold-historical"; 22 | import { ChartDataPoint } from "./types"; 23 | import { fetchKalshiData } from "./services/kalshi"; 24 | import { getNearestKalshiIndex } from "./kalshi/index-helpers"; 25 | 26 | interface HasDate { 27 | date: number; 28 | } 29 | 30 | interface ProcessedData extends HasDate { 31 | years: number[]; 32 | } 33 | 34 | /** 35 | * This function creates a year-by-year index of the probability of AGI arrival over time 36 | */ 37 | export function createIndex( 38 | weakAgiData: Awaited>, 39 | fullAgiData: Awaited>, 40 | turingTestData: Awaited>, 41 | manifoldData: Awaited>, 42 | kalshiData: Awaited>, 43 | ) { 44 | const fullAgiPre = fullAgiData.byYear; 45 | const weakAgiPre = weakAgiData.byYear; 46 | const turingTestPre = turingTestData.byYear; 47 | const manifoldPre = manifoldData.byYear; 48 | 49 | // Get start dates for each source 50 | const fullAgiStartDate = fullAgiPre[0].date; 51 | const weakAgiStartDate = weakAgiPre[0].date; 52 | const turingTestStartDate = turingTestPre[0].date; 53 | const manifoldStartDate = manifoldPre[0].date; 54 | const kalshiStartDate = kalshiData[0]?.date 55 | ? new Date(kalshiData[0].date).getTime() 56 | : Infinity; 57 | 58 | const fullAgi: ProcessedData[] = fullAgiData.byYear.map((item) => { 59 | const years = Array.from({ length: 176 }, (_, i) => { 60 | const year = 2024 + i; 61 | const found = item.years.find((x) => x.year === year); 62 | if (!found) return 0; 63 | return found.pdfValue; 64 | }); 65 | 66 | // Normalize the years so that they sum to 1 67 | const sum = years.reduce((acc, curr) => acc + curr, 0); 68 | const normalized = years.map((x) => x / sum); 69 | 70 | return { 71 | date: item.date, 72 | years: normalized, 73 | }; 74 | }); 75 | 76 | const weakAgi: ProcessedData[] = weakAgiData.byYear.map((item) => { 77 | const years = Array.from({ length: 176 }, (_, i) => { 78 | const year = 2024 + i; 79 | const found = item.years.find((x) => x.year === year); 80 | if (!found) return 0; 81 | return found.pdfValue; 82 | }); 83 | 84 | // Normalize the years so that they sum to 1 85 | const sum = years.reduce((acc, curr) => acc + curr, 0); 86 | const normalized = years.map((x) => x / sum); 87 | 88 | return { 89 | date: item.date, 90 | years: normalized, 91 | }; 92 | }); 93 | 94 | const turingTest: ProcessedData[] = turingTestData.byYear.map((item) => { 95 | const years = Array.from({ length: 176 }, (_, i) => { 96 | const year = 2024 + i; 97 | const found = item.years.find((x) => x.year === year); 98 | if (!found) return 0; 99 | return found.pdfValue; 100 | }); 101 | 102 | // Normalize the years so that they sum to 1 103 | const sum = years.reduce((acc, curr) => acc + curr, 0); 104 | const normalized = years.map((x) => x / sum); 105 | 106 | return { 107 | date: item.date, 108 | years: normalized, 109 | }; 110 | }); 111 | 112 | const manifold: ProcessedData[] = manifoldData.byYear.map((item) => { 113 | // Find last year present in the probabilities 114 | const lastYear = Math.max(...Object.keys(item.probabilities).map(Number)); 115 | 116 | // Find number of years from last year to 2199, inclusive 117 | const numYears = 2199 - lastYear + 1; 118 | 119 | // Spread the probability of the final year over all final years 120 | const spread = item.probabilities[lastYear]! / numYears; 121 | 122 | const years = Array.from({ length: 176 }, (_, i) => { 123 | const year = 2024 + i; 124 | if (year < lastYear) { 125 | const found = item.probabilities[year]; 126 | if (!found) return 0; 127 | return found; 128 | } 129 | 130 | return spread; 131 | }); 132 | 133 | // Normalize the years so that they sum to 1 134 | const sum = years.reduce((acc, curr) => acc + curr, 0); 135 | const normalized = years.map((x) => x / sum); 136 | 137 | return { 138 | date: item.date, 139 | years: normalized, 140 | }; 141 | }); 142 | 143 | // In all cases we have a property called "date" which has a unix timestamp 144 | // Let's pick the earliest 145 | const startDate = Math.min( 146 | fullAgiPre[0].date, 147 | weakAgiPre[0].date, 148 | turingTestPre[0].date, 149 | manifoldPre[0].date, 150 | ); 151 | 152 | // Now let's pick the latest 153 | const endDate = Math.max( 154 | fullAgiPre[fullAgiPre.length - 1].date, 155 | weakAgiPre[weakAgiPre.length - 1].date, 156 | turingTestPre[turingTestPre.length - 1].date, 157 | manifoldPre[manifoldPre.length - 1].date, 158 | ); 159 | 160 | const data: ChartDataPoint[] = []; 161 | 162 | // We'll iterate over each day in the range 163 | for (let date = startDate; date <= endDate; date += 86400000) { 164 | // Get the nearest data point for each of the three sources 165 | 166 | const samples = [ 167 | getNearest(fullAgi, date), 168 | getNearest(weakAgi, date), 169 | getNearest(turingTest, date), 170 | getNearest(manifold, date), 171 | ].filter((x) => x !== null); 172 | 173 | // For the samples that exist, find the average for each year 174 | let averages = Array.from({ length: 176 }, (_, i) => { 175 | const sum = samples.reduce((acc, curr) => acc + curr.years[i], 0); 176 | return sum / samples.length; 177 | }); 178 | 179 | // Manipulate averages using kalshi data here: 180 | // ... 181 | 182 | // Check for kalshi data for this date 183 | const kalshiIndex = getNearestKalshiIndex(date, kalshiData); 184 | if (kalshiIndex) { 185 | const kalshiValue = kalshiData[kalshiIndex]; 186 | const probabilityBefore2030 = averages 187 | .slice(0, 6) 188 | .reduce((acc, curr) => acc + curr, 0); 189 | const probabilityAfter2030 = averages 190 | .slice(6) 191 | .reduce((acc, curr) => acc + curr, 0); 192 | 193 | const kalshiProbability = kalshiValue.value / 100; 194 | const scalingFactorBefore = kalshiProbability / probabilityBefore2030; 195 | const scalingFactorAfter = (1 - kalshiProbability) / probabilityAfter2030; 196 | 197 | const kalshiAverages = averages.map((prob, i) => { 198 | const scalar = i < 6 ? scalingFactorBefore : scalingFactorAfter; 199 | return prob * scalar; 200 | }); 201 | 202 | // Factor in kalshi averages as 1/5 of the total 203 | averages = averages.map((prob, i) => { 204 | return prob * 0.8 + kalshiAverages[i] * 0.2; 205 | }); 206 | } 207 | 208 | // We're going to track lower, median, and upper bounds 209 | // at 10%, 50%, and 90% 210 | let lower = 0; 211 | let median = 0; 212 | let upper = 0; 213 | 214 | let sum = 0; 215 | for (let i = 0; i < 176; i++) { 216 | const value = averages[i]; 217 | sum += value; 218 | if (sum >= 0.1 && !lower) lower = i + 2024; 219 | if (sum >= 0.5 && !median) median = i + 2024; 220 | if (sum >= 0.9 && !upper) upper = i + 2024; 221 | } 222 | 223 | data.push({ 224 | value: median, 225 | range: [lower, upper], 226 | date: new Date(date).toISOString(), 227 | }); 228 | } 229 | 230 | // Slice the data to only begin after February 2nd, 2020 231 | const errorDate = new Date("2020-02-02"); 232 | const startIndex = data.findIndex((x) => new Date(x.date) >= errorDate); 233 | 234 | const computedStartDate = 235 | startIndex === -1 ? new Date(data[0].date).getTime() : errorDate.getTime(); 236 | 237 | // Get the latest start date between computed and individual sources 238 | const startDates = { 239 | computed: computedStartDate, 240 | fullAgi: Math.max(fullAgiStartDate, computedStartDate), 241 | weakAgi: Math.max(weakAgiStartDate, computedStartDate), 242 | turingTest: Math.max(turingTestStartDate, computedStartDate), 243 | manifold: Math.max(manifoldStartDate, computedStartDate), 244 | kalshi: Math.max(kalshiStartDate, computedStartDate), 245 | }; 246 | 247 | if (startIndex === -1) { 248 | return { data, startDates }; 249 | } 250 | 251 | return { data: data.slice(startIndex), startDates }; 252 | } 253 | 254 | /** 255 | * Given a date, this function will return the element in 256 | * the array that occurs on the same day, or null if none exists. 257 | */ 258 | function getNearest(data: T[], date: number): T | null { 259 | // Convert timestamps to start of day for comparison 260 | const targetDay = Math.floor(date / 86400000) * 86400000; 261 | 262 | const match = data.find((item) => { 263 | const itemDay = Math.floor(item.date / 86400000) * 86400000; 264 | return itemDay === targetDay; 265 | }); 266 | 267 | return match ?? null; 268 | } 269 | -------------------------------------------------------------------------------- /public/example-data/timeseries.json: -------------------------------------------------------------------------------- 1 | { 2 | "history": [ 3 | { 4 | "t": 1735326003, 5 | "p": 0.45 6 | }, 7 | { 8 | "t": 1735327803, 9 | "p": 0.39 10 | }, 11 | { 12 | "t": 1735329603, 13 | "p": 0.385 14 | }, 15 | { 16 | "t": 1735331403, 17 | "p": 0.385 18 | }, 19 | { 20 | "t": 1735333203, 21 | "p": 0.385 22 | }, 23 | { 24 | "t": 1735335003, 25 | "p": 0.355 26 | }, 27 | { 28 | "t": 1735336803, 29 | "p": 0.325 30 | }, 31 | { 32 | "t": 1735338603, 33 | "p": 0.325 34 | }, 35 | { 36 | "t": 1735340403, 37 | "p": 0.32 38 | }, 39 | { 40 | "t": 1735342203, 41 | "p": 0.265 42 | }, 43 | { 44 | "t": 1735344003, 45 | "p": 0.27 46 | }, 47 | { 48 | "t": 1735345803, 49 | "p": 0.27 50 | }, 51 | { 52 | "t": 1735347603, 53 | "p": 0.25 54 | }, 55 | { 56 | "t": 1735349403, 57 | "p": 0.31 58 | }, 59 | { 60 | "t": 1735351203, 61 | "p": 0.31 62 | }, 63 | { 64 | "t": 1735353003, 65 | "p": 0.305 66 | }, 67 | { 68 | "t": 1735354803, 69 | "p": 0.27 70 | }, 71 | { 72 | "t": 1735356603, 73 | "p": 0.26 74 | }, 75 | { 76 | "t": 1735358403, 77 | "p": 0.28 78 | }, 79 | { 80 | "t": 1735360203, 81 | "p": 0.285 82 | }, 83 | { 84 | "t": 1735362003, 85 | "p": 0.285 86 | }, 87 | { 88 | "t": 1735363803, 89 | "p": 0.29 90 | }, 91 | { 92 | "t": 1735365603, 93 | "p": 0.29 94 | }, 95 | { 96 | "t": 1735367403, 97 | "p": 0.29 98 | }, 99 | { 100 | "t": 1735369203, 101 | "p": 0.29 102 | }, 103 | { 104 | "t": 1735371003, 105 | "p": 0.34 106 | }, 107 | { 108 | "t": 1735372804, 109 | "p": 0.34 110 | }, 111 | { 112 | "t": 1735374604, 113 | "p": 0.35 114 | }, 115 | { 116 | "t": 1735376403, 117 | "p": 0.365 118 | }, 119 | { 120 | "t": 1735378203, 121 | "p": 0.35 122 | }, 123 | { 124 | "t": 1735380003, 125 | "p": 0.335 126 | }, 127 | { 128 | "t": 1735381803, 129 | "p": 0.325 130 | }, 131 | { 132 | "t": 1735383603, 133 | "p": 0.33 134 | }, 135 | { 136 | "t": 1735385404, 137 | "p": 0.33 138 | }, 139 | { 140 | "t": 1735387203, 141 | "p": 0.33 142 | }, 143 | { 144 | "t": 1735389003, 145 | "p": 0.33 146 | }, 147 | { 148 | "t": 1735390803, 149 | "p": 0.33 150 | }, 151 | { 152 | "t": 1735392604, 153 | "p": 0.295 154 | }, 155 | { 156 | "t": 1735394403, 157 | "p": 0.305 158 | }, 159 | { 160 | "t": 1735396203, 161 | "p": 0.29 162 | }, 163 | { 164 | "t": 1735398003, 165 | "p": 0.29 166 | }, 167 | { 168 | "t": 1735399804, 169 | "p": 0.285 170 | }, 171 | { 172 | "t": 1735401604, 173 | "p": 0.285 174 | }, 175 | { 176 | "t": 1735403404, 177 | "p": 0.285 178 | }, 179 | { 180 | "t": 1735405203, 181 | "p": 0.285 182 | }, 183 | { 184 | "t": 1735407004, 185 | "p": 0.285 186 | }, 187 | { 188 | "t": 1735408803, 189 | "p": 0.285 190 | }, 191 | { 192 | "t": 1735410604, 193 | "p": 0.285 194 | }, 195 | { 196 | "t": 1735412404, 197 | "p": 0.29 198 | }, 199 | { 200 | "t": 1735414203, 201 | "p": 0.285 202 | }, 203 | { 204 | "t": 1735416004, 205 | "p": 0.28 206 | }, 207 | { 208 | "t": 1735417803, 209 | "p": 0.285 210 | }, 211 | { 212 | "t": 1735419604, 213 | "p": 0.285 214 | }, 215 | { 216 | "t": 1735421403, 217 | "p": 0.285 218 | }, 219 | { 220 | "t": 1735423203, 221 | "p": 0.285 222 | }, 223 | { 224 | "t": 1735425004, 225 | "p": 0.285 226 | }, 227 | { 228 | "t": 1735426803, 229 | "p": 0.285 230 | }, 231 | { 232 | "t": 1735428604, 233 | "p": 0.285 234 | }, 235 | { 236 | "t": 1735430403, 237 | "p": 0.285 238 | }, 239 | { 240 | "t": 1735432203, 241 | "p": 0.285 242 | }, 243 | { 244 | "t": 1735434004, 245 | "p": 0.285 246 | }, 247 | { 248 | "t": 1735435803, 249 | "p": 0.285 250 | }, 251 | { 252 | "t": 1735437603, 253 | "p": 0.285 254 | }, 255 | { 256 | "t": 1735439403, 257 | "p": 0.285 258 | }, 259 | { 260 | "t": 1735441204, 261 | "p": 0.285 262 | }, 263 | { 264 | "t": 1735443003, 265 | "p": 0.285 266 | }, 267 | { 268 | "t": 1735444804, 269 | "p": 0.285 270 | }, 271 | { 272 | "t": 1735446602, 273 | "p": 0.285 274 | }, 275 | { 276 | "t": 1735448404, 277 | "p": 0.285 278 | }, 279 | { 280 | "t": 1735450203, 281 | "p": 0.285 282 | }, 283 | { 284 | "t": 1735452003, 285 | "p": 0.285 286 | }, 287 | { 288 | "t": 1735453804, 289 | "p": 0.285 290 | }, 291 | { 292 | "t": 1735455603, 293 | "p": 0.285 294 | }, 295 | { 296 | "t": 1735457404, 297 | "p": 0.285 298 | }, 299 | { 300 | "t": 1735459204, 301 | "p": 0.285 302 | }, 303 | { 304 | "t": 1735461004, 305 | "p": 0.285 306 | }, 307 | { 308 | "t": 1735462803, 309 | "p": 0.285 310 | }, 311 | { 312 | "t": 1735464604, 313 | "p": 0.285 314 | }, 315 | { 316 | "t": 1735466403, 317 | "p": 0.285 318 | }, 319 | { 320 | "t": 1735468204, 321 | "p": 0.285 322 | }, 323 | { 324 | "t": 1735470003, 325 | "p": 0.285 326 | }, 327 | { 328 | "t": 1735471803, 329 | "p": 0.285 330 | }, 331 | { 332 | "t": 1735473604, 333 | "p": 0.285 334 | }, 335 | { 336 | "t": 1735475404, 337 | "p": 0.285 338 | }, 339 | { 340 | "t": 1735477203, 341 | "p": 0.285 342 | }, 343 | { 344 | "t": 1735479003, 345 | "p": 0.285 346 | }, 347 | { 348 | "t": 1735480803, 349 | "p": 0.285 350 | }, 351 | { 352 | "t": 1735482604, 353 | "p": 0.285 354 | }, 355 | { 356 | "t": 1735484403, 357 | "p": 0.285 358 | }, 359 | { 360 | "t": 1735486203, 361 | "p": 0.285 362 | }, 363 | { 364 | "t": 1735488004, 365 | "p": 0.285 366 | }, 367 | { 368 | "t": 1735489803, 369 | "p": 0.285 370 | }, 371 | { 372 | "t": 1735491603, 373 | "p": 0.285 374 | }, 375 | { 376 | "t": 1735493403, 377 | "p": 0.285 378 | }, 379 | { 380 | "t": 1735495204, 381 | "p": 0.285 382 | }, 383 | { 384 | "t": 1735497003, 385 | "p": 0.285 386 | }, 387 | { 388 | "t": 1735498803, 389 | "p": 0.285 390 | }, 391 | { 392 | "t": 1735500604, 393 | "p": 0.285 394 | }, 395 | { 396 | "t": 1735502403, 397 | "p": 0.285 398 | }, 399 | { 400 | "t": 1735504203, 401 | "p": 0.285 402 | }, 403 | { 404 | "t": 1735506003, 405 | "p": 0.285 406 | }, 407 | { 408 | "t": 1735507804, 409 | "p": 0.285 410 | }, 411 | { 412 | "t": 1735509603, 413 | "p": 0.285 414 | }, 415 | { 416 | "t": 1735511403, 417 | "p": 0.285 418 | }, 419 | { 420 | "t": 1735513203, 421 | "p": 0.285 422 | }, 423 | { 424 | "t": 1735515003, 425 | "p": 0.285 426 | }, 427 | { 428 | "t": 1735516803, 429 | "p": 0.285 430 | }, 431 | { 432 | "t": 1735518603, 433 | "p": 0.285 434 | }, 435 | { 436 | "t": 1735520403, 437 | "p": 0.285 438 | }, 439 | { 440 | "t": 1735522203, 441 | "p": 0.285 442 | }, 443 | { 444 | "t": 1735524003, 445 | "p": 0.285 446 | }, 447 | { 448 | "t": 1735525803, 449 | "p": 0.285 450 | }, 451 | { 452 | "t": 1735527604, 453 | "p": 0.285 454 | }, 455 | { 456 | "t": 1735529404, 457 | "p": 0.285 458 | }, 459 | { 460 | "t": 1735531204, 461 | "p": 0.285 462 | }, 463 | { 464 | "t": 1735533003, 465 | "p": 0.285 466 | }, 467 | { 468 | "t": 1735534804, 469 | "p": 0.285 470 | }, 471 | { 472 | "t": 1735536604, 473 | "p": 0.285 474 | }, 475 | { 476 | "t": 1735538404, 477 | "p": 0.285 478 | }, 479 | { 480 | "t": 1735540203, 481 | "p": 0.295 482 | }, 483 | { 484 | "t": 1735542003, 485 | "p": 0.295 486 | }, 487 | { 488 | "t": 1735543803, 489 | "p": 0.295 490 | }, 491 | { 492 | "t": 1735545604, 493 | "p": 0.305 494 | }, 495 | { 496 | "t": 1735547403, 497 | "p": 0.305 498 | }, 499 | { 500 | "t": 1735549204, 501 | "p": 0.305 502 | }, 503 | { 504 | "t": 1735551003, 505 | "p": 0.305 506 | }, 507 | { 508 | "t": 1735552803, 509 | "p": 0.305 510 | }, 511 | { 512 | "t": 1735554603, 513 | "p": 0.305 514 | }, 515 | { 516 | "t": 1735556403, 517 | "p": 0.305 518 | }, 519 | { 520 | "t": 1735558203, 521 | "p": 0.305 522 | }, 523 | { 524 | "t": 1735560004, 525 | "p": 0.305 526 | }, 527 | { 528 | "t": 1735561804, 529 | "p": 0.305 530 | }, 531 | { 532 | "t": 1735563604, 533 | "p": 0.305 534 | }, 535 | { 536 | "t": 1735565404, 537 | "p": 0.305 538 | }, 539 | { 540 | "t": 1735567203, 541 | "p": 0.305 542 | }, 543 | { 544 | "t": 1735569004, 545 | "p": 0.305 546 | }, 547 | { 548 | "t": 1735570804, 549 | "p": 0.305 550 | }, 551 | { 552 | "t": 1735572603, 553 | "p": 0.305 554 | }, 555 | { 556 | "t": 1735574403, 557 | "p": 0.305 558 | }, 559 | { 560 | "t": 1735576203, 561 | "p": 0.305 562 | }, 563 | { 564 | "t": 1735578003, 565 | "p": 0.305 566 | }, 567 | { 568 | "t": 1735579803, 569 | "p": 0.305 570 | }, 571 | { 572 | "t": 1735581603, 573 | "p": 0.305 574 | }, 575 | { 576 | "t": 1735583403, 577 | "p": 0.305 578 | }, 579 | { 580 | "t": 1735585203, 581 | "p": 0.305 582 | }, 583 | { 584 | "t": 1735587004, 585 | "p": 0.305 586 | }, 587 | { 588 | "t": 1735588803, 589 | "p": 0.305 590 | }, 591 | { 592 | "t": 1735590603, 593 | "p": 0.305 594 | }, 595 | { 596 | "t": 1735592404, 597 | "p": 0.345 598 | }, 599 | { 600 | "t": 1735594204, 601 | "p": 0.335 602 | }, 603 | { 604 | "t": 1735596004, 605 | "p": 0.34 606 | }, 607 | { 608 | "t": 1735597803, 609 | "p": 0.34 610 | }, 611 | { 612 | "t": 1735599604, 613 | "p": 0.35 614 | }, 615 | { 616 | "t": 1735601403, 617 | "p": 0.35 618 | }, 619 | { 620 | "t": 1735603204, 621 | "p": 0.35 622 | }, 623 | { 624 | "t": 1735605003, 625 | "p": 0.35 626 | }, 627 | { 628 | "t": 1735606804, 629 | "p": 0.345 630 | }, 631 | { 632 | "t": 1735608604, 633 | "p": 0.345 634 | }, 635 | { 636 | "t": 1735610403, 637 | "p": 0.345 638 | }, 639 | { 640 | "t": 1735612204, 641 | "p": 0.35 642 | }, 643 | { 644 | "t": 1735614004, 645 | "p": 0.35 646 | }, 647 | { 648 | "t": 1735615804, 649 | "p": 0.35 650 | }, 651 | { 652 | "t": 1735617604, 653 | "p": 0.35 654 | }, 655 | { 656 | "t": 1735619403, 657 | "p": 0.35 658 | }, 659 | { 660 | "t": 1735621203, 661 | "p": 0.35 662 | }, 663 | { 664 | "t": 1735623004, 665 | "p": 0.38 666 | }, 667 | { 668 | "t": 1735624804, 669 | "p": 0.37 670 | }, 671 | { 672 | "t": 1735626603, 673 | "p": 0.38 674 | }, 675 | { 676 | "t": 1735628403, 677 | "p": 0.38 678 | }, 679 | { 680 | "t": 1735630203, 681 | "p": 0.38 682 | }, 683 | { 684 | "t": 1735632004, 685 | "p": 0.38 686 | }, 687 | { 688 | "t": 1735633803, 689 | "p": 0.38 690 | }, 691 | { 692 | "t": 1735635603, 693 | "p": 0.38 694 | }, 695 | { 696 | "t": 1735637403, 697 | "p": 0.395 698 | }, 699 | { 700 | "t": 1735639204, 701 | "p": 0.395 702 | }, 703 | { 704 | "t": 1735641004, 705 | "p": 0.56 706 | }, 707 | { 708 | "t": 1735642804, 709 | "p": 0.495 710 | }, 711 | { 712 | "t": 1735644603, 713 | "p": 0.435 714 | }, 715 | { 716 | "t": 1735646403, 717 | "p": 0.41 718 | }, 719 | { 720 | "t": 1735648203, 721 | "p": 0.425 722 | }, 723 | { 724 | "t": 1735650003, 725 | "p": 0.39 726 | }, 727 | { 728 | "t": 1735651804, 729 | "p": 0.395 730 | }, 731 | { 732 | "t": 1735653603, 733 | "p": 0.395 734 | }, 735 | { 736 | "t": 1735655404, 737 | "p": 0.395 738 | }, 739 | { 740 | "t": 1735657203, 741 | "p": 0.39 742 | }, 743 | { 744 | "t": 1735659004, 745 | "p": 0.39 746 | }, 747 | { 748 | "t": 1735660803, 749 | "p": 0.39 750 | }, 751 | { 752 | "t": 1735662604, 753 | "p": 0.39 754 | }, 755 | { 756 | "t": 1735664404, 757 | "p": 0.39 758 | }, 759 | { 760 | "t": 1735666203, 761 | "p": 0.39 762 | }, 763 | { 764 | "t": 1735668004, 765 | "p": 0.39 766 | }, 767 | { 768 | "t": 1735669804, 769 | "p": 0.39 770 | }, 771 | { 772 | "t": 1735671603, 773 | "p": 0.39 774 | }, 775 | { 776 | "t": 1735673403, 777 | "p": 0.39 778 | }, 779 | { 780 | "t": 1735673643, 781 | "p": 0.39 782 | } 783 | ] 784 | } 785 | -------------------------------------------------------------------------------- /scripts/kalshiData.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "date": "2024-03-29T04:00:00.000Z", 4 | "value": 75 5 | }, 6 | { 7 | "date": "2024-03-30T04:00:00.000Z", 8 | "value": 72 9 | }, 10 | { 11 | "date": "2024-03-31T04:00:00.000Z", 12 | "value": 72 13 | }, 14 | { 15 | "date": "2024-04-01T04:00:00.000Z", 16 | "value": 64 17 | }, 18 | { 19 | "date": "2024-04-02T04:00:00.000Z", 20 | "value": 64 21 | }, 22 | { 23 | "date": "2024-04-03T04:00:00.000Z", 24 | "value": 64 25 | }, 26 | { 27 | "date": "2024-04-04T04:00:00.000Z", 28 | "value": 64 29 | }, 30 | { 31 | "date": "2024-04-05T04:00:00.000Z", 32 | "value": 64 33 | }, 34 | { 35 | "date": "2024-04-06T04:00:00.000Z", 36 | "value": 64 37 | }, 38 | { 39 | "date": "2024-04-07T04:00:00.000Z", 40 | "value": 64 41 | }, 42 | { 43 | "date": "2024-04-08T04:00:00.000Z", 44 | "value": 64 45 | }, 46 | { 47 | "date": "2024-04-09T04:00:00.000Z", 48 | "value": 69 49 | }, 50 | { 51 | "date": "2024-04-10T04:00:00.000Z", 52 | "value": 69 53 | }, 54 | { 55 | "date": "2024-04-11T04:00:00.000Z", 56 | "value": 69 57 | }, 58 | { 59 | "date": "2024-04-12T04:00:00.000Z", 60 | "value": 69 61 | }, 62 | { 63 | "date": "2024-04-13T04:00:00.000Z", 64 | "value": 70 65 | }, 66 | { 67 | "date": "2024-04-14T04:00:00.000Z", 68 | "value": 70 69 | }, 70 | { 71 | "date": "2024-04-15T04:00:00.000Z", 72 | "value": 70 73 | }, 74 | { 75 | "date": "2024-04-16T04:00:00.000Z", 76 | "value": 69 77 | }, 78 | { 79 | "date": "2024-04-17T04:00:00.000Z", 80 | "value": 69 81 | }, 82 | { 83 | "date": "2024-04-18T04:00:00.000Z", 84 | "value": 69 85 | }, 86 | { 87 | "date": "2024-04-19T04:00:00.000Z", 88 | "value": 69 89 | }, 90 | { 91 | "date": "2024-04-20T04:00:00.000Z", 92 | "value": 69 93 | }, 94 | { 95 | "date": "2024-04-21T04:00:00.000Z", 96 | "value": 69 97 | }, 98 | { 99 | "date": "2024-04-22T04:00:00.000Z", 100 | "value": 65 101 | }, 102 | { 103 | "date": "2024-04-23T04:00:00.000Z", 104 | "value": 63 105 | }, 106 | { 107 | "date": "2024-04-24T04:00:00.000Z", 108 | "value": 63 109 | }, 110 | { 111 | "date": "2024-04-25T04:00:00.000Z", 112 | "value": 63 113 | }, 114 | { 115 | "date": "2024-04-26T04:00:00.000Z", 116 | "value": 63 117 | }, 118 | { 119 | "date": "2024-04-27T04:00:00.000Z", 120 | "value": 63 121 | }, 122 | { 123 | "date": "2024-04-28T04:00:00.000Z", 124 | "value": 64 125 | }, 126 | { 127 | "date": "2024-04-29T04:00:00.000Z", 128 | "value": 64 129 | }, 130 | { 131 | "date": "2024-04-30T04:00:00.000Z", 132 | "value": 64 133 | }, 134 | { 135 | "date": "2024-05-01T04:00:00.000Z", 136 | "value": 64 137 | }, 138 | { 139 | "date": "2024-05-02T04:00:00.000Z", 140 | "value": 64 141 | }, 142 | { 143 | "date": "2024-05-03T04:00:00.000Z", 144 | "value": 64 145 | }, 146 | { 147 | "date": "2024-05-04T04:00:00.000Z", 148 | "value": 64 149 | }, 150 | { 151 | "date": "2024-05-05T04:00:00.000Z", 152 | "value": 64 153 | }, 154 | { 155 | "date": "2024-05-06T04:00:00.000Z", 156 | "value": 64 157 | }, 158 | { 159 | "date": "2024-05-07T04:00:00.000Z", 160 | "value": 64 161 | }, 162 | { 163 | "date": "2024-05-08T04:00:00.000Z", 164 | "value": 64 165 | }, 166 | { 167 | "date": "2024-05-09T04:00:00.000Z", 168 | "value": 64 169 | }, 170 | { 171 | "date": "2024-05-10T04:00:00.000Z", 172 | "value": 64 173 | }, 174 | { 175 | "date": "2024-05-11T04:00:00.000Z", 176 | "value": 64 177 | }, 178 | { 179 | "date": "2024-05-12T04:00:00.000Z", 180 | "value": 60 181 | }, 182 | { 183 | "date": "2024-05-13T04:00:00.000Z", 184 | "value": 60 185 | }, 186 | { 187 | "date": "2024-05-14T04:00:00.000Z", 188 | "value": 60 189 | }, 190 | { 191 | "date": "2024-05-15T04:00:00.000Z", 192 | "value": 64 193 | }, 194 | { 195 | "date": "2024-05-16T04:00:00.000Z", 196 | "value": 64 197 | }, 198 | { 199 | "date": "2024-05-17T04:00:00.000Z", 200 | "value": 64 201 | }, 202 | { 203 | "date": "2024-05-18T04:00:00.000Z", 204 | "value": 64 205 | }, 206 | { 207 | "date": "2024-05-19T04:00:00.000Z", 208 | "value": 64 209 | }, 210 | { 211 | "date": "2024-05-20T04:00:00.000Z", 212 | "value": 64 213 | }, 214 | { 215 | "date": "2024-05-21T04:00:00.000Z", 216 | "value": 65 217 | }, 218 | { 219 | "date": "2024-05-22T04:00:00.000Z", 220 | "value": 65 221 | }, 222 | { 223 | "date": "2024-05-23T04:00:00.000Z", 224 | "value": 65 225 | }, 226 | { 227 | "date": "2024-05-24T04:00:00.000Z", 228 | "value": 65 229 | }, 230 | { 231 | "date": "2024-05-25T04:00:00.000Z", 232 | "value": 65 233 | }, 234 | { 235 | "date": "2024-05-26T04:00:00.000Z", 236 | "value": 65 237 | }, 238 | { 239 | "date": "2024-05-27T04:00:00.000Z", 240 | "value": 65 241 | }, 242 | { 243 | "date": "2024-05-28T04:00:00.000Z", 244 | "value": 65 245 | }, 246 | { 247 | "date": "2024-05-29T04:00:00.000Z", 248 | "value": 65 249 | }, 250 | { 251 | "date": "2024-05-30T04:00:00.000Z", 252 | "value": 68 253 | }, 254 | { 255 | "date": "2024-05-31T04:00:00.000Z", 256 | "value": 69 257 | }, 258 | { 259 | "date": "2024-06-01T04:00:00.000Z", 260 | "value": 69 261 | }, 262 | { 263 | "date": "2024-06-02T04:00:00.000Z", 264 | "value": 69 265 | }, 266 | { 267 | "date": "2024-06-03T04:00:00.000Z", 268 | "value": 69 269 | }, 270 | { 271 | "date": "2024-06-04T04:00:00.000Z", 272 | "value": 69 273 | }, 274 | { 275 | "date": "2024-06-05T04:00:00.000Z", 276 | "value": 69 277 | }, 278 | { 279 | "date": "2024-06-06T04:00:00.000Z", 280 | "value": 69 281 | }, 282 | { 283 | "date": "2024-06-07T04:00:00.000Z", 284 | "value": 69 285 | }, 286 | { 287 | "date": "2024-06-08T04:00:00.000Z", 288 | "value": 69 289 | }, 290 | { 291 | "date": "2024-06-09T04:00:00.000Z", 292 | "value": 69 293 | }, 294 | { 295 | "date": "2024-06-10T04:00:00.000Z", 296 | "value": 69 297 | }, 298 | { 299 | "date": "2024-06-11T04:00:00.000Z", 300 | "value": 69 301 | }, 302 | { 303 | "date": "2024-06-12T04:00:00.000Z", 304 | "value": 64 305 | }, 306 | { 307 | "date": "2024-06-13T04:00:00.000Z", 308 | "value": 64 309 | }, 310 | { 311 | "date": "2024-06-14T04:00:00.000Z", 312 | "value": 64 313 | }, 314 | { 315 | "date": "2024-06-15T04:00:00.000Z", 316 | "value": 64 317 | }, 318 | { 319 | "date": "2024-06-16T04:00:00.000Z", 320 | "value": 64 321 | }, 322 | { 323 | "date": "2024-06-17T04:00:00.000Z", 324 | "value": 64 325 | }, 326 | { 327 | "date": "2024-06-18T04:00:00.000Z", 328 | "value": 64 329 | }, 330 | { 331 | "date": "2024-06-19T04:00:00.000Z", 332 | "value": 64 333 | }, 334 | { 335 | "date": "2024-06-20T04:00:00.000Z", 336 | "value": 64 337 | }, 338 | { 339 | "date": "2024-06-21T04:00:00.000Z", 340 | "value": 64 341 | }, 342 | { 343 | "date": "2024-06-22T04:00:00.000Z", 344 | "value": 64 345 | }, 346 | { 347 | "date": "2024-06-23T04:00:00.000Z", 348 | "value": 64 349 | }, 350 | { 351 | "date": "2024-06-24T04:00:00.000Z", 352 | "value": 64 353 | }, 354 | { 355 | "date": "2024-06-25T04:00:00.000Z", 356 | "value": 69 357 | }, 358 | { 359 | "date": "2024-06-26T04:00:00.000Z", 360 | "value": 69 361 | }, 362 | { 363 | "date": "2024-06-27T04:00:00.000Z", 364 | "value": 69 365 | }, 366 | { 367 | "date": "2024-06-28T04:00:00.000Z", 368 | "value": 64 369 | }, 370 | { 371 | "date": "2024-06-29T04:00:00.000Z", 372 | "value": 64 373 | }, 374 | { 375 | "date": "2024-06-30T04:00:00.000Z", 376 | "value": 64 377 | }, 378 | { 379 | "date": "2024-07-01T04:00:00.000Z", 380 | "value": 64 381 | }, 382 | { 383 | "date": "2024-07-02T04:00:00.000Z", 384 | "value": 64 385 | }, 386 | { 387 | "date": "2024-07-03T04:00:00.000Z", 388 | "value": 64 389 | }, 390 | { 391 | "date": "2024-07-04T04:00:00.000Z", 392 | "value": 68 393 | }, 394 | { 395 | "date": "2024-07-05T04:00:00.000Z", 396 | "value": 68 397 | }, 398 | { 399 | "date": "2024-07-06T04:00:00.000Z", 400 | "value": 68 401 | }, 402 | { 403 | "date": "2024-07-07T04:00:00.000Z", 404 | "value": 68 405 | }, 406 | { 407 | "date": "2024-07-08T04:00:00.000Z", 408 | "value": 68 409 | }, 410 | { 411 | "date": "2024-07-09T04:00:00.000Z", 412 | "value": 68 413 | }, 414 | { 415 | "date": "2024-07-10T04:00:00.000Z", 416 | "value": 68 417 | }, 418 | { 419 | "date": "2024-07-11T04:00:00.000Z", 420 | "value": 68 421 | }, 422 | { 423 | "date": "2024-07-12T04:00:00.000Z", 424 | "value": 68 425 | }, 426 | { 427 | "date": "2024-07-13T04:00:00.000Z", 428 | "value": 68 429 | }, 430 | { 431 | "date": "2024-07-14T04:00:00.000Z", 432 | "value": 68 433 | }, 434 | { 435 | "date": "2024-07-15T04:00:00.000Z", 436 | "value": 68 437 | }, 438 | { 439 | "date": "2024-07-16T04:00:00.000Z", 440 | "value": 68 441 | }, 442 | { 443 | "date": "2024-07-17T04:00:00.000Z", 444 | "value": 68 445 | }, 446 | { 447 | "date": "2024-07-18T04:00:00.000Z", 448 | "value": 68 449 | }, 450 | { 451 | "date": "2024-07-19T04:00:00.000Z", 452 | "value": 68 453 | }, 454 | { 455 | "date": "2024-07-20T04:00:00.000Z", 456 | "value": 68 457 | }, 458 | { 459 | "date": "2024-07-21T04:00:00.000Z", 460 | "value": 68 461 | }, 462 | { 463 | "date": "2024-07-22T04:00:00.000Z", 464 | "value": 68 465 | }, 466 | { 467 | "date": "2024-07-23T04:00:00.000Z", 468 | "value": 68 469 | }, 470 | { 471 | "date": "2024-07-24T04:00:00.000Z", 472 | "value": 68 473 | }, 474 | { 475 | "date": "2024-07-25T04:00:00.000Z", 476 | "value": 67 477 | }, 478 | { 479 | "date": "2024-07-26T04:00:00.000Z", 480 | "value": 67 481 | }, 482 | { 483 | "date": "2024-07-27T04:00:00.000Z", 484 | "value": 67 485 | }, 486 | { 487 | "date": "2024-07-28T04:00:00.000Z", 488 | "value": 67 489 | }, 490 | { 491 | "date": "2024-07-29T04:00:00.000Z", 492 | "value": 63 493 | }, 494 | { 495 | "date": "2024-07-30T04:00:00.000Z", 496 | "value": 63 497 | }, 498 | { 499 | "date": "2024-07-31T04:00:00.000Z", 500 | "value": 67 501 | }, 502 | { 503 | "date": "2024-08-01T04:00:00.000Z", 504 | "value": 67 505 | }, 506 | { 507 | "date": "2024-08-02T04:00:00.000Z", 508 | "value": 67 509 | }, 510 | { 511 | "date": "2024-08-03T04:00:00.000Z", 512 | "value": 67 513 | }, 514 | { 515 | "date": "2024-08-04T04:00:00.000Z", 516 | "value": 67 517 | }, 518 | { 519 | "date": "2024-08-05T04:00:00.000Z", 520 | "value": 63 521 | }, 522 | { 523 | "date": "2024-08-06T04:00:00.000Z", 524 | "value": 63 525 | }, 526 | { 527 | "date": "2024-08-07T04:00:00.000Z", 528 | "value": 63 529 | }, 530 | { 531 | "date": "2024-08-08T04:00:00.000Z", 532 | "value": 63 533 | }, 534 | { 535 | "date": "2024-08-09T04:00:00.000Z", 536 | "value": 63 537 | }, 538 | { 539 | "date": "2024-08-10T04:00:00.000Z", 540 | "value": 63 541 | }, 542 | { 543 | "date": "2024-08-11T04:00:00.000Z", 544 | "value": 63 545 | }, 546 | { 547 | "date": "2024-08-12T04:00:00.000Z", 548 | "value": 63 549 | }, 550 | { 551 | "date": "2024-08-13T04:00:00.000Z", 552 | "value": 63 553 | }, 554 | { 555 | "date": "2024-08-14T04:00:00.000Z", 556 | "value": 63 557 | }, 558 | { 559 | "date": "2024-08-15T04:00:00.000Z", 560 | "value": 63 561 | }, 562 | { 563 | "date": "2024-08-16T04:00:00.000Z", 564 | "value": 63 565 | }, 566 | { 567 | "date": "2024-08-17T04:00:00.000Z", 568 | "value": 63 569 | }, 570 | { 571 | "date": "2024-08-18T04:00:00.000Z", 572 | "value": 63 573 | }, 574 | { 575 | "date": "2024-08-19T04:00:00.000Z", 576 | "value": 63 577 | }, 578 | { 579 | "date": "2024-08-20T04:00:00.000Z", 580 | "value": 63 581 | }, 582 | { 583 | "date": "2024-08-21T04:00:00.000Z", 584 | "value": 63 585 | }, 586 | { 587 | "date": "2024-08-22T04:00:00.000Z", 588 | "value": 67 589 | }, 590 | { 591 | "date": "2024-08-23T04:00:00.000Z", 592 | "value": 67 593 | }, 594 | { 595 | "date": "2024-08-24T04:00:00.000Z", 596 | "value": 67 597 | }, 598 | { 599 | "date": "2024-08-25T04:00:00.000Z", 600 | "value": 67 601 | }, 602 | { 603 | "date": "2024-08-26T04:00:00.000Z", 604 | "value": 67 605 | }, 606 | { 607 | "date": "2024-08-27T04:00:00.000Z", 608 | "value": 67 609 | }, 610 | { 611 | "date": "2024-08-28T04:00:00.000Z", 612 | "value": 67 613 | }, 614 | { 615 | "date": "2024-08-29T04:00:00.000Z", 616 | "value": 67 617 | }, 618 | { 619 | "date": "2024-08-30T04:00:00.000Z", 620 | "value": 67 621 | }, 622 | { 623 | "date": "2024-08-31T04:00:00.000Z", 624 | "value": 67 625 | }, 626 | { 627 | "date": "2024-09-01T04:00:00.000Z", 628 | "value": 67 629 | }, 630 | { 631 | "date": "2024-09-02T04:00:00.000Z", 632 | "value": 67 633 | }, 634 | { 635 | "date": "2024-09-03T04:00:00.000Z", 636 | "value": 67 637 | }, 638 | { 639 | "date": "2024-09-04T04:00:00.000Z", 640 | "value": 67 641 | }, 642 | { 643 | "date": "2024-09-05T04:00:00.000Z", 644 | "value": 67 645 | }, 646 | { 647 | "date": "2024-09-06T04:00:00.000Z", 648 | "value": 67 649 | }, 650 | { 651 | "date": "2024-09-07T04:00:00.000Z", 652 | "value": 67 653 | }, 654 | { 655 | "date": "2024-09-08T04:00:00.000Z", 656 | "value": 67 657 | }, 658 | { 659 | "date": "2024-09-09T04:00:00.000Z", 660 | "value": 67 661 | }, 662 | { 663 | "date": "2024-09-10T04:00:00.000Z", 664 | "value": 67 665 | }, 666 | { 667 | "date": "2024-09-11T04:00:00.000Z", 668 | "value": 67 669 | }, 670 | { 671 | "date": "2024-09-12T04:00:00.000Z", 672 | "value": 67 673 | }, 674 | { 675 | "date": "2024-09-13T04:00:00.000Z", 676 | "value": 65 677 | }, 678 | { 679 | "date": "2024-09-14T04:00:00.000Z", 680 | "value": 65 681 | }, 682 | { 683 | "date": "2024-09-15T04:00:00.000Z", 684 | "value": 65 685 | }, 686 | { 687 | "date": "2024-09-16T04:00:00.000Z", 688 | "value": 64 689 | }, 690 | { 691 | "date": "2024-09-17T04:00:00.000Z", 692 | "value": 64 693 | }, 694 | { 695 | "date": "2024-09-18T04:00:00.000Z", 696 | "value": 68 697 | }, 698 | { 699 | "date": "2024-09-19T04:00:00.000Z", 700 | "value": 68 701 | }, 702 | { 703 | "date": "2024-09-20T04:00:00.000Z", 704 | "value": 68 705 | }, 706 | { 707 | "date": "2024-09-21T04:00:00.000Z", 708 | "value": 68 709 | }, 710 | { 711 | "date": "2024-09-22T04:00:00.000Z", 712 | "value": 68 713 | }, 714 | { 715 | "date": "2024-09-23T04:00:00.000Z", 716 | "value": 68 717 | }, 718 | { 719 | "date": "2024-09-24T04:00:00.000Z", 720 | "value": 68 721 | }, 722 | { 723 | "date": "2024-09-25T04:00:00.000Z", 724 | "value": 68 725 | }, 726 | { 727 | "date": "2024-09-26T04:00:00.000Z", 728 | "value": 63 729 | }, 730 | { 731 | "date": "2024-09-27T04:00:00.000Z", 732 | "value": 63 733 | }, 734 | { 735 | "date": "2024-09-28T04:00:00.000Z", 736 | "value": 68 737 | }, 738 | { 739 | "date": "2024-09-29T04:00:00.000Z", 740 | "value": 68 741 | }, 742 | { 743 | "date": "2024-09-30T04:00:00.000Z", 744 | "value": 68 745 | }, 746 | { 747 | "date": "2024-10-01T04:00:00.000Z", 748 | "value": 68 749 | }, 750 | { 751 | "date": "2024-10-02T04:00:00.000Z", 752 | "value": 68 753 | }, 754 | { 755 | "date": "2024-10-03T04:00:00.000Z", 756 | "value": 63 757 | }, 758 | { 759 | "date": "2024-10-04T04:00:00.000Z", 760 | "value": 63 761 | }, 762 | { 763 | "date": "2024-10-05T04:00:00.000Z", 764 | "value": 63 765 | }, 766 | { 767 | "date": "2024-10-06T04:00:00.000Z", 768 | "value": 63 769 | }, 770 | { 771 | "date": "2024-10-07T04:00:00.000Z", 772 | "value": 63 773 | }, 774 | { 775 | "date": "2024-10-08T04:00:00.000Z", 776 | "value": 68 777 | }, 778 | { 779 | "date": "2024-10-09T04:00:00.000Z", 780 | "value": 68 781 | }, 782 | { 783 | "date": "2024-10-10T04:00:00.000Z", 784 | "value": 75 785 | }, 786 | { 787 | "date": "2024-10-11T04:00:00.000Z", 788 | "value": 71 789 | }, 790 | { 791 | "date": "2024-10-12T04:00:00.000Z", 792 | "value": 75 793 | }, 794 | { 795 | "date": "2024-10-13T04:00:00.000Z", 796 | "value": 72 797 | }, 798 | { 799 | "date": "2024-10-14T04:00:00.000Z", 800 | "value": 72 801 | }, 802 | { 803 | "date": "2024-10-15T04:00:00.000Z", 804 | "value": 68 805 | }, 806 | { 807 | "date": "2024-10-16T04:00:00.000Z", 808 | "value": 70 809 | }, 810 | { 811 | "date": "2024-10-17T04:00:00.000Z", 812 | "value": 70 813 | }, 814 | { 815 | "date": "2024-10-18T04:00:00.000Z", 816 | "value": 70 817 | }, 818 | { 819 | "date": "2024-10-19T04:00:00.000Z", 820 | "value": 70 821 | }, 822 | { 823 | "date": "2024-10-20T04:00:00.000Z", 824 | "value": 70 825 | }, 826 | { 827 | "date": "2024-10-21T04:00:00.000Z", 828 | "value": 70 829 | }, 830 | { 831 | "date": "2024-10-22T04:00:00.000Z", 832 | "value": 70 833 | }, 834 | { 835 | "date": "2024-10-23T04:00:00.000Z", 836 | "value": 70 837 | }, 838 | { 839 | "date": "2024-10-24T04:00:00.000Z", 840 | "value": 68 841 | }, 842 | { 843 | "date": "2024-10-25T04:00:00.000Z", 844 | "value": 66 845 | }, 846 | { 847 | "date": "2024-10-26T04:00:00.000Z", 848 | "value": 66 849 | }, 850 | { 851 | "date": "2024-10-27T04:00:00.000Z", 852 | "value": 66 853 | }, 854 | { 855 | "date": "2024-10-28T04:00:00.000Z", 856 | "value": 69 857 | }, 858 | { 859 | "date": "2024-10-29T04:00:00.000Z", 860 | "value": 69 861 | }, 862 | { 863 | "date": "2024-10-30T04:00:00.000Z", 864 | "value": 69 865 | }, 866 | { 867 | "date": "2024-10-31T04:00:00.000Z", 868 | "value": 66 869 | }, 870 | { 871 | "date": "2024-11-01T04:00:00.000Z", 872 | "value": 66 873 | }, 874 | { 875 | "date": "2024-11-02T04:00:00.000Z", 876 | "value": 66 877 | }, 878 | { 879 | "date": "2024-11-03T04:00:00.000Z", 880 | "value": 66 881 | }, 882 | { 883 | "date": "2024-11-04T04:00:00.000Z", 884 | "value": 66 885 | }, 886 | { 887 | "date": "2024-11-05T04:00:00.000Z", 888 | "value": 66 889 | }, 890 | { 891 | "date": "2024-11-06T04:00:00.000Z", 892 | "value": 66 893 | }, 894 | { 895 | "date": "2024-11-07T04:00:00.000Z", 896 | "value": 66 897 | }, 898 | { 899 | "date": "2024-11-08T04:00:00.000Z", 900 | "value": 66 901 | }, 902 | { 903 | "date": "2024-11-09T04:00:00.000Z", 904 | "value": 70 905 | }, 906 | { 907 | "date": "2024-11-10T04:00:00.000Z", 908 | "value": 70 909 | }, 910 | { 911 | "date": "2024-11-11T04:00:00.000Z", 912 | "value": 70 913 | }, 914 | { 915 | "date": "2024-11-12T04:00:00.000Z", 916 | "value": 70 917 | }, 918 | { 919 | "date": "2024-11-13T04:00:00.000Z", 920 | "value": 70 921 | }, 922 | { 923 | "date": "2024-11-14T04:00:00.000Z", 924 | "value": 74 925 | }, 926 | { 927 | "date": "2024-11-15T04:00:00.000Z", 928 | "value": 77 929 | }, 930 | { 931 | "date": "2024-11-16T04:00:00.000Z", 932 | "value": 77 933 | }, 934 | { 935 | "date": "2024-11-17T04:00:00.000Z", 936 | "value": 77 937 | }, 938 | { 939 | "date": "2024-11-18T04:00:00.000Z", 940 | "value": 77 941 | }, 942 | { 943 | "date": "2024-11-19T04:00:00.000Z", 944 | "value": 77 945 | }, 946 | { 947 | "date": "2024-11-20T04:00:00.000Z", 948 | "value": 79 949 | }, 950 | { 951 | "date": "2024-11-21T04:00:00.000Z", 952 | "value": 64 953 | }, 954 | { 955 | "date": "2024-11-22T04:00:00.000Z", 956 | "value": 71 957 | }, 958 | { 959 | "date": "2024-11-23T04:00:00.000Z", 960 | "value": 75 961 | }, 962 | { 963 | "date": "2024-11-24T04:00:00.000Z", 964 | "value": 70 965 | }, 966 | { 967 | "date": "2024-11-25T04:00:00.000Z", 968 | "value": 70 969 | }, 970 | { 971 | "date": "2024-11-26T04:00:00.000Z", 972 | "value": 70 973 | }, 974 | { 975 | "date": "2024-11-27T04:00:00.000Z", 976 | "value": 70 977 | }, 978 | { 979 | "date": "2024-11-28T04:00:00.000Z", 980 | "value": 70 981 | }, 982 | { 983 | "date": "2024-11-29T04:00:00.000Z", 984 | "value": 70 985 | }, 986 | { 987 | "date": "2024-11-30T04:00:00.000Z", 988 | "value": 70 989 | }, 990 | { 991 | "date": "2024-12-01T04:00:00.000Z", 992 | "value": 70 993 | }, 994 | { 995 | "date": "2024-12-02T04:00:00.000Z", 996 | "value": 75 997 | }, 998 | { 999 | "date": "2024-12-03T04:00:00.000Z", 1000 | "value": 75 1001 | }, 1002 | { 1003 | "date": "2024-12-04T04:00:00.000Z", 1004 | "value": 75 1005 | }, 1006 | { 1007 | "date": "2024-12-05T04:00:00.000Z", 1008 | "value": 75 1009 | }, 1010 | { 1011 | "date": "2024-12-06T04:00:00.000Z", 1012 | "value": 75 1013 | }, 1014 | { 1015 | "date": "2024-12-07T04:00:00.000Z", 1016 | "value": 75 1017 | }, 1018 | { 1019 | "date": "2024-12-08T04:00:00.000Z", 1020 | "value": 73 1021 | }, 1022 | { 1023 | "date": "2024-12-09T04:00:00.000Z", 1024 | "value": 73 1025 | }, 1026 | { 1027 | "date": "2024-12-10T04:00:00.000Z", 1028 | "value": 73 1029 | }, 1030 | { 1031 | "date": "2024-12-11T04:00:00.000Z", 1032 | "value": 73 1033 | }, 1034 | { 1035 | "date": "2024-12-12T04:00:00.000Z", 1036 | "value": 73 1037 | }, 1038 | { 1039 | "date": "2024-12-13T04:00:00.000Z", 1040 | "value": 75 1041 | }, 1042 | { 1043 | "date": "2024-12-14T04:00:00.000Z", 1044 | "value": 75 1045 | }, 1046 | { 1047 | "date": "2024-12-15T04:00:00.000Z", 1048 | "value": 75 1049 | }, 1050 | { 1051 | "date": "2024-12-16T04:00:00.000Z", 1052 | "value": 75 1053 | }, 1054 | { 1055 | "date": "2024-12-17T04:00:00.000Z", 1056 | "value": 75 1057 | }, 1058 | { 1059 | "date": "2024-12-18T04:00:00.000Z", 1060 | "value": 75 1061 | }, 1062 | { 1063 | "date": "2024-12-19T04:00:00.000Z", 1064 | "value": 75 1065 | }, 1066 | { 1067 | "date": "2024-12-20T04:00:00.000Z", 1068 | "value": 75 1069 | }, 1070 | { 1071 | "date": "2024-12-21T04:00:00.000Z", 1072 | "value": 75 1073 | }, 1074 | { 1075 | "date": "2024-12-22T04:00:00.000Z", 1076 | "value": 75 1077 | }, 1078 | { 1079 | "date": "2024-12-23T04:00:00.000Z", 1080 | "value": 75 1081 | }, 1082 | { 1083 | "date": "2024-12-24T04:00:00.000Z", 1084 | "value": 75 1085 | }, 1086 | { 1087 | "date": "2024-12-25T04:00:00.000Z", 1088 | "value": 75 1089 | }, 1090 | { 1091 | "date": "2024-12-26T04:00:00.000Z", 1092 | "value": 75 1093 | }, 1094 | { 1095 | "date": "2024-12-27T04:00:00.000Z", 1096 | "value": 75 1097 | }, 1098 | { 1099 | "date": "2024-12-28T04:00:00.000Z", 1100 | "value": 75 1101 | }, 1102 | { 1103 | "date": "2024-12-29T04:00:00.000Z", 1104 | "value": 75 1105 | }, 1106 | { 1107 | "date": "2024-12-30T04:00:00.000Z", 1108 | "value": 75 1109 | }, 1110 | { 1111 | "date": "2024-12-31T04:00:00.000Z", 1112 | "value": 75 1113 | }, 1114 | { 1115 | "date": "2025-01-01T04:00:00.000Z", 1116 | "value": 75 1117 | }, 1118 | { 1119 | "date": "2025-01-02T04:00:00.000Z", 1120 | "value": 75 1121 | }, 1122 | { 1123 | "date": "2025-01-03T04:00:00.000Z", 1124 | "value": 75 1125 | }, 1126 | { 1127 | "date": "2025-01-04T04:00:00.000Z", 1128 | "value": 75 1129 | }, 1130 | { 1131 | "date": "2025-01-05T04:00:00.000Z", 1132 | "value": 75 1133 | }, 1134 | { 1135 | "date": "2025-01-06T04:00:00.000Z", 1136 | "value": 75 1137 | }, 1138 | { 1139 | "date": "2025-01-07T04:00:00.000Z", 1140 | "value": 75 1141 | }, 1142 | { 1143 | "date": "2025-01-08T04:00:00.000Z", 1144 | "value": 69 1145 | }, 1146 | { 1147 | "date": "2025-01-09T04:00:00.000Z", 1148 | "value": 66 1149 | }, 1150 | { 1151 | "date": "2025-01-10T04:00:00.000Z", 1152 | "value": 66 1153 | }, 1154 | { 1155 | "date": "2025-01-11T04:00:00.000Z", 1156 | "value": 66 1157 | }, 1158 | { 1159 | "date": "2025-01-12T04:00:00.000Z", 1160 | "value": 67 1161 | }, 1162 | { 1163 | "date": "2025-01-13T04:00:00.000Z", 1164 | "value": 67 1165 | }, 1166 | { 1167 | "date": "2025-01-14T04:00:00.000Z", 1168 | "value": 65 1169 | }, 1170 | { 1171 | "date": "2025-01-15T04:00:00.000Z", 1172 | "value": 65 1173 | }, 1174 | { 1175 | "date": "2025-01-16T04:00:00.000Z", 1176 | "value": 65 1177 | }, 1178 | { 1179 | "date": "2025-01-17T04:00:00.000Z", 1180 | "value": 65 1181 | }, 1182 | { 1183 | "date": "2025-01-18T04:00:00.000Z", 1184 | "value": 65 1185 | }, 1186 | { 1187 | "date": "2025-01-19T04:00:00.000Z", 1188 | "value": 65 1189 | }, 1190 | { 1191 | "date": "2025-01-20T04:00:00.000Z", 1192 | "value": 65 1193 | }, 1194 | { 1195 | "date": "2025-01-21T04:00:00.000Z", 1196 | "value": 65 1197 | }, 1198 | { 1199 | "date": "2025-01-22T04:00:00.000Z", 1200 | "value": 65 1201 | }, 1202 | { 1203 | "date": "2025-01-23T04:00:00.000Z", 1204 | "value": 65 1205 | }, 1206 | { 1207 | "date": "2025-01-24T04:00:00.000Z", 1208 | "value": 65 1209 | }, 1210 | { 1211 | "date": "2025-01-25T04:00:00.000Z", 1212 | "value": 65 1213 | }, 1214 | { 1215 | "date": "2025-01-26T04:00:00.000Z", 1216 | "value": 65 1217 | }, 1218 | { 1219 | "date": "2025-01-27T04:00:00.000Z", 1220 | "value": 65 1221 | }, 1222 | { 1223 | "date": "2025-01-28T04:00:00.000Z", 1224 | "value": 65 1225 | }, 1226 | { 1227 | "date": "2025-01-29T04:00:00.000Z", 1228 | "value": 65 1229 | }, 1230 | { 1231 | "date": "2025-01-30T04:00:00.000Z", 1232 | "value": 65 1233 | }, 1234 | { 1235 | "date": "2025-01-31T04:00:00.000Z", 1236 | "value": 61 1237 | }, 1238 | { 1239 | "date": "2025-02-01T04:00:00.000Z", 1240 | "value": 61 1241 | }, 1242 | { 1243 | "date": "2025-02-02T04:00:00.000Z", 1244 | "value": 61 1245 | }, 1246 | { 1247 | "date": "2025-02-03T04:00:00.000Z", 1248 | "value": 61 1249 | }, 1250 | { 1251 | "date": "2025-02-04T04:00:00.000Z", 1252 | "value": 61 1253 | }, 1254 | { 1255 | "date": "2025-02-05T04:00:00.000Z", 1256 | "value": 61 1257 | }, 1258 | { 1259 | "date": "2025-02-06T04:00:00.000Z", 1260 | "value": 61 1261 | }, 1262 | { 1263 | "date": "2025-02-07T04:00:00.000Z", 1264 | "value": 61 1265 | }, 1266 | { 1267 | "date": "2025-02-08T04:00:00.000Z", 1268 | "value": 61 1269 | }, 1270 | { 1271 | "date": "2025-02-09T04:00:00.000Z", 1272 | "value": 61 1273 | }, 1274 | { 1275 | "date": "2025-02-10T04:00:00.000Z", 1276 | "value": 61 1277 | }, 1278 | { 1279 | "date": "2025-02-11T04:00:00.000Z", 1280 | "value": 65 1281 | }, 1282 | { 1283 | "date": "2025-02-12T04:00:00.000Z", 1284 | "value": 65 1285 | }, 1286 | { 1287 | "date": "2025-02-13T04:00:00.000Z", 1288 | "value": 65 1289 | }, 1290 | { 1291 | "date": "2025-02-14T04:00:00.000Z", 1292 | "value": 66 1293 | }, 1294 | { 1295 | "date": "2025-02-15T04:00:00.000Z", 1296 | "value": 66 1297 | }, 1298 | { 1299 | "date": "2025-02-16T04:00:00.000Z", 1300 | "value": 66 1301 | }, 1302 | { 1303 | "date": "2025-02-17T04:00:00.000Z", 1304 | "value": 66 1305 | }, 1306 | { 1307 | "date": "2025-02-18T04:00:00.000Z", 1308 | "value": 66 1309 | }, 1310 | { 1311 | "date": "2025-02-19T04:00:00.000Z", 1312 | "value": 66 1313 | }, 1314 | { 1315 | "date": "2025-02-20T04:00:00.000Z", 1316 | "value": 66 1317 | }, 1318 | { 1319 | "date": "2025-02-21T04:00:00.000Z", 1320 | "value": 66 1321 | }, 1322 | { 1323 | "date": "2025-02-22T04:00:00.000Z", 1324 | "value": 66 1325 | }, 1326 | { 1327 | "date": "2025-02-23T04:00:00.000Z", 1328 | "value": 66 1329 | }, 1330 | { 1331 | "date": "2025-02-24T04:00:00.000Z", 1332 | "value": 66 1333 | }, 1334 | { 1335 | "date": "2025-02-25T04:00:00.000Z", 1336 | "value": 66 1337 | }, 1338 | { 1339 | "date": "2025-02-26T04:00:00.000Z", 1340 | "value": 66 1341 | }, 1342 | { 1343 | "date": "2025-02-27T04:00:00.000Z", 1344 | "value": 66 1345 | }, 1346 | { 1347 | "date": "2025-02-28T04:00:00.000Z", 1348 | "value": 66 1349 | }, 1350 | { 1351 | "date": "2025-03-01T04:00:00.000Z", 1352 | "value": 66 1353 | }, 1354 | { 1355 | "date": "2025-03-02T04:00:00.000Z", 1356 | "value": 66 1357 | }, 1358 | { 1359 | "date": "2025-03-03T04:00:00.000Z", 1360 | "value": 66 1361 | }, 1362 | { 1363 | "date": "2025-03-04T04:00:00.000Z", 1364 | "value": 66 1365 | }, 1366 | { 1367 | "date": "2025-03-05T04:00:00.000Z", 1368 | "value": 66 1369 | }, 1370 | { 1371 | "date": "2025-03-06T04:00:00.000Z", 1372 | "value": 66 1373 | }, 1374 | { 1375 | "date": "2025-03-07T04:00:00.000Z", 1376 | "value": 66 1377 | }, 1378 | { 1379 | "date": "2025-03-08T04:00:00.000Z", 1380 | "value": 66 1381 | } 1382 | ] 1383 | --------------------------------------------------------------------------------