Rates as a Graph: Why Arbitrage Becomes a Negative Cycle

Lesson 5 of the arbitrage course. Currencies as points, exchange rates as arrows, why multiplication turns into addition with logarithms, and why a profitable circle becomes a circle with a negative total.

Lesson 5 of the course "Build your own arbitrage bot". Stage 1 gets smarter: instead of comparing two prices, we look at circles of currencies.

The problem

On one exchange you can exchange USD for EUR, EUR for GBP, GBP for USD, and many more pairs. Lesson 2 showed that a circle USD to EUR to GBP to USD can end with more money than you started with. With three currencies you can check it in your head. With fifty currencies there are thousands of circles. You need a method that does not try them one by one.

This lesson prepares the ground. It does not search yet. Lessons 6, 7 and 8 do the searching.

The head-on way, and where it stops

The blunt method: list every circle and multiply its rates. With 3 currencies there are only a few circles and you can do it in your head, as in lesson 2. With 50 currencies and circles of 3 steps there are 50 × 49 × 48 = 117 600 of them, with 4 steps 5 527 200, and the count multiplies by about 50 with every extra step. A program can still do this for a few steps, but it grows too fast for a real exchange. Lesson 6 runs this blunt search in code and measures where it breaks. This lesson prepares a better representation first.

Step 1: draw it

A graph is a set of points joined by arrows. Here the points are currencies, called nodes. An arrow from A to B means "you can exchange A for B", and the number written on the arrow is the rate: how many units of B you get for one unit of A. Arrows are called edges.

Our small market:

USD --0.90--> EUR
EUR --0.88--> GBP
GBP --1.30--> USD
EUR --1.10--> USD

A circle is a path that ends where it began. USD, EUR, GBP, USD is a circle with three edges.

Step 2: follow the money

Start with 100 USD and walk the circle:

  • after USD to EUR: 100 × 0.90 = 90 EUR
  • after EUR to GBP: 90 × 0.88 = 79.2 GBP
  • after GBP to USD: 79.2 × 1.30 = 102.96 USD

The circle multiplies your money by 0.90 × 0.88 × 1.30 = 1.0296. A product above 1 means profit. A product below 1 means loss. A product of exactly 1 means nothing happens.

So the question is: is there a circle whose product of rates is above 1?

Step 3: the problem with multiplying

Programs that search graphs are good at adding numbers along a path ("this road is 3 km, then 5 km, total 8"). They are not made for multiplying. Can we turn multiplication into addition?

Yes, with the logarithm. For this course you only need three facts about the natural logarithm, written ln:

  1. ln(a × b) = ln(a) + ln(b). Multiplication becomes addition.
  2. ln(1) = 0.
  3. If x is bigger than 1, ln(x) is positive. If x is between 0 and 1, ln(x) is negative.

You can check these on any calculator. For example, ln(2) = 0.693 and ln(3) = 1.099, and ln(6) = 1.792, which is 0.693 + 1.099.

Apply fact 1 to our circle:

ln(0.90 × 0.88 × 1.30) = ln(0.90) + ln(0.88) + ln(1.30)

Numbers: −0.1054 − 0.1278 + 0.2624 = +0.0292.

The circle has a total of +0.0292. A product above 1 became a sum above 0. We have turned "multiply" into "add".

Step 4: flip the sign

Graph algorithms usually look for short or cheap paths, so a small number is good for them, and a negative number is the best. We want profit to be small and negative, so we flip the sign. Define the weight of an edge:

weight = −ln(rate)

Then for our circle:

  • USD to EUR: −ln(0.90) = +0.1054
  • EUR to GBP: −ln(0.88) = +0.1278
  • GBP to USD: −ln(1.30) = −0.2624

Total weight = −0.0292, which is below zero.

A profitable circle is a circle whose weights add up to less than zero. It is called a negative cycle.

The rule in two lines:

  • product of rates above 1 is the same thing as sum of weights below 0;
  • the money multiplier of the circle is e raised to the power of minus the total weight (here e^0.0292 = 1.0296).

Step 5: see it in code

import math

rates = {
    ("USD", "EUR"): 0.90,
    ("EUR", "GBP"): 0.88,
    ("GBP", "USD"): 1.30,
    ("EUR", "USD"): 1.10,
}

money = 100.0
for a, b in [("USD", "EUR"), ("EUR", "GBP"), ("GBP", "USD")]:
    money *= rates[(a, b)]
    print(a, "->", b, "money =", round(money, 4))
print("product of rates:", round(0.90 * 0.88 * 1.30, 4))

weights = {edge: -math.log(rate) for edge, rate in rates.items()}
for edge, w in weights.items():
    print(edge, round(w, 4))

cycle = [("USD", "EUR"), ("EUR", "GBP"), ("GBP", "USD")]
total = sum(weights[e] for e in cycle)
print("sum of weights:", round(total, 4))
print("money multiplier:", round(math.exp(-total), 4))

Output:

USD -> EUR money = 90.0
EUR -> GBP money = 79.2
GBP -> USD money = 102.96
product of rates: 1.0296
('USD', 'EUR') 0.1054
('EUR', 'GBP') 0.1278
('GBP', 'USD') -0.2624
('EUR', 'USD') -0.0953
sum of weights: -0.0292
money multiplier: 1.0296

The two checks agree: the money went from 100 to 102.96, the sum of weights is −0.0292, and e raised to 0.0292 is the same 1.0296.

Where fees go

A fee of 0.1 percent on each trade means you get 0.999 of what the rate says. Replace each rate r by r × 0.999 before taking the logarithm. For a circle of three edges, the multiplier drops by about 0.3 percent, so a circle with a 0.29 percent margin (like ours) disappears. This is why the first filter in a real bot is to put the fees inside the weights, and we do that in lesson 10.

Let an AI agent write it

A vague request:

Find arbitrage in a graph of currencies.

A request that builds exactly this step:

Write graph.py. Take a dictionary of rates {("USD","EUR"): 0.90, ...}, build weights as minus natural log of rate, and add a function cycle_gain(cycle) that returns the money multiplier of a given circle as e to the power of minus the sum of weights. Test it on USD, EUR, GBP, USD with the rates 0.90, 0.88, 1.30 and print the result. It must print 1.0296. Do not search for cycles yet.

If the agent prints something else, the check fails and you ask it to fix it. This is the use of a number you can verify by hand.

Your exercise

  1. Run the code above. Do the five printed numbers match mine?
  2. Make a new market with four rates by hand: A to B 2.0, B to C 0.6, C to A 0.9. Compute the product by hand, then the weights, then the sum. Is it a profitable circle? (Product = 1.08, so yes.) Then check in code.
  3. Change one rate so that the product becomes exactly 1.0 and check that the sum of weights is 0.
  4. Take the same circle in the opposite direction. Prove that if one direction has product p, the other direction has about 1/p (when you use the reverse rate 1/r for each arrow). So at most one of the two directions can be profitable.

What comes next

Now the whole question is: how do you find a negative cycle in a graph with many nodes without trying all circles? Lesson 6 introduces the classic answer, Bellman-Ford.