Skip to content

Instantly share code, notes, and snippets.

@tvararu
Created December 25, 2024 20:38
Show Gist options
  • Save tvararu/d18b982a0bffc358dec302c4c5f4f445 to your computer and use it in GitHub Desktop.
Save tvararu/d18b982a0bffc358dec302c4c5f4f445 to your computer and use it in GitHub Desktop.
#!/usr/bin/env elixir
Mix.install([
{:plug_cowboy, "~> 2.5"},
{:jason, "~> 1.4"},
{:finch, "~> 0.16"}
])
defmodule MetaculusQuestion do
@enforce_keys [:title, :id, :forecasters, :yes_prob, :no_prob, :distance_from_fifty, :resolve_time]
defstruct @enforce_keys
def from_map(data) do
title = Map.get(data, "title", "No title")
id = Map.get(data, "id", "N/A")
forecasters = Map.get(data, "nr_forecasters", "N/A")
aggs = get_in(data, ["question", "aggregations"]) || %{}
latest = get_in(aggs, ["recency_weighted", "latest"]) || %{}
vals = Map.get(latest, "forecast_values", [nil, nil])
vals =
if is_list(vals) and length(vals) >= 2 do
vals
else
[nil, nil]
end
[yes_prob, no_prob] = vals
distance_from_fifty =
if yes_prob, do: abs(0.5 - yes_prob), else: :infinity
resolve_time = Map.get(data, "scheduled_resolve_time", "")
%__MODULE__{
title: title,
id: id,
forecasters: forecasters,
yes_prob: yes_prob,
no_prob: no_prob,
distance_from_fifty: distance_from_fifty,
resolve_time: resolve_time
}
end
def to_row(%__MODULE__{} = q) do
yes_str = if q.yes_prob, do: :io_lib.format("~.1f%", [q.yes_prob * 100]) |> List.to_string(), else: "N/A"
no_str = if q.no_prob, do: :io_lib.format("~.1f%", [q.no_prob * 100]) |> List.to_string(), else: "N/A"
date_str = parse_date(q.resolve_time)
"""
<tr>
<td>#{q.title}</td>
<td class="yes-prob">#{yes_str}</td>
<td class="no-prob">#{no_str}</td>
<td>#{q.forecasters}</td>
<td>#{date_str}</td>
<td><a href="https://www.metaculus.com/questions/#{q.id}" target="_blank">View</a></td>
</tr>
"""
end
defp parse_date(time_str) do
# Attempt parsing with microseconds, then without, else "N/A"
case DateTime.from_iso8601(time_str) do
{:ok, dt, _offset} ->
dt
|> DateTime.to_date()
|> Date.to_string()
_ ->
"N/A"
end
end
end
defmodule MetaculusFetcher do
use GenServer
@initial_state %{results: []}
@fetch_interval_ms 300_000 # 5 minutes
def start_link(_args), do: GenServer.start_link(__MODULE__, @initial_state, name: __MODULE__)
def init(state) do
schedule_fetch()
{:ok, state}
end
def handle_info(:fetch_data, state) do
results = do_fetch()
schedule_fetch()
{:noreply, %{state | results: results}}
end
# Retrieve current data from this GenServer
def current_data, do: GenServer.call(__MODULE__, :get_data)
def handle_call(:get_data, _from, state), do: {:reply, state, state}
defp schedule_fetch, do: Process.send_after(self(), :fetch_data, @fetch_interval_ms)
defp do_fetch do
url = "https://www.metaculus.com/api/posts/?" <>
"statuses=open&with_cp=true&forecast_type=binary&order_by=scheduled_resolve_time&" <>
"scheduled_resolve_time__lt=#{URI.encode_www_form("2025-01-05T00:00:00Z")}"
# Replace with your token
headers = [
{"Authorization", "THE_TOKEN"}
]
# Finch is the default HTTP client in modern Elixir
Finch.start_link(name: MyFinch)
all_results = fetch_paginated(url, headers, [], 1)
IO.puts("Data updated at #{Time.utc_now()}, total questions: #{length(all_results)}")
all_results
end
# Recursively fetch up to 9 pages
defp fetch_paginated(nil, _headers, acc, _page_count), do: acc
defp fetch_paginated(_url, _headers, acc, page_count) when page_count > 9, do: acc
defp fetch_paginated(url, headers, acc, page_count) do
uri = URI.parse(url)
request = Finch.build(:get, uri, headers)
case Finch.request(request, MyFinch) do
{:ok, %{status: 200, body: body}} ->
data = Jason.decode!(body)
results = data["results"] || []
IO.puts("Fetched page #{page_count}, got #{length(results)} questions")
fetch_paginated(
data["next"],
headers,
acc ++ results,
page_count + 1
)
{:ok, resp} ->
IO.puts("Request failed with HTTP status #{resp.status}")
acc
{:error, reason} ->
IO.puts("Error: #{inspect(reason)}")
acc
end
end
end
defmodule MetaculusPlug do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
if conn.request_path == "/" do
data = MetaculusFetcher.current_data()
body = build_html(data.results)
send_resp(conn, 200, body)
else
send_resp(conn, 404, "Not Found")
end
end
defp build_html(results) do
questions =
results
|> Enum.map(&MetaculusQuestion.from_map/1)
|> Enum.sort_by(& &1.distance_from_fifty)
rows = Enum.map_join(questions, &MetaculusQuestion.to_row/1)
~s"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Metaculus Questions - Closest to 50/50</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f4f4f4;
}
.yes-prob {
color: green;
}
.no-prob {
color: red;
}
.last-updated {
color: #666;
font-size: 0.9em;
margin-top: 10px;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
</style>
</head>
<body>
<h1>Metaculus Questions - Resolving Before Jan 5 2025 (#{length(questions)} questions)</h1>
<p>Sorted by closest to 50% probability</p>
<table>
<thead>
<tr>
<th>Question</th>
<th>Yes</th>
<th>No</th>
<th>Forecasters</th>
<th>Resolves</th>
<th>Link</th>
</tr>
</thead>
<tbody>
#{rows}
</tbody>
</table>
<div class="last-updated">
Last updated: #{NaiveDateTime.local_now() |> NaiveDateTime.truncate(:second)}
</div>
<script>
// Reload page every 5 minutes
setTimeout(() => { window.location.reload(); }, 5 * 60 * 1000);
</script>
</body>
</html>
"""
end
end
defmodule MetaculusServer do
def start(port \\ 8000) do
# Start background fetcher
{:ok, _pid} = MetaculusFetcher.start_link([])
# Start Plug/Cowboy
Plug.Cowboy.http(MetaculusPlug, [], port: port)
IO.puts("Server running on port #{port}")
# Wait forever
:timer.sleep(:infinity)
end
end
# Run if we're invoked as a script
if __ENV__.file == __FILE__ do
MetaculusServer.start(8000)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment