visit
Currency Arbitrage is a trading strategy wherein a currency trader leverages different spreads offered by brokers for a particular currency pair through multiple trades.
An Arbitrage Pair represents a numerical ratio between the values of two currencies (EUR and US Dollar, for example), determining the exchange rate between them.
Potentially, we can use multiple intermediate currencies for profitable trading, known as a sure bet.
Arbitrage sure bet is a set of pairs to be used in a circular manner.
Sure bet length refers to the number of pairs that constitute a set of potential arbitrage opportunities.
Arbitrage Circle entails acquiring a currency, transferring it to another platform, conducting an exchange for other currencies, and ultimately returning to the original currency.
The exchange rate between two currencies via one or more intermediate currencies is calculated as the product of exchange rates of these intermediate transactions.
1 USD | 1 CHF | 1 YEN
0.91 CHF | 163.16 YEN | 0.0067 USD
----------------|-------------------|--------------
1.098901099 | 0.006128953 | 149.2537313
Now, we need to find a product of those values. A sequence of transactions becomes profitable when this product yields a value less than one:
1.098901099 * 0.006128953 * 149.2537313 = 1.005240803
0.91 CHF * 163.16 (YEN per 1 CHF) * 0.0067 (USD per 1 YEN) = 0.99478652 US Dollars
In simpler terms, Arbitrage Cycle is profitable when one unit of currency can be obtained for less than one unit of the same currency.
1 USD | 1 CHF | 1 YEN
0.92 CHF | 163.16 YEN | 0.0067 USD
----------------|-------------------|--------------
1.086956522 | 0.006128953 | 149.2537313
1.086956522 * 0.006128953 * 149.2537313 = 0.994314272
0.92 CHF * 163.16 (YEN per 1 CHF) * 0.0067 (USD per 1 YEN) = 1.00571824 US Dollars
Wuolah, we got some PROFIT! Now, let's see how to automate this using graphs analysis.
So, the formula to check for profits or losses in an Arbitrage Circle of 3 Arbitrage Pairs would look like this:USD/CHF * CHF/YEN * YEN/USD < 1.0
EUR USD
1 1 EUR
1 1 USD
EUR USD YEN CHF
1 1 1 1 EUR
1 1 1 1 USD
1 1 1 1 YEN
1 1 1 1 CHF
USD CHF YEN
{ 1.0, 1.10, 0.0067 } USD
{ 0.91, 1.0, 0.0061 } CHF
{ 148.84, 163.16, 1.0 } YEN
class GraphNode
{
public:
string Name;
};
class Graph
{
public:
vector<vector<double>> Matrix;
vector<GraphNode> Nodes;
};
Classical graph algorithms are not well-suited for working with the product of edge lengths because they are designed to find paths defined as the sum of these lengths (see implementations of any well-known classic path-finding algorithms ).
LogE(USD/CHF) * LogE(CHF/YEN) * LogE(YEN/USD) < 0.0
This simple mathematical trick allows us to shift from searching for a cycle with an edge length product less than one to searching for a cycle where the sum of the edge lengths is less than zero.
Our matrix values converted to a LogE(x) and rounded with 2 digits after the point, now look like this: USD CHF YEN
{ 0.0, 0.1, -5.01 } USD
{ -0.09, 0.0, -5.1 } CHF
{ 5.0, 5.09, 0.0 } YEN
We cannot apply classical BFS, DFS or even Dijkstra here because our graph may contain negative weights, which may lead to Negative Cycles while it traverses the graph. Negative cycles pose a challenge to the algorithm since it continually finds better solutions on each iteration.
As the aim of this article is computer science, I will use imaginary exchange rates that has nothing to do with the real ones.
graph.Nodes.push_back({ "USD" });
graph.Nodes.push_back({ "CHF" });
graph.Nodes.push_back({ "YEN" });
graph.Nodes.push_back({ "GBP" });
graph.Nodes.push_back({ "CNY" });
graph.Nodes.push_back({ "EUR" });
// Define exchange rates for pairs of currencies below
// USD CHF YEN GBP CNY EUR
graph.Matrix = { { 0.0, 0.41, INF, INF, INF, 0.29 }, // USD
{ INF, 0.0, 0.51, INF, 0.32, INF }, // CHF
{ INF, INF, 0.0, 0.50, INF, INF }, // YEN
{ 0.45, INF, INF, 0.0, INF, -0.38 }, // GBP
{ INF, INF, 0.32, 0.36, 0.0, INF }, // CNY
{ INF, -0.29, INF, INF, 0.21, 0.0 } }; // EUR
vector<double> _shortestPath;
vector<int> _previousVertex;
void FindPath(Graph& graph, int start)
{
int verticesNumber = graph.Nodes.size();
_shortestPath.resize(verticesNumber, INF);
_previousVertex.resize(verticesNumber, -1);
_shortestPath[start] = 0;
// For each vertex, apply relaxation for all the edges V - 1 times.
for (int k = 0; k < verticesNumber - 1; k++)
for (int from = 0; from < verticesNumber; from++)
for (int to = 0; to < verticesNumber; to++)
if (_shortestPath[to] > _shortestPath[from] + graph.Matrix[from][to])
{
_shortestPath[to] = _shortestPath[from] + graph.Matrix[from][to];
_previousVertex[to] = from;
}
}
Running this code for the Chinese Yuan fills the _previousVertex array and yields results like this:
Path from 4 to 0 is : 4(CNY) 3(GBP) 0(USD)
Path from 4 to 1 is : 4(CNY) 3(GBP) 5(EUR) 1(CHF)
Path from 4 to 2 is : 4(CNY) 3(GBP) 5(EUR) 1(CHF) 2(YEN)
Path from 4 to 3 is : 4(CNY) 3(GBP)
Path from 4 to 4 is : 4(CNY)
Path from 4 to 5 is : 4(CNY) 3(GBP) 5(EUR)
And again, I will not focus on finding only one best one, as it is relatively simple task and not the goal of the article.
vector<double> _shortestPath;
vector<int> _previousVertex;
bool ContainsNegativeCycles(Graph& graph, int start)
{
int verticesNumber = graph.Nodes.size();
_shortestPath.resize(verticesNumber, INF);
_previousVertex.resize(verticesNumber, -1);
_shortestPath[start] = 0;
// For each vertex, apply relaxation for all the edges V - 1 times.
for (int k = 0; k < verticesNumber - 1; k++)
{
updated = false;
for (int from = 0; from < verticesNumber; from++)
{
for (int to = 0; to < verticesNumber; to++)
{
if (_shortestPath[to] > _shortestPath[from] + graph.Matrix[from][to])
{
_shortestPath[to] = _shortestPath[from] + graph.Matrix[from][to];
_previousVertex[to] = from;
updated = true;
}
}
}
if (!updated) // No changes in paths, means we can finish earlier.
break;
}
// Run one more relaxation step to detect which nodes are part of a negative cycle.
if (updated)
for (int from = 0; from < verticesNumber; from++)
for (int to = 0; to < verticesNumber; to++)
if (_shortestPath[to] > _shortestPath[from] + graph.Matrix[from][to])
// A negative cycle has occurred if we can find a better path beyond the optimal solution.
return true;
return false;
}
graph.Nodes.push_back({ "USD" }); // 1 (Index = 0)
graph.Nodes.push_back({ "CHF" });
graph.Nodes.push_back({ "YEN" });
graph.Nodes.push_back({ "GBP" });
graph.Nodes.push_back({ "CNY" });
graph.Nodes.push_back({ "EUR" });
graph.Nodes.push_back({ "XXX" });
graph.Nodes.push_back({ "YYY" }); // 8 (Index = 7)
// USD CHF YEN GBP CNY EUR XXX YYY
graph.Matrix = { { 0.0, 1.0, INF, INF, INF, INF, INF, INF }, // USD
{ INF, 0.0, 1.0, INF, INF, 4.0, 4.0, INF }, // CHF
{ INF, INF, 0.0, INF, 1.0, INF, INF, INF }, // YEN
{ INF, INF, 1.0, 0.0, INF, INF, INF, INF }, // GBP
{ INF, INF, INF, -3.0, 0.0, INF, INF, INF }, // CNY
{ INF, INF, INF, INF, INF, 0.0, 5.0, 3.0 }, // EUR
{ INF, INF, INF, INF, INF, INF, 0.0, 4.0 }, // XXX
{ INF, INF, INF, INF, INF, INF, INF, 0.0 } }; // YYY
Graph contains negative cycle.
bool FindPathsAndNegativeCycles(Graph& graph, int start)
{
int verticesNumber = graph.Nodes.size();
_shortestPath.resize(verticesNumber, INF);
_previousVertex.resize(verticesNumber, -1);
_shortestPath[start] = 0;
for (int k = 0; k < verticesNumber - 1; k++)
for (int from = 0; from < verticesNumber; from++)
for (int to = 0; to < verticesNumber; to++)
{
if (graph.Matrix[from][to] == INF) // Edge not exists
{
continue;
}
if (_shortestPath[to] > _shortestPath[from] + graph.Matrix[from][to])
{
_shortestPath[to] = _shortestPath[from] + graph.Matrix[from][to];
_previousVertex[to] = from;
}
}
bool negativeCycles = false;
for (int k = 0; k < verticesNumber - 1; k++)
for (int from = 0; from < verticesNumber; from++)
for (int to = 0; to < verticesNumber; to++)
{
if (graph.Matrix[from][to] == INF) // Edge not exists
{
continue;
}
if (_shortestPath[to] > _shortestPath[from] + graph.Matrix[from][to])
{
_shortestPath[to] = NEG_INF;
_previousVertex[to] = -2;
negativeCycles = true;
}
}
return negativeCycles;
}
Graph contains negative cycle.
Path from 0 to 0 is : 0(USD)
Path from 0 to 1 is : 0(USD) 1(CHF)
Path from 0 to 2 is : Infinite number of shortest paths (negative cycle).
Path from 0 to 3 is : Infinite number of shortest paths (negative cycle).
Path from 0 to 4 is : Infinite number of shortest paths (negative cycle).
Path from 0 to 5 is : 0(USD) 1(CHF) 5(EUR)
Path from 0 to 6 is : 0(USD) 1(CHF) 6(XXX)
Path from 0 to 7 is : 0(USD) 1(CHF) 5(EUR) 7(YYY)
graph.Nodes.push_back({ "USD" }); // 1 (Index = 0)
graph.Nodes.push_back({ "CHF" });
graph.Nodes.push_back({ "YEN" }); // 3 (Index = 2)
// LogE(x) table: USD CHF YEN
graph.Matrix = { { 0.0, 0.489, -0.402 }, // USD
{ -0.489, 0.0, -0.891 }, // CHF
{ 0.402, 0.89, 0.0 } }; // YEN
from = 0;
FindPathsAndNegativeCycles(graph, from);
Graph contains negative cycle.
Path from 0 to 0 is: Infinite number of shortest paths (negative cycle).
Path from 0 to 1 is: Infinite number of shortest paths (negative cycle).
Path from 0 to 2 is: Infinite number of shortest paths (negative cycle).
// LogE(x) table: USD CHF YEN
graph.Matrix = { { 0.0, 0.490, -0.402 }, // USD
{ -0.489, 0.0, -0.891 }, // CHF
{ 0.403, 0.891, 0.0 } }; // YEN
from = 0;
FindPathsAndNegativeCycles(graph, from);
Path from 0 to 0 is : 0(USD)
Path from 0 to 1 is : 0(USD) 2(YEN) 1(CHF)
Path from 0 to 2 is : 0(USD) 2(YEN)
graph.Nodes.push_back({ "USD" }); // 1 (Index = 0)
graph.Nodes.push_back({ "CHF" });
graph.Nodes.push_back({ "YEN" });
graph.Nodes.push_back({ "GBP" });
graph.Nodes.push_back({ "CNY" }); // 5 (Index = 4)
// LogE(x) table: USD CHF YEN GBP CNY
graph.Matrix = { { 0.0, 0.490, -0.402, 0.7, 0.413 }, // USD
{ -0.489, 0.0, -0.891, 0.89, 0.360 }, // CHF
{ 0.403, 0.891, 0.0, 0.91, 0.581 }, // YEN
{ 0.340, 0.405, 0.607, 0.0, 0.72 }, // GBP
{ 0.403, 0.350, 0.571, 0.71, 0.0 } }; // CNY
from = 0;
runDetectNegativeCycles(graph, from);
Path from 0 to 0 is : 0(USD)
Path from 0 to 1 is : 0(USD) 2(YEN) 1(CHF)
Path from 0 to 2 is : 0(USD) 2(YEN)
Path from 0 to 3 is : 0(USD) 2(YEN) 3(GBP)
Path from 0 to 4 is : 0(USD) 2(YEN) 4(CNY)
Therefore, minimizing time costs and reducing commissions are paramount, achieved by limiting the length of the sure bet.