Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new tutorial for multi-currencies arbitrage using Bellman-Ford Algorithm #48

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 311 additions & 0 deletions Documentation/Python/Bellman-Ford Currency Arbitrage.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [],
"toc_visible": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"source": [
"![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n",
"<hr>"
],
"metadata": {
"id": "j0a1UHtbMwGX"
}
},
{
"cell_type": "markdown",
"source": [
"# Crypto-Arbitrage with Bellman-Ford Algorithm written in Python\n",
"## Definitions\n",
"> Triangular arbitrage is the result of a discrepancy between three foreign currencies that occurs when the currency's exchange rates do not exactly match up. These opportunities are rare and traders who take advantage of them usually have advanced computer equipment and/or programs to automate the process.[^1]\n",
"\n",
"###### Graph:\n",
"> A graph is a combinatorial object composed of a set of vertices V (also known as nodes) and a set of edges E. The edges correspond to pairs of vertices, which are generally distinct, and without a notion of order in the sense where (u,v) and (v,u) denote the same edge.\n",
"At times, we consider a variant, the directed graph, where the edges have an ori- entation. In this case, the edges are usually known as arcs. The arc (u,v) has origin u and destination v. Most of the algorithms described in this book operate on directed graphs but can be applied to non-directed graphs by replacing each edge (u,v) by two arcs (u,v) and (v,u).\n",
"Graphs can contain additional information, such as weights or letters, in the form of labels on the vertices or the edges.\n",
"\n",
"###### Bellman-Ford algorithm:\n",
"> The Bellman-Ford algorithm finds the minimum weight path from a single source vertex to all other vertices on a weighted directed graph.\n",
"\n",
"Our goal is to develop a systematic method for detecting arbitrage opportunities by framing the problem in the language of graphs. \n",
"\n",
"## Approach\n",
"We will assign currencies to different vertices, and let the edge weight represent the exchange rate.\n",
"Find a cycle in the graph such that multiplying all the edge weights along that cycle results in a value greater than 1. In fact we have already described an algorithm that can find such a path – the problem is equivalent to finding a negative-weight cycle, provided we do some preprocessing on the edges.\n",
"\n",
"We note that Bellman-Ford computes the path weight by adding the individual edge weights. To make this work for exchange rates, which are multiplicative, a fix is to first take the logs of all the edge weights. Thus when we sum edge weights along a path we are actually multiplying exchange rates – we can recover the multiplied quantity by exponentiating the sum. Secondly, Bellman-Ford attempts to find minimum weight paths and negative edge cycles, whereas our arbitrage problem is about maximising the amoun t of currency received. Thus as a simple modification, we must also make our log weights negative.\n",
"Now we are able to apply Bellman-Ford. The minimal weight between two currency vertices corresponds to the optimal exchange rate, the value of which can be found by by exponentiating the negative sum of weights along the path. A negative-weight cycle on the negative-log graph corresponds to an arbitrage opportunity.\n",
"\n",
"## Code\n",
"> List of functions:\n",
"```\n",
"get_price()\n",
"```\n",
"- get last price from *QuantConnect[^3] API* and put together into a dataframe by using pandas\n",
"```\n",
"Trading.strategy()\n",
"```\n",
"- recall the `Graph` class[^2] and the `Graph.bellman_ford()` to perform the strategy and print the *boolean* variable `bol` (`True` if negative cycles were detected, `False` otherwise) and the **profit** expressed as %\n",
"\n",
"\n",
"\n",
"[^1]:[Investopedia: Triangular Arbitrage](https://www.investopedia.com/terms/t/triangulararbitrage.asp)\\\n",
"[^2]: The code to implement it has been taken from this [book](https://amzn.to/3bBI8tP)\\\n",
"[^3]: [Datasets from QuantConnect]https://www.quantconnect.com/docs/v2/research-environment/datasets/crypto\n"
],
"metadata": {
"id": "A2I-7YxuM2y6"
}
},
{
"cell_type": "markdown",
"source": [
"##### Importing modules\t"
],
"metadata": {
"id": "FVW7GG_IM98g"
}
},
{
"cell_type": "code",
"source": [
"import datetime as dt\n",
"import numpy as np\n",
"import pandas as pd\n",
"import warnings\n",
"warnings.filterwarnings(\"ignore\")\n",
"\n",
"\n",
"# QuantBook Analysis Tool \n",
"# For more information see [https://www.quantconnect.com/docs/v2/our-platform/research/getting-started]\n",
"qb = QuantBook()\n"
],
"metadata": {
"id": "39qjX9M2M3Mk"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"##### Defining function for getting data\t"
],
"metadata": {
"id": "vduiwBO_NEfW"
}
},
{
"cell_type": "code",
"source": [
"\n",
" \n",
"def get_data(sym1:str,sym2:str, sym3:str , start_time: datetime, end_time: datetime):\n",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - we use (self, arg1: type1 = a, arg2: type2 = b, arg3: type3 = c) -> type4:. Please fix the nit

" \"\"\"\n",
" @sym1: ticker name\n",
" @sym2: ticker name\n",
" @sym3: ticker name\n",
" @start_time: date to start getting data\n",
" @end_time: last day of getting data\n",
" \"\"\"\n",
"\n",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is some restrictions on how do you choose the crypto. Say if we choose BTCUSD, ETHUSD & BTCETH, there will be 4 currencies and 3 directional edges that always get positive results. If we're doing arbitrage, we should do BTCUSD, USDETH & ETHBTC to force a long-short balance in each currency. It will be good to add description above.

" ticker1 = qb.AddCrypto(sym1).Symbol\n",
" ticker2 = qb.AddCrypto(sym2).Symbol\n",
" ticker3 = qb.AddCrypto(sym3).Symbol\n",
"\n",
" price1 = qb.History(ticker1, start_time, end_time, Resolution.Tick)['lastprice']\n",
" price2 = qb.History(ticker2, start_time, end_time, Resolution.Tick)['lastprice']\n",
" price3 = qb.History(ticker3, start_time, end_time, Resolution.Tick)['lastprice']\n",
Comment on lines +129 to +131

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tick resolution might not align with time, recommend to use second instead. Also, there might not be trading information some illiquid currencies, it maybe better to use History[QuoteBar].

"\n",
" df = pd.DataFrame({'symbol':list(),'price':list()})\n",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May add a comment on why using negative log price (for minimum distance and eliminate compounding effect...)

" price1 = -np.log(price1.values.tolist())\n",
" price2 = -np.log(price2.values.tolist())\n",
" price3 = -np.log(price3.values.tolist())\n",
"\n",
" df=df.append([{'symbol': sym1 + '_' + str(price1.index()[-1]) , 'price': price1[-1]}], ignore_index=True)\n",
" df=df.append([{'symbol': sym2 + '_' + str(price2['Date'][-1]), 'price': price2[-1]}], ignore_index=True)\n",
" df=df.append([{'symbol': sym3 + '_' + str(price3['Date'][-1]) , 'price': price3[-1]}], ignore_index=True)\n",
" return df\n"
],
"metadata": {
"id": "3qWmjQHjNGsu"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"##### Create the Graph class\t"
],
"metadata": {
"id": "yGDNr1NhNUvB"
}
},
{
"cell_type": "code",
"source": [
"\n",
"class Graph:\n",
" def __init__(self):\n",
" self.neighbors = []\n",
" self.name2node = {}\n",
" self.node2name = []\n",
" self.weight = []\n",
" \n",
" def __len__(self):\n",
" return len(self.node2name)\n",
" def __getitem__(self,v):\n",
" return self.neighbors[v]\n",
" \n",
" def add_node(self,name):\n",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

" assert name not in self.name2node\n",
" self.name2node[name] = len(self.name2node)\n",
" self.node2name.append(name)\n",
" self.neighbors.append([]) \n",
" self.weight.append({})\n",
" return self.name2node[name]\n",
" \n",
" def add_edge(self,name_u,name_v,weight_uv=None):\n",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

" self.add_arc(name_u, name_v, weight_uv) \n",
" self.add_arc(name_v, name_u, weight_uv)\n",
"\n",
" def add_arc(self,name_u,name_v,weight_uv=None):\n",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

" u = self.name2node[name_u]\n",
" v = self.name2node[name_v] \n",
" self.neighbors[u].append(v)\n",
" self.weight[u][v] = weight_uv\n",
"\n",
" def bellman_ford(self, weight, source=0):\n",
" graph = self\n",
" n = len(graph)\n",
" dist = [float('inf')] * n\n",
" prec = [None]*n\n",
" dist[source] = 0\n",
" for nb_iterations in range(n):\n",
" changed = False\n",
" for node in range(n):\n",
" for neighbor in graph[node]:\n",
" alt = dist[node] + weight[node][neighbor]\n",
" if alt < dist[neighbor]:\n",
" dist[neighbor] = alt\n",
" prec[neighbor] = node\n",
" changed = True\n",
" if not changed:\n",
" return dist,prec,False\n",
" return dist, prec, True\n",
"\n",
"\n"
],
"metadata": {
"id": "DSPN5zyNNX4j"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"##### Defining the strategy\t"
],
"metadata": {
"id": "HHHCYHp4Nb1D"
}
},
{
"cell_type": "code",
"source": [
"def strategy(df: pd.DataFrame):\n",
" \"\"\"\n",
" df: DataFrame of prices\n",
" \"\"\"\n",
" global g\n",
" g = Graph()\n",
" for i in df.symbol:\n",
" g.add_node(i)\n",
" for j in df.price:\n",
" g.weight.append((j))\n",
" \n",
" for m in range(len(df)-1):\n",
" g.add_arc(df.symbol[m],df.symbol[m+1],df.price[m])\n",
" for n in reversed(range(len(df)-1)):\n",
" g.add_arc(df.symbol[n],df.symbol[n+1],df.price[n])\n",
" \n",
" dist, prec, bol = g.bellman_ford(g.weight,source=0)\n",
" #####\n",
" tot = 0\n",
" for i in dist:\n",
" tot *= i\n",
" profit = np.exp(-tot)-1\n",
" if bol:\n",
" print(f\"Profit from the strategy is: {profit*100:.2g}%\\n\")\n",
" return bol, profit\n",
"\n"
],
"metadata": {
"id": "GvU_aew-NcWu"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"##### implementing the strategy\t"
],
"metadata": {
"id": "2eoGzIJONgPX"
}
},
{
"cell_type": "code",
"source": [
"import time \n",
"start_time = datetime.datetime(2023, 5, 1)\n",
"end_time = datetime.datetime(2023, 5, 3)\n",
"for i in range(100):\n",
" df = get_data(\"BTCUSD\",\"ETHUSD\",\"LTCUSD\",start_time,end_time)\n",
" print(df)\n",
" print(strategy(df))\n",
" time.sleep(5)"
],
"metadata": {
"id": "0tymEMjwNgpw"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"##### Final Consideration:\n",
"This algorith was built for only three cryptocurrencies, but you can modify the code to apply with real time data and most important: **multi currencies**. You can actually add as many tickers as you want to test this algorithm (you need a proper subscription) and if you want to trade in a seconds environment, remember to change the code accordingly \t"
],
"metadata": {
"id": "XJThPMMRNmia"
}
},
{
"cell_type": "code",
"source": [],
"metadata": {
"id": "Rt0ADfeKNm4y"
},
"execution_count": null,
"outputs": []
}
]
}
Loading