Archive

Ineffective Theory

Automating arbitrage on Manifold markets

Here are two questions on Manifold markets:

  1. Will there be a federal mask requirement on US domestic flights on November 8, 2022?
  2. Will there be a federal mask requirement in place on domestic flights as of Nov. 8, 2022?

This is the obvious hazard of allowing anyone to create a market—people aren’t always rigorously careful to ensure that they’re not creating a duplicate.

This is also an opportunity: the first market has (as I write this) a probability of 12%, and the second of 8%. Since both creators are trustworthy, the market probabilities should agree more closely than that.

Of course one can do this by hand, but then I’d have to check in periodically to make sure the two markets still agree, and if not, make appropriate trades. Much better to automate the check and the trade. (Also, I’m invested in the idea of automating arbitrage on Manifold—literally.)


Manifold markets has a nice API available. To make it a bit easier to work with, I wrote a python wrapper vatic. Be warned, this is an ill-tested work-in-progress! It’s good enough for this arbitrage task, and probably nothing else.

This is not a difficult task. Since the markets to be arbitraged are already identified, the script only needs to get their probabilities, check that they’re sufficiently far apart for the trade to be worthwhile (using 2% as a crude threshold), and then buy some YES of one and some NO of the other.

from vatic import manifold

slug1 = 'will-there-be-a-federal-mask-requir-d236f8cd3553'
slug2 = 'will-there-be-a-federal-mask-requir'

mani = manifold.Manifold(auth='Key obviously-im-hiding-it')
mkt1 = mani._get_slug(slug1)
mkt2 = mani._get_slug(slug2)

if abs(mkt1.probability - mkt2.probability) > .02:
    print('Probabilities separated by more than 2%---arbitraging!')
    if mkt1.probability > mkt2.probability:
        mkt1,mkt2 = mkt2,mkt1
    # Buy some YES from mkt1; buy some NO from mkt2
    print(mkt1.bet(1, 'YES'))
    print(mkt2.bet(1, 'NO'))

Running repeatedly yields:

(env)$ ./arbitrage.py 
Probabilities separated by more than 2%---arbitraging!
{'betId': 'xskQpaEh9RnEfsnp3NsF'}
{'betId': 'LiWz0zkraXxkRlEynlQu'}
(env)$ ./arbitrage.py 
Probabilities separated by more than 2%---arbitraging!
{'betId': 'aLtAe8ivMQYNH0sBTCZt'}
{'betId': 'O5LjGUwJR53tEsclvKkW'}
(env)$ ./arbitrage.py 
Probabilities separated by more than 2%---arbitraging!
{'betId': 'j1IVf1Dod7qm6mc39CtK'}
{'betId': 'O36ddnYtgnKpM69zXwI1'}
(env)$ ./arbitrage.py 
Probabilities separated by more than 2%---arbitraging!
{'betId': 'IXTLdjQJM1h0tDrINNsL'}
{'betId': 'lIVkXN0UEaPKGXCK8CiY'}

At the end of which I landed myself 4 NO shares and 40 YES shares. So it “worked”!

On the other hand, I now realize that I’ve accumulated a net YES position, which was certainly not my intent! If I believe that the true probability is 10%, then in expectation this strategy turns a profit (at least ignoring trading fees), but I nevertheless expect the strategy to result in a loss. Not terrible, but not ideal. I’m a bit more risk-averse, and would like a guaranteed profit.

So, I sell everything and try again.


To guarantee outcome-independent profit, we want to end up with roughly the same number of YES and NO shares at the end. Ideally, then, we would buy a single share at a time, not a single M$ worth at time. Something like:

mkt1.bet(mkt1.probability, 'YES')
mkt2.bet(1-mkt2.probability, 'NO')

Unfortunately the manifold API does not currently allow arbitrarily small bets. The smallest possible bet size is M$1, and in cases where the total amount of M$ being bet is small and the probabilities are close to 0%, there’s no way to construct a sensible bet.

A “good enough” approach is to buy shares probabilistically. If we buy YES on the low market about 10% of the time, and buy NO on the high market about 90% of the time, then we’ll end up with a roughly equal number of share of each when that’s possible. (When it’s not possible, we’ll be back where we started—profit only in expectation.)

import random
from vatic import manifold

slug1 = 'will-there-be-a-federal-mask-requir-d236f8cd3553'
slug2 = 'will-there-be-a-federal-mask-requir'

mani = manifold.Manifold(auth='Key obviously-im-hiding-it')
mkt1 = mani._get_slug(slug1)
mkt2 = mani._get_slug(slug2)

if abs(mkt1.probability - mkt2.probability) > .02:
    if random.random() < mkt1.probability:
        print('Buying YES')
    if random.random() < 1-mkt2.probability:
        print('Buying NO')

This also has the advantage of being stateless, so that the script can make a single M$1 bet at a time, and then re-evaluate. This prevents the client from needing to do any complicated calculations about what probabilities will be after the bet. I just take the smallest step possible, and then re-evaluate.

Being only very slightly reckless:

(env)$ while true; do ./arbitrage.py; done
Buying NO
Buying YES
Buying NO
Buying NO
Buying NO
Buying NO
Buying NO
Buying NO
Buying NO
Buying NO
Buying NO
Buying NO
Buying NO
Buying NO
Buying NO
Buying YES
Buying NO

The end result: I spent M$17 to get 21.059 YES and 16.719 NO shares. This is not quite a guaranteed profit. In the event of NO, I lose M$.291. It is, however, much closer. The maximum loss is M$0.291, and the potential gain is M$4.059. Alternatively, trusting the 10% probability, the expected value of this position is M$17.15, so I gained $0.15 in expectation.

There’s more that should be tuned. For instance, a good bit was lost to creator fees in this process. I still managed to make a net (expected) profit, but presumably a bit more could be eked out by paying attention to fees.

There’s more to say, but this has already taken much more time than I expected, so I will simply close with one of those universally beloved exercises for the reader. Under what circumstances does the strategy above far underperform a human trader? (Can another trader, who knows I’m running this script, profit from that knowledge?)