Problem
The United States of America has apparently decided, for inscrutable reasons of its own, to hold an election in November. I have a preferred winner. The contest is likely to be close. If history is any guide, a definitive winner will only be announced by the press days after polls close. (Fun fact: the UK this year held an entire election campaign, from meeting the monarch to said monarch addressing the new parliament, in less time than it takes the US to go from polls closing to counting the electoral college votes.) Therefore, I will likely go to bed several times without knowing who won, but knowing that the result may be available while I sleep.
In my bedroom I have a smart light bulb perched on top of my wardrobe. Its job is to, on weekdays, gradually brighten, starting at 6AM and achieving full brightness at 7AM when my alarm goes off, so that I don’t have to blink when I turn the light on. This is driven by a python script running on a raspberry pi.
This US election season, I would like to implement the following workflow:
- Wake up in the middle of the night
- Light is off:
- Go back to sleep
- Light is red:
- Grunt
- Go back to sleep
- Light is blue:
- Smirk
- Go back to sleep
- Light is off:
I would like to avoid the following failure mode:
- Wake up in the middle of the night
- Fidget with phone
- Result is inconclusive
- Worry for half an hour
- Go back to sleep
I believe I have implemented the technical infrastructure for this. This blog post documents what I did. It was not hard, and the results are not clever. But there were Choices, that deserve to be entered into the ledger.
Wifi
Sidebar. My smart light bulb is made by LIFX. It uses 2.4GHz wifi.
I live in Friedrichshain. Friedrichshain has the world’s highest density of webdevs per square meter. My apartment building is built out of recycled Faraday cages from WWII. There is interference, and there are obstructions. 2.4GHz wifi just doesn’t.
The protocol for communicating with a LIFX light bulb is to simply spit out a UDP packet and hope it gets to its destination. In my case, it generally does not.
I cared a very great deal about avoiding the morning blinking problem. After a lot of experimentation, I eventually concluded that the best approach is to send the packet ten times per second for an hour. It’s absurd, but it usually works.
Data
That’s the solved problem. The unsolved problem is finding a reliable source of data. All I need is a URL and either an Xpath or a regex, to give me a clear signal that the result has been announced, and what it is.
The information I want, is that any major American news network has “called it”. Any one of them will do. And it’ll be hard to miss, if you’re a human. But I’m looking for a URL and a pattern to match. And it’s really hard to predict exactly what they’ll do on the day. These people love nothing better than to screw with their design at the last minute in a bid to make their site more compelling.
This whole exercise is not of critical importance, of course. But if I’m going to mumble and go back to sleep, I need to be fairly confident that the light being off really means that nothing has happened. So I’d like a pretty convincing story to tell myself.
If you want something with a spec, there are a million billion web APIs. But most of them you have to pay for. And they’re really designed for people who need to instantly know the result so that they can take dramatic action, probably selling a thing, maybe yelling down a telephone at a broker. This is the exact opposite of my use case.
So I retreated to PredictIt. This is not exactly what I’m after. It’s not directly connected to the major networks “calling it”. But if people are gambling on who’s going to win, and the major networks have called a result, then you’d assume the rational markets would reflect that in their prices.
I know I know, betting markets are vile. And they are a terrible way of predicting the future. They have no insight, it’s just the consensus of the community of people living with high money/sense ratios. But I’m not looking to make money off this. I just want a light to turn red or blue.
Markets
Here’s a chart of what happened last time round. See the point where the networks called it?
Hint: it’s not there. What happened was, it was pretty clear on polling day Nov 3 that Biden was doing alright. That pushed him to 87%, and that fluttered a little bit over the next few days. The networks called Pennsylvania and the election for Biden on November 7. But that result had already been priced in, so it only lifted his chances from 90% to 93%, and it fell back again afterwards.
That 10% of uncertainty was because everyone knew perfectly well that Trump and his lickspittles would do all they could to just dictate that he’d won the election anyway. He’d already stacked the Supreme Court. The betting markets gave a 10% probability that the Supreme Court would give up the pretence and hand the election to Trump, and that would be generally accepted, at least officially enough that PredictIt would pay out to people who backed Trump. It was only when the Supreme Court declined to hear the case on December 11 that the betting markets finally gave up.
It is very unpleasant to reflect how close America came to the brink. It was unpleasant to live through as well. That’s partly why I want this stupid light system.
Anyway. Overall, there is a clear signal here. If the price is over 90c, the election as such is over, even if machinations continue. But it makes no sense to wait until the price hits 99c. One of the persistent biases of the betting markets is that they just can’t give up on faint hopes. As I type this, RFK is on 2%. People, no. That is dumb. You can be sure that there are enough Americans willing to put money on aliens landing and installing him as president long after the votes have been counted to keep Harris below 98c. And Biden’s final price was only 95c. 90c is a reasonable threshold.
Edit: shit. 80c.
API
So I decided to do all that. The page I want to scrape is here. I did a cURL request. And oops: there’s nothing in it. Of course. This is all web frameworks. Damn. Actually getting the data was looking to be a lot of work.
Except! I opened up Firefox’s “network” tab to see the requests, and noticed a request to “Contracts”. Wouldn’t you know it, that’s a JSON file full of nicely formatted data, no scraping required. How civilised.
So after a great deal of googling how to write code in this ridiculous jq
syntax, I figured out that I can get the final market price for Joe Biden winning the 2020 election like this:
$ curl -s https://www.predictit.org/api/Market/3698/Contracts | jq '.[] | select(.contractName=="Joe Biden") | .lastTradePrice'
0.94
One nice thing about using PredictIt in this way, is that I can test it. First I can point it to the old contract, to make sure it behaves the right way when the result is known. And then, I can point it to the real contracts but give it a low threshold of 60%. I’m sure at some point over the next few months the markets will cross that threshold, and I’ll get a light that proves it’s working.
Script
So here is the whole script. I’ve got this set up to run once per hour. Let’s see how this plays out.
#!/usr/bin/python3
import lifx
import socket
import time
import requests
import json
threshold = 0.8
market_id = 7456
republican_candidate = "Donald Trump"
democrat_candidate = "Kamala Harris"
page = requests.get(f"https://www.predictit.org/api/Market/{market_id}/Contracts")
contracts = json.loads(page.content)
for contract in contracts:
if contract["contractName"] == democrat_candidate:
democrat = float(contract["lastTradePrice"]) > threshold
elif contract["contractName"] == republican_candidate:
republican = float(contract["lastTradePrice"]) > threshold
if republican:
brightness = 1
hue = 0
elif democrat:
brightness = 1
hue = 21845
else:
exit()
duration = 1800
start_time = time.time()
off_time = start_time + duration
power_body = lifx.lan.light.SetPower()
power_header = lifx.lan.header.make(power_body.state)
color_body = lifx.lan.light.SetColor()
color_body.hue = hue
color_body.saturation = 100
color_body.brightness = brightness
color_header = lifx.lan.header.make(color_body.state)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
now = time.time()
msg = lifx.lan.Msg.encode(color_header, color_body)
sock.sendto(bytes(msg), ("Wardrobe", 56700))
time.sleep(0.1)
if now > off_time:
exit()