├── .github └── workflows │ └── Formatter.yaml ├── .gitignore ├── LICENSE.md ├── README.md ├── examples ├── fetch_funding_rate.py ├── fetch_funding_rate_all_exchanges.py ├── fetch_funding_rate_history.py ├── get_commission.py ├── get_large_divergence_multi_exchange.py └── get_large_divergence_single_exchange.py ├── funding_rate_arbitrage ├── __init__.py └── frarb.py ├── img └── readme_funding_rate_history.png ├── requirements.txt └── setup.py /.github/workflows/Formatter.yaml: -------------------------------------------------------------------------------- 1 | name: Format code 2 | 3 | on: push 4 | 5 | jobs: 6 | formatter: 7 | name: formatter 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.11.0] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | ref: ${{ github.head_ref }} 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install Dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install autoflake black isort 25 | - name: autoflake 26 | run: autoflake -r . 27 | - name: black 28 | run: black . 29 | - name: isort 30 | run: isort . 31 | - name: Auto Commit 32 | uses: stefanzweifel/git-auto-commit-action@v4 33 | with: 34 | commit_message: Apply Code Formatter Change -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Hirotaka Aoki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # funding-rate-arbitrage 2 | [![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-3110//) 3 | 4 | ## Python library for funding rate arbitrage 5 | 6 | A framework to help you easily perform funding rate arbitrage on the following major centralized cryptocurrency exchanges (CEX). 7 | 8 | - binance 9 | - bybit 10 | - OKX 11 | - gate.io 12 | - CoinEx 13 | - Bitget 14 | 15 | This library can detect perpetual contract with a large divergence in funding rates between CEXs. 16 | 17 | **NOTE: This library does not include the feature to perform automatic funding rate arbitrage.** 18 | 19 | ## What's FR Arbitrage? 20 | Arbitrage of different funding rates among different exchanges is another trading strategy that takes advantage of the disparity in funding rates for the same cryptocurrency perpetual contracts between exchanges. It involves combining long positions with low funding rates from one exchange with short positions from another exchange with higher funding rates to generate profits. Funding rates are periodic payments between long and short traders to ensure that the perpetual contract price remains close to the underlying asset price. 21 | 22 | ## Installation 23 | 24 | 25 | ```bash 26 | pip install git+https://github.com/aoki-h-jp/funding-rate-arbitrage 27 | ``` 28 | 29 | ## Usage 30 | ### Fetch FR & commission 31 | 32 | ```python 33 | from funding_rate_arbitrage.frarb import FundingRateArbitrage 34 | 35 | fr = FundingRateArbitrage() 36 | 37 | # fetch all perp funding rate on binance 38 | fr_binance = fr.fetch_all_funding_rate(exchange='binance') 39 | 40 | # get commission on binance with futures, maker 41 | cm_binance = fr.get_commission(exchange='binance', trade='futures', taker=False) 42 | ``` 43 | 44 | ### Fetch FR history 45 | 46 | ```python 47 | from funding_rate_arbitrage.frarb import FundingRateArbitrage 48 | 49 | fr = FundingRateArbitrage() 50 | 51 | # figure funding rate history 52 | fr.fetch_funding_rate_history(exchange='binance', symbol='BTC/USDT:USDT') 53 | ``` 54 | !['funding rate history example'](./img/readme_funding_rate_history.png) 55 | 56 | 57 | ### Display large FR divergence on single CEX 58 | ```bash 59 | # display large funding rate divergence on bybit 60 | >>> fr.display_large_divergence_single_exchange(exchange='bybit', display_num=5) 61 | Funding Rate [%] Commission [%] Revenue [/100 USDT] 62 | CTC/USDT:USDT 0.1794 0.32 -0.1406 63 | CREAM/USDT:USDT 0.0338 0.32 -0.2862 64 | TWT/USDT:USDT 0.0295 0.32 -0.2905 65 | TLM/USDT:USDT 0.0252 0.32 -0.2948 66 | JASMY/USDT:USDT 0.0100 0.32 -0.3100 67 | 68 | # display Top 5 large funding rate divergence on bybit one by one. 69 | >>> fr.display_one_by_one_single_exchange(exchange='bybit', display_num=5) 70 | ------------------------------------------------ 71 | Revenue: -0.1663 / 100USDT 72 | SELL: CTC/USDT:USDT Perp 73 | BUY: CTC/USDT:USDT Spot 74 | Funding Rate: 0.1537 % 75 | Commission: 0.32 % 76 | ------------------------------------------------ 77 | Revenue: -0.17200000000000001 / 100USDT 78 | SELL: CREAM/USDT:USDT Perp 79 | BUY: CREAM/USDT:USDT Spot 80 | Funding Rate: 0.1480 % 81 | Commission: 0.32 % 82 | ------------------------------------------------ 83 | Revenue: -0.2107 / 100USDT 84 | SELL: BOBA/USDT:USDT Perp 85 | BUY: BOBA/USDT:USDT Spot 86 | Funding Rate: 0.1093 % 87 | Commission: 0.32 % 88 | ------------------------------------------------ 89 | Revenue: -0.2854 / 100USDT 90 | SELL: TLM/USDT:USDT Perp 91 | BUY: TLM/USDT:USDT Spot 92 | Funding Rate: 0.0346 % 93 | Commission: 0.32 % 94 | ------------------------------------------------ 95 | Revenue: -0.2953 / 100USDT 96 | SELL: TOMO/USDT:USDT Perp 97 | BUY: TOMO/USDT:USDT Spot 98 | Funding Rate: 0.0247 % 99 | Commission: 0.32 % 100 | 101 | # display Top 5 large funding rate divergence on bybit one by one (minus FR). 102 | >>> fr.display_one_by_one_single_exchange(exchange='bybit', display_num=5, minus=True) 103 | ------------------------------------------------ 104 | Revenue: -0.1458 / 100USDT 105 | SELL: ARPA/USDT:USDT Options 106 | BUY: ARPA/USDT:USDT Perp 107 | Funding Rate: -0.2342 % 108 | Commission: 0.38 % 109 | ------------------------------------------------ 110 | Revenue: -0.2569 / 100USDT 111 | SELL: MASK/USDT:USDT Options 112 | BUY: MASK/USDT:USDT Perp 113 | Funding Rate: -0.1231 % 114 | Commission: 0.38 % 115 | ------------------------------------------------ 116 | Revenue: -0.3056 / 100USDT 117 | SELL: APE/USD:USDC Options 118 | BUY: APE/USD:USDC Perp 119 | Funding Rate: -0.0744 % 120 | Commission: 0.38 % 121 | ------------------------------------------------ 122 | Revenue: -0.3158 / 100USDT 123 | SELL: SWEAT/USD:USDC Options 124 | BUY: SWEAT/USD:USDC Perp 125 | Funding Rate: -0.0642 % 126 | Commission: 0.38 % 127 | ------------------------------------------------ 128 | Revenue: -0.3166 / 100USDT 129 | SELL: APE/USDT:USDT Options 130 | BUY: APE/USDT:USDT Perp 131 | Funding Rate: -0.0634 % 132 | Commission: 0.38 % 133 | ``` 134 | 135 | ### Display large FR divergence between CEX 136 | ```bash 137 | # display large funding rate divergence between CEX. 138 | >>> fr.display_large_divergence_multi_exchange(display_num=5, sorted_by='divergence') 139 | binance bybit okx bitget gate coinex Divergence [%] Commission [%] Revenue [/100 USDT] 140 | FIL/USDT:USDT -0.008948 -0.0229 -0.334535 -0.0084 -0.0240 -0.737473 0.729073 0.202 0.527073 141 | HNT/USDT:USDT -0.023885 -0.0125 NaN NaN 0.0056 0.304442 0.328327 0.180 0.148327 142 | WAXP/USDT:USDT NaN NaN NaN NaN 0.0100 0.205733 0.195733 0.500 -0.304267 143 | AXS/USDT:USDT -0.021292 -0.0385 -0.205174 -0.0212 -0.0282 -0.215217 0.194017 0.202 -0.007983 144 | OP/USDT:USDT -0.060397 -0.0228 -0.206011 -0.0601 -0.0147 -0.148713 0.191311 0.200 -0.008689 145 | 146 | # sorted by revenue. 147 | >>> fr.display_large_divergence_multi_exchange(display_num=5, sorted_by='revenue') 148 | binance bybit okx bitget gate coinex Divergence [%] Commission [%] Revenue [/100 USDT] 149 | FIL/USDT:USDT -0.004703 -0.0232 -0.334535 -0.0047 -0.0245 -0.737473 0.732773 0.202 0.530773 150 | HNT/USDT:USDT -0.030722 -0.0141 NaN NaN 0.0051 0.304442 0.335164 0.180 0.155164 151 | OP/USDT:USDT -0.057856 -0.0235 -0.206011 -0.0589 -0.0162 -0.148713 0.189811 0.200 -0.010189 152 | MKR/USDT:USDT 0.010000 0.0100 -0.056437 0.0104 0.0100 0.075530 0.131967 0.200 -0.068033 153 | TON/USDT:USDT NaN NaN -0.023741 NaN 0.0100 -0.116483 0.126483 0.200 -0.073517 154 | 155 | # Display Top 5 large funding rate divergence between multi exchange. 156 | >>> fr.display_one_by_one_multi_exchanges(display_num=5) 157 | ------------------------------------------------ 158 | Revenue: 0.2184 USDT / 100USDT 159 | SELL: coinex IOTA/USDT:USDT Perp (Funding Rate 0.3478 %) 160 | BUY: okx IOTA/USDT:USDT Perp (Funding Rate -0.0706 %) 161 | Divergence: 0.4184 % 162 | Commission: 0.2000 % 163 | ------------------------------------------------ 164 | Revenue: 0.1191 USDT / 100USDT 165 | SELL: coinex DASH/USDT:USDT Perp (Funding Rate 0.4267 %) 166 | BUY: okx DASH/USDT:USDT Spot 167 | Divergence: 0.4191 % 168 | Commission: 0.3000 % 169 | ------------------------------------------------ 170 | Revenue: 0.1080 USDT / 100USDT 171 | SELL: okx TON/USDT:USDT Perp (Funding Rate 0.0482 %) 172 | BUY: coinex TON/USDT:USDT Perp (Funding Rate -0.2598 %) 173 | Divergence: 0.3080 % 174 | Commission: 0.2000 % 175 | ------------------------------------------------ 176 | Revenue: 0.0842 USDT / 100USDT 177 | SELL: binance GMX/USDT:USDT Perp (Funding Rate 0.0100 %) 178 | BUY: coinex GMX/USDT:USDT Perp (Funding Rate -0.2542 %) 179 | Divergence: 0.2642 % 180 | Commission: 0.1800 % 181 | ------------------------------------------------ 182 | Revenue: 0.0447 USDT / 100USDT 183 | SELL: okx FIL/USDT:USDT Perp (Funding Rate 0.2416 %) 184 | BUY: gate FIL/USDT:USDT Perp (Funding Rate -0.0031 %) 185 | Divergence: 0.2447 % 186 | Commission: 0.2000 % 187 | ``` 188 | 189 | ## Disclaimer 190 | This project is for educational purposes only. You should not construe any such information or other material as legal, 191 | tax, investment, financial, or other advice. Nothing contained here constitutes a solicitation, recommendation, 192 | endorsement, or offer by me or any third party service provider to buy or sell any securities or other financial 193 | instruments in this or in any other jurisdiction in which such solicitation or offer would be unlawful under the 194 | securities laws of such jurisdiction. 195 | 196 | Under no circumstances will I be held responsible or liable in any way for any claims, damages, losses, expenses, costs, 197 | or liabilities whatsoever, including, without limitation, any direct or indirect damages for loss of profits. 198 | -------------------------------------------------------------------------------- /examples/fetch_funding_rate.py: -------------------------------------------------------------------------------- 1 | """ 2 | An example of fetching funding rate 3 | """ 4 | from funding_rate_arbitrage.frarb import FundingRateArbitrage 5 | 6 | if __name__ == "__main__": 7 | # fetch from binance 8 | fr = FundingRateArbitrage() 9 | print(fr.fetch_all_funding_rate(exchange="binance")) 10 | -------------------------------------------------------------------------------- /examples/fetch_funding_rate_all_exchanges.py: -------------------------------------------------------------------------------- 1 | """ 2 | An example of fetching funding rate 3 | """ 4 | from funding_rate_arbitrage.frarb import FundingRateArbitrage 5 | 6 | if __name__ == "__main__": 7 | # fetch from all exchanges 8 | fr = FundingRateArbitrage() 9 | for ex in fr.get_exchanges(): 10 | print(ex) 11 | print(fr.fetch_all_funding_rate(exchange=ex)) 12 | -------------------------------------------------------------------------------- /examples/fetch_funding_rate_history.py: -------------------------------------------------------------------------------- 1 | """ 2 | An example of fetching funding rate history 3 | """ 4 | from funding_rate_arbitrage.frarb import FundingRateArbitrage 5 | 6 | if __name__ == "__main__": 7 | # fetch from binance 8 | fr = FundingRateArbitrage() 9 | # figure funding rate history 10 | fr.figure_funding_rate_history(exchange="binance", symbol="BTC/USDT:USDT") 11 | -------------------------------------------------------------------------------- /examples/get_commission.py: -------------------------------------------------------------------------------- 1 | """ 2 | An example of getting commission. 3 | """ 4 | from funding_rate_arbitrage.frarb import FundingRateArbitrage 5 | 6 | if __name__ == "__main__": 7 | fr = FundingRateArbitrage() 8 | # binance futures maker commission with BNB 9 | print("binance futures maker commission with BNB") 10 | print( 11 | fr.get_commission( 12 | exchange="binance", trade="futures", taker=False, by_token=True 13 | ) 14 | ) 15 | 16 | # bybit spot taker commission 17 | print("bybit spot taker commission") 18 | print(fr.get_commission(exchange="bybit", trade="spot")) 19 | 20 | # OKX spot maker commission 21 | print("OKX spot maker commission") 22 | print(fr.get_commission(exchange="okx", trade="spot", taker=False)) 23 | -------------------------------------------------------------------------------- /examples/get_large_divergence_multi_exchange.py: -------------------------------------------------------------------------------- 1 | """ 2 | An example of getting large divergence between multi exchange. 3 | """ 4 | from funding_rate_arbitrage.frarb import FundingRateArbitrage 5 | 6 | if __name__ == "__main__": 7 | fr = FundingRateArbitrage() 8 | # Display Top 5 large funding rate divergence between multi exchange. 9 | # print(fr.display_large_divergence_multi_exchange(display_num=5, sorted_by='divergence')) 10 | 11 | # TODO: Errors occur when running consecutively. 12 | # ccxt.base.errors.BadRequest: binance {"code":-1104,"msg":"Not all sent parameters were read; read '0' parameter(s) but was sent '1'."} 13 | # Display Top 5 large funding rate divergence between multi exchange sorted by revenue. 14 | # print(fr.display_large_divergence_multi_exchange(display_num=5, sorted_by='revenue')) 15 | 16 | # Display Top 5 large funding rate divergence between multi exchange. 17 | fr.display_one_by_one_multi_exchanges(display_num=5) 18 | -------------------------------------------------------------------------------- /examples/get_large_divergence_single_exchange.py: -------------------------------------------------------------------------------- 1 | """ 2 | An example of displaying large divergence by single exchange. 3 | """ 4 | from funding_rate_arbitrage.frarb import FundingRateArbitrage 5 | 6 | if __name__ == "__main__": 7 | fr = FundingRateArbitrage() 8 | # Display Top 5 large funding rate divergence on binance. 9 | print( 10 | fr.display_large_divergence_single_exchange(exchange="binance", display_num=5) 11 | ) 12 | 13 | # Display Top 5 large funding rate divergence on bybit (minus FR). 14 | print( 15 | fr.display_large_divergence_single_exchange( 16 | exchange="bybit", display_num=5, minus=True 17 | ) 18 | ) 19 | 20 | # Display Top 5 large funding rate divergence on binance one by one. 21 | fr.display_one_by_one_single_exchange(exchange="binance", display_num=5) 22 | 23 | # Display Top 5 large funding rate divergence on bybit one by one (minus FR). 24 | fr.display_one_by_one_single_exchange(exchange="bybit", display_num=5, minus=True) 25 | -------------------------------------------------------------------------------- /funding_rate_arbitrage/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Funding Rate Arbitrage: A trading strategy that takes advantage of the difference in funding rates between perpetual swaps. 3 | """ 4 | import funding_rate_arbitrage.frarb 5 | -------------------------------------------------------------------------------- /funding_rate_arbitrage/frarb.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main class of funding-rate-arbitrage 3 | """ 4 | import logging 5 | from datetime import datetime 6 | 7 | import ccxt 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | import pandas as pd 11 | from ccxt import ExchangeError 12 | from numpy import ndarray 13 | from rich import print 14 | from rich.logging import RichHandler 15 | 16 | logging.basicConfig( 17 | level=logging.INFO, 18 | format="%(message)s", 19 | datefmt="[%X]", 20 | handlers=[RichHandler(rich_tracebacks=True)], 21 | ) 22 | log = logging.getLogger("rich") 23 | 24 | 25 | class FundingRateArbitrage: 26 | def __init__(self): 27 | self.exchanges = ["binance", "bybit", "okx", "bitget", "gate", "coinex"] 28 | # commission 29 | self.is_taker = True 30 | self.by_token = False 31 | 32 | @staticmethod 33 | def fetch_all_funding_rate(exchange: str) -> dict: 34 | """ 35 | Fetch funding rates on all perpetual contracts listed on the exchange. 36 | 37 | Args: 38 | exchange (str): Name of exchange (binance, bybit, ...) 39 | 40 | Returns (dict): Dict of perpetual contract pair and funding rate. 41 | 42 | """ 43 | ex = getattr(ccxt, exchange)() 44 | info = ex.load_markets() 45 | perp = [p for p in info if info[p]["linear"]] 46 | fr_d = {} 47 | for p in perp: 48 | try: 49 | fr_d[p] = ex.fetch_funding_rate(p)["fundingRate"] 50 | except ExchangeError: 51 | log.exception(f"{p} is not perp.") 52 | return fr_d 53 | 54 | @staticmethod 55 | def fetch_funding_rate_history(exchange: str, symbol: str) -> tuple: 56 | """ 57 | Fetch funding rates on perpetual contracts listed on the exchange. 58 | 59 | Args: 60 | exchange (str): Name of exchange (binance, bybit, ...) 61 | symbol (str): Symbol (BTC/USDT:USDT, ETH/USDT:USDT, ...). 62 | 63 | Returns (tuple): settlement time, funding rate. 64 | 65 | """ 66 | ex = getattr(ccxt, exchange)() 67 | funding_history_dict = ex.fetch_funding_rate_history(symbol=symbol) 68 | funding_time = [ 69 | datetime.fromtimestamp(d["timestamp"] * 0.001) for d in funding_history_dict 70 | ] 71 | funding_rate = [d["fundingRate"] * 100 for d in funding_history_dict] 72 | return funding_time, funding_rate 73 | 74 | def figure_funding_rate_history(self, exchange: str, symbol: str) -> None: 75 | """ 76 | Figure funding rates on perpetual contracts listed on the exchange. 77 | 78 | Args: 79 | exchange (str): Name of exchange (binance, bybit, ...) 80 | symbol (str): Symbol (BTC/USDT:USDT, ETH/USDT:USDT, ...). 81 | 82 | Returns: None 83 | 84 | """ 85 | funding_time, funding_rate = self.fetch_funding_rate_history( 86 | exchange=exchange, symbol=symbol 87 | ) 88 | plt.plot(funding_time, funding_rate, label="funding rate") 89 | plt.hlines( 90 | xmin=funding_time[0], 91 | xmax=funding_time[-1], 92 | y=sum(funding_rate) / len(funding_rate), 93 | label="average", 94 | colors="r", 95 | linestyles="-.", 96 | ) 97 | plt.title(f"Funding rate history {symbol}") 98 | plt.xlabel("timestamp") 99 | plt.ylabel("Funding rate [%]") 100 | plt.xticks(rotation=45) 101 | plt.yticks(rotation=45) 102 | plt.legend() 103 | plt.tight_layout() 104 | plt.show() 105 | 106 | def get_funding_rate_volatility(self, exchange: str, symbol: str) -> ndarray: 107 | """ 108 | Get funding rate standard deviation volatility on all perpetual contracts listed on the exchange. 109 | 110 | Args: 111 | exchange (str): Name of exchange (binance, bybit, ...) 112 | symbol (str): Symbol (BTC/USDT:USDT, ETH/USDT:USDT, ...). 113 | 114 | Returns: Funding rate standard deviation volatility. 115 | 116 | """ 117 | _, funding_rate = self.fetch_funding_rate_history( 118 | exchange=exchange, symbol=symbol 119 | ) 120 | return np.std(funding_rate) 121 | 122 | def display_large_divergence_single_exchange( 123 | self, exchange: str, minus=False, display_num=10 124 | ) -> pd.DataFrame: 125 | """ 126 | Display large funding rate divergence on single CEX. 127 | Args: 128 | exchange (str): Name of exchange (binance, bybit, ...) 129 | minus (bool): Sorted by minus FR or plus FR. 130 | display_num (int): Number of display. 131 | 132 | Returns (pd.DataFrame): DataFrame sorted by large funding rate divergence. 133 | 134 | """ 135 | return ( 136 | self.get_large_divergence_dataframe_single_exchange( 137 | exchange=exchange, minus=minus 138 | ) 139 | .sort_values(by="Funding Rate [%]", ascending=minus) 140 | .head(display_num) 141 | ) 142 | 143 | def display_large_divergence_multi_exchange( 144 | self, display_num=10, sorted_by="revenue" 145 | ) -> pd.DataFrame: 146 | """ 147 | Display large funding rate divergence between multi CEX. 148 | "multi CEX" refers to self.exchanges. 149 | Args: 150 | display_num (int): Number of display. 151 | sorted_by (str): Sorted by "revenue" or "divergence" 152 | 153 | Returns (pd.DataFrame): DataFrame sorted by large funding rate divergence. 154 | 155 | """ 156 | if sorted_by == "revenue": 157 | sorted_by = "Revenue [/100 USDT]" 158 | elif sorted_by == "divergence": 159 | sorted_by = "Divergence [%]" 160 | else: 161 | log.error(f"{sorted_by} is not available.") 162 | raise KeyError 163 | 164 | return ( 165 | self.get_large_divergence_dataframe_multi_exchanges() 166 | .sort_values(by=sorted_by, ascending=False) 167 | .head(display_num) 168 | ) 169 | 170 | def get_large_divergence_dataframe_single_exchange( 171 | self, exchange: str, minus=False 172 | ): 173 | """ 174 | Get large funding rate divergence on single CEX. 175 | Args: 176 | exchange (str): Name of exchange (binance, bybit, ...) 177 | minus (bool): Sorted by minus FR or plus FR. 178 | 179 | Returns (pd.DataFrame): large funding rate divergence DataFrame. 180 | 181 | """ 182 | fr = self.fetch_all_funding_rate(exchange=exchange) 183 | columns = ["Funding Rate [%]", "Commission [%]", "Revenue [/100 USDT]"] 184 | sr_fr = pd.Series(list(fr.values())) * 100 185 | # TODO: Check perp or spot or options exists on CEX. 186 | if minus: 187 | cm = ( 188 | self.get_commission( 189 | exchange=exchange, 190 | trade="futures", 191 | taker=self.is_taker, 192 | by_token=self.by_token, 193 | ) 194 | + self.get_commission( 195 | exchange=exchange, 196 | trade="options", 197 | taker=self.is_taker, 198 | by_token=self.by_token, 199 | ) 200 | + self.get_commission( 201 | exchange=exchange, 202 | trade="spot", 203 | taker=self.is_taker, 204 | by_token=self.by_token, 205 | ) 206 | ) 207 | sr_cm = pd.Series([cm * 2 for i in range(len(sr_fr))]) 208 | sr_rv = abs(sr_fr) - sr_cm 209 | else: 210 | cm = self.get_commission( 211 | exchange=exchange, 212 | trade="futures", 213 | taker=self.is_taker, 214 | by_token=self.by_token, 215 | ) + self.get_commission( 216 | exchange=exchange, 217 | trade="spot", 218 | taker=self.is_taker, 219 | by_token=self.by_token, 220 | ) 221 | sr_cm = pd.Series([cm * 2 for i in range(len(sr_fr))]) 222 | sr_rv = sr_fr - sr_cm 223 | 224 | df = pd.concat([sr_fr, sr_cm, sr_rv], axis=1) 225 | df.index = list(fr.keys()) 226 | df.columns = columns 227 | return df 228 | 229 | def get_large_divergence_dataframe_multi_exchanges(self): 230 | """ 231 | Get large funding rate divergence between multi CEX. 232 | "multi CEX" refers to self.exchanges. 233 | Returns (pd.DataFrame): large funding rate divergence DataFrame. 234 | 235 | """ 236 | df = pd.DataFrame() 237 | for ex in self.exchanges: 238 | log.info(f"fetching {ex}") 239 | fr = self.fetch_all_funding_rate(exchange=ex) 240 | df_ex = pd.DataFrame(fr.values(), index=list(fr.keys()), columns=[ex]).T 241 | df = pd.concat([df, df_ex]) 242 | df = df.T * 100 243 | 244 | diff_d = {} 245 | for i, data in df.iterrows(): 246 | diff = data.max() - data.min() 247 | diff_d[i] = diff 248 | 249 | df_diff = pd.DataFrame( 250 | diff_d.values(), index=list(diff_d.keys()), columns=["Divergence [%]"] 251 | ).T 252 | df = pd.concat([df.T, df_diff]).T 253 | 254 | comm_list = [] 255 | for i in df.index: 256 | max_fr_exchange = df.loc[i][:-1].idxmax() 257 | min_fr_exchange = df.loc[i][:-1].idxmin() 258 | max_fr = df.loc[i][:-1].max() 259 | min_fr = df.loc[i][:-1].min() 260 | # TODO: Check perp or spot or options exists on CEX. 261 | if max_fr >= 0 and min_fr >= 0: 262 | min_commission = self.get_commission( 263 | exchange=min_fr_exchange, trade="spot" 264 | ) 265 | max_commission = self.get_commission( 266 | exchange=max_fr_exchange, trade="futures" 267 | ) 268 | elif max_fr >= 0 > min_fr: 269 | max_commission = self.get_commission( 270 | exchange=max_fr_exchange, trade="futures" 271 | ) 272 | min_commission = self.get_commission( 273 | exchange=min_fr_exchange, trade="futures" 274 | ) 275 | else: 276 | try: 277 | max_commission = self.get_commission( 278 | exchange=max_fr_exchange, trade="options" 279 | ) + self.get_commission(exchange=max_fr_exchange, trade="spot") 280 | min_commission = self.get_commission( 281 | exchange=min_fr_exchange, trade="futures" 282 | ) 283 | except KeyError: 284 | max_commission = self.get_commission( 285 | exchange=max_fr_exchange, trade="futures" 286 | ) 287 | min_commission = self.get_commission( 288 | exchange=min_fr_exchange, trade="futures" 289 | ) 290 | sum_of_commission = 2 * (max_commission + min_commission) 291 | comm_list.append(sum_of_commission) 292 | 293 | comm_d = {index: commission for index, commission in zip(df.index, comm_list)} 294 | df_comm = pd.DataFrame( 295 | comm_d.values(), index=list(comm_d.keys()), columns=["Commission [%]"] 296 | ).T 297 | df = pd.concat([df.T, df_comm]).T 298 | 299 | revenue = [ 300 | diff_value - comm_value 301 | for diff_value, comm_value in zip(diff_d.values(), comm_d.values()) 302 | ] 303 | df_rv = pd.DataFrame( 304 | revenue, index=list(comm_d.keys()), columns=["Revenue [/100 USDT]"] 305 | ).T 306 | df = pd.concat([df.T, df_rv]).T 307 | 308 | return df 309 | 310 | def display_one_by_one_single_exchange( 311 | self, exchange: str, minus=False, display_num=10 312 | ): 313 | """ 314 | 315 | Args: 316 | exchange (str): Name of exchange (binance, bybit, ...) 317 | minus (bool): Sorted by minus FR or plus FR. 318 | display_num (int): Number of display. 319 | 320 | Returns: None 321 | 322 | """ 323 | df = self.get_large_divergence_dataframe_single_exchange( 324 | exchange=exchange, minus=minus 325 | ) 326 | # TODO: Check perp or spot or options exists on CEX. 327 | for i in ( 328 | df.sort_values(by="Funding Rate [%]", ascending=minus) 329 | .head(display_num) 330 | .index 331 | ): 332 | print("------------------------------------------------") 333 | revenue = df.loc[i]["Revenue [/100 USDT]"] 334 | if revenue > 0: 335 | print(f"[bold deep_sky_blue1]Revenue: {revenue} / 100USDT[/]") 336 | else: 337 | print(f"[bold red]Revenue: {revenue} / 100USDT[/]") 338 | if minus: 339 | print(f"[bold red]SELL: {i} Options[/]") 340 | print(f"[bold blue]BUY: {i} Perp[/]") 341 | else: 342 | print(f"[bold red]SELL: {i} Perp[/]") 343 | print(f"[bold blue]BUY: {i} Spot[/]") 344 | print(f'Funding Rate: {df.loc[i]["Funding Rate [%]"]:.4f} %') 345 | print(f'Commission: {df.loc[i]["Commission [%]"]} %') 346 | 347 | def display_one_by_one_multi_exchanges(self, display_num=10, sorted_by="revenue"): 348 | """ 349 | 350 | Args: 351 | display_num (int): Number of display. 352 | sorted_by (str): Sorted by "revenue" or "divergence" 353 | 354 | Returns: None 355 | 356 | """ 357 | if sorted_by == "revenue": 358 | sorted_by = "Revenue [/100 USDT]" 359 | elif sorted_by == "divergence": 360 | sorted_by = "Divergence [%]" 361 | else: 362 | log.error(f"{sorted_by} is not available.") 363 | raise KeyError 364 | df = self.get_large_divergence_dataframe_multi_exchanges() 365 | # TODO: Check perp or spot or options exists on CEX. 366 | for i in df.sort_values(by=sorted_by, ascending=False).head(display_num).index: 367 | print("------------------------------------------------") 368 | revenue = df.loc[i]["Revenue [/100 USDT]"] 369 | if revenue > 0: 370 | print(f"[bold deep_sky_blue1]Revenue: {revenue:.4f} USDT / 100USDT[/]") 371 | else: 372 | print(f"[bold red]Revenue: {revenue:.4f} USDT / 100USDT[/]") 373 | max_fr_exchange = df.loc[i][:-3].idxmax() 374 | min_fr_exchange = df.loc[i][:-3].idxmin() 375 | max_fr = df.loc[i][:-3].max() 376 | min_fr = df.loc[i][:-3].min() 377 | if max_fr > 0 and min_fr > 0: 378 | print( 379 | f"[bold red]SELL: {max_fr_exchange} {i} Perp (Funding Rate {max_fr:.4f} %)[/]" 380 | ) 381 | print(f"[bold blue]BUY: {min_fr_exchange} {i} Spot[/]") 382 | elif max_fr > 0 > min_fr: 383 | print( 384 | f"[bold red]SELL: {max_fr_exchange} {i} Perp (Funding Rate {max_fr:.4f} %)[/]" 385 | ) 386 | print( 387 | f"[bold blue]BUY: {min_fr_exchange} {i} Perp (Funding Rate {min_fr:.4f} %)[/]" 388 | ) 389 | else: 390 | print(f"[bold red]SELL: {max_fr_exchange} {i} Options[/]") 391 | print( 392 | f"[bold blue]BUY: {min_fr_exchange} {i} Perp (Funding Rate {min_fr:.4f} %)[/]" 393 | ) 394 | print(f'Divergence: {df.loc[i]["Divergence [%]"]:.4f} %') 395 | print(f'Commission: {df.loc[i]["Commission [%]"]:.4f} %') 396 | 397 | def get_exchanges(self) -> list: 398 | """ 399 | Get a list of exchanges. 400 | Returns (list): List of exchanges. 401 | 402 | """ 403 | return self.exchanges 404 | 405 | def add_exchanges(self, exchange: str) -> list: 406 | """ 407 | Add exchanges. 408 | Args: 409 | exchange (str): Name of the exchange you want to add. 410 | 411 | Returns (list): List of exchanges. 412 | 413 | """ 414 | self.exchanges.append(exchange) 415 | return self.exchanges 416 | 417 | @staticmethod 418 | def get_commission(exchange: str, trade: str, taker=True, by_token=False) -> float: 419 | """ 420 | Get commission. 421 | TODO: Get with ccxt or CEX API. 422 | Args: 423 | exchange (str): Name of exchanges (binance, bybit, ...) 424 | trade (str): Spot Trade or Futures Trade 425 | taker (bool): is Taker or is Maker 426 | by_token (bool): Pay with exchange tokens (BNB, CET, ...) 427 | 428 | Returns (float): Commission. 429 | 430 | """ 431 | # https://www.binance.com/en/fee/schedule 432 | if exchange == "binance": 433 | if trade == "spot": 434 | if by_token: 435 | return 0.075 436 | else: 437 | return 0.1 438 | elif trade == "futures": 439 | if taker: 440 | if by_token: 441 | return 0.036 442 | else: 443 | return 0.04 444 | else: 445 | if by_token: 446 | return 0.018 447 | else: 448 | return 0.02 449 | elif trade == "options": 450 | return 0.02 451 | else: 452 | log.error(f"{trade} is not available on {exchange}.") 453 | raise KeyError 454 | 455 | # https://www.bybit.com/ja-JP/help-center/bybitHC_Article?id=360039261154&language=ja 456 | if exchange == "bybit": 457 | if trade == "spot": 458 | return 0.1 459 | elif trade == "futures": 460 | if taker: 461 | return 0.06 462 | else: 463 | return 0.01 464 | elif trade == "options": 465 | return 0.03 466 | else: 467 | log.error(f"{trade} is not available on {exchange}.") 468 | raise KeyError 469 | 470 | # https://www.okx.com/fees 471 | if exchange == "okx": 472 | if trade == "spot": 473 | if taker: 474 | return 0.1 475 | else: 476 | return 0.08 477 | elif trade == "futures": 478 | if taker: 479 | return 0.05 480 | else: 481 | return 0.02 482 | elif trade == "options": 483 | if taker: 484 | return 0.03 485 | else: 486 | return 0.02 487 | else: 488 | log.error(f"{trade} is not available on {exchange}.") 489 | raise KeyError 490 | 491 | # https://www.bitget.com/ja/rate/ 492 | if exchange == "bitget": 493 | if trade == "spot": 494 | if by_token: 495 | return 0.08 496 | else: 497 | return 0.1 498 | elif trade == "futures": 499 | if taker: 500 | return 0.051 501 | else: 502 | return 0.017 503 | else: 504 | log.error(f"{trade} is not available on {exchange}.") 505 | raise KeyError 506 | 507 | # https://www.gate.io/ja/fee 508 | if exchange == "gate": 509 | if trade == "spot": 510 | if by_token: 511 | return 0.15 512 | else: 513 | return 0.2 514 | elif trade == "futures": 515 | if taker: 516 | return 0.05 517 | else: 518 | return 0.015 519 | else: 520 | log.error(f"{trade} is not available on {exchange}.") 521 | raise KeyError 522 | 523 | # https://www.coinex.zone/fees?type=spot&market=normal 524 | if exchange == "coinex": 525 | if trade == "spot": 526 | if by_token: 527 | return 0.16 528 | else: 529 | return 0.2 530 | elif trade == "futures": 531 | if taker: 532 | return 0.05 533 | else: 534 | return 0.03 535 | else: 536 | log.error(f"{trade} is not available on {exchange}.") 537 | raise KeyError 538 | -------------------------------------------------------------------------------- /img/readme_funding_rate_history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aoki-h-jp/funding-rate-arbitrage/cca35315b443ff7ffe94151d8f22eba0d2cbd350/img/readme_funding_rate_history.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ccxt 2 | pandas 3 | rich 4 | matplotlib -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="funding-rate-arbitrage", 5 | version="1.2.1", 6 | description="A framework to help you easily perform funding rate arbitrage on major centralized cryptocurrency " 7 | "exchanges.", 8 | install_requires=["ccxt", "pandas", "rich", "matplotlib"], 9 | packages=find_packages(include=["funding_rate_arbitrage*"], exclude=["img"]), 10 | author="aoki-h-jp", 11 | author_email="aoki.hirotaka.biz@gmail.com", 12 | license="MIT", 13 | ) 14 | --------------------------------------------------------------------------------